use crate::{
info::{
CandlesSnapshotResponse, FundingHistoryResponse, L2SnapshotResponse, OpenOrdersResponse,
OrderInfo, RecentTradesResponse, UserFillsResponse, UserStateResponse,
},
meta::{FundingRate, Meta, MetaAndAssetCtxs, SpotMeta, SpotMetaAndAssetCtxs},
prelude::*,
req::HttpClient,
ws::{Subscription, WsManager},
BaseUrl, Error, Message, OrderStatusResponse, ReferralResponse, UserFeesResponse,
UserFundingResponse, UserTokenBalanceResponse,
};
use ethers::types::H160;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use tokio::sync::mpsc::UnboundedSender;
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CandleSnapshotRequest {
coin: String,
interval: String,
start_time: u64,
end_time: u64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum InfoRequest {
#[serde(rename = "clearinghouseState")]
UserState {
user: H160,
},
#[serde(rename = "batchClearinghouseStates")]
UserStates {
users: Vec<H160>,
},
#[serde(rename = "spotClearinghouseState")]
UserTokenBalances {
user: H160,
},
UserFees {
user: H160,
},
OpenOrders {
user: H160,
},
OrderStatus {
user: H160,
oid: u64,
},
Meta,
MetaAndAssetCtxs,
SpotMeta,
SpotMetaAndAssetCtxs,
AllMids,
UserFills {
user: H160,
},
#[serde(rename_all = "camelCase")]
FundingHistory {
coin: String,
start_time: u64,
end_time: Option<u64>,
},
#[serde(rename_all = "camelCase")]
UserFunding {
user: H160,
start_time: u64,
end_time: Option<u64>,
},
PredictedFundings,
L2Book {
coin: String,
},
RecentTrades {
coin: String,
},
#[serde(rename_all = "camelCase")]
CandleSnapshot {
req: CandleSnapshotRequest,
},
Referral {
user: H160,
},
HistoricalOrders {
user: H160,
},
}
#[derive(Debug, Clone)]
pub struct InfoClient {
pub http_client: HttpClient,
pub(crate) ws_manager: Option<WsManager>,
reconnect: bool,
}
impl InfoClient {
pub async fn new(client: Option<Client>, base_url: Option<BaseUrl>) -> Result<InfoClient> {
Self::new_internal(client, base_url, false).await
}
pub async fn with_reconnect(
client: Option<Client>,
base_url: Option<BaseUrl>,
) -> Result<InfoClient> {
Self::new_internal(client, base_url, true).await
}
async fn new_internal(
client: Option<Client>,
base_url: Option<BaseUrl>,
reconnect: bool,
) -> Result<InfoClient> {
let client = client.unwrap_or_default();
let base_url = base_url.unwrap_or(BaseUrl::Mainnet).get_url();
Ok(InfoClient {
http_client: HttpClient { client, base_url },
ws_manager: None,
reconnect,
})
}
pub async fn subscribe(
&mut self,
subscription: Subscription,
sender_channel: UnboundedSender<Message>,
) -> Result<u32> {
if self.ws_manager.is_none() {
let ws_manager = WsManager::new(
format!("ws{}/ws", &self.http_client.base_url[4..]),
self.reconnect,
)
.await?;
self.ws_manager = Some(ws_manager);
}
let identifier =
serde_json::to_string(&subscription).map_err(|e| Error::JsonParse(e.to_string()))?;
self.ws_manager
.as_mut()
.ok_or(Error::WsManagerNotFound)?
.add_subscription(identifier, sender_channel)
.await
}
pub async fn unsubscribe(&mut self, subscription_id: u32) -> Result<()> {
if self.ws_manager.is_none() {
let ws_manager = WsManager::new(
format!("ws{}/ws", &self.http_client.base_url[4..]),
self.reconnect,
)
.await?;
self.ws_manager = Some(ws_manager);
}
self.ws_manager
.as_mut()
.ok_or(Error::WsManagerNotFound)?
.remove_subscription(subscription_id)
.await
}
async fn send_info_request<T: for<'a> Deserialize<'a>>(
&self,
info_request: InfoRequest,
) -> Result<T> {
let data =
serde_json::to_string(&info_request).map_err(|e| Error::JsonParse(e.to_string()))?;
let return_data = self.http_client.post("/info", data).await?;
serde_json::from_str(&return_data).map_err(|e| Error::JsonParse(e.to_string()))
}
pub async fn open_orders(&self, address: H160) -> Result<Vec<OpenOrdersResponse>> {
let input = InfoRequest::OpenOrders { user: address };
self.send_info_request(input).await
}
pub async fn user_state(&self, address: H160) -> Result<UserStateResponse> {
let input = InfoRequest::UserState { user: address };
self.send_info_request(input).await
}
pub async fn user_states(&self, addresses: Vec<H160>) -> Result<Vec<UserStateResponse>> {
let input = InfoRequest::UserStates { users: addresses };
self.send_info_request(input).await
}
pub async fn user_token_balances(&self, address: H160) -> Result<UserTokenBalanceResponse> {
let input = InfoRequest::UserTokenBalances { user: address };
self.send_info_request(input).await
}
pub async fn user_fees(&self, address: H160) -> Result<UserFeesResponse> {
let input = InfoRequest::UserFees { user: address };
self.send_info_request(input).await
}
pub async fn meta(&self) -> Result<Meta> {
let input = InfoRequest::Meta;
self.send_info_request(input).await
}
pub async fn meta_and_asset_contexts(&self) -> Result<Vec<MetaAndAssetCtxs>> {
let input = InfoRequest::MetaAndAssetCtxs;
self.send_info_request(input).await
}
pub async fn spot_meta(&self) -> Result<SpotMeta> {
let input = InfoRequest::SpotMeta;
self.send_info_request(input).await
}
pub async fn spot_meta_and_asset_contexts(&self) -> Result<Vec<SpotMetaAndAssetCtxs>> {
let input = InfoRequest::SpotMetaAndAssetCtxs;
self.send_info_request(input).await
}
pub async fn all_mids(&self) -> Result<HashMap<String, String>> {
let input = InfoRequest::AllMids;
self.send_info_request(input).await
}
pub async fn user_fills(&self, address: H160) -> Result<Vec<UserFillsResponse>> {
let input = InfoRequest::UserFills { user: address };
self.send_info_request(input).await
}
pub async fn funding_rates(&self) -> Result<Vec<FundingRate>> {
let mut meta_and_contexts = self.meta_and_asset_contexts().await?;
let predicted_fundings = self.predicted_fundings().await?;
let context = match meta_and_contexts
.pop()
.unwrap_or(MetaAndAssetCtxs::Context(vec![]))
{
MetaAndAssetCtxs::Context(context) => context,
_ => return Err(Error::GenericParse("Context not found".to_string())),
};
let meta = match meta_and_contexts
.pop()
.unwrap_or(MetaAndAssetCtxs::Meta(Meta { universe: vec![] }))
{
MetaAndAssetCtxs::Meta(meta) => meta.universe,
_ => return Err(Error::GenericParse("Meta not found".to_string())),
};
Ok(FundingRate::construct(meta, context, predicted_fundings))
}
pub async fn predicted_fundings(&self) -> Result<HashMap<String, String>> {
let input = InfoRequest::PredictedFundings;
let json_values: Vec<Vec<Value>> = self.send_info_request(input).await?;
let mut predicts = HashMap::new();
for value in json_values {
let mut rates = vec![];
if let Value::Array(values) = &value[1] {
if let Value::Array(item) = &values[1] {
if let Value::Object(obj) = &item[1] {
if let Some(Value::String(rate)) = obj.get("fundingRate") {
rates.push(rate.parse::<f64>().unwrap_or_default());
}
}
}
}
let rate = rates.iter().sum::<f64>() / rates.len() as f64;
predicts.insert(
value[0].as_str().unwrap_or_default().to_string(),
rate.to_string(),
);
}
Ok(predicts)
}
pub async fn funding_history(
&self,
coin: String,
start_time: u64,
end_time: Option<u64>,
) -> Result<Vec<FundingHistoryResponse>> {
let input = InfoRequest::FundingHistory {
coin,
start_time,
end_time,
};
self.send_info_request(input).await
}
pub async fn user_funding_history(
&self,
user: H160,
start_time: u64,
end_time: Option<u64>,
) -> Result<Vec<UserFundingResponse>> {
let input = InfoRequest::UserFunding {
user,
start_time,
end_time,
};
self.send_info_request(input).await
}
pub async fn recent_trades(&self, coin: String) -> Result<Vec<RecentTradesResponse>> {
let input = InfoRequest::RecentTrades { coin };
self.send_info_request(input).await
}
pub async fn l2_snapshot(&self, coin: String) -> Result<L2SnapshotResponse> {
let input = InfoRequest::L2Book { coin };
self.send_info_request(input).await
}
pub async fn candles_snapshot(
&self,
coin: String,
interval: String,
start_time: u64,
end_time: u64,
) -> Result<Vec<CandlesSnapshotResponse>> {
let input = InfoRequest::CandleSnapshot {
req: CandleSnapshotRequest {
coin,
interval,
start_time,
end_time,
},
};
self.send_info_request(input).await
}
pub async fn query_order_by_oid(&self, address: H160, oid: u64) -> Result<OrderStatusResponse> {
let input = InfoRequest::OrderStatus { user: address, oid };
self.send_info_request(input).await
}
pub async fn query_referral_state(&self, address: H160) -> Result<ReferralResponse> {
let input = InfoRequest::Referral { user: address };
self.send_info_request(input).await
}
pub async fn historical_orders(&self, address: H160) -> Result<Vec<OrderInfo>> {
let input = InfoRequest::HistoricalOrders { user: address };
self.send_info_request(input).await
}
}
#[cfg(test)]
mod tests {
use crate::meta::AssetContext;
use super::*;
#[tokio::test]
async fn test_meta_and_asset_contexts() {
let info_client = InfoClient::new(None, Some(BaseUrl::Testnet)).await.unwrap();
let meta_and_asset_contexts = match info_client.meta_and_asset_contexts().await {
Ok(meta_and_asset_contexts) => meta_and_asset_contexts,
Err(e) => panic!("Error: {e}"),
};
assert!(meta_and_asset_contexts.len() == 2);
}
#[tokio::test]
async fn test_meta_and_asset_contexts_raw() {
let client = Client::new();
let resp = client
.post("https://api.hyperliquid.xyz/info")
.header("Content-Type", "application/json")
.body(serde_json::to_string(&InfoRequest::MetaAndAssetCtxs).unwrap())
.send()
.await
.unwrap()
.text()
.await
.unwrap();
let resp: Vec<serde_json::Value> = serde_json::from_str(&resp).unwrap();
let _asset_context: Vec<AssetContext> = serde_json::from_value(resp[1].clone()).unwrap();
}
#[tokio::test]
async fn test_predicted_fundings() {
let info_client = InfoClient::new(None, Some(BaseUrl::Testnet)).await.unwrap();
let predicted_fundings = match info_client.predicted_fundings().await {
Ok(predicted_fundings) => predicted_fundings,
Err(e) => panic!("Error: {e}"),
};
println!("{:?}", predicted_fundings);
}
#[tokio::test]
async fn test_funding_rates() {
let info_client = InfoClient::new(None, Some(BaseUrl::Testnet)).await.unwrap();
let funding_rates = match info_client.funding_rates().await {
Ok(funding_rates) => funding_rates,
Err(e) => panic!("Error: {e}"),
};
println!("{:?}", funding_rates);
}
}