1use std::{
30 fmt::Debug,
31 sync::{
32 Arc,
33 atomic::{AtomicBool, AtomicU64, Ordering},
34 },
35};
36
37use ahash::AHashMap;
38use nautilus_network::{RECONNECTED, websocket::WebSocketClient};
39use tokio_tungstenite::tungstenite::Message;
40
41use super::{
42 client::BINANCE_WS_RATE_LIMIT_KEY_ORDER,
43 error::{BinanceWsApiError, BinanceWsApiResult},
44 messages::{
45 BinanceSpotWsTradingCommand, BinanceSpotWsTradingMessage, BinanceSpotWsTradingRequest,
46 BinanceSpotWsTradingRequestMeta, method,
47 },
48};
49use crate::{
50 common::credential::{SigningCredential, canonical_ws_query_string},
51 spot::{
52 enums::BinanceSpotUserDataEventType,
53 http::parse,
54 sbe::spot::{
55 ReadBuf,
56 error_response_codec::ErrorResponseDecoder,
57 message_header_codec,
58 web_socket_response_codec::{SBE_TEMPLATE_ID, WebSocketResponseDecoder},
59 },
60 },
61};
62
63pub struct BinanceSpotWsTradingHandler {
69 signal: Arc<AtomicBool>,
70 inner: Option<WebSocketClient>,
71 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<BinanceSpotWsTradingCommand>,
72 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
73 out_tx: tokio::sync::mpsc::UnboundedSender<BinanceSpotWsTradingMessage>,
74 credential: Arc<SigningCredential>,
75 pending_requests: AHashMap<String, BinanceSpotWsTradingRequestMeta>,
76 request_id_counter: AtomicU64,
77 recv_window_ms: Option<u64>,
78}
79
80impl Debug for BinanceSpotWsTradingHandler {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct(stringify!(BinanceSpotWsTradingHandler))
83 .field("inner", &self.inner.as_ref().map(|_| "<client>"))
84 .field(
85 "pending_requests",
86 &format!("{} pending", self.pending_requests.len()),
87 )
88 .finish_non_exhaustive()
89 }
90}
91
92impl BinanceSpotWsTradingHandler {
93 #[must_use]
95 pub fn new(
96 signal: Arc<AtomicBool>,
97 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<BinanceSpotWsTradingCommand>,
98 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
99 out_tx: tokio::sync::mpsc::UnboundedSender<BinanceSpotWsTradingMessage>,
100 credential: Arc<SigningCredential>,
101 ) -> Self {
102 Self {
103 signal,
104 inner: None,
105 cmd_rx,
106 raw_rx,
107 out_tx,
108 credential,
109 pending_requests: AHashMap::new(),
110 request_id_counter: AtomicU64::new(1000),
111 recv_window_ms: None,
112 }
113 }
114
115 #[must_use]
117 pub const fn with_recv_window(mut self, recv_window_ms: Option<u64>) -> Self {
118 self.recv_window_ms = recv_window_ms;
119 self
120 }
121
122 pub async fn run(&mut self) -> bool {
127 loop {
128 if self.signal.load(Ordering::Relaxed) {
129 return false;
130 }
131
132 tokio::select! {
133 Some(cmd) = self.cmd_rx.recv() => {
134 match cmd {
135 BinanceSpotWsTradingCommand::SetClient(client) => {
136 log::debug!("Handler received WebSocket client");
137 self.inner = Some(client);
138 self.emit(BinanceSpotWsTradingMessage::Connected);
139 }
140 BinanceSpotWsTradingCommand::Disconnect => {
141 log::debug!("Handler disconnecting WebSocket client");
142 self.inner = None;
143 return false;
144 }
145 BinanceSpotWsTradingCommand::PlaceOrder { id, params } => {
146 if let Err(e) = self.handle_place_order(id.clone(), params).await {
147 log::debug!("Failed to handle place order command: {e}");
148 self.pending_requests.remove(&id);
149 self.emit(BinanceSpotWsTradingMessage::RequestFailed {
150 request_id: id,
151 msg: e.to_string(),
152 });
153 }
154 }
155 BinanceSpotWsTradingCommand::CancelOrder { id, params } => {
156 if let Err(e) = self.handle_cancel_order(id.clone(), params).await {
157 log::debug!("Failed to handle cancel order command: {e}");
158 self.pending_requests.remove(&id);
159 self.emit(BinanceSpotWsTradingMessage::RequestFailed {
160 request_id: id,
161 msg: e.to_string(),
162 });
163 }
164 }
165 BinanceSpotWsTradingCommand::CancelReplaceOrder { id, params } => {
166 if let Err(e) = self.handle_cancel_replace_order(id.clone(), params).await {
167 log::debug!("Failed to handle cancel replace command: {e}");
168 self.pending_requests.remove(&id);
169 self.emit(BinanceSpotWsTradingMessage::RequestFailed {
170 request_id: id,
171 msg: e.to_string(),
172 });
173 }
174 }
175 BinanceSpotWsTradingCommand::CancelAllOrders { id, symbol } => {
176 if let Err(e) = self.handle_cancel_all_orders(id.clone(), symbol).await {
177 log::debug!("Failed to handle cancel all command: {e}");
178 self.pending_requests.remove(&id);
179 self.emit(BinanceSpotWsTradingMessage::RequestFailed {
180 request_id: id,
181 msg: e.to_string(),
182 });
183 }
184 }
185 BinanceSpotWsTradingCommand::SessionLogon => {
186 if let Err(e) = self.handle_session_logon().await {
187 log::error!("Session logon failed: {e}");
188 self.emit(BinanceSpotWsTradingMessage::AuthenticationRejected(
189 format!("Session logon failed: {e}"),
190 ));
191 }
192 }
193 BinanceSpotWsTradingCommand::SubscribeUserData => {
194 if let Err(e) = self.handle_subscribe_user_data().await {
195 log::error!("User data subscribe failed: {e}");
196 self.emit(
197 BinanceSpotWsTradingMessage::UserDataSubscriptionRejected(
198 format!("User data subscribe failed: {e}"),
199 ),
200 );
201 }
202 }
203 }
204 }
205 Some(msg) = self.raw_rx.recv() => {
206 if let Message::Text(ref text) = msg
207 && text.as_str() == RECONNECTED
208 {
209 log::debug!("Handler received reconnection signal");
210
211 self.fail_pending_requests();
213
214 self.emit(BinanceSpotWsTradingMessage::Reconnected);
215 continue;
216 }
217
218 self.handle_message(msg);
219 }
220 else => {
221 return false;
223 }
224 }
225 }
226 }
227
228 fn emit(&self, msg: BinanceSpotWsTradingMessage) {
230 if let Err(e) = self.out_tx.send(msg) {
231 log::error!("Failed to send message to output channel: {e}");
232 }
233 }
234
235 fn fail_pending_requests(&mut self) {
237 if self.pending_requests.is_empty() {
238 return;
239 }
240
241 let count = self.pending_requests.len();
242 log::warn!("Failing {count} pending requests after reconnection");
243
244 let pending = std::mem::take(&mut self.pending_requests);
245 for (request_id, _meta) in pending {
246 self.emit(BinanceSpotWsTradingMessage::RequestFailed {
247 request_id,
248 msg: "Connection lost before response received".to_string(),
249 });
250 }
251 }
252
253 async fn handle_place_order(
254 &mut self,
255 id: String,
256 params: crate::spot::http::query::NewOrderParams,
257 ) -> BinanceWsApiResult<()> {
258 let params_json = serde_json::to_value(¶ms)
259 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?;
260 let signed_params = self.sign_params(params_json)?;
261
262 let request = BinanceSpotWsTradingRequest::new(&id, method::ORDER_PLACE, signed_params);
263 self.pending_requests
264 .insert(id.clone(), BinanceSpotWsTradingRequestMeta::PlaceOrder);
265 self.send_request(request).await
266 }
267
268 async fn handle_cancel_order(
269 &mut self,
270 id: String,
271 params: crate::spot::http::query::CancelOrderParams,
272 ) -> BinanceWsApiResult<()> {
273 let params_json = serde_json::to_value(¶ms)
274 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?;
275 let signed_params = self.sign_params(params_json)?;
276
277 let request = BinanceSpotWsTradingRequest::new(&id, method::ORDER_CANCEL, signed_params);
278 self.pending_requests
279 .insert(id.clone(), BinanceSpotWsTradingRequestMeta::CancelOrder);
280 self.send_request(request).await
281 }
282
283 async fn handle_cancel_replace_order(
284 &mut self,
285 id: String,
286 params: crate::spot::http::query::CancelReplaceOrderParams,
287 ) -> BinanceWsApiResult<()> {
288 let params_json = serde_json::to_value(¶ms)
289 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?;
290 let signed_params = self.sign_params(params_json)?;
291
292 let request =
293 BinanceSpotWsTradingRequest::new(&id, method::ORDER_CANCEL_REPLACE, signed_params);
294 self.pending_requests.insert(
295 id.clone(),
296 BinanceSpotWsTradingRequestMeta::CancelReplaceOrder,
297 );
298 self.send_request(request).await
299 }
300
301 async fn handle_cancel_all_orders(
302 &mut self,
303 id: String,
304 symbol: String,
305 ) -> BinanceWsApiResult<()> {
306 let params_json = serde_json::json!({ "symbol": symbol });
307 let signed_params = self.sign_params(params_json)?;
308
309 let request =
310 BinanceSpotWsTradingRequest::new(&id, method::OPEN_ORDERS_CANCEL_ALL, signed_params);
311 self.pending_requests
312 .insert(id.clone(), BinanceSpotWsTradingRequestMeta::CancelAllOrders);
313 self.send_request(request).await
314 }
315
316 async fn handle_session_logon(&mut self) -> BinanceWsApiResult<()> {
317 let id = self.next_request_id();
318 let params_json = serde_json::json!({});
319 let signed_params = self.sign_params(params_json)?;
320
321 let request = BinanceSpotWsTradingRequest::new(&id, method::SESSION_LOGON, signed_params);
322 self.pending_requests
323 .insert(id, BinanceSpotWsTradingRequestMeta::SessionLogon);
324 self.send_request(request).await
325 }
326
327 async fn handle_subscribe_user_data(&mut self) -> BinanceWsApiResult<()> {
328 let id = self.next_request_id();
329 let request = BinanceSpotWsTradingRequest::new(
330 &id,
331 "userDataStream.subscribe",
332 serde_json::json!({}),
333 );
334 self.pending_requests
335 .insert(id, BinanceSpotWsTradingRequestMeta::SubscribeUserData);
336 self.send_request(request).await
337 }
338
339 fn next_request_id(&self) -> String {
340 let id = self.request_id_counter.fetch_add(1, Ordering::Relaxed);
341 format!("ws-{id}")
342 }
343
344 fn sign_params(&self, mut params: serde_json::Value) -> BinanceWsApiResult<serde_json::Value> {
345 let timestamp = std::time::SystemTime::now()
346 .duration_since(std::time::UNIX_EPOCH)
347 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?
348 .as_millis() as i64;
349
350 if let Some(obj) = params.as_object_mut() {
351 obj.insert("timestamp".to_string(), serde_json::json!(timestamp));
352 obj.insert(
353 "apiKey".to_string(),
354 serde_json::json!(self.credential.api_key()),
355 );
356
357 if let Some(recv_window_ms) = self.recv_window_ms {
358 obj.insert("recvWindow".to_string(), serde_json::json!(recv_window_ms));
359 }
360 }
361
362 let query_string = canonical_ws_query_string(
366 params
367 .as_object()
368 .into_iter()
369 .flatten()
370 .map(|(k, v)| (k.as_str(), v)),
371 )
372 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?;
373 let signature = self.credential.sign(&query_string);
374
375 if let Some(obj) = params.as_object_mut() {
376 obj.insert("signature".to_string(), serde_json::json!(signature));
377 }
378
379 Ok(params)
380 }
381
382 async fn send_request(
383 &mut self,
384 request: BinanceSpotWsTradingRequest,
385 ) -> BinanceWsApiResult<()> {
386 let client = self.inner.as_mut().ok_or_else(|| {
387 BinanceWsApiError::ConnectionError("WebSocket not connected".to_string())
388 })?;
389
390 let json = serde_json::to_string(&request)
391 .map_err(|e| BinanceWsApiError::ClientError(e.to_string()))?;
392
393 log::debug!(
394 "Sending WebSocket API request id={} method={}",
395 request.id,
396 request.method
397 );
398
399 let keys = Some(BINANCE_WS_RATE_LIMIT_KEY_ORDER.as_slice());
400
401 let is_session_setup = matches!(
402 self.pending_requests.get(&request.id),
403 Some(
404 BinanceSpotWsTradingRequestMeta::SessionLogon
405 | BinanceSpotWsTradingRequestMeta::SubscribeUserData
406 )
407 );
408
409 let result = if is_session_setup {
410 client
411 .send_text_on_connection(json, keys, client.connection_epoch())
412 .await
413 } else {
414 client.send_text(json, keys).await
415 };
416
417 result.map_err(|e| {
418 BinanceWsApiError::ConnectionError(format!("Failed to send request: {e}"))
419 })?;
420
421 Ok(())
422 }
423
424 fn handle_message(&mut self, msg: Message) {
425 match msg {
426 Message::Binary(data) => self.handle_binary_response(&data),
427 Message::Text(text) => self.handle_text_response(&text),
428 Message::Ping(_) | Message::Pong(_) => {}
429 Message::Close(frame) => {
430 log::debug!("WebSocket closed: {frame:?}");
431 }
432 Message::Frame(_) => {}
433 }
434 }
435
436 fn handle_binary_response(&mut self, data: &[u8]) {
437 match self.decode_ws_api_response(data) {
438 Ok(response) => self.emit(response),
439 Err(e) => {
440 log::error!("Failed to decode WebSocket API response: {e}");
441 self.emit(BinanceSpotWsTradingMessage::Error(e.to_string()));
442 }
443 }
444 }
445
446 fn handle_text_response(&mut self, text: &str) {
447 let json: serde_json::Value = match serde_json::from_str(text) {
448 Ok(j) => j,
449 Err(e) => {
450 log::warn!("Failed to parse text response as JSON: {e}");
451 return;
452 }
453 };
454
455 if let Some(event) = json.get("event") {
457 self.handle_user_data_event(event);
458 return;
459 }
460
461 if json.get("e").is_some() {
463 self.handle_user_data_event(&json);
464 return;
465 }
466
467 if let Some(id) = json.get("id") {
469 let id_str = match id {
470 serde_json::Value::String(s) => s.clone(),
471 serde_json::Value::Number(n) => n.to_string(),
472 _ => return,
473 };
474
475 if let Some(meta) = self.pending_requests.remove(&id_str) {
476 let error_info = json
479 .get("error")
480 .map(|e| {
481 (
482 e.get("code")
483 .and_then(|v| v.as_i64())
484 .and_then(|code| i32::try_from(code).ok()),
485 e.get("msg")
486 .and_then(|v| v.as_str())
487 .unwrap_or("Unknown error")
488 .to_string(),
489 )
490 })
491 .or_else(|| {
492 json.get("code").and_then(|c| c.as_i64()).map(|code| {
493 let msg = json
494 .get("msg")
495 .and_then(|v| v.as_str())
496 .unwrap_or("Unknown error")
497 .to_string();
498 (i32::try_from(code).ok(), msg)
499 })
500 });
501
502 if let Some((code, msg)) = error_info {
503 let status = json
504 .get("status")
505 .and_then(|v| v.as_u64())
506 .map(|s| s as u16);
507 let rejection = match code {
508 Some(code) => {
509 self.create_rejection(id_str, status.unwrap_or(0), code, msg, meta)
510 }
511 None => BinanceSpotWsTradingMessage::RequestFailed {
512 request_id: id_str,
513 msg: format!("Missing or invalid venue error code: {msg}"),
514 },
515 };
516 self.emit(rejection);
517 return;
518 }
519
520 match meta {
522 BinanceSpotWsTradingRequestMeta::SessionLogon => {
523 log::debug!("Session authenticated");
524 self.emit(BinanceSpotWsTradingMessage::Authenticated);
525 }
526 BinanceSpotWsTradingRequestMeta::SubscribeUserData => {
527 let subscription_id = json
528 .get("result")
529 .and_then(|r| r.get("subscriptionId"))
530 .map(|v| v.to_string())
531 .unwrap_or_default();
532 log::debug!("User data stream subscribed: id={subscription_id}");
533 self.emit(BinanceSpotWsTradingMessage::UserDataSubscribed {
534 subscription_id,
535 });
536 }
537 _ => {
538 log::debug!("Unexpected JSON success for request {id_str}");
541 }
542 }
543 return;
544 }
545
546 if let Some(code) = json.get("code").and_then(|v| v.as_i64()) {
548 let msg = json
549 .get("msg")
550 .and_then(|v| v.as_str())
551 .unwrap_or("Unknown error");
552 log::warn!(
553 "Received error response without matching request ID: code={code} msg={msg}"
554 );
555 }
556 return;
557 }
558
559 if json.get("eventStreamTerminated").is_some() {
561 log::warn!("User data stream terminated, resubscribe needed");
562 return;
563 }
564
565 log::debug!("Unhandled text message: {} bytes", text.len());
566 }
567
568 fn handle_user_data_event(&self, event: &serde_json::Value) {
569 if let Some(msg) = classify_user_data_event(event) {
570 self.emit(msg);
571 }
572 }
573
574 fn decode_ws_api_response(
575 &mut self,
576 data: &[u8],
577 ) -> Result<BinanceSpotWsTradingMessage, BinanceWsApiError> {
578 if data.len() >= message_header_codec::ENCODED_LENGTH {
580 let buf = ReadBuf::new(data);
581 let template_id = buf.get_u16_at(2);
582
583 match template_id {
586 601 => {
587 log::debug!("Received SBE BalanceUpdateEvent ({} bytes)", data.len());
588 match super::decode_sbe::decode_balance_update(data) {
589 Ok(msg) => {
590 log::debug!(
591 "SBE balance update: asset={}, delta={}",
592 msg.asset,
593 msg.delta
594 );
595 return Ok(BinanceSpotWsTradingMessage::BalanceUpdate(msg));
596 }
597 Err(e) => {
598 log::error!("Failed to decode SBE BalanceUpdateEvent: {e}");
599 return Ok(BinanceSpotWsTradingMessage::Error(format!(
600 "SBE BalanceUpdateEvent decode failed: {e}"
601 )));
602 }
603 }
604 }
605 603 => {
606 log::debug!("Received SBE ExecutionReportEvent ({} bytes)", data.len());
607 match super::decode_sbe::decode_execution_report(data) {
608 Ok(report) => {
609 log::debug!(
610 "SBE execution report: symbol={}, order_id={}, exec={:?}, status={:?}",
611 report.symbol,
612 report.order_id,
613 report.execution_type,
614 report.order_status
615 );
616 return Ok(BinanceSpotWsTradingMessage::ExecutionReport(Box::new(
617 report,
618 )));
619 }
620 Err(e) => {
621 log::error!("Failed to decode SBE ExecutionReportEvent: {e}");
622 return Ok(BinanceSpotWsTradingMessage::Error(format!(
623 "SBE ExecutionReportEvent decode failed: {e}"
624 )));
625 }
626 }
627 }
628 606 => {
629 return Ok(match super::decode_sbe::decode_list_status(data) {
630 Ok(status) => BinanceSpotWsTradingMessage::ListStatus(status),
631 Err(e) => BinanceSpotWsTradingMessage::Error(format!(
632 "SBE ListStatusEvent decode failed: {e}"
633 )),
634 });
635 }
636 607 => {
637 log::debug!(
638 "Received SBE OutboundAccountPositionEvent ({} bytes)",
639 data.len()
640 );
641
642 match super::decode_sbe::decode_account_position(data) {
643 Ok(msg) => {
644 log::debug!("SBE account position: {} balance(s)", msg.balances.len());
645 return Ok(BinanceSpotWsTradingMessage::AccountPosition(msg));
646 }
647 Err(e) => {
648 log::error!("Failed to decode SBE OutboundAccountPositionEvent: {e}");
649 return Ok(BinanceSpotWsTradingMessage::Error(format!(
650 "SBE OutboundAccountPositionEvent decode failed: {e}"
651 )));
652 }
653 }
654 }
655 610 => {
656 let event_time = parse_server_shutdown_event_time_ms(data);
657 return Ok(BinanceSpotWsTradingMessage::ServerShutdown { event_time });
658 }
659 _ => {} }
661 }
662
663 let (request_id, status, result_data) = self.parse_envelope(data)?;
665
666 let meta = self.pending_requests.remove(&request_id).ok_or_else(|| {
668 BinanceWsApiError::UnknownRequestId(format!("No pending request for ID: {request_id}"))
669 })?;
670
671 if status != 200 {
673 return Ok(match Self::try_decode_sbe_error(&result_data) {
674 Some((code, msg)) => self.create_rejection(request_id, status, code, msg, meta),
675 None => BinanceSpotWsTradingMessage::RequestFailed {
677 request_id,
678 msg: format!("Request failed with status {status}; error payload undecodable"),
679 },
680 });
681 }
682
683 match meta {
685 BinanceSpotWsTradingRequestMeta::PlaceOrder => {
686 let response = parse::decode_new_order_full(&result_data)?;
687 Ok(BinanceSpotWsTradingMessage::OrderAccepted {
688 request_id,
689 response,
690 })
691 }
692 BinanceSpotWsTradingRequestMeta::CancelOrder => {
693 let response = parse::decode_cancel_order(&result_data)?;
694 Ok(BinanceSpotWsTradingMessage::OrderCanceled {
695 request_id,
696 response,
697 })
698 }
699 BinanceSpotWsTradingRequestMeta::CancelReplaceOrder => {
700 let (cancel_response, new_order_response) =
701 parse::decode_cancel_replace_orders(&result_data)?;
702 Ok(BinanceSpotWsTradingMessage::CancelReplaceAccepted {
703 request_id,
704 cancel_response,
705 new_order_response,
706 })
707 }
708 BinanceSpotWsTradingRequestMeta::CancelAllOrders => {
709 let responses = parse::decode_cancel_open_orders(&result_data)?;
710 Ok(BinanceSpotWsTradingMessage::AllOrdersCanceled {
711 request_id,
712 responses,
713 })
714 }
715 BinanceSpotWsTradingRequestMeta::SessionLogon => {
716 log::debug!("Session authenticated (SBE response)");
717 Ok(BinanceSpotWsTradingMessage::Authenticated)
718 }
719 BinanceSpotWsTradingRequestMeta::SubscribeUserData => {
720 log::debug!("User data stream subscribed (SBE response)");
721 Ok(BinanceSpotWsTradingMessage::UserDataSubscribed {
722 subscription_id: request_id,
723 })
724 }
725 }
726 }
727
728 fn parse_envelope(&self, data: &[u8]) -> Result<(String, u16, Vec<u8>), BinanceWsApiError> {
732 if data.len() < message_header_codec::ENCODED_LENGTH {
733 return Err(BinanceWsApiError::DecodeError(
734 crate::spot::sbe::error::SbeDecodeError::BufferTooShort {
735 expected: message_header_codec::ENCODED_LENGTH,
736 actual: data.len(),
737 },
738 ));
739 }
740
741 let buf = ReadBuf::new(data);
742
743 let block_length = buf.get_u16_at(0);
745 let template_id = buf.get_u16_at(2);
746
747 if template_id != SBE_TEMPLATE_ID {
748 return Err(BinanceWsApiError::DecodeError(
749 crate::spot::sbe::error::SbeDecodeError::UnknownTemplateId(template_id),
750 ));
751 }
752
753 let version = buf.get_u16_at(6);
754
755 let decoder = WebSocketResponseDecoder::default().wrap(
757 buf,
758 message_header_codec::ENCODED_LENGTH,
759 block_length,
760 version,
761 );
762
763 let status = decoder.status();
765
766 let mut rate_limits = decoder.rate_limits_decoder();
768 while rate_limits.advance().unwrap_or(None).is_some() {}
769 let mut decoder = rate_limits.parent().map_err(|e| {
770 BinanceWsApiError::ClientError(format!("Failed to get parent from rate_limits: {e}"))
771 })?;
772
773 let id_coords = decoder.id_decoder();
775 let id_bytes = decoder.id_slice(id_coords);
776 let request_id = String::from_utf8_lossy(id_bytes).to_string();
777
778 let result_coords = decoder.result_decoder();
780 let result_data = decoder.result_slice(result_coords).to_vec();
781
782 Ok((request_id, status, result_data))
783 }
784
785 fn create_rejection(
786 &self,
787 request_id: String,
788 status: u16,
789 code: i32,
790 msg: String,
791 meta: BinanceSpotWsTradingRequestMeta,
792 ) -> BinanceSpotWsTradingMessage {
793 match meta {
794 BinanceSpotWsTradingRequestMeta::PlaceOrder => {
795 BinanceSpotWsTradingMessage::OrderRejected {
796 request_id,
797 status,
798 code,
799 msg,
800 }
801 }
802 BinanceSpotWsTradingRequestMeta::CancelOrder => {
803 BinanceSpotWsTradingMessage::CancelRejected {
804 request_id,
805 status,
806 code,
807 msg,
808 }
809 }
810 BinanceSpotWsTradingRequestMeta::CancelReplaceOrder => {
811 BinanceSpotWsTradingMessage::CancelReplaceRejected {
812 request_id,
813 status,
814 code,
815 msg,
816 }
817 }
818 BinanceSpotWsTradingRequestMeta::CancelAllOrders => {
819 BinanceSpotWsTradingMessage::CancelRejected {
820 request_id,
821 status,
822 code,
823 msg,
824 }
825 }
826 BinanceSpotWsTradingRequestMeta::SessionLogon => {
827 BinanceSpotWsTradingMessage::AuthenticationRejected(format!("code={code}: {msg}"))
828 }
829 BinanceSpotWsTradingRequestMeta::SubscribeUserData => {
830 BinanceSpotWsTradingMessage::UserDataSubscriptionRejected(format!(
831 "code={code}: {msg}"
832 ))
833 }
834 }
835 }
836
837 fn try_decode_sbe_error(data: &[u8]) -> Option<(i32, String)> {
839 const HEADER_LEN: usize = 8;
840
841 if data.len()
842 < HEADER_LEN + crate::spot::sbe::spot::error_response_codec::SBE_BLOCK_LENGTH as usize
843 {
844 return None;
845 }
846
847 let buf = ReadBuf::new(data);
848 let header = message_header_codec::MessageHeaderDecoder::default().wrap(buf, 0);
849 if header.template_id() != crate::spot::sbe::spot::error_response_codec::SBE_TEMPLATE_ID {
850 return None;
851 }
852
853 let mut decoder = ErrorResponseDecoder::default().header(header, 0);
854 let code = i32::from(decoder.code());
855 let msg_coords = decoder.msg_decoder();
856 let msg_bytes = decoder.msg_slice(msg_coords);
857 let msg = String::from_utf8_lossy(msg_bytes).into_owned();
858
859 Some((code, msg))
860 }
861}
862
863pub(crate) fn classify_user_data_event(
868 event: &serde_json::Value,
869) -> Option<BinanceSpotWsTradingMessage> {
870 let event_type = event
871 .get("e")
872 .and_then(|v| serde_json::from_value::<BinanceSpotUserDataEventType>(v.clone()).ok())
873 .unwrap_or(BinanceSpotUserDataEventType::Unknown);
874
875 match event_type {
876 BinanceSpotUserDataEventType::ExecutionReport => {
877 match serde_json::from_value::<super::user_data::BinanceSpotExecutionReport>(
878 event.clone(),
879 ) {
880 Ok(report) => {
881 log::debug!(
882 "Execution report: symbol={}, order_id={}, exec={:?}, status={:?}",
883 report.symbol,
884 report.order_id,
885 report.execution_type,
886 report.order_status
887 );
888 Some(BinanceSpotWsTradingMessage::ExecutionReport(Box::new(
889 report,
890 )))
891 }
892 Err(e) => {
893 log::warn!("Failed to parse execution report: {e}");
894 None
895 }
896 }
897 }
898 BinanceSpotUserDataEventType::OutboundAccountPosition => {
899 match serde_json::from_value::<super::user_data::BinanceSpotAccountPositionMsg>(
900 event.clone(),
901 ) {
902 Ok(msg) => {
903 log::debug!("Account position update: {} balance(s)", msg.balances.len());
904 Some(BinanceSpotWsTradingMessage::AccountPosition(msg))
905 }
906 Err(e) => {
907 log::warn!("Failed to parse account position: {e}");
908 None
909 }
910 }
911 }
912 BinanceSpotUserDataEventType::BalanceUpdate => {
913 match serde_json::from_value::<super::user_data::BinanceSpotBalanceUpdateMsg>(
914 event.clone(),
915 ) {
916 Ok(msg) => {
917 log::debug!("Balance update: asset={}, delta={}", msg.asset, msg.delta);
918 Some(BinanceSpotWsTradingMessage::BalanceUpdate(msg))
919 }
920 Err(e) => {
921 log::warn!("Failed to parse balance update: {e}");
922 None
923 }
924 }
925 }
926 BinanceSpotUserDataEventType::ServerShutdown => {
927 let event_time = event.get("E").and_then(|v| v.as_i64()).unwrap_or_default();
928 Some(BinanceSpotWsTradingMessage::ServerShutdown { event_time })
929 }
930 BinanceSpotUserDataEventType::ListenKeyExpired
931 | BinanceSpotUserDataEventType::ExternalLockUpdate
932 | BinanceSpotUserDataEventType::EventStreamTerminated
933 | BinanceSpotUserDataEventType::Unknown => {
934 log::debug!("Unhandled user data event type: {event_type:?}");
935 None
936 }
937 }
938}
939
940pub(crate) fn parse_server_shutdown_event_time_ms(data: &[u8]) -> i64 {
946 if data.len() < message_header_codec::ENCODED_LENGTH + 8 {
947 return 0;
948 }
949 let buf = ReadBuf::new(data);
950 buf.get_i64_at(message_header_codec::ENCODED_LENGTH) / 1_000
951}
952
953#[cfg(test)]
954mod tests {
955 use std::time::Duration;
956
957 use futures_util::StreamExt;
958 use nautilus_common::testing::wait_until_async;
959 use nautilus_network::{
960 error::SendError,
961 websocket::{AuthTracker, WebSocketConfig},
962 };
963 use rstest::rstest;
964
965 use super::*;
966 use crate::spot::sbe::spot::{
967 cancel_order_response_codec::CancelOrderResponseDecoder,
968 new_order_full_response_codec::NewOrderFullResponseDecoder,
969 self_trade_prevention_mode::SelfTradePreventionMode,
970 };
971
972 #[tokio::test]
973 async fn test_authentication_with_full_replay_buffer() {
974 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
975 let address = listener.local_addr().unwrap();
976 let (replayed_tx, replayed_rx) = tokio::sync::oneshot::channel();
977
978 let server = tokio::spawn(async move {
979 let (first, _) = listener.accept().await.unwrap();
980 let _first = tokio_tungstenite::accept_async(first).await.unwrap();
981 let (replacement, _) = listener.accept().await.unwrap();
982 let mut replacement = tokio_tungstenite::accept_async(replacement).await.unwrap();
983 let auth = replacement.next().await.unwrap().unwrap();
984 let auth: serde_json::Value = serde_json::from_str(auth.to_text().unwrap()).unwrap();
985 assert_eq!(auth["method"], method::SESSION_LOGON);
986 assert_eq!(auth["id"], "ws-1000");
987 assert_eq!(auth["params"]["apiKey"], "api-key");
988 let subscribe = replacement.next().await.unwrap().unwrap();
989 let subscribe: serde_json::Value =
990 serde_json::from_str(subscribe.to_text().unwrap()).unwrap();
991 assert_eq!(subscribe["method"], "userDataStream.subscribe");
992 assert_eq!(subscribe["id"], "ws-1001");
993 assert_eq!(subscribe["params"], serde_json::json!({}));
994 assert_eq!(
995 replacement.next().await.unwrap().unwrap(),
996 Message::text("held")
997 );
998 replayed_tx.send(()).unwrap();
999 std::future::pending::<()>().await;
1000 });
1001
1002 let tracker = AuthTracker::new();
1003 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1004 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1005 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
1006
1007 let mut handler = BinanceSpotWsTradingHandler::new(
1008 Arc::new(AtomicBool::new(false)),
1009 cmd_rx,
1010 raw_rx,
1011 out_tx,
1012 Arc::new(SigningCredential::new(
1013 "api-key".to_string(),
1014 "secret".to_string(),
1015 )),
1016 );
1017 let config = WebSocketConfig::builder()
1018 .url(format!("ws://{address}"))
1019 .writer_capacity(1)
1020 .reconnect_delay_initial_ms(1)
1021 .reconnect_delay_max_ms(1)
1022 .reconnect_jitter_ms(0)
1023 .build()
1024 .unwrap();
1025 let client = WebSocketClient::builder()
1026 .config(config)
1027 .message_handler(Arc::new(|_| {}))
1028 .connect()
1029 .await
1030 .unwrap();
1031 client.set_auth_tracker(tracker.clone(), true);
1032
1033 client.send_text("held".to_string(), None).await.unwrap();
1035 assert!(client.request_reconnect());
1036 wait_until_async(
1037 || async { client.is_active() && client.connection_epoch() == 1 },
1038 Duration::from_secs(5),
1039 )
1040 .await;
1041 assert!(matches!(
1042 client.send_text("overflow".to_string(), None).await,
1043 Err(SendError::BufferFull)
1044 ));
1045 handler.inner = Some(client);
1046 tokio::time::timeout(Duration::from_secs(5), handler.handle_session_logon())
1047 .await
1048 .unwrap()
1049 .unwrap();
1050 tokio::time::timeout(Duration::from_secs(5), handler.handle_subscribe_user_data())
1051 .await
1052 .unwrap()
1053 .unwrap();
1054 tracker.succeed();
1055 tokio::time::timeout(Duration::from_secs(5), replayed_rx)
1056 .await
1057 .unwrap()
1058 .unwrap();
1059 handler.inner.as_ref().unwrap().disconnect().await;
1060 server.abort();
1061 }
1062
1063 #[rstest]
1064 #[case::valid(false)]
1065 #[case::malformed(true)]
1066 fn test_list_status_routes_typed_message_or_controlled_error(#[case] malformed: bool) {
1067 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1068 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1069 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
1070
1071 let mut handler = BinanceSpotWsTradingHandler::new(
1072 Arc::new(AtomicBool::new(false)),
1073 cmd_rx,
1074 raw_rx,
1075 out_tx,
1076 Arc::new(SigningCredential::new(
1077 "api-key".to_owned(),
1078 "secret".to_owned(),
1079 )),
1080 );
1081 let mut data = crate::common::testing::load_fixture_bytes(
1082 "spot/user_data_sbe/list_status_executing.sbe",
1083 );
1084
1085 if malformed {
1086 data.pop();
1087 }
1088
1089 let message = handler.decode_ws_api_response(&data).unwrap();
1090 if malformed {
1091 let BinanceSpotWsTradingMessage::Error(reason) = message else {
1092 panic!("Expected controlled error");
1093 };
1094
1095 assert!(reason.starts_with("SBE ListStatusEvent decode failed:"));
1096 } else {
1097 let BinanceSpotWsTradingMessage::ListStatus(status) = message else {
1098 panic!("Expected typed list status");
1099 };
1100
1101 assert_eq!(
1102 status,
1103 super::super::decode_sbe::decode_list_status(&data).unwrap()
1104 );
1105 }
1106 }
1107
1108 #[rstest]
1109 fn test_cancel_replace_response_decodes_both_orders() {
1110 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1111 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1112 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
1113 let mut handler = BinanceSpotWsTradingHandler::new(
1114 Arc::new(AtomicBool::new(false)),
1115 cmd_rx,
1116 raw_rx,
1117 out_tx,
1118 Arc::new(SigningCredential::new(
1119 "api-key".to_string(),
1120 "secret".to_string(),
1121 )),
1122 );
1123 let placed = include_bytes!(
1124 "../../../../test_data/spot/user_data_sbe/mainnet/web_socket_response_1.sbe"
1125 );
1126 let canceled = include_bytes!(
1127 "../../../../test_data/spot/user_data_sbe/mainnet/web_socket_response_2.sbe"
1128 );
1129 let (request_id, _, replacement) = handler.parse_envelope(placed).unwrap();
1130 let (_, _, cancellation) = handler.parse_envelope(canceled).unwrap();
1131 let expected_new = parse::decode_new_order_full(&replacement).unwrap();
1132 let expected_cancel = parse::decode_cancel_order(&cancellation).unwrap();
1133 let mut payload = Vec::new();
1134
1135 for field in [
1136 2_u16,
1137 crate::spot::sbe::spot::cancel_replace_order_response_codec::SBE_TEMPLATE_ID,
1138 crate::spot::sbe::spot::SBE_SCHEMA_ID,
1139 crate::spot::sbe::spot::SBE_SCHEMA_VERSION,
1140 ] {
1141 payload.extend_from_slice(&field.to_le_bytes());
1142 }
1143 let success =
1144 crate::spot::sbe::spot::cancel_replace_status::CancelReplaceStatus::Success as u8;
1145 payload.extend_from_slice(&[success, success]);
1146 payload.extend_from_slice(&(cancellation.len() as u16).to_le_bytes());
1147 payload.extend_from_slice(&cancellation);
1148 payload.extend_from_slice(&(replacement.len() as u32).to_le_bytes());
1149 payload.extend_from_slice(&replacement);
1150 let mut response = placed[..placed.len() - replacement.len() - 4].to_vec();
1151 response.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1152 response.extend_from_slice(&payload);
1153 handler.pending_requests.insert(
1154 request_id.clone(),
1155 BinanceSpotWsTradingRequestMeta::CancelReplaceOrder,
1156 );
1157
1158 let decoded = handler.decode_ws_api_response(&response).unwrap();
1159
1160 let BinanceSpotWsTradingMessage::CancelReplaceAccepted {
1161 request_id: actual_id,
1162 cancel_response,
1163 new_order_response,
1164 } = decoded
1165 else {
1166 panic!("Expected cancel-replace acceptance");
1167 };
1168 assert_eq!(actual_id, request_id);
1169 assert_eq!(cancel_response, expected_cancel);
1170 assert_eq!(new_order_response, expected_new);
1171 assert!(handler.pending_requests.is_empty());
1172 }
1173
1174 #[rstest]
1175 fn test_mainnet_order_responses_match_generated_decoders() {
1176 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1177 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1178 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
1179 let handler = BinanceSpotWsTradingHandler::new(
1180 Arc::new(AtomicBool::new(false)),
1181 cmd_rx,
1182 raw_rx,
1183 out_tx,
1184 Arc::new(SigningCredential::new(
1185 "api-key".to_string(),
1186 "secret".to_string(),
1187 )),
1188 );
1189 let placed = include_bytes!(
1190 "../../../../test_data/spot/user_data_sbe/mainnet/web_socket_response_1.sbe"
1191 );
1192 let canceled = include_bytes!(
1193 "../../../../test_data/spot/user_data_sbe/mainnet/web_socket_response_2.sbe"
1194 );
1195 let (_, _, replacement) = handler.parse_envelope(placed).unwrap();
1196 let (_, _, cancellation) = handler.parse_envelope(canceled).unwrap();
1197 let placed_header = message_header_codec::MessageHeaderDecoder::default()
1198 .wrap(ReadBuf::new(&replacement), 0);
1199 let placed_decoder = NewOrderFullResponseDecoder::default().header(placed_header, 0);
1200 let canceled_header = message_header_codec::MessageHeaderDecoder::default()
1201 .wrap(ReadBuf::new(&cancellation), 0);
1202 let canceled_decoder = CancelOrderResponseDecoder::default().header(canceled_header, 0);
1203
1204 let new_order = parse::decode_new_order_full(&replacement).unwrap();
1205 let cancel = parse::decode_cancel_order(&cancellation).unwrap();
1206
1207 assert_eq!(
1208 new_order.self_trade_prevention_mode,
1209 placed_decoder.self_trade_prevention_mode()
1210 );
1211 assert_eq!(new_order.working_time, placed_decoder.working_time());
1212 assert_eq!(new_order.stop_price_mantissa, placed_decoder.stop_price());
1213 assert_eq!(
1214 cancel.self_trade_prevention_mode,
1215 canceled_decoder.self_trade_prevention_mode()
1216 );
1217 assert_eq!(
1218 cancel.self_trade_prevention_mode,
1219 SelfTradePreventionMode::ExpireMaker
1220 );
1221 }
1222
1223 #[rstest]
1224 #[case::missing(None)]
1225 #[case::overflow(Some(i64::MAX))]
1226 fn test_json_error_without_valid_code_is_ambiguous(#[case] code: Option<i64>) {
1227 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1228 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1229 let (out_tx, mut out_rx) = tokio::sync::mpsc::unbounded_channel();
1230 let mut handler = BinanceSpotWsTradingHandler::new(
1231 Arc::new(AtomicBool::new(false)),
1232 cmd_rx,
1233 raw_rx,
1234 out_tx,
1235 Arc::new(SigningCredential::new(
1236 "test-key".to_string(),
1237 "test-secret".to_string(),
1238 )),
1239 );
1240 handler.pending_requests.insert(
1241 "order-1".to_string(),
1242 BinanceSpotWsTradingRequestMeta::PlaceOrder,
1243 );
1244 let response = serde_json::json!({"id": "order-1", "status": 400, "error": {"code": code, "msg": "invalid request"}});
1245
1246 handler.handle_text_response(&response.to_string());
1247
1248 let BinanceSpotWsTradingMessage::RequestFailed { request_id, msg } =
1249 out_rx.try_recv().unwrap()
1250 else {
1251 panic!("expected ambiguous request failure");
1252 };
1253 assert_eq!(request_id, "order-1");
1254 assert_eq!(msg, "Missing or invalid venue error code: invalid request");
1255 assert!(handler.pending_requests.is_empty());
1256 assert!(out_rx.try_recv().is_err());
1257 }
1258
1259 #[rstest]
1260 #[case::microseconds_converted_to_ms(1_700_000_000_000_000_i64, 1_700_000_000_000_i64)]
1261 #[case::zero(0_i64, 0_i64)]
1262 #[case::negative(-1_000_i64, -1_i64)]
1263 fn test_parse_server_shutdown_event_time_ms(
1264 #[case] event_time_us: i64,
1265 #[case] expected_ms: i64,
1266 ) {
1267 let mut buf = vec![0u8; message_header_codec::ENCODED_LENGTH];
1268 buf.extend_from_slice(&event_time_us.to_le_bytes());
1269 assert_eq!(parse_server_shutdown_event_time_ms(&buf), expected_ms);
1270 }
1271
1272 #[rstest]
1273 fn test_parse_server_shutdown_event_time_ms_short_buffer_returns_zero() {
1274 let buf = vec![0u8; message_header_codec::ENCODED_LENGTH + 4];
1275 assert_eq!(parse_server_shutdown_event_time_ms(&buf), 0);
1276 }
1277
1278 #[rstest]
1279 fn test_classify_user_data_event_server_shutdown_emits_variant() {
1280 let event = serde_json::json!({"e": "serverShutdown", "E": 1_700_000_000_000_i64});
1281 let msg = classify_user_data_event(&event).expect("expected ServerShutdown");
1282 match msg {
1283 BinanceSpotWsTradingMessage::ServerShutdown { event_time } => {
1284 assert_eq!(event_time, 1_700_000_000_000);
1285 }
1286 other => panic!("expected ServerShutdown variant, was {other:?}"),
1287 }
1288 }
1289
1290 #[rstest]
1291 fn test_classify_user_data_event_server_shutdown_missing_event_time_defaults_to_zero() {
1292 let event = serde_json::json!({"e": "serverShutdown"});
1293 let msg = classify_user_data_event(&event).expect("expected ServerShutdown");
1294 match msg {
1295 BinanceSpotWsTradingMessage::ServerShutdown { event_time } => {
1296 assert_eq!(event_time, 0);
1297 }
1298 other => panic!("expected ServerShutdown variant, was {other:?}"),
1299 }
1300 }
1301
1302 #[rstest]
1303 fn test_classify_user_data_event_unknown_returns_none() {
1304 let event = serde_json::json!({"e": "somethingElse"});
1305 assert!(classify_user_data_event(&event).is_none());
1306 }
1307
1308 #[rstest]
1309 fn test_sign_params_includes_recv_window_in_signature() {
1310 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
1311 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
1312 let (out_tx, _out_rx) = tokio::sync::mpsc::unbounded_channel();
1313 let credential = Arc::new(SigningCredential::new(
1314 "api-key".to_string(),
1315 "hmac-secret".to_string(),
1316 ));
1317 let handler = BinanceSpotWsTradingHandler::new(
1318 Arc::new(AtomicBool::new(false)),
1319 cmd_rx,
1320 raw_rx,
1321 out_tx,
1322 credential.clone(),
1323 )
1324 .with_recv_window(Some(45_000));
1325
1326 let signed = handler
1327 .sign_params(serde_json::json!({"symbol": "BTCUSDT"}))
1328 .unwrap();
1329 let mut unsigned = signed.clone();
1330 let signature = unsigned
1331 .as_object_mut()
1332 .unwrap()
1333 .remove("signature")
1334 .unwrap();
1335 let query = canonical_ws_query_string(
1336 unsigned
1337 .as_object()
1338 .unwrap()
1339 .iter()
1340 .map(|(key, value)| (key.as_str(), value)),
1341 )
1342 .unwrap();
1343
1344 assert_eq!(signed["recvWindow"], 45_000);
1345 assert_eq!(signature, credential.sign(&query));
1346 }
1347}