Skip to main content

drasi_source_hyperliquid/
rest.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! REST client for Hyperliquid /info endpoints.
16
17use crate::types::{AssetCtx, InfoRequest, L2Book, MetaResponse, SpotMetaResponse};
18use anyhow::{anyhow, Result};
19use reqwest::Client;
20
21#[derive(Clone)]
22pub struct HyperliquidRestClient {
23    base_url: String,
24    client: Client,
25}
26
27impl HyperliquidRestClient {
28    pub fn new(base_url: impl Into<String>) -> Self {
29        Self {
30            base_url: base_url.into(),
31            client: Client::new(),
32        }
33    }
34
35    async fn post_info<T: serde::de::DeserializeOwned>(
36        &self,
37        request: serde_json::Value,
38    ) -> Result<T> {
39        let response = self
40            .client
41            .post(&self.base_url)
42            .json(&request)
43            .send()
44            .await?
45            .error_for_status()?;
46
47        Ok(response.json::<T>().await?)
48    }
49
50    pub async fn fetch_meta(&self) -> Result<MetaResponse> {
51        self.post_info(serde_json::json!({ "type": "meta" })).await
52    }
53
54    pub async fn fetch_spot_meta(&self) -> Result<SpotMetaResponse> {
55        self.post_info(serde_json::json!({ "type": "spotMeta" }))
56            .await
57    }
58
59    pub async fn fetch_all_mids(&self) -> Result<std::collections::HashMap<String, String>> {
60        self.post_info(serde_json::json!({ "type": "allMids" }))
61            .await
62    }
63
64    pub async fn fetch_meta_and_asset_ctxs(&self) -> Result<(MetaResponse, Vec<AssetCtx>)> {
65        let value: serde_json::Value = self
66            .post_info(serde_json::json!({ "type": "metaAndAssetCtxs" }))
67            .await?;
68
69        let array = value
70            .as_array()
71            .ok_or_else(|| anyhow!("metaAndAssetCtxs response is not an array"))?;
72        let meta_value = array
73            .first()
74            .ok_or_else(|| anyhow!("metaAndAssetCtxs missing meta response"))?
75            .clone();
76        let ctxs_value = array
77            .get(1)
78            .filter(|v| !v.is_null())
79            .cloned()
80            .unwrap_or(serde_json::Value::Array(vec![]));
81
82        let meta: MetaResponse = serde_json::from_value(meta_value)
83            .map_err(|e| anyhow!("Failed to parse meta response: {e}"))?;
84        let ctxs: Vec<AssetCtx> = serde_json::from_value(ctxs_value).map_err(|e| {
85            anyhow!("Failed to parse asset contexts from metaAndAssetCtxs response: {e}")
86        })?;
87
88        Ok((meta, ctxs))
89    }
90
91    pub async fn fetch_l2_book(&self, coin: &str) -> Result<L2Book> {
92        self.post_info(serde_json::json!({ "type": "l2Book", "coin": coin }))
93            .await
94    }
95
96    pub async fn resolve_all_coins(&self) -> Result<Vec<String>> {
97        let meta = self.fetch_meta().await?;
98        Ok(meta.universe.into_iter().map(|asset| asset.name).collect())
99    }
100
101    pub async fn post_custom(&self, request: InfoRequest) -> Result<serde_json::Value> {
102        let response = self
103            .client
104            .post(&self.base_url)
105            .json(&request)
106            .send()
107            .await?
108            .error_for_status()?;
109
110        Ok(response.json::<serde_json::Value>().await?)
111    }
112}