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 #[serde(default)]
297 cloid: Option<String>,
298}
299
300#[derive(Deserialize)]
301struct WsFunding {
302 time: i64,
303 coin: String,
304 usdc: String,
305}
306
307#[derive(Deserialize)]
308struct WsTrade {
309 coin: String,
310 side: String,
311 px: String,
312 sz: String,
313 time: i64,
314 tid: i64,
315}
316
317#[derive(Deserialize)]
318struct WsOrderUpdate {
319 order: WsOrderInfo,
320 status: String,
321 #[serde(rename = "statusTimestamp")]
322 status_timestamp: i64,
323}
324
325#[derive(Deserialize)]
326#[serde(rename_all = "camelCase")]
327struct WsOrderInfo {
328 coin: String,
329 side: String,
330 limit_px: String,
331 sz: String,
332 oid: i64,
333 orig_sz: String,
334 #[serde(default)]
336 cloid: Option<String>,
337}
338
339#[derive(Deserialize)]
342struct WsLedgerUpdates {
343 updates: Vec<WsLedgerEntry>,
344}
345
346#[derive(Deserialize)]
347struct WsLedgerEntry {
348 time: i64,
349 delta: WsLedgerDelta,
350}
351
352#[derive(Deserialize)]
353struct WsLedgerDelta {
354 #[serde(rename = "type")]
355 kind: String,
356 usdc: Option<String>,
357}
358
359fn parse_decimal(s: &str) -> Option<Decimal> {
362 Decimal::from_str(s).ok()
363}
364
365fn keccak256(data: &[u8]) -> [u8; 32] {
366 use sha3::{Digest, Keccak256};
367 Keccak256::digest(data).into()
368}
369
370fn hyperliquid_domain_separator() -> [u8; 32] {
372 let type_hash = keccak256(
373 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
374 );
375 let name_hash = keccak256(b"Exchange");
376 let version_hash = keccak256(b"1");
377 let mut chain_id = [0u8; 32];
378 chain_id[28..32].copy_from_slice(&42161u32.to_be_bytes());
379 let verifying_contract = [0u8; 32];
380
381 let mut data = [0u8; 160];
382 data[..32].copy_from_slice(&type_hash);
383 data[32..64].copy_from_slice(&name_hash);
384 data[64..96].copy_from_slice(&version_hash);
385 data[96..128].copy_from_slice(&chain_id);
386 data[128..160].copy_from_slice(&verifying_contract);
387 keccak256(&data)
388}
389
390fn sign_action(
393 private_key: &str,
394 action: &Value,
395 vault_address: Option<&str>,
396 nonce: u64,
397) -> Result<(String, String, u8), String> {
398 use k256::ecdsa::SigningKey;
399
400 let msgpack_bytes = rmp_serde::to_vec(action).map_err(|e| e.to_string())?;
402 let mut data = msgpack_bytes;
403 data.extend_from_slice(&nonce.to_be_bytes());
404 match vault_address {
405 None => data.push(0u8),
406 Some(addr) => {
407 data.push(1u8);
408 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
409 .map_err(|e| format!("invalid vault address: {}", e))?;
410 data.extend_from_slice(&addr_bytes);
411 }
412 }
413 let connection_id = keccak256(&data);
414
415 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
417 let source_hash = keccak256(b"a"); let mut struct_data = [0u8; 96];
419 struct_data[..32].copy_from_slice(&agent_type_hash);
420 struct_data[32..64].copy_from_slice(&source_hash);
421 struct_data[64..96].copy_from_slice(&connection_id);
422 let struct_hash = keccak256(&struct_data);
423
424 let domain_sep = hyperliquid_domain_separator();
426 let mut final_data = Vec::with_capacity(66);
427 final_data.extend_from_slice(b"\x19\x01");
428 final_data.extend_from_slice(&domain_sep);
429 final_data.extend_from_slice(&struct_hash);
430 let final_hash = keccak256(&final_data);
431
432 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
434 .map_err(|e| format!("invalid private key: {}", e))?;
435 let signing_key =
436 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
437 let (sig, recovery_id) = signing_key
438 .sign_prehash_recoverable(&final_hash)
439 .map_err(|e| e.to_string())?;
440
441 let sig_bytes = sig.to_bytes();
442 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
443 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
444 let v = 27u8 + recovery_id.to_byte();
445
446 Ok((r, s, v))
447}
448
449#[allow(async_fn_in_trait)]
452impl guilder_abstraction::TestServer for HyperliquidClient {
453 async fn ping(&self) -> Result<bool, String> {
455 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
457 .await
458 .map(|r| r.status().is_success())
459 }
460
461 async fn get_server_time(&self) -> Result<i64, String> {
463 Ok(std::time::SystemTime::now()
464 .duration_since(std::time::UNIX_EPOCH)
465 .map(|d| d.as_millis() as i64)
466 .unwrap_or(0))
467 }
468}
469
470#[allow(async_fn_in_trait)]
471impl guilder_abstraction::GetMarketData for HyperliquidClient {
472 async fn get_symbol(&self) -> Result<Vec<String>, String> {
474 let resp = self
476 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
477 .await?;
478 parse_response::<MetaResponse>(resp)
479 .await
480 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
481 }
482
483 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
485 let resp = self
487 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_open_interest")
488 .await?;
489 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
490 .await?
491 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
492 meta.universe
493 .iter()
494 .position(|a| a.name == symbol)
495 .and_then(|i| ctxs.get(i))
496 .and_then(|ctx| parse_decimal(&ctx.open_interest))
497 .ok_or_else(|| format!("symbol {} not found", symbol))
498 }
499
500 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
502 let resp = self
504 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_asset_context")
505 .await?;
506 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
507 .await?
508 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
509 let idx = meta
510 .universe
511 .iter()
512 .position(|a| a.name == symbol)
513 .ok_or_else(|| format!("symbol {} not found", symbol))?;
514 let ctx = ctxs
515 .get(idx)
516 .ok_or_else(|| format!("symbol {} not found", symbol))?;
517 Ok(AssetContext {
518 symbol,
519 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
520 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
521 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
522 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
523 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
524 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
525 premium: ctx.premium.as_deref().and_then(parse_decimal),
526 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
527 })
528 }
529
530 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
533 let resp = self
535 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_all_asset_contexts")
536 .await?;
537 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
538 .await?
539 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
540 let mut result = Vec::with_capacity(meta.universe.len());
541 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
542 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
543 continue;
544 };
545 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
546 continue;
547 };
548 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
549 continue;
550 };
551 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
552 continue;
553 };
554 result.push(AssetContext {
555 symbol: asset.name.clone(),
556 open_interest,
557 funding_rate,
558 mark_price,
559 day_volume,
560 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
561 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
562 premium: ctx.premium.as_deref().and_then(parse_decimal),
563 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
564 });
565 }
566 Ok(result)
567 }
568
569 async fn get_l2_orderbook(&self, symbol: String) -> Result<Vec<L2Update>, String> {
572 let resp = self
574 .info_post(serde_json::json!({"type": "l2Book", "coin": symbol}), 2, "get_l2_orderbook")
575 .await?;
576 let book: Option<WsBook> = parse_response(resp).await?;
577 let book = match book {
578 Some(b) => b,
579 None => return Ok(vec![]),
580 };
581 let mut levels = Vec::new();
582 for level in book.levels.first().into_iter().flatten() {
583 if let (Some(price), Some(volume)) =
584 (parse_decimal(&level.px), parse_decimal(&level.sz))
585 {
586 levels.push(L2Update {
587 symbol: book.coin.clone(),
588 price,
589 volume,
590 side: Side::Ask,
591 sequence: book.time,
592 });
593 }
594 }
595 for level in book.levels.get(1).into_iter().flatten() {
596 if let (Some(price), Some(volume)) =
597 (parse_decimal(&level.px), parse_decimal(&level.sz))
598 {
599 levels.push(L2Update {
600 symbol: book.coin.clone(),
601 price,
602 volume,
603 side: Side::Bid,
604 sequence: book.time,
605 });
606 }
607 }
608 Ok(levels)
609 }
610
611 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
613 let resp = self
615 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
616 .await?;
617 parse_response::<HashMap<String, String>>(resp)
618 .await?
619 .get(&symbol)
620 .and_then(|s| parse_decimal(s))
621 .ok_or_else(|| format!("symbol {} not found", symbol))
622 }
623
624 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
627 let resp = self
629 .info_post(serde_json::json!({"type": "predictedFundings"}), 20, "get_predicted_fundings")
630 .await?;
631 let data: PredictedFundingsResponse = parse_response(resp).await?;
632 let mut result = Vec::new();
633 for (symbol, venues) in data {
634 for (venue, entry) in venues {
635 let Some(entry) = entry else { continue };
636 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
637 result.push(PredictedFunding {
638 symbol: symbol.clone(),
639 venue,
640 funding_rate,
641 next_funding_time_ms: entry.next_funding_time,
642 });
643 }
644 }
645 }
646 Ok(result)
647 }
648}
649
650#[allow(async_fn_in_trait)]
651impl guilder_abstraction::ManageOrder for HyperliquidClient {
652 async fn place_order(
659 &self,
660 symbol: String,
661 side: OrderSide,
662 price: Decimal,
663 volume: Decimal,
664 order_type: OrderType,
665 time_in_force: TimeInForce,
666 cloid: Option<String>,
667 ) -> Result<OrderPlacement, String> {
668 let asset_idx = self.get_asset_index(&symbol).await?;
669 let is_buy = matches!(side, OrderSide::Buy);
670
671 let tif_str = match time_in_force {
672 TimeInForce::Gtc => "Gtc",
673 TimeInForce::Ioc => "Ioc",
674 TimeInForce::Fok => "Fok",
675 };
676 let order_type_val = match order_type {
678 OrderType::Limit => serde_json::json!({"limit": {"tif": tif_str}}),
679 OrderType::Market => serde_json::json!({"limit": {"tif": "Ioc"}}),
680 };
681
682 let mut order_json = serde_json::json!({
683 "a": asset_idx,
684 "b": is_buy,
685 "p": price.to_string(),
686 "s": volume.to_string(),
687 "r": false,
688 "t": order_type_val
689 });
690 if let Some(ref c) = cloid {
691 order_json["c"] = serde_json::json!(c);
692 }
693
694 let action = serde_json::json!({
695 "type": "order",
696 "orders": [order_json],
697 "grouping": "na"
698 });
699
700 let resp = self.submit_signed_action(action, None).await?;
701 let oid = resp["response"]["data"]["statuses"][0]["resting"]["oid"]
702 .as_i64()
703 .or_else(|| resp["response"]["data"]["statuses"][0]["filled"]["oid"].as_i64())
704 .ok_or_else(|| format!("unexpected response: {}", resp))?;
705
706 let timestamp_ms = std::time::SystemTime::now()
707 .duration_since(std::time::UNIX_EPOCH)
708 .unwrap()
709 .as_millis() as i64;
710
711 Ok(OrderPlacement {
712 order_id: oid,
713 symbol,
714 side,
715 price,
716 quantity: volume,
717 timestamp_ms,
718 cloid,
719 })
720 }
721
722 async fn change_order_by_cloid(
725 &self,
726 cloid: i64,
727 price: Decimal,
728 volume: Decimal,
729 ) -> Result<i64, String> {
730 let user = self.require_user_address()?;
731
732 let resp = self
734 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "change_order_by_cloid")
735 .await?;
736 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
737 let order = orders
738 .iter()
739 .find(|o| o.oid == cloid)
740 .ok_or_else(|| format!("order {} not found", cloid))?;
741
742 let asset_idx = self.get_asset_index(&order.coin).await?;
743 let is_buy = order.side == "B";
744
745 let action = serde_json::json!({
746 "type": "batchModify",
747 "modifies": [{
748 "oid": cloid,
749 "order": {
750 "a": asset_idx,
751 "b": is_buy,
752 "p": price.to_string(),
753 "s": volume.to_string(),
754 "r": false,
755 "t": {"limit": {"tif": "Gtc"}}
756 }
757 }]
758 });
759
760 self.submit_signed_action(action, None).await?;
761 Ok(cloid)
762 }
763
764 async fn cancel_order(&self, cloid: i64) -> Result<i64, String> {
767 let user = self.require_user_address()?;
768
769 let resp = self
771 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "cancel_order")
772 .await?;
773 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
774 let order = orders
775 .iter()
776 .find(|o| o.oid == cloid)
777 .ok_or_else(|| format!("order {} not found", cloid))?;
778
779 let asset_idx = self.get_asset_index(&order.coin).await?;
780 let action = serde_json::json!({
781 "type": "cancel",
782 "cancels": [{"a": asset_idx, "o": cloid}]
783 });
784
785 self.submit_signed_action(action, None).await?;
786 Ok(cloid)
787 }
788
789 async fn cancel_all_order(&self) -> Result<bool, String> {
792 let user = self.require_user_address()?;
793
794 let resp = self
796 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "cancel_all_order")
797 .await?;
798 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
799 if orders.is_empty() {
800 return Ok(true);
801 }
802
803 let meta_resp = self
805 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
806 .await?;
807 let meta: MetaResponse = parse_response(meta_resp).await?;
808
809 let cancels: Vec<Value> = orders
810 .iter()
811 .filter_map(|o| {
812 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
813 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
814 })
815 .collect();
816
817 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
818 self.submit_signed_action(action, None).await?;
819 Ok(true)
820 }
821}
822
823#[allow(async_fn_in_trait)]
824impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
825 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
826 let sub = serde_json::json!({
827 "method": "subscribe",
828 "subscription": {"type": "l2Book", "coin": symbol.clone()}
829 });
830 let key = crate::ws::SubKey {
831 channel: "l2Book".to_string(),
832 routing_key: symbol,
833 };
834 let stream = self.ws_mux.subscribe(key, sub);
835 Box::pin(async_stream::stream! {
836 for await msg in stream {
837 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
838 continue;
839 };
840 if env.channel != "l2Book" {
841 continue;
842 }
843 let Ok(book) = serde_json::from_value::<WsBook>(env.data) else {
844 continue;
845 };
846 for level in book.levels.first().into_iter().flatten() {
847 if let (Some(price), Some(volume)) =
848 (parse_decimal(&level.px), parse_decimal(&level.sz))
849 {
850 yield Ok(L2Update {
851 symbol: book.coin.clone(),
852 price,
853 volume,
854 side: Side::Ask,
855 sequence: book.time,
856 });
857 }
858 }
859 for level in book.levels.get(1).into_iter().flatten() {
860 if let (Some(price), Some(volume)) =
861 (parse_decimal(&level.px), parse_decimal(&level.sz))
862 {
863 yield Ok(L2Update {
864 symbol: book.coin.clone(),
865 price,
866 volume,
867 side: Side::Bid,
868 sequence: book.time,
869 });
870 }
871 }
872 }
873 })
874 }
875
876 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
877 let sub = serde_json::json!({
878 "method": "subscribe",
879 "subscription": {"type": "activeAssetCtx", "coin": symbol.clone()}
880 });
881 let key = crate::ws::SubKey {
882 channel: "activeAssetCtx".to_string(),
883 routing_key: symbol,
884 };
885 let stream = self.ws_mux.subscribe(key, sub);
886 Box::pin(async_stream::stream! {
887 for await msg in stream {
888 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
889 continue;
890 };
891 if env.channel != "activeAssetCtx" {
892 continue;
893 }
894 let Ok(update) = serde_json::from_value::<WsAssetCtx>(env.data) else {
895 continue;
896 };
897 let ctx = &update.ctx;
898 let (Some(open_interest), Some(funding_rate), Some(mark_price), Some(day_volume)) = (
899 parse_decimal(&ctx.open_interest),
900 parse_decimal(&ctx.funding),
901 parse_decimal(&ctx.mark_px),
902 parse_decimal(&ctx.day_ntl_vlm),
903 ) else {
904 continue;
905 };
906 yield Ok(AssetContext {
907 symbol: update.coin,
908 open_interest,
909 funding_rate,
910 mark_price,
911 day_volume,
912 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
913 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
914 premium: ctx.premium.as_deref().and_then(parse_decimal),
915 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
916 });
917 }
918 })
919 }
920
921 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
922 let sub = serde_json::json!({
923 "method": "subscribe",
924 "subscription": {"type": "userEvents", "user": user.clone()}
925 });
926 let key = crate::ws::SubKey {
927 channel: "userEvents".to_string(),
928 routing_key: user,
929 };
930 let raw_stream = self.ws_mux.subscribe(key, sub);
931 Box::pin(raw_stream.filter_map(|text| async move {
932 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
933 return None;
934 };
935 if env.channel != "userEvents" {
936 return None;
937 }
938 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
939 return None;
940 };
941 let Some(liq) = event.liquidation else {
942 return None;
943 };
944 let (Some(notional_position), Some(account_value)) = (
945 parse_decimal(&liq.liquidated_ntl_pos),
946 parse_decimal(&liq.liquidated_account_value),
947 ) else {
948 return None;
949 };
950 let item = Liquidation {
951 symbol: String::new(),
952 side: OrderSide::Sell,
953 liquidated_user: liq.liquidated_user,
954 notional_position,
955 account_value,
956 };
957 Some(stream::iter(vec![Ok(item)].into_iter()))
958 }).flatten())
959 }
960
961 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
962 let sub = serde_json::json!({
963 "method": "subscribe",
964 "subscription": {"type": "trades", "coin": symbol.clone()}
965 });
966 let key = crate::ws::SubKey {
967 channel: "trades".to_string(),
968 routing_key: symbol,
969 };
970 let stream = self.ws_mux.subscribe(key, sub);
971 Box::pin(async_stream::stream! {
972 for await msg in stream {
973 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
974 continue;
975 };
976 if env.channel != "trades" {
977 continue;
978 }
979 let Ok(trades) = serde_json::from_value::<Vec<WsTrade>>(env.data) else {
980 continue;
981 };
982 for trade in trades {
983 let side = if trade.side == "B" {
984 OrderSide::Buy
985 } else {
986 OrderSide::Sell
987 };
988 let price = parse_decimal(&trade.px);
989 let volume = parse_decimal(&trade.sz);
990 if let (Some(price), Some(volume)) = (price, volume) {
991 yield Ok(Fill {
992 symbol: trade.coin,
993 price,
994 volume,
995 side,
996 timestamp_ms: trade.time,
997 trade_id: trade.tid,
998 });
999 }
1000 }
1001 }
1002 })
1003 }
1004}
1005
1006#[allow(async_fn_in_trait)]
1007impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
1008 async fn get_positions(&self) -> Result<Vec<Position>, String> {
1011 let user = self.require_user_address()?;
1012 let resp = self
1014 .info_post(
1015 serde_json::json!({"type": "clearinghouseState", "user": user}),
1016 2,
1017 "get_positions",
1018 )
1019 .await?;
1020 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1021
1022 Ok(state
1023 .asset_positions
1024 .into_iter()
1025 .filter_map(|ap| {
1026 let p = ap.position;
1027 let size = parse_decimal(&p.szi)?;
1028 if size.is_zero() {
1029 return None;
1030 }
1031 let entry_price = p
1032 .entry_px
1033 .as_deref()
1034 .and_then(parse_decimal)
1035 .unwrap_or_default();
1036 let side = if size > Decimal::ZERO {
1037 OrderSide::Buy
1038 } else {
1039 OrderSide::Sell
1040 };
1041 Some(Position {
1042 symbol: p.coin,
1043 side,
1044 size: size.abs(),
1045 entry_price,
1046 })
1047 })
1048 .collect())
1049 }
1050
1051 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1054 let user = self.require_user_address()?;
1055 let resp = self
1057 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "get_open_orders")
1058 .await?;
1059 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1060
1061 Ok(orders
1062 .into_iter()
1063 .filter_map(|o| {
1064 let price = parse_decimal(&o.limit_px)?;
1065 let quantity = parse_decimal(&o.orig_sz)?;
1066 let remaining = parse_decimal(&o.sz)?;
1067 let filled_quantity = quantity - remaining;
1068 let side = if o.side == "B" {
1069 OrderSide::Buy
1070 } else {
1071 OrderSide::Sell
1072 };
1073 Some(OpenOrder {
1074 order_id: o.oid,
1075 symbol: o.coin,
1076 side,
1077 price,
1078 quantity,
1079 filled_quantity,
1080 })
1081 })
1082 .collect())
1083 }
1084
1085 async fn get_collateral(&self) -> Result<Decimal, String> {
1087 let user = self.require_user_address()?;
1088 let resp = self
1090 .info_post(
1091 serde_json::json!({"type": "clearinghouseState", "user": user}),
1092 2,
1093 "get_collateral",
1094 )
1095 .await?;
1096 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1097 parse_decimal(&state.margin_summary.account_value)
1098 .ok_or_else(|| "invalid account value".to_string())
1099 }
1100}
1101
1102#[allow(async_fn_in_trait)]
1103impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1104 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1105 let Some(addr) = self.user_address else {
1106 return Box::pin(stream::empty());
1107 };
1108 let addr_str = format!("{:#x}", addr);
1109 let sub = serde_json::json!({
1110 "method": "subscribe",
1111 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1112 });
1113 let key = crate::ws::SubKey {
1114 channel: "userEvents".to_string(),
1115 routing_key: addr_str,
1116 };
1117 let raw_stream = self.ws_mux.subscribe(key, sub);
1118 Box::pin(raw_stream.filter_map(|text| async move {
1119 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1120 return None;
1121 };
1122 if env.channel != "userEvents" {
1123 return None;
1124 }
1125 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1126 return None;
1127 };
1128 let items: Vec<_> = event
1129 .fills
1130 .unwrap_or_default()
1131 .into_iter()
1132 .filter_map(|fill| {
1133 let side = if fill.side == "B" {
1134 OrderSide::Buy
1135 } else {
1136 OrderSide::Sell
1137 };
1138 let price = parse_decimal(&fill.px)?;
1139 let quantity = parse_decimal(&fill.sz)?;
1140 let fee_usd = parse_decimal(&fill.fee)?;
1141 Some(UserFill {
1142 order_id: fill.oid,
1143 symbol: fill.coin,
1144 side,
1145 price,
1146 quantity,
1147 fee_usd,
1148 timestamp_ms: fill.time,
1149 cloid: fill.cloid,
1150 })
1151 })
1152 .collect();
1153 if items.is_empty() {
1154 None
1155 } else {
1156 Some(stream::iter(items.into_iter().map(Ok)))
1157 }
1158 }).flatten())
1159 }
1160
1161 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1162 let Some(addr) = self.user_address else {
1163 return Box::pin(stream::empty());
1164 };
1165 let addr_str = format!("{:#x}", addr);
1166 let sub = serde_json::json!({
1167 "method": "subscribe",
1168 "subscription": {"type": "orderUpdates", "user": addr_str.clone()}
1169 });
1170 let key = crate::ws::SubKey {
1171 channel: "orderUpdates".to_string(),
1172 routing_key: addr_str,
1173 };
1174 let raw_stream = self.ws_mux.subscribe(key, sub);
1175 Box::pin(raw_stream.filter_map(|text| async move {
1176 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1177 return None;
1178 };
1179 if env.channel != "orderUpdates" {
1180 return None;
1181 }
1182 let Ok(updates) = serde_json::from_value::<Vec<WsOrderUpdate>>(env.data) else {
1183 return None;
1184 };
1185 let items: Vec<_> = updates
1186 .into_iter()
1187 .map(|upd| {
1188 let status = match upd.status.as_str() {
1189 "open" => OrderStatus::Placed,
1190 "filled" => OrderStatus::Filled,
1191 "canceled" | "cancelled" => OrderStatus::Cancelled,
1192 _ => OrderStatus::PartiallyFilled,
1193 };
1194 let side = if upd.order.side == "B" {
1195 OrderSide::Buy
1196 } else {
1197 OrderSide::Sell
1198 };
1199 OrderUpdate {
1200 order_id: upd.order.oid,
1201 symbol: upd.order.coin,
1202 status,
1203 side: Some(side),
1204 price: parse_decimal(&upd.order.limit_px),
1205 quantity: parse_decimal(&upd.order.orig_sz),
1206 remaining_quantity: parse_decimal(&upd.order.sz),
1207 timestamp_ms: upd.status_timestamp,
1208 cloid: upd.order.cloid,
1209 }
1210 })
1211 .collect();
1212 if items.is_empty() {
1213 None
1214 } else {
1215 Some(stream::iter(items.into_iter().map(Ok)))
1216 }
1217 }).flatten())
1218 }
1219
1220 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1221 let Some(addr) = self.user_address else {
1222 return Box::pin(stream::empty());
1223 };
1224 let addr_str = format!("{:#x}", addr);
1225 let sub = serde_json::json!({
1226 "method": "subscribe",
1227 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1228 });
1229 let key = crate::ws::SubKey {
1230 channel: "userEvents".to_string(),
1231 routing_key: addr_str,
1232 };
1233 let raw_stream = self.ws_mux.subscribe(key, sub);
1234 Box::pin(raw_stream.filter_map(|text| async move {
1235 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1236 return None;
1237 };
1238 if env.channel != "userEvents" {
1239 return None;
1240 }
1241 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1242 return None;
1243 };
1244 let Some(funding) = event.funding else {
1245 return None;
1246 };
1247 let Some(amount_usd) = parse_decimal(&funding.usdc) else {
1248 return None;
1249 };
1250 let item = FundingPayment {
1251 symbol: funding.coin,
1252 amount_usd,
1253 timestamp_ms: funding.time,
1254 };
1255 Some(stream::iter(vec![Ok(item)].into_iter()))
1256 }).flatten())
1257 }
1258
1259 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
1260 let Some(addr) = self.user_address else {
1261 return Box::pin(stream::empty());
1262 };
1263 let addr_str = format!("{:#x}", addr);
1264 let sub = serde_json::json!({
1265 "method": "subscribe",
1266 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
1267 });
1268 let key = crate::ws::SubKey {
1269 channel: "userNonFundingLedgerUpdates".to_string(),
1270 routing_key: addr_str,
1271 };
1272 let raw_stream = self.ws_mux.subscribe(key, sub);
1273 Box::pin(raw_stream.filter_map(|text| async move {
1274 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1275 return None;
1276 };
1277 if env.channel != "userNonFundingLedgerUpdates" {
1278 return None;
1279 }
1280 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
1281 return None;
1282 };
1283 let items: Vec<_> = ledger
1284 .updates
1285 .into_iter()
1286 .filter_map(|e| {
1287 if e.delta.kind != "deposit" {
1288 return None;
1289 }
1290 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
1291 Some(Deposit {
1292 asset: "USDC".to_string(),
1293 amount_usd,
1294 timestamp_ms: e.time,
1295 })
1296 })
1297 .collect();
1298 if items.is_empty() {
1299 None
1300 } else {
1301 Some(stream::iter(items.into_iter().map(Ok)))
1302 }
1303 }).flatten())
1304 }
1305
1306 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
1307 let Some(addr) = self.user_address else {
1308 return Box::pin(stream::empty());
1309 };
1310 let addr_str = format!("{:#x}", addr);
1311 let sub = serde_json::json!({
1312 "method": "subscribe",
1313 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
1314 });
1315 let key = crate::ws::SubKey {
1316 channel: "userNonFundingLedgerUpdates".to_string(),
1317 routing_key: addr_str,
1318 };
1319 let raw_stream = self.ws_mux.subscribe(key, sub);
1320 Box::pin(raw_stream.filter_map(|text| async move {
1321 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1322 return None;
1323 };
1324 if env.channel != "userNonFundingLedgerUpdates" {
1325 return None;
1326 }
1327 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
1328 return None;
1329 };
1330 let items: Vec<_> = ledger
1331 .updates
1332 .into_iter()
1333 .filter_map(|e| {
1334 if e.delta.kind != "withdraw" {
1335 return None;
1336 }
1337 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
1338 Some(Withdrawal {
1339 asset: "USDC".to_string(),
1340 amount_usd,
1341 timestamp_ms: e.time,
1342 })
1343 })
1344 .collect();
1345 if items.is_empty() {
1346 None
1347 } else {
1348 Some(stream::iter(items.into_iter().map(Ok)))
1349 }
1350 }).flatten())
1351 }
1352}