diff --git a/src/info/info_client.rs b/src/info/info_client.rs
index b3d8ba2..920f922 100644
--- a/src/info/info_client.rs
+++ b/src/info/info_client.rs
@@ -36,10 +36,6 @@ pub enum InfoRequest {
UserState {
user: Address,
},
- #[serde(rename = "batchClearinghouseStates")]
- UserStates {
- users: Vec
,
- },
#[serde(rename = "spotClearinghouseState")]
UserTokenBalances {
user: Address,
@@ -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) -> Result> {
- 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 {
@@ -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());
+ }
+ }
+}
diff --git a/tests/batch_clearinghouse_states.rs b/tests/batch_clearinghouse_states.rs
new file mode 100644
index 0000000..fb3b756
--- /dev/null
+++ b/tests/batch_clearinghouse_states.rs
@@ -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 = 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::()
+ .expect("account_value should parse as f64");
+ assert!(account_value >= 0.0, "user {} has valid margin_summary", i);
+ }
+}