1use serde::Deserialize;
5
6#[derive(Debug)]
22pub enum UserDataEvent {
23 OrderUpdate(Box<OrderUpdateEvent>),
25 AccountUpdate(Box<AccountUpdateEvent>),
27 AccountConfigUpdate(AccountConfigUpdateEvent),
29 MarginCall(MarginCallEvent),
31 ListenKeyExpired(ListenKeyExpiredEvent),
33 TradePro(TradeProEvent),
35 Unknown(serde_json::Value),
37}
38
39#[derive(Debug, Deserialize)]
41pub struct OrderUpdateEvent {
42 #[serde(rename = "e")] pub event_type: String,
43 #[serde(rename = "E")] pub event_time: u64,
44 #[serde(rename = "T")] pub transaction_time: u64,
45 #[serde(rename = "o")] pub order: OrderUpdateData,
46}
47
48#[derive(Debug, Deserialize)]
50pub struct OrderUpdateData {
51 #[serde(rename = "s")] pub symbol: String,
52 #[serde(rename = "c")] pub client_order_id: String,
53 #[serde(rename = "S")] pub side: String,
54 #[serde(rename = "o")] pub order_type: String,
55 #[serde(rename = "f")] pub time_in_force: String,
56 #[serde(rename = "q")] pub quantity: String,
57 #[serde(rename = "p")] pub price: String,
58 #[serde(rename = "ap")] pub avg_price: String,
59 #[serde(rename = "sp")] pub stop_price: String,
60 #[serde(rename = "x")] pub execution_type: String,
61 #[serde(rename = "X")] pub order_status: String,
62 #[serde(rename = "i")] pub order_id: i64,
63 #[serde(rename = "l")] pub last_filled_qty: String,
64 #[serde(rename = "z")] pub cumulative_filled_qty: String,
65 #[serde(rename = "L")] pub last_filled_price: String,
66 #[serde(rename = "N", default)] pub commission_asset: Option<String>,
68 #[serde(rename = "n", default)] pub commission: Option<String>,
70 #[serde(rename = "t")] pub trade_id: i64,
71 #[serde(rename = "b", default)] pub bids_notional: String,
72 #[serde(rename = "a", default)] pub ask_notional: String,
73 #[serde(rename = "m", default)] pub is_maker: bool,
74 #[serde(rename = "R", default)] pub is_reduce_only: bool,
75 #[serde(rename = "wt", default)] pub working_type: String,
76 #[serde(rename = "ot", default)] pub orig_order_type: String,
77 #[serde(rename = "ps")] pub position_side: String,
78 #[serde(rename = "cp", default)] pub close_position: bool,
79 #[serde(rename = "AP", default)] pub activation_price: Option<String>,
81 #[serde(rename = "cr", default)] pub callback_rate: Option<String>,
83 #[serde(rename = "rp")] pub realized_profit: String,
84 #[serde(rename = "T")] pub order_trade_time: u64,
85}
86
87#[derive(Debug, Deserialize)]
89pub struct AccountUpdateEvent {
90 #[serde(rename = "e")] pub event_type: String,
91 #[serde(rename = "E")] pub event_time: u64,
92 #[serde(rename = "T")] pub transaction_time: u64,
93 #[serde(rename = "a")] pub account: AccountUpdateData,
94}
95
96#[derive(Debug, Deserialize)]
98pub struct AccountUpdateData {
99 #[serde(rename = "m")] pub event_reason: String,
100 #[serde(rename = "B")] pub balances: Vec<BalanceDelta>,
101 #[serde(rename = "P")] pub positions: Vec<PositionDelta>,
102}
103
104#[derive(Debug, Deserialize)]
106pub struct BalanceDelta {
107 #[serde(rename = "a")] pub asset: String,
108 #[serde(rename = "wb")] pub wallet_balance: String,
109 #[serde(rename = "cw")] pub cross_wallet_balance: String,
110 #[serde(rename = "bc")] pub balance_change: String,
111}
112
113#[derive(Debug, Deserialize)]
115pub struct PositionDelta {
116 #[serde(rename = "s")] pub symbol: String,
117 #[serde(rename = "pa")] pub position_amount: String,
118 #[serde(rename = "ep")] pub entry_price: String,
119 #[serde(rename = "cr")] pub accumulated_realized: String,
120 #[serde(rename = "up")] pub unrealized_pnl: String,
121 #[serde(rename = "mt")] pub margin_type: String,
122 #[serde(rename = "iw")] pub isolated_wallet: String,
123 #[serde(rename = "ps")] pub position_side: String,
124}
125
126#[derive(Debug, Deserialize)]
131pub struct AccountConfigUpdateEvent {
132 #[serde(rename = "e")] pub event_type: String,
133 #[serde(rename = "E")] pub event_time: u64,
134 #[serde(rename = "T")] pub transaction_time: u64,
135 #[serde(rename = "ac")] pub leverage_change: Option<LeverageChangeData>,
136 #[serde(rename = "ai")] pub multi_assets_margin: Option<MultiAssetsModeData>,
137}
138
139#[derive(Debug, Deserialize)]
141pub struct LeverageChangeData {
142 #[serde(rename = "s")] pub symbol: String,
143 #[serde(rename = "l")] pub leverage: u32,
144}
145
146#[derive(Debug, Deserialize)]
148pub struct MultiAssetsModeData {
149 #[serde(rename = "j")] pub is_enabled: bool,
150 #[serde(rename = "f", default)] pub fee_deduction: Option<bool>,
152 #[serde(rename = "d", default)] pub position_mode_dual: Option<bool>,
154}
155
156#[derive(Debug, Deserialize)]
158pub struct MarginCallEvent {
159 #[serde(rename = "e")] pub event_type: String,
160 #[serde(rename = "E")] pub event_time: u64,
161 #[serde(rename = "cw", default)] pub cross_wallet_balance: Option<String>,
163 #[serde(rename = "p")] pub positions: Vec<MarginCallPosition>,
164}
165
166#[derive(Debug, Deserialize)]
168pub struct MarginCallPosition {
169 #[serde(rename = "s")] pub symbol: String,
170 #[serde(rename = "ps")] pub position_side: String,
171 #[serde(rename = "pa")] pub position_amount: String,
172 #[serde(rename = "mt")] pub margin_type: String,
173 #[serde(rename = "iw")] pub isolated_wallet: String,
174 #[serde(rename = "mp")] pub mark_price: String,
175 #[serde(rename = "up")] pub unrealized_pnl: String,
176 #[serde(rename = "mm")] pub maintenance_margin_required: String,
177}
178
179#[derive(Debug, Deserialize)]
181pub struct TradeProEvent {
182 #[serde(rename = "e")] pub event_type: String,
183 #[serde(rename = "E")] pub event_time: u64,
184 #[serde(rename = "T")] pub transaction_time: u64,
185 #[serde(rename = "s")] pub symbol: String,
186 #[serde(rename = "t")] pub trade_id: i64,
187 #[serde(rename = "p")] pub price: String,
188 #[serde(rename = "q")] pub quantity: String,
189 #[serde(rename = "h")] pub tx_hash: String,
191 #[serde(rename = "m")] pub participants: Vec<String>,
193}
194
195#[derive(Debug, Deserialize)]
201pub struct ListenKeyExpiredEvent {
202 #[serde(rename = "e")] pub event_type: String,
203 #[serde(rename = "E")] pub event_time: u64,
204}
205
206impl<'de> serde::Deserialize<'de> for UserDataEvent {
211 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
212 let v = serde_json::Value::deserialize(deserializer)?;
213 let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
214 match event_type {
215 "ORDER_TRADE_UPDATE" => serde_json::from_value::<OrderUpdateEvent>(v)
216 .map(|e| UserDataEvent::OrderUpdate(Box::new(e)))
217 .map_err(serde::de::Error::custom),
218 "ACCOUNT_UPDATE" => serde_json::from_value::<AccountUpdateEvent>(v)
219 .map(|e| UserDataEvent::AccountUpdate(Box::new(e)))
220 .map_err(serde::de::Error::custom),
221 "ACCOUNT_CONFIG_UPDATE" => serde_json::from_value::<AccountConfigUpdateEvent>(v)
222 .map(UserDataEvent::AccountConfigUpdate)
223 .map_err(serde::de::Error::custom),
224 "MARGIN_CALL" => serde_json::from_value::<MarginCallEvent>(v)
225 .map(UserDataEvent::MarginCall)
226 .map_err(serde::de::Error::custom),
227 "listenKeyExpired" => serde_json::from_value::<ListenKeyExpiredEvent>(v)
228 .map(UserDataEvent::ListenKeyExpired)
229 .map_err(serde::de::Error::custom),
230 "tradepro" => serde_json::from_value::<TradeProEvent>(v)
231 .map(UserDataEvent::TradePro)
232 .map_err(serde::de::Error::custom),
233 _ => {
234 tracing::warn!(event_type = event_type, "Unknown user data event type");
235 Ok(UserDataEvent::Unknown(v))
236 }
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn order_update_event_deserializes() {
247 let json = r#"{
248 "e": "ORDER_TRADE_UPDATE",
249 "E": 1625184000000,
250 "T": 1625184000000,
251 "o": {
252 "s": "BTCUSDT",
253 "c": "client-id-1",
254 "S": "BUY",
255 "o": "LIMIT",
256 "f": "GTC",
257 "q": "0.001",
258 "p": "30000",
259 "ap": "0",
260 "sp": "0",
261 "x": "NEW",
262 "X": "NEW",
263 "i": 123456789,
264 "l": "0",
265 "z": "0",
266 "L": "0",
267 "t": -1,
268 "T": 1625184000000,
269 "ps": "BOTH",
270 "rp": "0"
271 }
272 }"#;
273 let event: UserDataEvent = serde_json::from_str(json).unwrap();
274 assert!(matches!(event, UserDataEvent::OrderUpdate(ref e) if e.order.symbol == "BTCUSDT"));
275 }
276
277 #[test]
278 fn account_update_event_deserializes() {
279 let json = r#"{
280 "e": "ACCOUNT_UPDATE",
281 "E": 1625184000000,
282 "T": 1625184000000,
283 "a": {
284 "m": "DEPOSIT",
285 "B": [{"a": "USDT", "wb": "1000.00", "cw": "1000.00", "bc": "100.00"}],
286 "P": []
287 }
288 }"#;
289 let event: UserDataEvent = serde_json::from_str(json).unwrap();
290 assert!(matches!(event, UserDataEvent::AccountUpdate(_)));
291 if let UserDataEvent::AccountUpdate(e) = event {
292 assert_eq!(e.account.balances[0].asset, "USDT");
293 assert_eq!(e.account.balances[0].wallet_balance, "1000.00");
294 assert!(e.account.positions.is_empty());
295 }
296 }
297
298 #[test]
299 fn unknown_event_type_maps_to_unknown() {
300 let json = r#"{"e":"NEW_FUTURE_EVENT","x":"data"}"#;
301 let event: UserDataEvent = serde_json::from_str(json).unwrap();
302 assert!(matches!(event, UserDataEvent::Unknown(_)));
303 }
304
305 #[test]
306 fn listen_key_expired_deserializes() {
307 let json = r#"{"e":"listenKeyExpired","E":1576653824250}"#;
308 let event: UserDataEvent = serde_json::from_str(json).unwrap();
309 assert!(matches!(event, UserDataEvent::ListenKeyExpired(_)));
310 if let UserDataEvent::ListenKeyExpired(e) = event {
311 assert_eq!(e.event_type, "listenKeyExpired");
312 assert_eq!(e.event_time, 1576653824250);
313 }
314 }
315
316 #[test]
317 fn account_config_update_leverage_deserializes() {
318 let json = r#"{
319 "e": "ACCOUNT_CONFIG_UPDATE",
320 "E": 1625184000000,
321 "T": 1625184000000,
322 "ac": { "s": "BTCUSDT", "l": 25 }
323 }"#;
324 let event: UserDataEvent = serde_json::from_str(json).unwrap();
325 assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
326 if let UserDataEvent::AccountConfigUpdate(e) = event {
327 assert!(e.leverage_change.is_some());
328 assert!(e.multi_assets_margin.is_none());
329 }
330 }
331
332 #[test]
333 fn account_config_update_multi_assets_deserializes() {
334 let json = r#"{
335 "e": "ACCOUNT_CONFIG_UPDATE",
336 "E": 1625184000000,
337 "T": 1625184000000,
338 "ai": { "j": true }
339 }"#;
340 let event: UserDataEvent = serde_json::from_str(json).unwrap();
341 assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
342 if let UserDataEvent::AccountConfigUpdate(e) = event {
343 assert!(e.multi_assets_margin.is_some());
344 assert!(e.leverage_change.is_none());
345 }
346 }
347}