Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions src/info/info_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ pub enum InfoRequest {
UserState {
user: Address,
},
#[serde(rename = "batchClearinghouseStates")]
UserStates {
users: Vec<Address>,
},
#[serde(rename = "spotClearinghouseState")]
UserTokenBalances {
user: Address,
Expand Down Expand Up @@ -192,9 +188,17 @@ impl InfoClient {
self.send_info_request(input).await
}

/// Fetches clearinghouse state for multiple users.
///
/// Hyperliquid's public `/info` API does not currently expose a server-side
/// batch clearinghouse endpoint, so this helper calls `clearinghouseState`
/// once per address and returns responses in request order.
pub async fn user_states(&self, addresses: Vec<Address>) -> Result<Vec<UserStateResponse>> {
let input = InfoRequest::UserStates { users: addresses };
self.send_info_request(input).await
let mut states = Vec::with_capacity(addresses.len());
for address in addresses {
states.push(self.user_state(address).await?);
}
Ok(states)
}

pub async fn user_token_balances(&self, address: Address) -> Result<UserTokenBalanceResponse> {
Expand Down Expand Up @@ -321,3 +325,18 @@ impl InfoClient {
self.send_info_request(input).await
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn clearinghouse_state_serializes_single_user_request() {
for user in [Address::ZERO, Address::from([1u8; 20])] {
let json = serde_json::to_value(InfoRequest::UserState { user }).unwrap();
assert_eq!(json["type"], "clearinghouseState");
assert_eq!(json["user"], serde_json::to_value(user).unwrap());
assert!(json.get("users").is_none());
}
}
}
48 changes: 48 additions & 0 deletions tests/batch_clearinghouse_states.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Integration test: user_states() fetches clearinghouseState for multiple users.
//!
//! Hyperliquid's public /info API currently returns null for a
//! batchClearinghouseStates request, so user_states() is intentionally a
//! client-side helper over the supported clearinghouseState request.
//!
//! Run with:
//! HYPERLIQUID_TEST_LIVE=1 HYPERLIQUID_TEST_USERS=0x...,0x... cargo test test_user_states_live --test batch_clearinghouse_states -- --ignored

use alloy::primitives::Address;
use hyperliquid_rust_sdk::{BaseUrl, InfoClient};

#[tokio::test]
#[ignore = "hits live API; run with HYPERLIQUID_TEST_LIVE=1 cargo test --test batch_clearinghouse_states -- --ignored"]
async fn test_user_states_live() {
if std::env::var("HYPERLIQUID_TEST_LIVE").ok().as_deref() != Some("1") {
eprintln!("Skipping live test (set HYPERLIQUID_TEST_LIVE=1 to run)");
return;
}
let users = std::env::var("HYPERLIQUID_TEST_USERS")
.expect("set HYPERLIQUID_TEST_USERS to a comma-separated list of 0x addresses");
let addrs: Vec<Address> = users
.split(',')
.map(|user| user.trim().parse().expect("valid address"))
.collect();
assert!(!addrs.is_empty(), "provide at least one test user");

let client = InfoClient::new(None, Some(BaseUrl::Mainnet))
.await
.expect("InfoClient::new");
let states = client
.user_states(addrs.clone())
.await
.expect("user_states");
assert_eq!(
states.len(),
addrs.len(),
"user_states returns one clearinghouse state per user in request order"
);
for (i, state) in states.iter().enumerate() {
let account_value = state
.margin_summary
.account_value
.parse::<f64>()
.expect("account_value should parse as f64");
assert!(account_value >= 0.0, "user {} has valid margin_summary", i);
}
}