asterdex-sdk 0.1.1

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// Tech Lead mini-task (Wave 8): user data stream model types from 05_detail_design.md §2.3
// Manual Deserialize impl for UserDataEvent is implemented in US-013.

use serde::Deserialize;

/// All user data stream event types received over the WebSocket user data stream.
///
/// Deserializes from the raw JSON received over a `UserDataStream`.
/// Use `UserDataStream::next_event()` to receive typed events.
///
/// # Event Types
/// - `ORDER_TRADE_UPDATE` → `OrderUpdate`
/// - `ACCOUNT_UPDATE`     → `AccountUpdate`
/// - `ACCOUNT_CONFIG_UPDATE` → `AccountConfigUpdate`
/// - Unknown types → `Unknown` (logged as warning, never panics)
///
/// Large variants (`OrderUpdate`, `AccountUpdate`) are boxed to reduce stack size.
#[derive(Debug)]
pub enum UserDataEvent {
    /// Order trade update event (boxed to reduce enum stack size — US-016 clippy fix).
    OrderUpdate(Box<OrderUpdateEvent>),
    /// Account balance/position update event (boxed to reduce enum stack size — US-016 clippy fix).
    AccountUpdate(Box<AccountUpdateEvent>),
    /// Account configuration update event (leverage or multi-assets margin mode change).
    AccountConfigUpdate(AccountConfigUpdateEvent),
    /// Unknown event type — captured as raw JSON for forward compatibility.
    Unknown(serde_json::Value),
}

/// Order trade update event — dispatched when `"e": "ORDER_TRADE_UPDATE"`.
#[derive(Debug, Deserialize)]
pub struct OrderUpdateEvent {
    #[serde(rename = "E")] pub event_time: u64,
    #[serde(rename = "T")] pub transaction_time: u64,
    #[serde(rename = "o")] pub order: OrderUpdateData,
}

/// Order details within an `OrderUpdateEvent`.
#[derive(Debug, Deserialize)]
pub struct OrderUpdateData {
    #[serde(rename = "s")] pub symbol: String,
    #[serde(rename = "c")] pub client_order_id: String,
    #[serde(rename = "S")] pub side: String,
    #[serde(rename = "o")] pub order_type: String,
    #[serde(rename = "f")] pub time_in_force: String,
    #[serde(rename = "q")] pub quantity: String,
    #[serde(rename = "p")] pub price: String,
    #[serde(rename = "ap")] pub avg_price: String,
    #[serde(rename = "sp")] pub stop_price: String,
    #[serde(rename = "x")] pub execution_type: String,
    #[serde(rename = "X")] pub order_status: String,
    #[serde(rename = "i")] pub order_id: i64,
    #[serde(rename = "l")] pub last_filled_qty: String,
    #[serde(rename = "z")] pub cumulative_filled_qty: String,
    #[serde(rename = "L")] pub last_filled_price: String,
    #[serde(rename = "t")] pub trade_id: i64,
    #[serde(rename = "T")] pub order_trade_time: u64,
    #[serde(rename = "ps")] pub position_side: String,
    #[serde(rename = "rp")] pub realized_profit: String,
}

/// Account update event — dispatched when `"e": "ACCOUNT_UPDATE"`.
#[derive(Debug, Deserialize)]
pub struct AccountUpdateEvent {
    #[serde(rename = "E")] pub event_time: u64,
    #[serde(rename = "T")] pub transaction_time: u64,
    #[serde(rename = "a")] pub account: AccountUpdateData,
}

/// Account data within an `AccountUpdateEvent`.
#[derive(Debug, Deserialize)]
pub struct AccountUpdateData {
    #[serde(rename = "m")] pub event_reason: String,
    #[serde(rename = "B")] pub balances: Vec<BalanceDelta>,
    #[serde(rename = "P")] pub positions: Vec<PositionDelta>,
}

/// Balance change within an `AccountUpdateData`.
#[derive(Debug, Deserialize)]
pub struct BalanceDelta {
    #[serde(rename = "a")] pub asset: String,
    #[serde(rename = "wb")] pub wallet_balance: String,
    #[serde(rename = "cw")] pub cross_wallet_balance: String,
    #[serde(rename = "bc")] pub balance_change: String,
}

/// Position change within an `AccountUpdateData`.
#[derive(Debug, Deserialize)]
pub struct PositionDelta {
    #[serde(rename = "s")] pub symbol: String,
    #[serde(rename = "pa")] pub position_amount: String,
    #[serde(rename = "ep")] pub entry_price: String,
    #[serde(rename = "cr")] pub accumulated_realized: String,
    #[serde(rename = "up")] pub unrealized_pnl: String,
    #[serde(rename = "mt")] pub margin_type: String,
    #[serde(rename = "iw")] pub isolated_wallet: String,
    #[serde(rename = "ps")] pub position_side: String,
}

/// Account configuration update event — dispatched when `"e": "ACCOUNT_CONFIG_UPDATE"`.
///
/// Contains either a leverage change (`ac`) or a multi-assets margin mode change (`ai`),
/// but not both at the same time.
#[derive(Debug, Deserialize)]
pub struct AccountConfigUpdateEvent {
    #[serde(rename = "E")] pub event_time: u64,
    #[serde(rename = "T")] pub transaction_time: u64,
    #[serde(rename = "ac")] pub leverage_change: Option<LeverageChangeData>,
    #[serde(rename = "ai")] pub multi_assets_margin: Option<MultiAssetsModeData>,
}

