1use std::time::Duration;
22
23use futures_util::SinkExt;
24use futures_util::StreamExt;
25use serde::Deserialize;
26use tokio::net::TcpStream;
27use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
28use tokio_tungstenite::tungstenite::Message;
29use tokio_tungstenite::MaybeTlsStream;
30use tokio_tungstenite::WebSocketStream;
31
32use crate::error::{Error, Result};
33use wickra_core::Candle;
34
35#[derive(Debug, Clone)]
40pub struct BinanceConfig {
41 pub base_url: String,
44 pub read_timeout: Duration,
48 pub initial_reconnect_delay: Duration,
51 pub max_reconnect_backoff: Duration,
53 pub max_reconnect_attempts: u32,
56 pub max_message_size: usize,
60 pub max_frame_size: usize,
62}
63
64impl Default for BinanceConfig {
65 fn default() -> Self {
66 Self {
67 base_url: "wss://stream.binance.com:9443".to_string(),
68 read_timeout: Duration::from_secs(300),
69 initial_reconnect_delay: Duration::from_secs(1),
70 max_reconnect_backoff: Duration::from_secs(30),
71 max_reconnect_attempts: 6,
72 max_message_size: 8 << 20,
73 max_frame_size: 2 << 20,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Interval {
82 OneSecond,
83 OneMinute,
84 ThreeMinutes,
85 FiveMinutes,
86 FifteenMinutes,
87 ThirtyMinutes,
88 OneHour,
89 TwoHours,
90 FourHours,
91 SixHours,
92 EightHours,
93 TwelveHours,
94 OneDay,
95 ThreeDays,
96 OneWeek,
97 OneMonth,
98}
99
100impl Interval {
101 pub fn as_str(self) -> &'static str {
103 match self {
104 Self::OneSecond => "1s",
105 Self::OneMinute => "1m",
106 Self::ThreeMinutes => "3m",
107 Self::FiveMinutes => "5m",
108 Self::FifteenMinutes => "15m",
109 Self::ThirtyMinutes => "30m",
110 Self::OneHour => "1h",
111 Self::TwoHours => "2h",
112 Self::FourHours => "4h",
113 Self::SixHours => "6h",
114 Self::EightHours => "8h",
115 Self::TwelveHours => "12h",
116 Self::OneDay => "1d",
117 Self::ThreeDays => "3d",
118 Self::OneWeek => "1w",
119 Self::OneMonth => "1M",
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct KlineEvent {
127 pub symbol: String,
129 pub interval: Interval,
131 pub candle: Candle,
133 pub is_closed: bool,
136}
137
138#[derive(Debug)]
140pub struct BinanceKlineStream {
141 socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
142 symbols: Vec<String>,
145 interval: Interval,
147 closed: bool,
150 config: BinanceConfig,
152}
153
154#[derive(Debug, Clone, Deserialize)]
157pub struct RawWsEnvelope {
158 pub stream: String,
160 pub data: RawKlinePayload,
161}
162
163#[derive(Debug, Clone, Deserialize)]
164pub struct RawKlinePayload {
165 #[serde(rename = "e")]
166 pub event_type: String,
167 #[serde(rename = "E")]
168 pub event_time: i64,
169 #[serde(rename = "s")]
170 pub symbol: String,
171 #[serde(rename = "k")]
172 pub kline: RawKline,
173}
174
175#[derive(Debug, Clone, Deserialize)]
176pub struct RawKline {
177 #[serde(rename = "t")]
178 pub open_time: i64,
179 #[serde(rename = "T")]
180 pub close_time: i64,
181 #[serde(rename = "s")]
182 pub symbol: String,
183 #[serde(rename = "i")]
184 pub interval: String,
185 #[serde(rename = "o")]
186 pub open: String,
187 #[serde(rename = "c")]
188 pub close: String,
189 #[serde(rename = "h")]
190 pub high: String,
191 #[serde(rename = "l")]
192 pub low: String,
193 #[serde(rename = "v")]
194 pub volume: String,
195 #[serde(rename = "x")]
196 pub is_closed: bool,
197}
198
199impl BinanceKlineStream {
200 async fn open(
203 symbols: &[String],
204 interval: Interval,
205 config: &BinanceConfig,
206 ) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
207 let streams: Vec<String> = symbols
208 .iter()
209 .map(|s| format!("{}@kline_{}", s, interval.as_str()))
210 .collect();
211 let url = format!("{}/stream?streams={}", config.base_url, streams.join("/"));
212 let url = url::Url::parse(&url).map_err(|e| Error::Malformed(e.to_string()))?;
213 let ws_config = WebSocketConfig::default()
217 .max_message_size(Some(config.max_message_size))
218 .max_frame_size(Some(config.max_frame_size));
219 let (socket, _) =
220 tokio_tungstenite::connect_async_with_config(url.as_str(), Some(ws_config), false)
221 .await?;
222 Ok(socket)
223 }
224
225 pub async fn connect(symbols: &[String], interval: Interval) -> Result<Self> {
231 Self::connect_with_config(symbols, interval, BinanceConfig::default()).await
232 }
233
234 pub async fn connect_with_config(
238 symbols: &[String],
239 interval: Interval,
240 config: BinanceConfig,
241 ) -> Result<Self> {
242 if symbols.is_empty() {
243 return Err(Error::Malformed(
244 "BinanceKlineStream requires at least one symbol".into(),
245 ));
246 }
247 let symbols: Vec<String> = symbols.iter().map(|s| s.to_lowercase()).collect();
248 let socket = Self::open(&symbols, interval, &config).await?;
249 Ok(Self {
250 socket,
251 symbols,
252 interval,
253 closed: false,
254 config,
255 })
256 }
257
258 pub fn is_closed(&self) -> bool {
261 self.closed
262 }
263
264 async fn reconnect(&mut self) -> Result<()> {
267 let mut delay = self.config.initial_reconnect_delay;
268 let mut last_err = None;
269 for _ in 0..self.config.max_reconnect_attempts {
270 tokio::time::sleep(delay).await;
271 match Self::open(&self.symbols, self.interval, &self.config).await {
272 Ok(socket) => {
273 self.socket = socket;
274 return Ok(());
275 }
276 Err(e) => {
277 last_err = Some(e);
278 delay = delay
279 .saturating_mul(2)
280 .min(self.config.max_reconnect_backoff);
281 }
282 }
283 }
284 Err(last_err.expect("max_reconnect_attempts is non-zero"))
285 }
286
287 pub async fn next_event(&mut self) -> Result<Option<KlineEvent>> {
293 if self.closed {
294 return Ok(None);
295 }
296 loop {
297 let Ok(Some(Ok(msg))) =
300 tokio::time::timeout(self.config.read_timeout, self.socket.next()).await
301 else {
302 self.reconnect().await?;
303 continue;
304 };
305 match msg {
306 Message::Text(text) => {
307 if let Some(event) = Self::parse_frame(&text, self.interval)? {
308 return Ok(Some(event));
309 }
310 }
313 Message::Binary(bytes) => {
314 let text = String::from_utf8_lossy(&bytes);
315 if let Some(event) = Self::parse_frame(&text, self.interval)? {
316 return Ok(Some(event));
317 }
318 }
319 Message::Ping(payload) => {
320 let _ = self.socket.send(Message::Pong(payload)).await;
325 }
326 Message::Pong(_) | Message::Frame(_) => {}
327 Message::Close(_) => {
328 self.reconnect().await?;
329 }
330 }
331 }
332 }
333
334 fn parse_frame(text: &str, interval: Interval) -> Result<Option<KlineEvent>> {
340 let value: serde_json::Value = serde_json::from_str(text)?;
341 let is_kline = value
344 .get("data")
345 .and_then(|d| d.get("e"))
346 .and_then(serde_json::Value::as_str)
347 == Some("kline");
348 if !is_kline {
349 return Ok(None);
350 }
351 let envelope: RawWsEnvelope = serde_json::from_value(value)?;
352 Ok(Some(envelope.into_event(interval)?))
353 }
354
355 pub async fn close(&mut self) -> Result<()> {
359 self.closed = true;
360 self.socket.close(None).await?;
361 Ok(())
362 }
363}
364
365impl RawWsEnvelope {
366 fn into_event(self, interval: Interval) -> Result<KlineEvent> {
367 let k = self.data.kline;
368 let open: f64 = k
369 .open
370 .parse()
371 .map_err(|_| Error::Malformed(format!("bad open '{}'", k.open)))?;
372 let high: f64 = k
373 .high
374 .parse()
375 .map_err(|_| Error::Malformed(format!("bad high '{}'", k.high)))?;
376 let low: f64 = k
377 .low
378 .parse()
379 .map_err(|_| Error::Malformed(format!("bad low '{}'", k.low)))?;
380 let close: f64 = k
381 .close
382 .parse()
383 .map_err(|_| Error::Malformed(format!("bad close '{}'", k.close)))?;
384 let volume: f64 = k
385 .volume
386 .parse()
387 .map_err(|_| Error::Malformed(format!("bad volume '{}'", k.volume)))?;
388 let candle = Candle::new(open, high, low, close, volume, k.open_time)?;
389 Ok(KlineEvent {
390 symbol: self.data.symbol.to_lowercase(),
391 interval,
392 candle,
393 is_closed: k.is_closed,
394 })
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn interval_as_str_covers_every_variant() {
404 let pairs: &[(Interval, &str)] = &[
407 (Interval::OneSecond, "1s"),
408 (Interval::OneMinute, "1m"),
409 (Interval::ThreeMinutes, "3m"),
410 (Interval::FiveMinutes, "5m"),
411 (Interval::FifteenMinutes, "15m"),
412 (Interval::ThirtyMinutes, "30m"),
413 (Interval::OneHour, "1h"),
414 (Interval::TwoHours, "2h"),
415 (Interval::FourHours, "4h"),
416 (Interval::SixHours, "6h"),
417 (Interval::EightHours, "8h"),
418 (Interval::TwelveHours, "12h"),
419 (Interval::OneDay, "1d"),
420 (Interval::ThreeDays, "3d"),
421 (Interval::OneWeek, "1w"),
422 (Interval::OneMonth, "1M"),
423 ];
424 for (iv, expected) in pairs {
425 assert_eq!(iv.as_str(), *expected);
426 }
427 }
428
429 #[test]
430 fn binance_config_default_matches_production_endpoint() {
431 let cfg = BinanceConfig::default();
432 assert_eq!(cfg.base_url, "wss://stream.binance.com:9443");
433 assert_eq!(cfg.read_timeout, Duration::from_secs(300));
434 assert_eq!(cfg.initial_reconnect_delay, Duration::from_secs(1));
435 assert_eq!(cfg.max_reconnect_backoff, Duration::from_secs(30));
436 assert_eq!(cfg.max_reconnect_attempts, 6);
437 assert_eq!(cfg.max_message_size, 8 << 20);
438 assert_eq!(cfg.max_frame_size, 2 << 20);
439 }
440
441 #[tokio::test]
442 async fn connect_rejects_an_empty_symbol_list() {
443 let err = BinanceKlineStream::connect(&[], Interval::OneMinute)
444 .await
445 .unwrap_err();
446 assert!(matches!(err, Error::Malformed(_)));
447 }
448
449 #[test]
450 fn parses_real_binance_payload() {
451 let json = r#"{
453 "stream": "btcusdt@kline_1m",
454 "data": {
455 "e": "kline",
456 "E": 1700000000000,
457 "s": "BTCUSDT",
458 "k": {
459 "t": 1700000000000,
460 "T": 1700000059999,
461 "s": "BTCUSDT",
462 "i": "1m",
463 "f": 1,
464 "L": 100,
465 "o": "30000.0",
466 "c": "30050.0",
467 "h": "30100.0",
468 "l": "29950.0",
469 "v": "12.5",
470 "n": 50,
471 "x": false,
472 "q": "375000.0",
473 "V": "6.25",
474 "Q": "187500.0",
475 "B": "0"
476 }
477 }
478 }"#;
479 let env: RawWsEnvelope = serde_json::from_str(json).unwrap();
480 let evt = env.into_event(Interval::OneMinute).unwrap();
481 assert_eq!(evt.symbol, "btcusdt");
482 assert_eq!(evt.candle.open, 30_000.0);
483 assert_eq!(evt.candle.close, 30_050.0);
484 assert!(!evt.is_closed);
485 assert_eq!(evt.interval, Interval::OneMinute);
486 }
487
488 #[test]
489 fn rejects_non_parsable_numbers() {
490 let json = r#"{
491 "stream": "btcusdt@kline_1m",
492 "data": {
493 "e": "kline", "E": 0, "s": "BTCUSDT",
494 "k": {
495 "t": 0, "T": 0, "s": "BTCUSDT", "i": "1m",
496 "f": 0, "L": 0,
497 "o": "not-a-number", "c": "0", "h": "0", "l": "0",
498 "v": "0", "n": 0, "x": false, "q": "0", "V": "0", "Q": "0", "B": "0"
499 }
500 }
501 }"#;
502 let env: RawWsEnvelope = serde_json::from_str(json).unwrap();
503 let err = env.into_event(Interval::OneMinute).unwrap_err();
504 assert!(matches!(err, Error::Malformed(_)));
505 }
506
507 #[test]
508 fn skips_non_kline_frames() {
509 let ack = r#"{"result":null,"id":1}"#;
511 assert!(BinanceKlineStream::parse_frame(ack, Interval::OneMinute)
512 .unwrap()
513 .is_none());
514 let err = r#"{"error":{"code":2,"msg":"Invalid request"}}"#;
516 assert!(BinanceKlineStream::parse_frame(err, Interval::OneMinute)
517 .unwrap()
518 .is_none());
519 }
520
521 #[test]
522 fn parse_frame_decodes_a_kline() {
523 let json = r#"{
524 "stream": "btcusdt@kline_1m",
525 "data": {
526 "e": "kline", "E": 1700000000000, "s": "BTCUSDT",
527 "k": {
528 "t": 1700000000000, "T": 1700000059999, "s": "BTCUSDT", "i": "1m",
529 "f": 1, "L": 100, "o": "30000.0", "c": "30050.0", "h": "30100.0",
530 "l": "29950.0", "v": "12.5", "n": 50, "x": true,
531 "q": "375000.0", "V": "6.25", "Q": "187500.0", "B": "0"
532 }
533 }
534 }"#;
535 let event = BinanceKlineStream::parse_frame(json, Interval::OneMinute)
536 .unwrap()
537 .expect("a kline frame yields an event");
538 assert_eq!(event.symbol, "btcusdt");
539 assert!(event.is_closed);
540 }
541
542 use std::sync::atomic::{AtomicU32, Ordering};
549 use std::sync::Arc;
550 use tokio::net::TcpListener;
551
552 fn sample_kline_text() -> String {
555 r#"{"stream":"btcusdt@kline_1m","data":{"e":"kline","E":1700000000000,"s":"BTCUSDT","k":{"t":1700000000000,"T":1700000059999,"s":"BTCUSDT","i":"1m","f":1,"L":100,"o":"30000.0","c":"30050.0","h":"30100.0","l":"29950.0","v":"12.5","n":50,"x":true,"q":"375000.0","V":"6.25","Q":"187500.0","B":"0"}}}"#.to_string()
556 }
557
558 fn test_config(base_url: String) -> BinanceConfig {
561 BinanceConfig {
562 base_url,
563 read_timeout: Duration::from_millis(200),
564 initial_reconnect_delay: Duration::from_millis(5),
565 max_reconnect_backoff: Duration::from_millis(10),
566 max_reconnect_attempts: 3,
567 ..BinanceConfig::default()
568 }
569 }
570
571 async fn one_shot_server<F, Fut>(handler: F) -> String
576 where
577 F: FnOnce(WebSocketStream<TcpStream>) -> Fut + Send + 'static,
578 Fut: std::future::Future<Output = ()> + Send + 'static,
579 {
580 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
581 let base_url = format!("ws://{}", listener.local_addr().unwrap());
582 tokio::spawn(async move {
583 let (stream, _) = listener.accept().await.unwrap();
584 drop(listener);
585 let ws = tokio_tungstenite::accept_async(stream).await.unwrap();
586 handler(ws).await;
587 });
588 base_url
589 }
590
591 async fn multi_shot_server<F, Fut>(
597 n_accepts: u32,
598 handler: F,
599 ) -> (String, tokio::task::JoinHandle<()>)
600 where
601 F: Fn(u32, WebSocketStream<TcpStream>) -> Fut + Send + Sync + 'static,
602 Fut: std::future::Future<Output = ()> + Send + 'static,
603 {
604 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
605 let base_url = format!("ws://{}", listener.local_addr().unwrap());
606 let handler = Arc::new(handler);
607 let h = tokio::spawn(async move {
608 let mut joins = Vec::with_capacity(n_accepts as usize);
609 for index in 0..n_accepts {
610 let (stream, _) = listener.accept().await.unwrap();
611 let handler = handler.clone();
612 joins.push(tokio::spawn(async move {
613 let ws = tokio_tungstenite::accept_async(stream).await.unwrap();
614 handler(index, ws).await;
615 }));
616 }
617 for j in joins {
618 j.await.unwrap();
619 }
620 });
621 (base_url, h)
622 }
623
624 #[tokio::test]
625 async fn next_event_decodes_a_text_kline_frame() {
626 let kline = sample_kline_text();
627 let base = one_shot_server(move |mut ws| async move {
628 let _ = ws.send(Message::Text(kline.into())).await;
629 while let Some(Ok(_)) = ws.next().await {}
630 })
631 .await;
632 let mut stream = BinanceKlineStream::connect_with_config(
633 &["BTCUSDT".to_string()],
634 Interval::OneMinute,
635 test_config(base),
636 )
637 .await
638 .unwrap();
639 assert!(!stream.is_closed());
640 let event = stream
641 .next_event()
642 .await
643 .unwrap()
644 .expect("server pushes a kline");
645 assert_eq!(event.symbol, "btcusdt");
646 assert!(event.is_closed);
647 }
648
649 #[tokio::test]
650 async fn next_event_decodes_a_binary_kline_frame() {
651 let kline = sample_kline_text();
652 let base = one_shot_server(move |mut ws| async move {
653 let bytes: Vec<u8> = kline.into_bytes();
654 let _ = ws.send(Message::Binary(bytes.into())).await;
655 while let Some(Ok(_)) = ws.next().await {}
656 })
657 .await;
658 let mut stream = BinanceKlineStream::connect_with_config(
659 &["BTCUSDT".to_string()],
660 Interval::OneMinute,
661 test_config(base),
662 )
663 .await
664 .unwrap();
665 let event = stream
666 .next_event()
667 .await
668 .unwrap()
669 .expect("server pushes a kline as Binary");
670 assert_eq!(event.symbol, "btcusdt");
671 }
672
673 #[tokio::test]
674 async fn next_event_replies_to_a_ping_with_a_pong() {
675 let kline = sample_kline_text();
676 let base = one_shot_server(move |mut ws| async move {
677 let _ = ws
678 .send(Message::Ping(b"binance-ping".as_slice().into()))
679 .await;
680 let _ = ws.send(Message::Text(kline.into())).await;
681 while let Some(Ok(_)) = ws.next().await {}
682 })
683 .await;
684 let mut stream = BinanceKlineStream::connect_with_config(
685 &["BTCUSDT".to_string()],
686 Interval::OneMinute,
687 test_config(base),
688 )
689 .await
690 .unwrap();
691 let event = stream
695 .next_event()
696 .await
697 .unwrap()
698 .expect("kline arrives right after the ping");
699 assert_eq!(event.symbol, "btcusdt");
700 }
701
702 #[tokio::test]
703 async fn next_event_skips_inbound_pong_frames() {
704 let kline = sample_kline_text();
705 let base = one_shot_server(move |mut ws| async move {
706 let _ = ws
707 .send(Message::Pong(b"unsolicited".as_slice().into()))
708 .await;
709 let _ = ws.send(Message::Text(kline.into())).await;
710 while let Some(Ok(_)) = ws.next().await {}
711 })
712 .await;
713 let mut stream = BinanceKlineStream::connect_with_config(
714 &["BTCUSDT".to_string()],
715 Interval::OneMinute,
716 test_config(base),
717 )
718 .await
719 .unwrap();
720 let event = stream
721 .next_event()
722 .await
723 .unwrap()
724 .expect("kline follows the ignored Pong");
725 assert_eq!(event.symbol, "btcusdt");
726 }
727
728 #[tokio::test]
729 async fn next_event_reconnects_after_a_server_close_frame() {
730 let kline = sample_kline_text();
731 let (base, server_done) = multi_shot_server(2, move |index, mut ws| {
732 let kline = kline.clone();
733 async move {
734 let msg = if index == 0 {
735 Message::Close(None)
738 } else {
739 Message::Text(kline.into())
740 };
741 let _ = ws.send(msg).await;
742 }
743 })
744 .await;
745 let mut stream = BinanceKlineStream::connect_with_config(
746 &["BTCUSDT".to_string()],
747 Interval::OneMinute,
748 test_config(base),
749 )
750 .await
751 .unwrap();
752 let event = stream
753 .next_event()
754 .await
755 .unwrap()
756 .expect("reconnect succeeds and the second connection serves a kline");
757 assert_eq!(event.symbol, "btcusdt");
758 tokio::time::timeout(Duration::from_secs(1), server_done)
760 .await
761 .unwrap()
762 .unwrap();
763 }
764
765 #[tokio::test]
766 async fn next_event_reconnects_after_a_read_timeout() {
767 let kline = sample_kline_text();
768 let stall_token = Arc::new(AtomicU32::new(0));
769 let stall_token_h = stall_token.clone();
770 let (base, server_done) = multi_shot_server(2, move |index, mut ws| {
771 let kline = kline.clone();
772 let stall_token = stall_token_h.clone();
773 async move {
774 if index == 0 {
775 stall_token.fetch_add(1, Ordering::SeqCst);
779 tokio::time::sleep(Duration::from_millis(250)).await;
780 } else {
781 let _ = ws.send(Message::Text(kline.into())).await;
782 }
783 }
784 })
785 .await;
786 let cfg = BinanceConfig {
787 read_timeout: Duration::from_millis(80),
788 ..test_config(base)
789 };
790 let mut stream = BinanceKlineStream::connect_with_config(
791 &["BTCUSDT".to_string()],
792 Interval::OneMinute,
793 cfg,
794 )
795 .await
796 .unwrap();
797 let event = stream
798 .next_event()
799 .await
800 .unwrap()
801 .expect("client times out, reconnects, and reads the kline");
802 assert_eq!(event.symbol, "btcusdt");
803 assert!(stall_token.load(Ordering::SeqCst) >= 1);
804 tokio::time::timeout(Duration::from_secs(1), server_done)
805 .await
806 .unwrap()
807 .unwrap();
808 }
809
810 #[tokio::test]
811 async fn next_event_yields_none_after_close() {
812 let base = one_shot_server(|mut ws| async move {
813 while let Some(Ok(_)) = ws.next().await {}
816 })
817 .await;
818 let mut stream = BinanceKlineStream::connect_with_config(
819 &["BTCUSDT".to_string()],
820 Interval::OneMinute,
821 test_config(base),
822 )
823 .await
824 .unwrap();
825 stream.close().await.unwrap();
826 assert!(stream.is_closed());
827 assert!(stream.next_event().await.unwrap().is_none());
828 }
829
830 #[tokio::test]
831 async fn next_event_surfaces_an_error_when_reconnect_attempts_are_exhausted() {
832 let base = one_shot_server(|mut ws| async move {
835 let _ = ws.send(Message::Close(None)).await;
836 })
840 .await;
841 let cfg = BinanceConfig {
842 max_reconnect_attempts: 2,
843 initial_reconnect_delay: Duration::from_millis(1),
844 max_reconnect_backoff: Duration::from_millis(2),
845 ..test_config(base)
846 };
847 let mut stream = BinanceKlineStream::connect_with_config(
848 &["BTCUSDT".to_string()],
849 Interval::OneMinute,
850 cfg,
851 )
852 .await
853 .unwrap();
854 let err = stream
855 .next_event()
856 .await
857 .expect_err("reconnect attempts are exhausted");
858 let _ = err;
861 }
862
863 #[tokio::test]
864 async fn next_event_skips_non_kline_frames_and_returns_the_next_kline() {
865 let kline = sample_kline_text();
869 let base = one_shot_server(move |mut ws| async move {
870 let _ = ws
871 .send(Message::Text(r#"{"result":null,"id":1}"#.into()))
872 .await;
873 let _ = ws
874 .send(Message::Binary(b"{\"id\":2}".to_vec().into()))
875 .await;
876 let _ = ws.send(Message::Text(kline.into())).await;
877 })
878 .await;
879 let mut stream = BinanceKlineStream::connect_with_config(
880 &["BTCUSDT".to_string()],
881 Interval::OneMinute,
882 test_config(base),
883 )
884 .await
885 .unwrap();
886 let event = stream
887 .next_event()
888 .await
889 .unwrap()
890 .expect("kline arrives after the two skipped control frames");
891 assert_eq!(event.symbol, "btcusdt");
892 }
893
894 #[tokio::test]
895 async fn next_event_propagates_a_parse_error_from_a_malformed_kline() {
896 let bad = r#"{"stream":"btcusdt@kline_1m","data":{"e":"kline","E":0,"s":"BTCUSDT","k":{"t":0,"T":0,"s":"BTCUSDT","i":"1m","o":"not-a-number","c":"0","h":"0","l":"0","v":"0","x":false}}}"#.to_string();
900 let base = one_shot_server(move |mut ws| async move {
901 let _ = ws.send(Message::Text(bad.into())).await;
902 while let Some(Ok(_)) = ws.next().await {}
903 })
904 .await;
905 let mut stream = BinanceKlineStream::connect_with_config(
906 &["BTCUSDT".to_string()],
907 Interval::OneMinute,
908 test_config(base),
909 )
910 .await
911 .unwrap();
912 let err = stream.next_event().await.unwrap_err();
913 assert!(matches!(err, Error::Malformed(_)));
914 }
915}