use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::error::ClientError;
use crate::rest::RestClient;
use crate::types::{
MarketId, VaultId,
pm::PmState,
position::UserState,
rfq::{RfqId, RfqState},
vault::VaultState,
};
use crate::wallet::Address;
#[derive(Debug)]
pub struct Info<'a> {
pub(crate) client: &'a RestClient,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct L2Level {
pub px: String,
pub size: String,
pub n_orders: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct L2Book {
pub bids: Vec<L2Level>,
pub asks: Vec<L2Level>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderSide {
Bid,
Ask,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct OpenOrder {
pub oid: u64,
pub market_id: u32,
pub side: OrderSide,
pub px: String,
pub size: String,
pub inserted_at_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct OpenOrders {
pub address: Address,
#[serde(default)]
pub account_id: Option<u64>,
#[serde(default)]
pub orders: Vec<OpenOrder>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Candle {
pub coin: String,
pub interval: String,
pub open_time: u64,
pub close_time: u64,
pub open: String,
pub close: String,
pub high: String,
pub low: String,
pub volume: String,
pub num_trades: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FeeTier {
pub maker_bps: String,
pub taker_bps: String,
pub volume_30d: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FeeSchedule {
#[serde(default)]
pub maker_bps: Option<String>,
#[serde(default)]
pub taker_bps: Option<String>,
pub referrer_share_bps: String,
pub builder_rebate_bps: String,
pub burn_ratio: String,
pub tiers: Vec<FeeTier>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StakingState {
pub address: Address,
pub total_staked: u128,
pub pending_rewards: u128,
pub delegations: Vec<Delegation>,
pub unbonding: Vec<UnbondingEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Delegation {
pub validator: Address,
pub amount: u128,
pub since_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct UnbondingEntry {
pub validator: Address,
pub amount: u128,
pub claim_at_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct NodeInfo {
pub network: String,
pub chain_id: u64,
pub protocol_version: String,
pub validator_index: u32,
pub build_commit: String,
pub uptime_seconds: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Tier {
Safe,
T0,
T1,
T2,
T3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum MarginMode {
Cross,
Isolated,
StrictIso,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AccountPosition {
pub asset: u32,
pub size: String,
#[serde(rename = "entry")]
pub entry_px: String,
#[serde(rename = "upnl")]
pub unrealised_pnl: String,
pub isolated: bool,
#[serde(rename = "lev")]
pub leverage: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Balances {
pub usdc: String,
#[serde(default)]
pub spot: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AccountState {
pub address: Address,
pub account_value: String,
pub free_collateral: String,
pub maint_margin: String,
pub init_margin: String,
pub health: String,
pub tier: Tier,
#[serde(rename = "mode")]
pub margin_mode: MarginMode,
pub pm_enabled: bool,
#[serde(default)]
pub positions: Vec<AccountPosition>,
pub balances: Balances,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketKind {
Perp,
Spot,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Funding {
pub rate_per_hr: String,
pub cap_per_hr: String,
pub interval_ms: u64,
pub next_payment_ts: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct MarketInfo {
pub asset_id: u32,
pub name: String,
pub kind: MarketKind,
pub sz_decimals: u8,
pub mark_px: String,
pub oracle_px: String,
pub tick_size: String,
pub step_size: String,
pub min_order: String,
pub max_leverage: u32,
pub maint_margin_ratio: String,
pub init_margin_ratio: String,
pub funding: Funding,
pub mark_source: String,
pub fba_enabled: bool,
pub open_interest: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotPair {
pub id: u32,
pub name: String,
pub base: u32,
pub quote: u32,
pub taker_fee_bps: u16,
pub min_notional: String,
pub active: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotToken {
pub id: u32,
pub name: String,
pub sz_decimals: u8,
pub wei_decimals: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotMeta {
pub pairs: Vec<SpotPair>,
pub tokens: Vec<SpotToken>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotBalance {
pub asset: u32,
pub name: String,
pub balance: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotClearinghouseState {
pub address: Address,
pub balances: Vec<SpotBalance>,
}
impl<'a> Info<'a> {
pub async fn markets(&self) -> Result<Vec<MarketInfo>, ClientError> {
self.client
.post_json("/info", &json!({ "type": "markets" }))
.await
}
pub async fn l2_book(&self, market: MarketId, depth: u32) -> Result<L2Book, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "l2_book", "market_id": market.0, "depth": depth }),
)
.await
}
pub async fn user_state(&self, addr: Address) -> Result<UserState, ClientError> {
self.client
.post_json("/info", &json!({ "type": "user_state", "address": addr }))
.await
}
pub async fn vault_state(&self, vault_id: VaultId) -> Result<VaultState, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "vault_state", "vault_id": vault_id.0 }),
)
.await
}
pub async fn staking_state(&self, account_id: u64) -> Result<StakingState, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "staking_state", "account_id": account_id }),
)
.await
}
pub async fn node_info(&self) -> Result<NodeInfo, ClientError> {
self.client
.post_json("/info", &json!({ "type": "node_info" }))
.await
}
pub async fn account_state(&self, addr: Address) -> Result<AccountState, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "account_state", "address": addr }),
)
.await
}
pub async fn open_orders(&self, addr: Address) -> Result<OpenOrders, ClientError> {
self.client
.post_json("/info", &json!({ "type": "open_orders", "address": addr }))
.await
}
pub async fn candle(
&self,
coin: &str,
interval: &str,
start_time: Option<u64>,
end_time: Option<u64>,
) -> Result<Vec<Candle>, ClientError> {
let mut req = json!({ "type": "candle", "coin": coin, "interval": interval });
if let Some(s) = start_time {
req["start_time"] = json!(s);
}
if let Some(e) = end_time {
req["end_time"] = json!(e);
}
self.client.post_json("/info", &req).await
}
pub async fn market_info(&self, market: MarketId) -> Result<MarketInfo, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "market_info", "asset_id": market.0 }),
)
.await
}
pub async fn market_info_by_coin(&self, coin: &str) -> Result<MarketInfo, ClientError> {
self.client
.post_json("/info", &json!({ "type": "market_info", "coin": coin }))
.await
}
pub async fn fee_schedule(&self) -> Result<FeeSchedule, ClientError> {
self.client
.post_json("/info", &json!({ "type": "fee_schedule" }))
.await
}
pub async fn spot_meta(&self) -> Result<SpotMeta, ClientError> {
self.client
.post_json("/info", &json!({ "type": "spot_meta" }))
.await
}
pub async fn spot_clearinghouse_state(
&self,
addr: Address,
) -> Result<SpotClearinghouseState, ClientError> {
self.client
.post_json(
"/info",
&json!({ "type": "spot_clearinghouse_state", "address": addr }),
)
.await
}
pub async fn delegations(&self, account_id: u64) -> Result<Vec<Delegation>, ClientError> {
Ok(self.staking_state(account_id).await?.delegations)
}
pub async fn pm_state(&self, addr: Address) -> Result<PmState, ClientError> {
self.client
.post_json("/info", &json!({ "type": "pm_state", "user": addr }))
.await
}
pub async fn rfq_state(&self, rfq_id: RfqId) -> Result<RfqState, ClientError> {
self.client
.post_json("/info", &json!({ "type": "rfq_state", "rfq_id": rfq_id.0 }))
.await
}
pub async fn raw(&self, body: Value) -> Result<Value, ClientError> {
self.client.post_json("/info", &body).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_info_decodes_doc_fixture() {
let data = serde_json::json!({
"network": "devnet",
"chain_id": 31337,
"protocol_version": "1.0.0",
"validator_index": 3,
"build_commit": "deadbeef",
"uptime_seconds": 123456u64
});
let n: NodeInfo = serde_json::from_value(data).unwrap();
assert_eq!(n.network, "devnet");
assert_eq!(n.chain_id, 31337);
assert_eq!(n.protocol_version, "1.0.0");
assert_eq!(n.validator_index, 3);
assert_eq!(n.uptime_seconds, 123456);
let dec: NodeInfo = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap();
assert_eq!(n, dec);
}
#[test]
fn market_info_decodes_doc_fixture() {
let data = serde_json::json!({
"asset_id": 0,
"name": "BTC",
"kind": "perp",
"sz_decimals": 5,
"mark_px": "50000",
"oracle_px": "50000",
"tick_size": "100",
"step_size": "10000",
"min_order": "10000",
"max_leverage": 50,
"maint_margin_ratio": "5000",
"init_margin_ratio": "10000",
"funding": {
"rate_per_hr": "1000",
"cap_per_hr": "50000",
"interval_ms": 3600000u64,
"next_payment_ts": 1735693200000u64
},
"mark_source": "MedianOfOraclesAndMid",
"fba_enabled": false,
"open_interest": "5000000000"
});
let m: MarketInfo = serde_json::from_value(data).unwrap();
assert_eq!(m.asset_id, 0);
assert_eq!(m.name, "BTC");
assert_eq!(m.kind, MarketKind::Perp);
assert_eq!(m.sz_decimals, 5);
assert_eq!(m.mark_px, "50000");
assert_eq!(m.oracle_px, "50000");
assert_eq!(m.tick_size, "100");
assert_eq!(m.max_leverage, 50);
assert_eq!(m.funding.interval_ms, 3_600_000);
assert_eq!(m.mark_source, "MedianOfOraclesAndMid");
assert_eq!(m.open_interest, "5000000000");
let j = serde_json::to_value(&m).unwrap();
assert_eq!(j["kind"], "perp");
assert!(j["sz_decimals"].is_number());
assert!(j["tick_size"].is_string());
assert!(j["open_interest"].is_string());
assert!(j["asset_id"].is_number());
}
#[test]
fn account_state_decodes_doc_fixture() {
let data = serde_json::json!({
"address": "0x000000000000000000000000000000000000beef",
"account_value": "100000000",
"free_collateral": "80000000",
"maint_margin": "10000000",
"init_margin": "20000000",
"health": "10000000",
"tier": "Safe",
"mode": "Cross",
"pm_enabled": false,
"positions": [{
"asset": 0,
"size": "100000000",
"entry": "10000000000",
"upnl": "500000",
"isolated": false,
"lev": 10
}],
"balances": {
"usdc": "100000000",
"spot": { "ETH": "5000000000" }
}
});
let a: AccountState = serde_json::from_value(data).unwrap();
assert_eq!(a.account_value, "100000000");
assert_eq!(a.free_collateral, "80000000");
assert_eq!(a.health, "10000000");
assert_eq!(a.tier, Tier::Safe);
assert_eq!(a.margin_mode, MarginMode::Cross);
assert!(!a.pm_enabled);
assert_eq!(a.positions.len(), 1);
assert_eq!(a.positions[0].asset, 0);
assert_eq!(a.positions[0].leverage, 10);
assert_eq!(a.balances.usdc, "100000000");
assert_eq!(
a.balances.spot.get("ETH").map(String::as_str),
Some("5000000000")
);
let dec: AccountState = serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap();
assert_eq!(a, dec);
}
#[test]
fn l2_book_decodes_doc_fixture() {
let data = serde_json::json!({
"bids": [{ "px": "10049000000", "size": "100000000", "n_orders": 5 }],
"asks": [{ "px": "10051000000", "size": "200000000", "n_orders": 3 }]
});
let b: L2Book = serde_json::from_value(data).unwrap();
assert_eq!(b.bids.len(), 1);
assert_eq!(b.bids[0].px, "10049000000");
assert_eq!(b.bids[0].size, "100000000");
assert_eq!(b.bids[0].n_orders, 5);
assert_eq!(b.asks[0].n_orders, 3);
let j = serde_json::to_value(&b).unwrap();
assert!(j["bids"][0]["px"].is_string());
assert!(j["bids"][0]["size"].is_string());
assert!(j["bids"][0]["n_orders"].is_number());
}
#[test]
fn spot_meta_decodes_node_fixture() {
let data = serde_json::json!({
"pairs": [{
"id": 101,
"name": "BTC/USDC",
"base": 0,
"quote": 100,
"taker_fee_bps": 5,
"min_notional": "1000",
"active": true
}],
"tokens": [
{ "id": 0, "name": "BTC", "sz_decimals": 5, "wei_decimals": 8 },
{ "id": 100, "name": "USDC", "sz_decimals": 2, "wei_decimals": 6 }
]
});
let m: SpotMeta = serde_json::from_value(data).unwrap();
assert_eq!(m.pairs.len(), 1);
assert_eq!(m.pairs[0].id, 101);
assert_eq!(m.pairs[0].name, "BTC/USDC");
assert_eq!(m.pairs[0].base, 0);
assert_eq!(m.pairs[0].quote, 100);
assert_eq!(m.pairs[0].taker_fee_bps, 5);
assert_eq!(m.pairs[0].min_notional, "1000");
assert!(m.pairs[0].active);
assert_eq!(m.tokens.len(), 2);
assert_eq!(m.tokens[0].name, "BTC");
assert_eq!(m.tokens[0].wei_decimals, 8);
assert_eq!(m.tokens[1].id, 100);
assert_eq!(m.tokens[1].sz_decimals, 2);
let j = serde_json::to_value(&m).unwrap();
assert!(j["pairs"][0]["min_notional"].is_string());
assert!(j["pairs"][0]["id"].is_number());
let dec: SpotMeta = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
assert_eq!(m, dec);
}
#[test]
fn spot_clearinghouse_state_decodes_node_fixture() {
let data = serde_json::json!({
"address": "0x4242424242424242424242424242424242424242",
"balances": [
{ "asset": 101, "name": "BTC/USDC", "balance": "500" }
]
});
let s: SpotClearinghouseState = serde_json::from_value(data).unwrap();
assert_eq!(s.balances.len(), 1);
assert_eq!(s.balances[0].asset, 101);
assert_eq!(s.balances[0].name, "BTC/USDC");
assert_eq!(s.balances[0].balance, "500");
let j = serde_json::to_value(&s).unwrap();
assert!(j["balances"][0]["balance"].is_string());
assert!(j["balances"][0]["asset"].is_number());
let dec: SpotClearinghouseState =
serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
assert_eq!(s, dec);
}
#[test]
fn fee_schedule_decodes_gateway_fixture() {
let data = serde_json::json!({
"maker_bps": "1.0",
"taker_bps": "5.0",
"referrer_share_bps": "5.0",
"builder_rebate_bps": "0",
"burn_ratio": "0.8",
"tiers": [{ "maker_bps": "1.0", "taker_bps": "5.0", "volume_30d": "0" }]
});
let f: FeeSchedule = serde_json::from_value(data).unwrap();
assert_eq!(f.maker_bps.as_deref(), Some("1.0"));
assert_eq!(f.referrer_share_bps, "5.0");
assert_eq!(f.builder_rebate_bps, "0");
assert_eq!(f.burn_ratio, "0.8");
assert_eq!(f.tiers.len(), 1);
assert_eq!(f.tiers[0].taker_bps, "5.0");
assert_eq!(f.tiers[0].volume_30d, "0");
let dec: FeeSchedule = serde_json::from_str(&serde_json::to_string(&f).unwrap()).unwrap();
assert_eq!(f, dec);
let data2 = serde_json::json!({
"referrer_share_bps": "5.0",
"builder_rebate_bps": "0",
"burn_ratio": "0.8",
"tiers": [{ "maker_bps": "1.0", "taker_bps": "5.0", "volume_30d": "0" }]
});
let f2: FeeSchedule = serde_json::from_value(data2).unwrap();
assert!(f2.maker_bps.is_none() && f2.taker_bps.is_none());
}
#[test]
fn open_orders_decodes_gateway_fixture() {
let data = serde_json::json!({
"address": "0x000000000000000000000000000000000000beef",
"orders": [
{ "oid": 0, "market_id": 0, "side": "bid", "px": "2500000000000", "size": "60", "inserted_at_ms": 0 }
]
});
let o: OpenOrders = serde_json::from_value(data).unwrap();
assert!(o.account_id.is_none());
assert_eq!(o.orders.len(), 1);
assert_eq!(o.orders[0].side, OrderSide::Bid);
assert_eq!(o.orders[0].px, "2500000000000");
assert_eq!(o.orders[0].size, "60");
let j = serde_json::to_value(&o).unwrap();
assert_eq!(j["orders"][0]["side"], "bid");
assert!(j["orders"][0]["oid"].is_number());
let dec: OpenOrders = serde_json::from_str(&serde_json::to_string(&o).unwrap()).unwrap();
assert_eq!(o, dec);
}
#[test]
fn candle_decodes_gateway_fixture() {
let data = serde_json::json!([
{
"coin": "BTC",
"interval": "1m",
"open_time": 1_700_000_040_000u64,
"close_time": 1_700_000_099_999u64,
"open": "67000.00",
"close": "67042.50",
"high": "67080.00",
"low": "66990.00",
"volume": "12.5",
"num_trades": 37
}
]);
let bars: Vec<Candle> = serde_json::from_value(data).unwrap();
assert_eq!(bars.len(), 1);
assert_eq!(bars[0].coin, "BTC");
assert_eq!(bars[0].interval, "1m");
assert_eq!(bars[0].open_time, 1_700_000_040_000);
assert_eq!(bars[0].close_time, 1_700_000_099_999);
assert_eq!(bars[0].close, "67042.50");
assert_eq!(bars[0].num_trades, 37);
let j = serde_json::to_value(&bars[0]).unwrap();
assert!(j["open"].is_string());
assert!(j["volume"].is_string());
assert!(j["open_time"].is_number());
assert!(j["num_trades"].is_number());
}
}