/// Leverage change within an `AccountConfigUpdateEvent`.
#[derive(Debug, Deserialize)]
pub struct LeverageChangeData {
    #[serde(rename = "s")] pub symbol: String,
    #[serde(rename = "l")] pub leverage: u32,
}

/// Multi-assets margin mode change within an `AccountConfigUpdateEvent`.
#[derive(Debug, Deserialize)]
pub struct MultiAssetsModeData {
    #[serde(rename = "j")] pub is_enabled: bool,
}

// US-013: Manual Deserialize impl for UserDataEvent — two-pass approach.
// `#[serde(tag = "e")]` with an `#[serde(other)]` variant that captures a Value is
// not supported by serde. We deserialize to a raw Value first, dispatch on "e", then
// deserialize the full Value into the correct typed struct.
impl<'de> serde::Deserialize<'de> for UserDataEvent {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let v = serde_json::Value::deserialize(deserializer)?;
        let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
        match event_type {
            "ORDER_TRADE_UPDATE" => serde_json::from_value::<OrderUpdateEvent>(v)
                .map(|e| UserDataEvent::OrderUpdate(Box::new(e)))
                .map_err(serde::de::Error::custom),
            "ACCOUNT_UPDATE" => serde_json::from_value::<AccountUpdateEvent>(v)
                .map(|e| UserDataEvent::AccountUpdate(Box::new(e)))
                .map_err(serde::de::Error::custom),
            "ACCOUNT_CONFIG_UPDATE" => serde_json::from_value::<AccountConfigUpdateEvent>(v)
                .map(UserDataEvent::AccountConfigUpdate)
                .map_err(serde::de::Error::custom),
            _ => {
                tracing::warn!(event_type = event_type, "Unknown user data event type");
                Ok(UserDataEvent::Unknown(v))
            }
        }
    }
}

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

    #[test]
    fn order_update_event_deserializes() {
        let json = r#"{
            "e": "ORDER_TRADE_UPDATE",
            "E": 1625184000000,
            "T": 1625184000000,
            "o": {
                "s": "BTCUSDT",
                "c": "client-id-1",
                "S": "BUY",
                "o": "LIMIT",
                "f": "GTC",
                "q": "0.001",
                "p": "30000",
                "ap": "0",
                "sp": "0",
                "x": "NEW",
                "X": "NEW",
                "i": 123456789,
                "l": "0",
                "z": "0",
                "L": "0",
                "t": -1,
                "T": 1625184000000,
                "ps": "BOTH",
                "rp": "0"
            }
        }"#;
        let event: UserDataEvent = serde_json::from_str(json).unwrap();
        assert!(matches!(event, UserDataEvent::OrderUpdate(ref e) if e.order.symbol == "BTCUSDT"));
    }

    #[test]
    fn account_update_event_deserializes() {
        let json = r#"{
            "e": "ACCOUNT_UPDATE",
            "E": 1625184000000,
            "T": 1625184000000,
            "a": {
                "m": "DEPOSIT",
                "B": [{"a": "USDT", "wb": "1000.00", "cw": "1000.00", "bc": "100.00"}],
                "P": []
            }
        }"#;
        let event: UserDataEvent = serde_json::from_str(json).unwrap();
        assert!(matches!(event, UserDataEvent::AccountUpdate(_)));
        if let UserDataEvent::AccountUpdate(e) = event {
            assert_eq!(e.account.balances[0].asset, "USDT");
            assert_eq!(e.account.balances[0].wallet_balance, "1000.00");
            assert!(e.account.positions.is_empty());
        }
    }

    #[test]
    fn unknown_event_type_maps_to_unknown() {
        let json = r#"{"e":"NEW_FUTURE_EVENT","x":"data"}"#;
        let event: UserDataEvent = serde_json::from_str(json).unwrap();
        assert!(matches!(event, UserDataEvent::Unknown(_)));
    }

    #[test]
    fn account_config_update_leverage_deserializes() {
        let json = r#"{
            "e": "ACCOUNT_CONFIG_UPDATE",
            "E": 1625184000000,
            "T": 1625184000000,
            "ac": { "s": "BTCUSDT", "l": 25 }
        }"#;
        let event: UserDataEvent = serde_json::from_str(json).unwrap();
        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
        if let UserDataEvent::AccountConfigUpdate(e) = event {
            assert!(e.leverage_change.is_some());
            assert!(e.multi_assets_margin.is_none());
        }
    }

    #[test]
    fn account_config_update_multi_assets_deserializes() {
        let json = r#"{
            "e": "ACCOUNT_CONFIG_UPDATE",
            "E": 1625184000000,
            "T": 1625184000000,
            "ai": { "j": true }
        }"#;
        let event: UserDataEvent = serde_json::from_str(json).unwrap();
        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
        if let UserDataEvent::AccountConfigUpdate(e) = event {
            assert!(e.multi_assets_margin.is_some());
            assert!(e.leverage_change.is_none());
        }
    }
}