Skip to main content

quicknode_hyperliquid_sdk/
hypercore.rs

1//! HyperCore API client for Hyperliquid.
2//!
3//! Provides access to block-level data, trades, orders, and book updates.
4
5use serde_json::{json, Value};
6use std::sync::Arc;
7
8use crate::client::HyperliquidSDKInner;
9use crate::error::Result;
10
11/// HyperCore API client
12pub struct HyperCore {
13    inner: Arc<HyperliquidSDKInner>,
14}
15
16impl HyperCore {
17    pub(crate) fn new(inner: Arc<HyperliquidSDKInner>) -> Self {
18        Self { inner }
19    }
20
21    /// Get the HyperCore endpoint URL
22    fn hypercore_url(&self) -> String {
23        self.inner.hypercore_url()
24    }
25
26    /// Make a JSON-RPC request to HyperCore
27    async fn rpc(&self, method: &str, params: Value) -> Result<Value> {
28        let url = self.hypercore_url();
29
30        let body = json!({
31            "jsonrpc": "2.0",
32            "method": method,
33            "params": params,
34            "id": 1,
35        });
36
37        let response = self
38            .inner
39            .http_client
40            .post(&url)
41            .json(&body)
42            .send()
43            .await?;
44
45        let status = response.status();
46        let text = response.text().await?;
47
48        if !status.is_success() {
49            return Err(crate::Error::NetworkError(format!(
50                "HyperCore request failed {}: {}",
51                status, text
52            )));
53        }
54
55        let result: Value = serde_json::from_str(&text)?;
56
57        // Check for JSON-RPC error
58        if let Some(error) = result.get("error") {
59            let message = error
60                .get("message")
61                .and_then(|m| m.as_str())
62                .unwrap_or("Unknown error");
63            return Err(crate::Error::ApiError {
64                code: crate::error::ErrorCode::Unknown,
65                message: message.to_string(),
66                guidance: "Check your HyperCore request parameters.".to_string(),
67                raw: Some(error.to_string()),
68            });
69        }
70
71        Ok(result.get("result").cloned().unwrap_or(Value::Null))
72    }
73
74    // ──────────────────────────────────────────────────────────────────────────
75    // Block Data
76    // ──────────────────────────────────────────────────────────────────────────
77
78    /// Get the latest block number for a stream
79    pub async fn latest_block_number(&self, stream: Option<&str>) -> Result<u64> {
80        let stream = stream.unwrap_or("trades");
81        let result = self.rpc("hl_getLatestBlockNumber", json!({"stream": stream})).await?;
82        result
83            .as_u64()
84            .ok_or_else(|| crate::Error::JsonError("Invalid block number".to_string()))
85    }
86
87    /// Get a specific block
88    pub async fn get_block(&self, block_number: u64, stream: Option<&str>) -> Result<Value> {
89        let stream = stream.unwrap_or("trades");
90        self.rpc("hl_getBlock", json!([stream, block_number])).await
91    }
92
93    /// Get a batch of blocks
94    pub async fn get_batch_blocks(
95        &self,
96        from_block: u64,
97        to_block: u64,
98        stream: Option<&str>,
99    ) -> Result<Value> {
100        let stream = stream.unwrap_or("trades");
101        self.rpc("hl_getBatchBlocks", json!({"stream": stream, "from": from_block, "to": to_block}))
102            .await
103    }
104
105    /// Get latest blocks
106    pub async fn latest_blocks(&self, stream: Option<&str>, count: Option<u32>) -> Result<Value> {
107        let stream = stream.unwrap_or("trades");
108        let count = count.unwrap_or(10);
109        self.rpc("hl_getLatestBlocks", json!({"stream": stream, "count": count})).await
110    }
111
112    // ──────────────────────────────────────────────────────────────────────────
113    // Recent Data
114    // ──────────────────────────────────────────────────────────────────────────
115
116    /// Get latest trades from recent blocks
117    /// This is an alternative to Info.recent_trades() for QuickNode endpoints
118    pub async fn latest_trades(&self, count: Option<u32>, coin: Option<&str>) -> Result<Value> {
119        let count = count.unwrap_or(10);
120        let blocks = self.latest_blocks(Some("trades"), Some(count)).await?;
121
122        let mut trades = Vec::new();
123        if let Some(blocks_arr) = blocks.get("blocks").and_then(|b| b.as_array()) {
124            for block in blocks_arr {
125                if let Some(events) = block.get("events").and_then(|e| e.as_array()) {
126                    for event in events {
127                        if let Some(arr) = event.as_array() {
128                            if arr.len() >= 2 {
129                                let user = &arr[0];
130                                if let Some(trade) = arr.get(1) {
131                                    // Apply coin filter if specified
132                                    if let Some(c) = coin {
133                                        if trade.get("coin").and_then(|tc| tc.as_str()) != Some(c) {
134                                            continue;
135                                        }
136                                    }
137                                    let mut trade_obj = trade.clone();
138                                    if let Some(obj) = trade_obj.as_object_mut() {
139                                        obj.insert("user".to_string(), user.clone());
140                                    }
141                                    trades.push(trade_obj);
142                                }
143                            }
144                        }
145                    }
146                }
147            }
148        }
149        Ok(json!(trades))
150    }
151
152    /// Get latest orders from recent blocks
153    pub async fn latest_orders(&self, count: Option<u32>) -> Result<Value> {
154        let count = count.unwrap_or(10);
155        let blocks = self.latest_blocks(Some("orders"), Some(count)).await?;
156
157        let mut orders = Vec::new();
158        if let Some(blocks_arr) = blocks.get("blocks").and_then(|b| b.as_array()) {
159            for block in blocks_arr {
160                if let Some(events) = block.get("events").and_then(|e| e.as_array()) {
161                    for event in events {
162                        if let Some(arr) = event.as_array() {
163                            if arr.len() >= 2 {
164                                let user = &arr[0];
165                                if let Some(order) = arr.get(1) {
166                                    let mut order_obj = order.clone();
167                                    if let Some(obj) = order_obj.as_object_mut() {
168                                        obj.insert("user".to_string(), user.clone());
169                                    }
170                                    orders.push(order_obj);
171                                }
172                            }
173                        }
174                    }
175                }
176            }
177        }
178        Ok(json!(orders))
179    }
180
181    /// Get latest book updates from recent blocks
182    pub async fn latest_book_updates(&self, count: Option<u32>, coin: Option<&str>) -> Result<Value> {
183        let count = count.unwrap_or(10);
184        let blocks = self.latest_blocks(Some("book_updates"), Some(count)).await?;
185
186        let mut updates = Vec::new();
187        if let Some(blocks_arr) = blocks.get("blocks").and_then(|b| b.as_array()) {
188            for block in blocks_arr {
189                if let Some(events) = block.get("events").and_then(|e| e.as_array()) {
190                    for event in events {
191                        // Apply coin filter if specified
192                        if let Some(c) = coin {
193                            if event.get("coin").and_then(|ec| ec.as_str()) != Some(c) {
194                                continue;
195                            }
196                        }
197                        updates.push(event.clone());
198                    }
199                }
200            }
201        }
202        Ok(json!(updates))
203    }
204
205    /// Get latest TWAP updates
206    pub async fn latest_twap(&self, count: Option<u32>) -> Result<Value> {
207        let count = count.unwrap_or(10);
208        self.latest_blocks(Some("twap"), Some(count)).await
209    }
210
211    /// Get latest events
212    pub async fn latest_events(&self, count: Option<u32>) -> Result<Value> {
213        let count = count.unwrap_or(10);
214        self.latest_blocks(Some("events"), Some(count)).await
215    }
216
217    // ──────────────────────────────────────────────────────────────────────────
218    // Discovery
219    // ──────────────────────────────────────────────────────────────────────────
220
221    /// List all DEXes
222    pub async fn list_dexes(&self) -> Result<Value> {
223        self.rpc("hl_listDexes", json!({})).await
224    }
225
226    /// List all markets (optionally for a specific DEX)
227    pub async fn list_markets(&self, dex: Option<&str>) -> Result<Value> {
228        if let Some(d) = dex {
229            self.rpc("hl_listMarkets", json!({"dex": d})).await
230        } else {
231            self.rpc("hl_listMarkets", json!({})).await
232        }
233    }
234
235    // ──────────────────────────────────────────────────────────────────────────
236    // Order Queries
237    // ──────────────────────────────────────────────────────────────────────────
238
239    /// Get open orders for a user
240    pub async fn open_orders(&self, user: &str) -> Result<Value> {
241        self.rpc("hl_openOrders", json!({"user": user})).await
242    }
243
244    /// Get status of a specific order
245    pub async fn order_status(&self, user: &str, oid: u64) -> Result<Value> {
246        self.rpc("hl_orderStatus", json!({"user": user, "oid": oid}))
247            .await
248    }
249
250    /// Validate an order before signing (preflight check)
251    pub async fn preflight(
252        &self,
253        coin: &str,
254        is_buy: bool,
255        limit_px: &str,
256        sz: &str,
257        user: &str,
258        reduce_only: bool,
259        order_type: Option<&Value>,
260    ) -> Result<Value> {
261        let mut params = json!({
262            "coin": coin,
263            "isBuy": is_buy,
264            "limitPx": limit_px,
265            "sz": sz,
266            "user": user,
267            "reduceOnly": reduce_only,
268        });
269        if let Some(ot) = order_type {
270            params["orderType"] = ot.clone();
271        }
272        self.rpc("hl_preflight", params).await
273    }
274
275    // ──────────────────────────────────────────────────────────────────────────
276    // Builder Fee
277    // ──────────────────────────────────────────────────────────────────────────
278
279    /// Get maximum builder fee for a user-builder pair
280    pub async fn get_max_builder_fee(&self, user: &str, builder: &str) -> Result<Value> {
281        self.rpc("hl_getMaxBuilderFee", json!({"user": user, "builder": builder}))
282            .await
283    }
284
285    // ──────────────────────────────────────────────────────────────────────────
286    // Order Building (Returns unsigned actions for signing)
287    // ──────────────────────────────────────────────────────────────────────────
288
289    /// Build an order for signing
290    pub async fn build_order(
291        &self,
292        coin: &str,
293        is_buy: bool,
294        limit_px: &str,
295        sz: &str,
296        user: &str,
297        reduce_only: bool,
298        order_type: Option<&Value>,
299        cloid: Option<&str>,
300    ) -> Result<Value> {
301        let mut params = json!({
302            "coin": coin,
303            "isBuy": is_buy,
304            "limitPx": limit_px,
305            "sz": sz,
306            "user": user,
307            "reduceOnly": reduce_only,
308        });
309        if let Some(ot) = order_type {
310            params["orderType"] = ot.clone();
311        }
312        if let Some(c) = cloid {
313            params["cloid"] = json!(c);
314        }
315        self.rpc("hl_buildOrder", params).await
316    }
317
318    /// Build a cancel action for signing
319    pub async fn build_cancel(&self, coin: &str, oid: u64, user: &str) -> Result<Value> {
320        self.rpc("hl_buildCancel", json!({"coin": coin, "oid": oid, "user": user}))
321            .await
322    }
323
324    /// Build a modify action for signing
325    pub async fn build_modify(
326        &self,
327        coin: &str,
328        oid: u64,
329        user: &str,
330        limit_px: Option<&str>,
331        sz: Option<&str>,
332        is_buy: Option<bool>,
333    ) -> Result<Value> {
334        let mut params = json!({"coin": coin, "oid": oid, "user": user});
335        if let Some(px) = limit_px {
336            params["limitPx"] = json!(px);
337        }
338        if let Some(s) = sz {
339            params["sz"] = json!(s);
340        }
341        if let Some(buy) = is_buy {
342            params["isBuy"] = json!(buy);
343        }
344        self.rpc("hl_buildModify", params).await
345    }
346
347    /// Build a builder fee approval for signing
348    pub async fn build_approve_builder_fee(
349        &self,
350        user: &str,
351        builder: &str,
352        max_fee_rate: &str,
353        nonce: u64,
354    ) -> Result<Value> {
355        self.rpc(
356            "hl_buildApproveBuilderFee",
357            json!({
358                "user": user,
359                "builder": builder,
360                "maxFeeRate": max_fee_rate,
361                "nonce": nonce,
362            }),
363        )
364        .await
365    }
366
367    /// Build a builder fee revocation for signing
368    pub async fn build_revoke_builder_fee(
369        &self,
370        user: &str,
371        builder: &str,
372        nonce: u64,
373    ) -> Result<Value> {
374        self.rpc(
375            "hl_buildRevokeBuilderFee",
376            json!({
377                "user": user,
378                "builder": builder,
379                "nonce": nonce,
380            }),
381        )
382        .await
383    }
384
385    // ──────────────────────────────────────────────────────────────────────────
386    // Sending Signed Actions
387    // ──────────────────────────────────────────────────────────────────────────
388
389    /// Send a signed order
390    pub async fn send_order(
391        &self,
392        action: &Value,
393        signature: &str,
394        nonce: u64,
395    ) -> Result<Value> {
396        self.rpc(
397            "hl_sendOrder",
398            json!({"action": action, "signature": signature, "nonce": nonce}),
399        )
400        .await
401    }
402
403    /// Send a signed cancel
404    pub async fn send_cancel(
405        &self,
406        action: &Value,
407        signature: &str,
408        nonce: u64,
409    ) -> Result<Value> {
410        self.rpc(
411            "hl_sendCancel",
412            json!({"action": action, "signature": signature, "nonce": nonce}),
413        )
414        .await
415    }
416
417    /// Send a signed modify
418    pub async fn send_modify(
419        &self,
420        action: &Value,
421        signature: &str,
422        nonce: u64,
423    ) -> Result<Value> {
424        self.rpc(
425            "hl_sendModify",
426            json!({"action": action, "signature": signature, "nonce": nonce}),
427        )
428        .await
429    }
430
431    /// Send a signed builder fee approval
432    pub async fn send_approval(&self, action: &Value, signature: &str) -> Result<Value> {
433        self.rpc(
434            "hl_sendApproval",
435            json!({"action": action, "signature": signature}),
436        )
437        .await
438    }
439
440    /// Send a signed builder fee revocation
441    pub async fn send_revocation(&self, action: &Value, signature: &str) -> Result<Value> {
442        self.rpc(
443            "hl_sendRevocation",
444            json!({"action": action, "signature": signature}),
445        )
446        .await
447    }
448
449    // ──────────────────────────────────────────────────────────────────────────
450    // WebSocket Subscriptions (via JSON-RPC)
451    // ──────────────────────────────────────────────────────────────────────────
452
453    /// Subscribe to a WebSocket stream via JSON-RPC
454    pub async fn subscribe(&self, subscription: &Value) -> Result<Value> {
455        self.rpc("hl_subscribe", json!({"subscription": subscription}))
456            .await
457    }
458
459    /// Unsubscribe from a WebSocket stream
460    pub async fn unsubscribe(&self, subscription: &Value) -> Result<Value> {
461        self.rpc("hl_unsubscribe", json!({"subscription": subscription}))
462            .await
463    }
464}