1#![allow(unused_imports)]
20use anyhow::Context;
21use async_trait::async_trait;
22use derive_builder::Builder;
23use rust_decimal::prelude::*;
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use std::{collections::BTreeMap, sync::Arc};
27
28use crate::common::{
29 errors::WebsocketError,
30 models::{ParamBuildError, WebsocketApiResponse},
31 utils::remove_empty_value,
32 websocket::{WebsocketApi, WebsocketMessageSendOptions},
33};
34use crate::spot::websocket_api::models;
35
36#[async_trait]
37pub trait MarketApi: Send + Sync {
38 async fn avg_price(
39 &self,
40 params: AvgPriceParams,
41 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AvgPriceResponseResult>>>;
42 async fn block_trades_historical(
43 &self,
44 params: BlockTradesHistoricalParams,
45 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::BlockTradesHistoricalResponseResultInner>>>;
46 async fn depth(
47 &self,
48 params: DepthParams,
49 ) -> anyhow::Result<WebsocketApiResponse<Box<models::DepthResponseResult>>>;
50 async fn klines(
51 &self,
52 params: KlinesParams,
53 ) -> anyhow::Result<WebsocketApiResponse<Vec<Vec<models::KlinesItemInner>>>>;
54 async fn reference_price(
55 &self,
56 params: ReferencePriceParams,
57 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ReferencePriceResponseResult>>>;
58 async fn reference_price_calculation(
59 &self,
60 params: ReferencePriceCalculationParams,
61 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ReferencePriceCalculationResponseResult>>>;
62 async fn ticker(
63 &self,
64 params: TickerParams,
65 ) -> anyhow::Result<WebsocketApiResponse<models::TickerResponse>>;
66 async fn ticker24hr(
67 &self,
68 params: Ticker24hrParams,
69 ) -> anyhow::Result<WebsocketApiResponse<models::Ticker24hrResponse>>;
70 async fn ticker_book(
71 &self,
72 params: TickerBookParams,
73 ) -> anyhow::Result<WebsocketApiResponse<models::TickerBookResponse>>;
74 async fn ticker_price(
75 &self,
76 params: TickerPriceParams,
77 ) -> anyhow::Result<WebsocketApiResponse<models::TickerPriceResponse>>;
78 async fn ticker_trading_day(
79 &self,
80 params: TickerTradingDayParams,
81 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TickerTradingDayResponseResultInner>>>;
82 async fn trades_aggregate(
83 &self,
84 params: TradesAggregateParams,
85 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesAggregateResponseResultInner>>>;
86 async fn trades_historical(
87 &self,
88 params: TradesHistoricalParams,
89 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesHistoricalResponseResultInner>>>;
90 async fn trades_recent(
91 &self,
92 params: TradesRecentParams,
93 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesRecentResponseResultInner>>>;
94 async fn ui_klines(
95 &self,
96 params: UiKlinesParams,
97 ) -> anyhow::Result<WebsocketApiResponse<Vec<Vec<models::KlinesItemInner>>>>;
98}
99
100#[derive(Clone)]
101pub struct MarketApiClient {
102 websocket_api_base: Arc<WebsocketApi>,
103}
104
105impl MarketApiClient {
106 pub fn new(websocket_api_base: Arc<WebsocketApi>) -> Self {
107 Self { websocket_api_base }
108 }
109}
110
111#[allow(non_camel_case_types)]
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum DepthSymbolStatusEnum {
114 #[serde(rename = "TRADING")]
115 Trading,
116 #[serde(rename = "END_OF_DAY")]
117 EndOfDay,
118 #[serde(rename = "HALT")]
119 Halt,
120 #[serde(rename = "BREAK")]
121 Break,
122 #[serde(rename = "NON_REPRESENTABLE")]
123 NonRepresentable,
124}
125
126impl DepthSymbolStatusEnum {
127 #[must_use]
128 pub fn as_str(&self) -> &'static str {
129 match self {
130 Self::Trading => "TRADING",
131 Self::EndOfDay => "END_OF_DAY",
132 Self::Halt => "HALT",
133 Self::Break => "BREAK",
134 Self::NonRepresentable => "NON_REPRESENTABLE",
135 }
136 }
137}
138
139impl std::str::FromStr for DepthSymbolStatusEnum {
140 type Err = Box<dyn std::error::Error + Send + Sync>;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 match s {
144 "TRADING" => Ok(Self::Trading),
145 "END_OF_DAY" => Ok(Self::EndOfDay),
146 "HALT" => Ok(Self::Halt),
147 "BREAK" => Ok(Self::Break),
148 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
149 other => Err(format!("invalid DepthSymbolStatusEnum: {}", other).into()),
150 }
151 }
152}
153
154#[allow(non_camel_case_types)]
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub enum KlinesIntervalEnum {
157 #[serde(rename = "1s")]
158 Interval1s,
159 #[serde(rename = "1m")]
160 Interval1m,
161 #[serde(rename = "3m")]
162 Interval3m,
163 #[serde(rename = "5m")]
164 Interval5m,
165 #[serde(rename = "15m")]
166 Interval15m,
167 #[serde(rename = "30m")]
168 Interval30m,
169 #[serde(rename = "1h")]
170 Interval1h,
171 #[serde(rename = "2h")]
172 Interval2h,
173 #[serde(rename = "4h")]
174 Interval4h,
175 #[serde(rename = "6h")]
176 Interval6h,
177 #[serde(rename = "8h")]
178 Interval8h,
179 #[serde(rename = "12h")]
180 Interval12h,
181 #[serde(rename = "1d")]
182 Interval1d,
183 #[serde(rename = "3d")]
184 Interval3d,
185 #[serde(rename = "1w")]
186 Interval1w,
187 #[serde(rename = "1M")]
188 Interval1M,
189}
190
191impl KlinesIntervalEnum {
192 #[must_use]
193 pub fn as_str(&self) -> &'static str {
194 match self {
195 Self::Interval1s => "1s",
196 Self::Interval1m => "1m",
197 Self::Interval3m => "3m",
198 Self::Interval5m => "5m",
199 Self::Interval15m => "15m",
200 Self::Interval30m => "30m",
201 Self::Interval1h => "1h",
202 Self::Interval2h => "2h",
203 Self::Interval4h => "4h",
204 Self::Interval6h => "6h",
205 Self::Interval8h => "8h",
206 Self::Interval12h => "12h",
207 Self::Interval1d => "1d",
208 Self::Interval3d => "3d",
209 Self::Interval1w => "1w",
210 Self::Interval1M => "1M",
211 }
212 }
213}
214
215impl std::str::FromStr for KlinesIntervalEnum {
216 type Err = Box<dyn std::error::Error + Send + Sync>;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 match s {
220 "1s" => Ok(Self::Interval1s),
221 "1m" => Ok(Self::Interval1m),
222 "3m" => Ok(Self::Interval3m),
223 "5m" => Ok(Self::Interval5m),
224 "15m" => Ok(Self::Interval15m),
225 "30m" => Ok(Self::Interval30m),
226 "1h" => Ok(Self::Interval1h),
227 "2h" => Ok(Self::Interval2h),
228 "4h" => Ok(Self::Interval4h),
229 "6h" => Ok(Self::Interval6h),
230 "8h" => Ok(Self::Interval8h),
231 "12h" => Ok(Self::Interval12h),
232 "1d" => Ok(Self::Interval1d),
233 "3d" => Ok(Self::Interval3d),
234 "1w" => Ok(Self::Interval1w),
235 "1M" => Ok(Self::Interval1M),
236 other => Err(format!("invalid KlinesIntervalEnum: {}", other).into()),
237 }
238 }
239}
240
241#[allow(non_camel_case_types)]
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub enum ReferencePriceCalculationSymbolStatusEnum {
244 #[serde(rename = "TRADING")]
245 Trading,
246 #[serde(rename = "END_OF_DAY")]
247 EndOfDay,
248 #[serde(rename = "HALT")]
249 Halt,
250 #[serde(rename = "BREAK")]
251 Break,
252 #[serde(rename = "NON_REPRESENTABLE")]
253 NonRepresentable,
254}
255
256impl ReferencePriceCalculationSymbolStatusEnum {
257 #[must_use]
258 pub fn as_str(&self) -> &'static str {
259 match self {
260 Self::Trading => "TRADING",
261 Self::EndOfDay => "END_OF_DAY",
262 Self::Halt => "HALT",
263 Self::Break => "BREAK",
264 Self::NonRepresentable => "NON_REPRESENTABLE",
265 }
266 }
267}
268
269impl std::str::FromStr for ReferencePriceCalculationSymbolStatusEnum {
270 type Err = Box<dyn std::error::Error + Send + Sync>;
271
272 fn from_str(s: &str) -> Result<Self, Self::Err> {
273 match s {
274 "TRADING" => Ok(Self::Trading),
275 "END_OF_DAY" => Ok(Self::EndOfDay),
276 "HALT" => Ok(Self::Halt),
277 "BREAK" => Ok(Self::Break),
278 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
279 other => Err(format!(
280 "invalid ReferencePriceCalculationSymbolStatusEnum: {}",
281 other
282 )
283 .into()),
284 }
285 }
286}
287
288#[allow(non_camel_case_types)]
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub enum TickerTypeEnum {
291 #[serde(rename = "FULL")]
292 Full,
293 #[serde(rename = "MINI")]
294 Mini,
295}
296
297impl TickerTypeEnum {
298 #[must_use]
299 pub fn as_str(&self) -> &'static str {
300 match self {
301 Self::Full => "FULL",
302 Self::Mini => "MINI",
303 }
304 }
305}
306
307impl std::str::FromStr for TickerTypeEnum {
308 type Err = Box<dyn std::error::Error + Send + Sync>;
309
310 fn from_str(s: &str) -> Result<Self, Self::Err> {
311 match s {
312 "FULL" => Ok(Self::Full),
313 "MINI" => Ok(Self::Mini),
314 other => Err(format!("invalid TickerTypeEnum: {}", other).into()),
315 }
316 }
317}
318
319#[allow(non_camel_case_types)]
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub enum TickerWindowSizeEnum {
322 #[serde(rename = "1m")]
323 WindowSize1m,
324 #[serde(rename = "2m")]
325 WindowSize2m,
326 #[serde(rename = "3m")]
327 WindowSize3m,
328 #[serde(rename = "4m")]
329 WindowSize4m,
330 #[serde(rename = "5m")]
331 WindowSize5m,
332 #[serde(rename = "6m")]
333 WindowSize6m,
334 #[serde(rename = "7m")]
335 WindowSize7m,
336 #[serde(rename = "8m")]
337 WindowSize8m,
338 #[serde(rename = "9m")]
339 WindowSize9m,
340 #[serde(rename = "10m")]
341 WindowSize10m,
342 #[serde(rename = "11m")]
343 WindowSize11m,
344 #[serde(rename = "12m")]
345 WindowSize12m,
346 #[serde(rename = "13m")]
347 WindowSize13m,
348 #[serde(rename = "14m")]
349 WindowSize14m,
350 #[serde(rename = "15m")]
351 WindowSize15m,
352 #[serde(rename = "16m")]
353 WindowSize16m,
354 #[serde(rename = "17m")]
355 WindowSize17m,
356 #[serde(rename = "18m")]
357 WindowSize18m,
358 #[serde(rename = "19m")]
359 WindowSize19m,
360 #[serde(rename = "20m")]
361 WindowSize20m,
362 #[serde(rename = "21m")]
363 WindowSize21m,
364 #[serde(rename = "22m")]
365 WindowSize22m,
366 #[serde(rename = "23m")]
367 WindowSize23m,
368 #[serde(rename = "24m")]
369 WindowSize24m,
370 #[serde(rename = "25m")]
371 WindowSize25m,
372 #[serde(rename = "26m")]
373 WindowSize26m,
374 #[serde(rename = "27m")]
375 WindowSize27m,
376 #[serde(rename = "28m")]
377 WindowSize28m,
378 #[serde(rename = "29m")]
379 WindowSize29m,
380 #[serde(rename = "30m")]
381 WindowSize30m,
382 #[serde(rename = "31m")]
383 WindowSize31m,
384 #[serde(rename = "32m")]
385 WindowSize32m,
386 #[serde(rename = "33m")]
387 WindowSize33m,
388 #[serde(rename = "34m")]
389 WindowSize34m,
390 #[serde(rename = "35m")]
391 WindowSize35m,
392 #[serde(rename = "36m")]
393 WindowSize36m,
394 #[serde(rename = "37m")]
395 WindowSize37m,
396 #[serde(rename = "38m")]
397 WindowSize38m,
398 #[serde(rename = "39m")]
399 WindowSize39m,
400 #[serde(rename = "40m")]
401 WindowSize40m,
402 #[serde(rename = "41m")]
403 WindowSize41m,
404 #[serde(rename = "42m")]
405 WindowSize42m,
406 #[serde(rename = "43m")]
407 WindowSize43m,
408 #[serde(rename = "44m")]
409 WindowSize44m,
410 #[serde(rename = "45m")]
411 WindowSize45m,
412 #[serde(rename = "46m")]
413 WindowSize46m,
414 #[serde(rename = "47m")]
415 WindowSize47m,
416 #[serde(rename = "48m")]
417 WindowSize48m,
418 #[serde(rename = "49m")]
419 WindowSize49m,
420 #[serde(rename = "50m")]
421 WindowSize50m,
422 #[serde(rename = "51m")]
423 WindowSize51m,
424 #[serde(rename = "52m")]
425 WindowSize52m,
426 #[serde(rename = "53m")]
427 WindowSize53m,
428 #[serde(rename = "54m")]
429 WindowSize54m,
430 #[serde(rename = "55m")]
431 WindowSize55m,
432 #[serde(rename = "56m")]
433 WindowSize56m,
434 #[serde(rename = "57m")]
435 WindowSize57m,
436 #[serde(rename = "58m")]
437 WindowSize58m,
438 #[serde(rename = "59m")]
439 WindowSize59m,
440 #[serde(rename = "1h")]
441 WindowSize1h,
442 #[serde(rename = "2h")]
443 WindowSize2h,
444 #[serde(rename = "3h")]
445 WindowSize3h,
446 #[serde(rename = "4h")]
447 WindowSize4h,
448 #[serde(rename = "5h")]
449 WindowSize5h,
450 #[serde(rename = "6h")]
451 WindowSize6h,
452 #[serde(rename = "7h")]
453 WindowSize7h,
454 #[serde(rename = "8h")]
455 WindowSize8h,
456 #[serde(rename = "9h")]
457 WindowSize9h,
458 #[serde(rename = "10h")]
459 WindowSize10h,
460 #[serde(rename = "11h")]
461 WindowSize11h,
462 #[serde(rename = "12h")]
463 WindowSize12h,
464 #[serde(rename = "13h")]
465 WindowSize13h,
466 #[serde(rename = "14h")]
467 WindowSize14h,
468 #[serde(rename = "15h")]
469 WindowSize15h,
470 #[serde(rename = "16h")]
471 WindowSize16h,
472 #[serde(rename = "17h")]
473 WindowSize17h,
474 #[serde(rename = "18h")]
475 WindowSize18h,
476 #[serde(rename = "19h")]
477 WindowSize19h,
478 #[serde(rename = "20h")]
479 WindowSize20h,
480 #[serde(rename = "21h")]
481 WindowSize21h,
482 #[serde(rename = "22h")]
483 WindowSize22h,
484 #[serde(rename = "23h")]
485 WindowSize23h,
486 #[serde(rename = "1d")]
487 WindowSize1d,
488 #[serde(rename = "2d")]
489 WindowSize2d,
490 #[serde(rename = "3d")]
491 WindowSize3d,
492 #[serde(rename = "4d")]
493 WindowSize4d,
494 #[serde(rename = "5d")]
495 WindowSize5d,
496 #[serde(rename = "6d")]
497 WindowSize6d,
498}
499
500impl TickerWindowSizeEnum {
501 #[must_use]
502 pub fn as_str(&self) -> &'static str {
503 match self {
504 Self::WindowSize1m => "1m",
505 Self::WindowSize2m => "2m",
506 Self::WindowSize3m => "3m",
507 Self::WindowSize4m => "4m",
508 Self::WindowSize5m => "5m",
509 Self::WindowSize6m => "6m",
510 Self::WindowSize7m => "7m",
511 Self::WindowSize8m => "8m",
512 Self::WindowSize9m => "9m",
513 Self::WindowSize10m => "10m",
514 Self::WindowSize11m => "11m",
515 Self::WindowSize12m => "12m",
516 Self::WindowSize13m => "13m",
517 Self::WindowSize14m => "14m",
518 Self::WindowSize15m => "15m",
519 Self::WindowSize16m => "16m",
520 Self::WindowSize17m => "17m",
521 Self::WindowSize18m => "18m",
522 Self::WindowSize19m => "19m",
523 Self::WindowSize20m => "20m",
524 Self::WindowSize21m => "21m",
525 Self::WindowSize22m => "22m",
526 Self::WindowSize23m => "23m",
527 Self::WindowSize24m => "24m",
528 Self::WindowSize25m => "25m",
529 Self::WindowSize26m => "26m",
530 Self::WindowSize27m => "27m",
531 Self::WindowSize28m => "28m",
532 Self::WindowSize29m => "29m",
533 Self::WindowSize30m => "30m",
534 Self::WindowSize31m => "31m",
535 Self::WindowSize32m => "32m",
536 Self::WindowSize33m => "33m",
537 Self::WindowSize34m => "34m",
538 Self::WindowSize35m => "35m",
539 Self::WindowSize36m => "36m",
540 Self::WindowSize37m => "37m",
541 Self::WindowSize38m => "38m",
542 Self::WindowSize39m => "39m",
543 Self::WindowSize40m => "40m",
544 Self::WindowSize41m => "41m",
545 Self::WindowSize42m => "42m",
546 Self::WindowSize43m => "43m",
547 Self::WindowSize44m => "44m",
548 Self::WindowSize45m => "45m",
549 Self::WindowSize46m => "46m",
550 Self::WindowSize47m => "47m",
551 Self::WindowSize48m => "48m",
552 Self::WindowSize49m => "49m",
553 Self::WindowSize50m => "50m",
554 Self::WindowSize51m => "51m",
555 Self::WindowSize52m => "52m",
556 Self::WindowSize53m => "53m",
557 Self::WindowSize54m => "54m",
558 Self::WindowSize55m => "55m",
559 Self::WindowSize56m => "56m",
560 Self::WindowSize57m => "57m",
561 Self::WindowSize58m => "58m",
562 Self::WindowSize59m => "59m",
563 Self::WindowSize1h => "1h",
564 Self::WindowSize2h => "2h",
565 Self::WindowSize3h => "3h",
566 Self::WindowSize4h => "4h",
567 Self::WindowSize5h => "5h",
568 Self::WindowSize6h => "6h",
569 Self::WindowSize7h => "7h",
570 Self::WindowSize8h => "8h",
571 Self::WindowSize9h => "9h",
572 Self::WindowSize10h => "10h",
573 Self::WindowSize11h => "11h",
574 Self::WindowSize12h => "12h",
575 Self::WindowSize13h => "13h",
576 Self::WindowSize14h => "14h",
577 Self::WindowSize15h => "15h",
578 Self::WindowSize16h => "16h",
579 Self::WindowSize17h => "17h",
580 Self::WindowSize18h => "18h",
581 Self::WindowSize19h => "19h",
582 Self::WindowSize20h => "20h",
583 Self::WindowSize21h => "21h",
584 Self::WindowSize22h => "22h",
585 Self::WindowSize23h => "23h",
586 Self::WindowSize1d => "1d",
587 Self::WindowSize2d => "2d",
588 Self::WindowSize3d => "3d",
589 Self::WindowSize4d => "4d",
590 Self::WindowSize5d => "5d",
591 Self::WindowSize6d => "6d",
592 }
593 }
594}
595
596impl std::str::FromStr for TickerWindowSizeEnum {
597 type Err = Box<dyn std::error::Error + Send + Sync>;
598
599 fn from_str(s: &str) -> Result<Self, Self::Err> {
600 match s {
601 "1m" => Ok(Self::WindowSize1m),
602 "2m" => Ok(Self::WindowSize2m),
603 "3m" => Ok(Self::WindowSize3m),
604 "4m" => Ok(Self::WindowSize4m),
605 "5m" => Ok(Self::WindowSize5m),
606 "6m" => Ok(Self::WindowSize6m),
607 "7m" => Ok(Self::WindowSize7m),
608 "8m" => Ok(Self::WindowSize8m),
609 "9m" => Ok(Self::WindowSize9m),
610 "10m" => Ok(Self::WindowSize10m),
611 "11m" => Ok(Self::WindowSize11m),
612 "12m" => Ok(Self::WindowSize12m),
613 "13m" => Ok(Self::WindowSize13m),
614 "14m" => Ok(Self::WindowSize14m),
615 "15m" => Ok(Self::WindowSize15m),
616 "16m" => Ok(Self::WindowSize16m),
617 "17m" => Ok(Self::WindowSize17m),
618 "18m" => Ok(Self::WindowSize18m),
619 "19m" => Ok(Self::WindowSize19m),
620 "20m" => Ok(Self::WindowSize20m),
621 "21m" => Ok(Self::WindowSize21m),
622 "22m" => Ok(Self::WindowSize22m),
623 "23m" => Ok(Self::WindowSize23m),
624 "24m" => Ok(Self::WindowSize24m),
625 "25m" => Ok(Self::WindowSize25m),
626 "26m" => Ok(Self::WindowSize26m),
627 "27m" => Ok(Self::WindowSize27m),
628 "28m" => Ok(Self::WindowSize28m),
629 "29m" => Ok(Self::WindowSize29m),
630 "30m" => Ok(Self::WindowSize30m),
631 "31m" => Ok(Self::WindowSize31m),
632 "32m" => Ok(Self::WindowSize32m),
633 "33m" => Ok(Self::WindowSize33m),
634 "34m" => Ok(Self::WindowSize34m),
635 "35m" => Ok(Self::WindowSize35m),
636 "36m" => Ok(Self::WindowSize36m),
637 "37m" => Ok(Self::WindowSize37m),
638 "38m" => Ok(Self::WindowSize38m),
639 "39m" => Ok(Self::WindowSize39m),
640 "40m" => Ok(Self::WindowSize40m),
641 "41m" => Ok(Self::WindowSize41m),
642 "42m" => Ok(Self::WindowSize42m),
643 "43m" => Ok(Self::WindowSize43m),
644 "44m" => Ok(Self::WindowSize44m),
645 "45m" => Ok(Self::WindowSize45m),
646 "46m" => Ok(Self::WindowSize46m),
647 "47m" => Ok(Self::WindowSize47m),
648 "48m" => Ok(Self::WindowSize48m),
649 "49m" => Ok(Self::WindowSize49m),
650 "50m" => Ok(Self::WindowSize50m),
651 "51m" => Ok(Self::WindowSize51m),
652 "52m" => Ok(Self::WindowSize52m),
653 "53m" => Ok(Self::WindowSize53m),
654 "54m" => Ok(Self::WindowSize54m),
655 "55m" => Ok(Self::WindowSize55m),
656 "56m" => Ok(Self::WindowSize56m),
657 "57m" => Ok(Self::WindowSize57m),
658 "58m" => Ok(Self::WindowSize58m),
659 "59m" => Ok(Self::WindowSize59m),
660 "1h" => Ok(Self::WindowSize1h),
661 "2h" => Ok(Self::WindowSize2h),
662 "3h" => Ok(Self::WindowSize3h),
663 "4h" => Ok(Self::WindowSize4h),
664 "5h" => Ok(Self::WindowSize5h),
665 "6h" => Ok(Self::WindowSize6h),
666 "7h" => Ok(Self::WindowSize7h),
667 "8h" => Ok(Self::WindowSize8h),
668 "9h" => Ok(Self::WindowSize9h),
669 "10h" => Ok(Self::WindowSize10h),
670 "11h" => Ok(Self::WindowSize11h),
671 "12h" => Ok(Self::WindowSize12h),
672 "13h" => Ok(Self::WindowSize13h),
673 "14h" => Ok(Self::WindowSize14h),
674 "15h" => Ok(Self::WindowSize15h),
675 "16h" => Ok(Self::WindowSize16h),
676 "17h" => Ok(Self::WindowSize17h),
677 "18h" => Ok(Self::WindowSize18h),
678 "19h" => Ok(Self::WindowSize19h),
679 "20h" => Ok(Self::WindowSize20h),
680 "21h" => Ok(Self::WindowSize21h),
681 "22h" => Ok(Self::WindowSize22h),
682 "23h" => Ok(Self::WindowSize23h),
683 "1d" => Ok(Self::WindowSize1d),
684 "2d" => Ok(Self::WindowSize2d),
685 "3d" => Ok(Self::WindowSize3d),
686 "4d" => Ok(Self::WindowSize4d),
687 "5d" => Ok(Self::WindowSize5d),
688 "6d" => Ok(Self::WindowSize6d),
689 other => Err(format!("invalid TickerWindowSizeEnum: {}", other).into()),
690 }
691 }
692}
693
694#[allow(non_camel_case_types)]
695#[derive(Debug, Clone, Serialize, Deserialize)]
696pub enum TickerSymbolStatusEnum {
697 #[serde(rename = "TRADING")]
698 Trading,
699 #[serde(rename = "END_OF_DAY")]
700 EndOfDay,
701 #[serde(rename = "HALT")]
702 Halt,
703 #[serde(rename = "BREAK")]
704 Break,
705 #[serde(rename = "NON_REPRESENTABLE")]
706 NonRepresentable,
707}
708
709impl TickerSymbolStatusEnum {
710 #[must_use]
711 pub fn as_str(&self) -> &'static str {
712 match self {
713 Self::Trading => "TRADING",
714 Self::EndOfDay => "END_OF_DAY",
715 Self::Halt => "HALT",
716 Self::Break => "BREAK",
717 Self::NonRepresentable => "NON_REPRESENTABLE",
718 }
719 }
720}
721
722impl std::str::FromStr for TickerSymbolStatusEnum {
723 type Err = Box<dyn std::error::Error + Send + Sync>;
724
725 fn from_str(s: &str) -> Result<Self, Self::Err> {
726 match s {
727 "TRADING" => Ok(Self::Trading),
728 "END_OF_DAY" => Ok(Self::EndOfDay),
729 "HALT" => Ok(Self::Halt),
730 "BREAK" => Ok(Self::Break),
731 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
732 other => Err(format!("invalid TickerSymbolStatusEnum: {}", other).into()),
733 }
734 }
735}
736
737#[allow(non_camel_case_types)]
738#[derive(Debug, Clone, Serialize, Deserialize)]
739pub enum Ticker24hrTypeEnum {
740 #[serde(rename = "FULL")]
741 Full,
742 #[serde(rename = "MINI")]
743 Mini,
744}
745
746impl Ticker24hrTypeEnum {
747 #[must_use]
748 pub fn as_str(&self) -> &'static str {
749 match self {
750 Self::Full => "FULL",
751 Self::Mini => "MINI",
752 }
753 }
754}
755
756impl std::str::FromStr for Ticker24hrTypeEnum {
757 type Err = Box<dyn std::error::Error + Send + Sync>;
758
759 fn from_str(s: &str) -> Result<Self, Self::Err> {
760 match s {
761 "FULL" => Ok(Self::Full),
762 "MINI" => Ok(Self::Mini),
763 other => Err(format!("invalid Ticker24hrTypeEnum: {}", other).into()),
764 }
765 }
766}
767
768#[allow(non_camel_case_types)]
769#[derive(Debug, Clone, Serialize, Deserialize)]
770pub enum Ticker24hrSymbolStatusEnum {
771 #[serde(rename = "TRADING")]
772 Trading,
773 #[serde(rename = "END_OF_DAY")]
774 EndOfDay,
775 #[serde(rename = "HALT")]
776 Halt,
777 #[serde(rename = "BREAK")]
778 Break,
779 #[serde(rename = "NON_REPRESENTABLE")]
780 NonRepresentable,
781}
782
783impl Ticker24hrSymbolStatusEnum {
784 #[must_use]
785 pub fn as_str(&self) -> &'static str {
786 match self {
787 Self::Trading => "TRADING",
788 Self::EndOfDay => "END_OF_DAY",
789 Self::Halt => "HALT",
790 Self::Break => "BREAK",
791 Self::NonRepresentable => "NON_REPRESENTABLE",
792 }
793 }
794}
795
796impl std::str::FromStr for Ticker24hrSymbolStatusEnum {
797 type Err = Box<dyn std::error::Error + Send + Sync>;
798
799 fn from_str(s: &str) -> Result<Self, Self::Err> {
800 match s {
801 "TRADING" => Ok(Self::Trading),
802 "END_OF_DAY" => Ok(Self::EndOfDay),
803 "HALT" => Ok(Self::Halt),
804 "BREAK" => Ok(Self::Break),
805 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
806 other => Err(format!("invalid Ticker24hrSymbolStatusEnum: {}", other).into()),
807 }
808 }
809}
810
811#[allow(non_camel_case_types)]
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub enum TickerBookSymbolStatusEnum {
814 #[serde(rename = "TRADING")]
815 Trading,
816 #[serde(rename = "END_OF_DAY")]
817 EndOfDay,
818 #[serde(rename = "HALT")]
819 Halt,
820 #[serde(rename = "BREAK")]
821 Break,
822 #[serde(rename = "NON_REPRESENTABLE")]
823 NonRepresentable,
824}
825
826impl TickerBookSymbolStatusEnum {
827 #[must_use]
828 pub fn as_str(&self) -> &'static str {
829 match self {
830 Self::Trading => "TRADING",
831 Self::EndOfDay => "END_OF_DAY",
832 Self::Halt => "HALT",
833 Self::Break => "BREAK",
834 Self::NonRepresentable => "NON_REPRESENTABLE",
835 }
836 }
837}
838
839impl std::str::FromStr for TickerBookSymbolStatusEnum {
840 type Err = Box<dyn std::error::Error + Send + Sync>;
841
842 fn from_str(s: &str) -> Result<Self, Self::Err> {
843 match s {
844 "TRADING" => Ok(Self::Trading),
845 "END_OF_DAY" => Ok(Self::EndOfDay),
846 "HALT" => Ok(Self::Halt),
847 "BREAK" => Ok(Self::Break),
848 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
849 other => Err(format!("invalid TickerBookSymbolStatusEnum: {}", other).into()),
850 }
851 }
852}
853
854#[allow(non_camel_case_types)]
855#[derive(Debug, Clone, Serialize, Deserialize)]
856pub enum TickerPriceSymbolStatusEnum {
857 #[serde(rename = "TRADING")]
858 Trading,
859 #[serde(rename = "END_OF_DAY")]
860 EndOfDay,
861 #[serde(rename = "HALT")]
862 Halt,
863 #[serde(rename = "BREAK")]
864 Break,
865 #[serde(rename = "NON_REPRESENTABLE")]
866 NonRepresentable,
867}
868
869impl TickerPriceSymbolStatusEnum {
870 #[must_use]
871 pub fn as_str(&self) -> &'static str {
872 match self {
873 Self::Trading => "TRADING",
874 Self::EndOfDay => "END_OF_DAY",
875 Self::Halt => "HALT",
876 Self::Break => "BREAK",
877 Self::NonRepresentable => "NON_REPRESENTABLE",
878 }
879 }
880}
881
882impl std::str::FromStr for TickerPriceSymbolStatusEnum {
883 type Err = Box<dyn std::error::Error + Send + Sync>;
884
885 fn from_str(s: &str) -> Result<Self, Self::Err> {
886 match s {
887 "TRADING" => Ok(Self::Trading),
888 "END_OF_DAY" => Ok(Self::EndOfDay),
889 "HALT" => Ok(Self::Halt),
890 "BREAK" => Ok(Self::Break),
891 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
892 other => Err(format!("invalid TickerPriceSymbolStatusEnum: {}", other).into()),
893 }
894 }
895}
896
897#[allow(non_camel_case_types)]
898#[derive(Debug, Clone, Serialize, Deserialize)]
899pub enum TickerTradingDayTypeEnum {
900 #[serde(rename = "FULL")]
901 Full,
902 #[serde(rename = "MINI")]
903 Mini,
904}
905
906impl TickerTradingDayTypeEnum {
907 #[must_use]
908 pub fn as_str(&self) -> &'static str {
909 match self {
910 Self::Full => "FULL",
911 Self::Mini => "MINI",
912 }
913 }
914}
915
916impl std::str::FromStr for TickerTradingDayTypeEnum {
917 type Err = Box<dyn std::error::Error + Send + Sync>;
918
919 fn from_str(s: &str) -> Result<Self, Self::Err> {
920 match s {
921 "FULL" => Ok(Self::Full),
922 "MINI" => Ok(Self::Mini),
923 other => Err(format!("invalid TickerTradingDayTypeEnum: {}", other).into()),
924 }
925 }
926}
927
928#[allow(non_camel_case_types)]
929#[derive(Debug, Clone, Serialize, Deserialize)]
930pub enum TickerTradingDaySymbolStatusEnum {
931 #[serde(rename = "TRADING")]
932 Trading,
933 #[serde(rename = "END_OF_DAY")]
934 EndOfDay,
935 #[serde(rename = "HALT")]
936 Halt,
937 #[serde(rename = "BREAK")]
938 Break,
939 #[serde(rename = "NON_REPRESENTABLE")]
940 NonRepresentable,
941}
942
943impl TickerTradingDaySymbolStatusEnum {
944 #[must_use]
945 pub fn as_str(&self) -> &'static str {
946 match self {
947 Self::Trading => "TRADING",
948 Self::EndOfDay => "END_OF_DAY",
949 Self::Halt => "HALT",
950 Self::Break => "BREAK",
951 Self::NonRepresentable => "NON_REPRESENTABLE",
952 }
953 }
954}
955
956impl std::str::FromStr for TickerTradingDaySymbolStatusEnum {
957 type Err = Box<dyn std::error::Error + Send + Sync>;
958
959 fn from_str(s: &str) -> Result<Self, Self::Err> {
960 match s {
961 "TRADING" => Ok(Self::Trading),
962 "END_OF_DAY" => Ok(Self::EndOfDay),
963 "HALT" => Ok(Self::Halt),
964 "BREAK" => Ok(Self::Break),
965 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
966 other => Err(format!("invalid TickerTradingDaySymbolStatusEnum: {}", other).into()),
967 }
968 }
969}
970
971#[allow(non_camel_case_types)]
972#[derive(Debug, Clone, Serialize, Deserialize)]
973pub enum UiKlinesIntervalEnum {
974 #[serde(rename = "1s")]
975 Interval1s,
976 #[serde(rename = "1m")]
977 Interval1m,
978 #[serde(rename = "3m")]
979 Interval3m,
980 #[serde(rename = "5m")]
981 Interval5m,
982 #[serde(rename = "15m")]
983 Interval15m,
984 #[serde(rename = "30m")]
985 Interval30m,
986 #[serde(rename = "1h")]
987 Interval1h,
988 #[serde(rename = "2h")]
989 Interval2h,
990 #[serde(rename = "4h")]
991 Interval4h,
992 #[serde(rename = "6h")]
993 Interval6h,
994 #[serde(rename = "8h")]
995 Interval8h,
996 #[serde(rename = "12h")]
997 Interval12h,
998 #[serde(rename = "1d")]
999 Interval1d,
1000 #[serde(rename = "3d")]
1001 Interval3d,
1002 #[serde(rename = "1w")]
1003 Interval1w,
1004 #[serde(rename = "1M")]
1005 Interval1M,
1006}
1007
1008impl UiKlinesIntervalEnum {
1009 #[must_use]
1010 pub fn as_str(&self) -> &'static str {
1011 match self {
1012 Self::Interval1s => "1s",
1013 Self::Interval1m => "1m",
1014 Self::Interval3m => "3m",
1015 Self::Interval5m => "5m",
1016 Self::Interval15m => "15m",
1017 Self::Interval30m => "30m",
1018 Self::Interval1h => "1h",
1019 Self::Interval2h => "2h",
1020 Self::Interval4h => "4h",
1021 Self::Interval6h => "6h",
1022 Self::Interval8h => "8h",
1023 Self::Interval12h => "12h",
1024 Self::Interval1d => "1d",
1025 Self::Interval3d => "3d",
1026 Self::Interval1w => "1w",
1027 Self::Interval1M => "1M",
1028 }
1029 }
1030}
1031
1032impl std::str::FromStr for UiKlinesIntervalEnum {
1033 type Err = Box<dyn std::error::Error + Send + Sync>;
1034
1035 fn from_str(s: &str) -> Result<Self, Self::Err> {
1036 match s {
1037 "1s" => Ok(Self::Interval1s),
1038 "1m" => Ok(Self::Interval1m),
1039 "3m" => Ok(Self::Interval3m),
1040 "5m" => Ok(Self::Interval5m),
1041 "15m" => Ok(Self::Interval15m),
1042 "30m" => Ok(Self::Interval30m),
1043 "1h" => Ok(Self::Interval1h),
1044 "2h" => Ok(Self::Interval2h),
1045 "4h" => Ok(Self::Interval4h),
1046 "6h" => Ok(Self::Interval6h),
1047 "8h" => Ok(Self::Interval8h),
1048 "12h" => Ok(Self::Interval12h),
1049 "1d" => Ok(Self::Interval1d),
1050 "3d" => Ok(Self::Interval3d),
1051 "1w" => Ok(Self::Interval1w),
1052 "1M" => Ok(Self::Interval1M),
1053 other => Err(format!("invalid UiKlinesIntervalEnum: {}", other).into()),
1054 }
1055 }
1056}
1057
1058#[derive(Clone, Debug, Builder)]
1063#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1064pub struct AvgPriceParams {
1065 #[builder(setter(into))]
1070 pub symbol: String,
1071 #[builder(setter(into), default)]
1075 pub id: Option<String>,
1076}
1077
1078impl AvgPriceParams {
1079 #[must_use]
1086 pub fn builder(symbol: String) -> AvgPriceParamsBuilder {
1087 AvgPriceParamsBuilder::default().symbol(symbol)
1088 }
1089}
1090#[derive(Clone, Debug, Builder)]
1095#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1096pub struct BlockTradesHistoricalParams {
1097 #[builder(setter(into))]
1102 pub symbol: String,
1103 #[builder(setter(into))]
1107 pub from_id: i64,
1108 #[builder(setter(into), default)]
1112 pub id: Option<String>,
1113 #[builder(setter(into), default)]
1117 pub limit: Option<i64>,
1118}
1119
1120impl BlockTradesHistoricalParams {
1121 #[must_use]
1129 pub fn builder(symbol: String, from_id: i64) -> BlockTradesHistoricalParamsBuilder {
1130 BlockTradesHistoricalParamsBuilder::default()
1131 .symbol(symbol)
1132 .from_id(from_id)
1133 }
1134}
1135#[derive(Clone, Debug, Builder)]
1140#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1141pub struct DepthParams {
1142 #[builder(setter(into))]
1147 pub symbol: String,
1148 #[builder(setter(into), default)]
1152 pub id: Option<String>,
1153 #[builder(setter(into), default)]
1157 pub limit: Option<i32>,
1158 #[builder(setter(into), default)]
1163 pub symbol_status: Option<DepthSymbolStatusEnum>,
1164}
1165
1166impl DepthParams {
1167 #[must_use]
1174 pub fn builder(symbol: String) -> DepthParamsBuilder {
1175 DepthParamsBuilder::default().symbol(symbol)
1176 }
1177}
1178#[derive(Clone, Debug, Builder)]
1183#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1184pub struct KlinesParams {
1185 #[builder(setter(into))]
1190 pub symbol: String,
1191 #[builder(setter(into))]
1196 pub interval: KlinesIntervalEnum,
1197 #[builder(setter(into), default)]
1201 pub id: Option<String>,
1202 #[builder(setter(into), default)]
1207 pub start_time: Option<i64>,
1208 #[builder(setter(into), default)]
1213 pub end_time: Option<i64>,
1214 #[builder(setter(into), default)]
1218 pub time_zone: Option<String>,
1219 #[builder(setter(into), default)]
1223 pub limit: Option<i32>,
1224}
1225
1226impl KlinesParams {
1227 #[must_use]
1235 pub fn builder(symbol: String, interval: KlinesIntervalEnum) -> KlinesParamsBuilder {
1236 KlinesParamsBuilder::default()
1237 .symbol(symbol)
1238 .interval(interval)
1239 }
1240}
1241#[derive(Clone, Debug, Builder)]
1246#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1247pub struct ReferencePriceParams {
1248 #[builder(setter(into))]
1253 pub symbol: String,
1254 #[builder(setter(into), default)]
1258 pub id: Option<String>,
1259}
1260
1261impl ReferencePriceParams {
1262 #[must_use]
1269 pub fn builder(symbol: String) -> ReferencePriceParamsBuilder {
1270 ReferencePriceParamsBuilder::default().symbol(symbol)
1271 }
1272}
1273#[derive(Clone, Debug, Builder)]
1278#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1279pub struct ReferencePriceCalculationParams {
1280 #[builder(setter(into))]
1285 pub symbol: String,
1286 #[builder(setter(into), default)]
1290 pub id: Option<String>,
1291 #[builder(setter(into), default)]
1296 pub symbol_status: Option<ReferencePriceCalculationSymbolStatusEnum>,
1297}
1298
1299impl ReferencePriceCalculationParams {
1300 #[must_use]
1307 pub fn builder(symbol: String) -> ReferencePriceCalculationParamsBuilder {
1308 ReferencePriceCalculationParamsBuilder::default().symbol(symbol)
1309 }
1310}
1311#[derive(Clone, Debug, Builder, Default)]
1316#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1317pub struct TickerParams {
1318 #[builder(setter(into), default)]
1322 pub id: Option<String>,
1323 #[builder(setter(into), default)]
1327 pub symbol: Option<String>,
1328 #[builder(setter(into), default)]
1332 pub symbols: Option<Vec<String>>,
1333 #[builder(setter(into), default)]
1338 pub r#type: Option<TickerTypeEnum>,
1339 #[builder(setter(into), default)]
1344 pub window_size: Option<TickerWindowSizeEnum>,
1345 #[builder(setter(into), default)]
1350 pub symbol_status: Option<TickerSymbolStatusEnum>,
1351}
1352
1353impl TickerParams {
1354 #[must_use]
1357 pub fn builder() -> TickerParamsBuilder {
1358 TickerParamsBuilder::default()
1359 }
1360}
1361#[derive(Clone, Debug, Builder, Default)]
1366#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1367pub struct Ticker24hrParams {
1368 #[builder(setter(into), default)]
1372 pub id: Option<String>,
1373 #[builder(setter(into), default)]
1377 pub symbol: Option<String>,
1378 #[builder(setter(into), default)]
1382 pub symbols: Option<Vec<String>>,
1383 #[builder(setter(into), default)]
1388 pub r#type: Option<Ticker24hrTypeEnum>,
1389 #[builder(setter(into), default)]
1394 pub symbol_status: Option<Ticker24hrSymbolStatusEnum>,
1395}
1396
1397impl Ticker24hrParams {
1398 #[must_use]
1401 pub fn builder() -> Ticker24hrParamsBuilder {
1402 Ticker24hrParamsBuilder::default()
1403 }
1404}
1405#[derive(Clone, Debug, Builder, Default)]
1410#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1411pub struct TickerBookParams {
1412 #[builder(setter(into), default)]
1416 pub id: Option<String>,
1417 #[builder(setter(into), default)]
1421 pub symbol: Option<String>,
1422 #[builder(setter(into), default)]
1426 pub symbols: Option<Vec<String>>,
1427 #[builder(setter(into), default)]
1432 pub symbol_status: Option<TickerBookSymbolStatusEnum>,
1433}
1434
1435impl TickerBookParams {
1436 #[must_use]
1439 pub fn builder() -> TickerBookParamsBuilder {
1440 TickerBookParamsBuilder::default()
1441 }
1442}
1443#[derive(Clone, Debug, Builder, Default)]
1448#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1449pub struct TickerPriceParams {
1450 #[builder(setter(into), default)]
1454 pub id: Option<String>,
1455 #[builder(setter(into), default)]
1459 pub symbol: Option<String>,
1460 #[builder(setter(into), default)]
1464 pub symbols: Option<Vec<String>>,
1465 #[builder(setter(into), default)]
1470 pub symbol_status: Option<TickerPriceSymbolStatusEnum>,
1471}
1472
1473impl TickerPriceParams {
1474 #[must_use]
1477 pub fn builder() -> TickerPriceParamsBuilder {
1478 TickerPriceParamsBuilder::default()
1479 }
1480}
1481#[derive(Clone, Debug, Builder, Default)]
1486#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1487pub struct TickerTradingDayParams {
1488 #[builder(setter(into), default)]
1492 pub id: Option<String>,
1493 #[builder(setter(into), default)]
1497 pub symbol: Option<String>,
1498 #[builder(setter(into), default)]
1502 pub symbols: Option<Vec<String>>,
1503 #[builder(setter(into), default)]
1507 pub time_zone: Option<String>,
1508 #[builder(setter(into), default)]
1513 pub r#type: Option<TickerTradingDayTypeEnum>,
1514 #[builder(setter(into), default)]
1519 pub symbol_status: Option<TickerTradingDaySymbolStatusEnum>,
1520}
1521
1522impl TickerTradingDayParams {
1523 #[must_use]
1526 pub fn builder() -> TickerTradingDayParamsBuilder {
1527 TickerTradingDayParamsBuilder::default()
1528 }
1529}
1530#[derive(Clone, Debug, Builder)]
1535#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1536pub struct TradesAggregateParams {
1537 #[builder(setter(into))]
1542 pub symbol: String,
1543 #[builder(setter(into), default)]
1547 pub id: Option<String>,
1548 #[builder(setter(into), default)]
1552 pub from_id: Option<i64>,
1553 #[builder(setter(into), default)]
1558 pub start_time: Option<i64>,
1559 #[builder(setter(into), default)]
1564 pub end_time: Option<i64>,
1565 #[builder(setter(into), default)]
1569 pub limit: Option<i64>,
1570}
1571
1572impl TradesAggregateParams {
1573 #[must_use]
1580 pub fn builder(symbol: String) -> TradesAggregateParamsBuilder {
1581 TradesAggregateParamsBuilder::default().symbol(symbol)
1582 }
1583}
1584#[derive(Clone, Debug, Builder)]
1589#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1590pub struct TradesHistoricalParams {
1591 #[builder(setter(into))]
1596 pub symbol: String,
1597 #[builder(setter(into), default)]
1601 pub id: Option<String>,
1602 #[builder(setter(into), default)]
1606 pub from_id: Option<i32>,
1607 #[builder(setter(into), default)]
1611 pub limit: Option<i32>,
1612}
1613
1614impl TradesHistoricalParams {
1615 #[must_use]
1622 pub fn builder(symbol: String) -> TradesHistoricalParamsBuilder {
1623 TradesHistoricalParamsBuilder::default().symbol(symbol)
1624 }
1625}
1626#[derive(Clone, Debug, Builder)]
1631#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1632pub struct TradesRecentParams {
1633 #[builder(setter(into))]
1638 pub symbol: String,
1639 #[builder(setter(into), default)]
1643 pub id: Option<String>,
1644 #[builder(setter(into), default)]
1648 pub limit: Option<i32>,
1649}
1650
1651impl TradesRecentParams {
1652 #[must_use]
1659 pub fn builder(symbol: String) -> TradesRecentParamsBuilder {
1660 TradesRecentParamsBuilder::default().symbol(symbol)
1661 }
1662}
1663#[derive(Clone, Debug, Builder)]
1668#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
1669pub struct UiKlinesParams {
1670 #[builder(setter(into))]
1675 pub symbol: String,
1676 #[builder(setter(into))]
1681 pub interval: UiKlinesIntervalEnum,
1682 #[builder(setter(into), default)]
1686 pub id: Option<String>,
1687 #[builder(setter(into), default)]
1692 pub start_time: Option<i64>,
1693 #[builder(setter(into), default)]
1698 pub end_time: Option<i64>,
1699 #[builder(setter(into), default)]
1703 pub time_zone: Option<String>,
1704 #[builder(setter(into), default)]
1708 pub limit: Option<i32>,
1709}
1710
1711impl UiKlinesParams {
1712 #[must_use]
1720 pub fn builder(symbol: String, interval: UiKlinesIntervalEnum) -> UiKlinesParamsBuilder {
1721 UiKlinesParamsBuilder::default()
1722 .symbol(symbol)
1723 .interval(interval)
1724 }
1725}
1726
1727#[async_trait]
1728impl MarketApi for MarketApiClient {
1729 async fn avg_price(
1730 &self,
1731 params: AvgPriceParams,
1732 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AvgPriceResponseResult>>> {
1733 let AvgPriceParams { symbol, id } = params;
1734
1735 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1736 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1737 if let Some(value) = id {
1738 payload.insert("id".to_string(), serde_json::json!(value));
1739 }
1740 let payload = remove_empty_value(payload);
1741
1742 self.websocket_api_base
1743 .send_message::<Box<models::AvgPriceResponseResult>>(
1744 "/avgPrice".trim_start_matches('/'),
1745 payload,
1746 WebsocketMessageSendOptions::new(),
1747 )
1748 .await
1749 .map_err(anyhow::Error::from)?
1750 .into_iter()
1751 .next()
1752 .ok_or(WebsocketError::NoResponse)
1753 .map_err(anyhow::Error::from)
1754 }
1755
1756 async fn block_trades_historical(
1757 &self,
1758 params: BlockTradesHistoricalParams,
1759 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::BlockTradesHistoricalResponseResultInner>>>
1760 {
1761 let BlockTradesHistoricalParams {
1762 symbol,
1763 from_id,
1764 id,
1765 limit,
1766 } = params;
1767
1768 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1769 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1770 payload.insert("fromId".to_string(), serde_json::json!(from_id));
1771 if let Some(value) = id {
1772 payload.insert("id".to_string(), serde_json::json!(value));
1773 }
1774 if let Some(value) = limit {
1775 payload.insert("limit".to_string(), serde_json::json!(value));
1776 }
1777 let payload = remove_empty_value(payload);
1778
1779 self.websocket_api_base
1780 .send_message::<Vec<models::BlockTradesHistoricalResponseResultInner>>(
1781 "/blockTrades.historical".trim_start_matches('/'),
1782 payload,
1783 WebsocketMessageSendOptions::new(),
1784 )
1785 .await
1786 .map_err(anyhow::Error::from)?
1787 .into_iter()
1788 .next()
1789 .ok_or(WebsocketError::NoResponse)
1790 .map_err(anyhow::Error::from)
1791 }
1792
1793 async fn depth(
1794 &self,
1795 params: DepthParams,
1796 ) -> anyhow::Result<WebsocketApiResponse<Box<models::DepthResponseResult>>> {
1797 let DepthParams {
1798 symbol,
1799 id,
1800 limit,
1801 symbol_status,
1802 } = params;
1803
1804 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1805 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1806 if let Some(value) = id {
1807 payload.insert("id".to_string(), serde_json::json!(value));
1808 }
1809 if let Some(value) = limit {
1810 payload.insert("limit".to_string(), serde_json::json!(value));
1811 }
1812 if let Some(value) = symbol_status {
1813 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
1814 }
1815 let payload = remove_empty_value(payload);
1816
1817 self.websocket_api_base
1818 .send_message::<Box<models::DepthResponseResult>>(
1819 "/depth".trim_start_matches('/'),
1820 payload,
1821 WebsocketMessageSendOptions::new(),
1822 )
1823 .await
1824 .map_err(anyhow::Error::from)?
1825 .into_iter()
1826 .next()
1827 .ok_or(WebsocketError::NoResponse)
1828 .map_err(anyhow::Error::from)
1829 }
1830
1831 async fn klines(
1832 &self,
1833 params: KlinesParams,
1834 ) -> anyhow::Result<WebsocketApiResponse<Vec<Vec<models::KlinesItemInner>>>> {
1835 let KlinesParams {
1836 symbol,
1837 interval,
1838 id,
1839 start_time,
1840 end_time,
1841 time_zone,
1842 limit,
1843 } = params;
1844
1845 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1846 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1847 payload.insert("interval".to_string(), serde_json::json!(interval));
1848 if let Some(value) = id {
1849 payload.insert("id".to_string(), serde_json::json!(value));
1850 }
1851 if let Some(value) = start_time {
1852 payload.insert("startTime".to_string(), serde_json::json!(value));
1853 }
1854 if let Some(value) = end_time {
1855 payload.insert("endTime".to_string(), serde_json::json!(value));
1856 }
1857 if let Some(value) = time_zone {
1858 payload.insert("timeZone".to_string(), serde_json::json!(value));
1859 }
1860 if let Some(value) = limit {
1861 payload.insert("limit".to_string(), serde_json::json!(value));
1862 }
1863 let payload = remove_empty_value(payload);
1864
1865 self.websocket_api_base
1866 .send_message::<Vec<Vec<models::KlinesItemInner>>>(
1867 "/klines".trim_start_matches('/'),
1868 payload,
1869 WebsocketMessageSendOptions::new(),
1870 )
1871 .await
1872 .map_err(anyhow::Error::from)?
1873 .into_iter()
1874 .next()
1875 .ok_or(WebsocketError::NoResponse)
1876 .map_err(anyhow::Error::from)
1877 }
1878
1879 async fn reference_price(
1880 &self,
1881 params: ReferencePriceParams,
1882 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ReferencePriceResponseResult>>> {
1883 let ReferencePriceParams { symbol, id } = params;
1884
1885 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1886 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1887 if let Some(value) = id {
1888 payload.insert("id".to_string(), serde_json::json!(value));
1889 }
1890 let payload = remove_empty_value(payload);
1891
1892 self.websocket_api_base
1893 .send_message::<Box<models::ReferencePriceResponseResult>>(
1894 "/referencePrice".trim_start_matches('/'),
1895 payload,
1896 WebsocketMessageSendOptions::new(),
1897 )
1898 .await
1899 .map_err(anyhow::Error::from)?
1900 .into_iter()
1901 .next()
1902 .ok_or(WebsocketError::NoResponse)
1903 .map_err(anyhow::Error::from)
1904 }
1905
1906 async fn reference_price_calculation(
1907 &self,
1908 params: ReferencePriceCalculationParams,
1909 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ReferencePriceCalculationResponseResult>>>
1910 {
1911 let ReferencePriceCalculationParams {
1912 symbol,
1913 id,
1914 symbol_status,
1915 } = params;
1916
1917 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1918 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1919 if let Some(value) = id {
1920 payload.insert("id".to_string(), serde_json::json!(value));
1921 }
1922 if let Some(value) = symbol_status {
1923 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
1924 }
1925 let payload = remove_empty_value(payload);
1926
1927 self.websocket_api_base
1928 .send_message::<Box<models::ReferencePriceCalculationResponseResult>>(
1929 "/referencePrice.calculation".trim_start_matches('/'),
1930 payload,
1931 WebsocketMessageSendOptions::new(),
1932 )
1933 .await
1934 .map_err(anyhow::Error::from)?
1935 .into_iter()
1936 .next()
1937 .ok_or(WebsocketError::NoResponse)
1938 .map_err(anyhow::Error::from)
1939 }
1940
1941 async fn ticker(
1942 &self,
1943 params: TickerParams,
1944 ) -> anyhow::Result<WebsocketApiResponse<models::TickerResponse>> {
1945 let TickerParams {
1946 id,
1947 symbol,
1948 symbols,
1949 r#type,
1950 window_size,
1951 symbol_status,
1952 } = params;
1953
1954 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1955 if let Some(value) = id {
1956 payload.insert("id".to_string(), serde_json::json!(value));
1957 }
1958 if let Some(value) = symbol {
1959 payload.insert("symbol".to_string(), serde_json::json!(value));
1960 }
1961 if let Some(value) = symbols {
1962 payload.insert("symbols".to_string(), serde_json::json!(value));
1963 }
1964 if let Some(value) = r#type {
1965 payload.insert("type".to_string(), serde_json::json!(value));
1966 }
1967 if let Some(value) = window_size {
1968 payload.insert("windowSize".to_string(), serde_json::json!(value));
1969 }
1970 if let Some(value) = symbol_status {
1971 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
1972 }
1973 let payload = remove_empty_value(payload);
1974
1975 self.websocket_api_base
1976 .send_message::<models::TickerResponse>(
1977 "/ticker".trim_start_matches('/'),
1978 payload,
1979 WebsocketMessageSendOptions::new(),
1980 )
1981 .await
1982 .map_err(anyhow::Error::from)?
1983 .into_iter()
1984 .next()
1985 .ok_or(WebsocketError::NoResponse)
1986 .map_err(anyhow::Error::from)
1987 }
1988
1989 async fn ticker24hr(
1990 &self,
1991 params: Ticker24hrParams,
1992 ) -> anyhow::Result<WebsocketApiResponse<models::Ticker24hrResponse>> {
1993 let Ticker24hrParams {
1994 id,
1995 symbol,
1996 symbols,
1997 r#type,
1998 symbol_status,
1999 } = params;
2000
2001 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2002 if let Some(value) = id {
2003 payload.insert("id".to_string(), serde_json::json!(value));
2004 }
2005 if let Some(value) = symbol {
2006 payload.insert("symbol".to_string(), serde_json::json!(value));
2007 }
2008 if let Some(value) = symbols {
2009 payload.insert("symbols".to_string(), serde_json::json!(value));
2010 }
2011 if let Some(value) = r#type {
2012 payload.insert("type".to_string(), serde_json::json!(value));
2013 }
2014 if let Some(value) = symbol_status {
2015 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
2016 }
2017 let payload = remove_empty_value(payload);
2018
2019 self.websocket_api_base
2020 .send_message::<models::Ticker24hrResponse>(
2021 "/ticker.24hr".trim_start_matches('/'),
2022 payload,
2023 WebsocketMessageSendOptions::new(),
2024 )
2025 .await
2026 .map_err(anyhow::Error::from)?
2027 .into_iter()
2028 .next()
2029 .ok_or(WebsocketError::NoResponse)
2030 .map_err(anyhow::Error::from)
2031 }
2032
2033 async fn ticker_book(
2034 &self,
2035 params: TickerBookParams,
2036 ) -> anyhow::Result<WebsocketApiResponse<models::TickerBookResponse>> {
2037 let TickerBookParams {
2038 id,
2039 symbol,
2040 symbols,
2041 symbol_status,
2042 } = params;
2043
2044 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2045 if let Some(value) = id {
2046 payload.insert("id".to_string(), serde_json::json!(value));
2047 }
2048 if let Some(value) = symbol {
2049 payload.insert("symbol".to_string(), serde_json::json!(value));
2050 }
2051 if let Some(value) = symbols {
2052 payload.insert("symbols".to_string(), serde_json::json!(value));
2053 }
2054 if let Some(value) = symbol_status {
2055 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
2056 }
2057 let payload = remove_empty_value(payload);
2058
2059 self.websocket_api_base
2060 .send_message::<models::TickerBookResponse>(
2061 "/ticker.book".trim_start_matches('/'),
2062 payload,
2063 WebsocketMessageSendOptions::new(),
2064 )
2065 .await
2066 .map_err(anyhow::Error::from)?
2067 .into_iter()
2068 .next()
2069 .ok_or(WebsocketError::NoResponse)
2070 .map_err(anyhow::Error::from)
2071 }
2072
2073 async fn ticker_price(
2074 &self,
2075 params: TickerPriceParams,
2076 ) -> anyhow::Result<WebsocketApiResponse<models::TickerPriceResponse>> {
2077 let TickerPriceParams {
2078 id,
2079 symbol,
2080 symbols,
2081 symbol_status,
2082 } = params;
2083
2084 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2085 if let Some(value) = id {
2086 payload.insert("id".to_string(), serde_json::json!(value));
2087 }
2088 if let Some(value) = symbol {
2089 payload.insert("symbol".to_string(), serde_json::json!(value));
2090 }
2091 if let Some(value) = symbols {
2092 payload.insert("symbols".to_string(), serde_json::json!(value));
2093 }
2094 if let Some(value) = symbol_status {
2095 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
2096 }
2097 let payload = remove_empty_value(payload);
2098
2099 self.websocket_api_base
2100 .send_message::<models::TickerPriceResponse>(
2101 "/ticker.price".trim_start_matches('/'),
2102 payload,
2103 WebsocketMessageSendOptions::new(),
2104 )
2105 .await
2106 .map_err(anyhow::Error::from)?
2107 .into_iter()
2108 .next()
2109 .ok_or(WebsocketError::NoResponse)
2110 .map_err(anyhow::Error::from)
2111 }
2112
2113 async fn ticker_trading_day(
2114 &self,
2115 params: TickerTradingDayParams,
2116 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TickerTradingDayResponseResultInner>>>
2117 {
2118 let TickerTradingDayParams {
2119 id,
2120 symbol,
2121 symbols,
2122 time_zone,
2123 r#type,
2124 symbol_status,
2125 } = params;
2126
2127 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2128 if let Some(value) = id {
2129 payload.insert("id".to_string(), serde_json::json!(value));
2130 }
2131 if let Some(value) = symbol {
2132 payload.insert("symbol".to_string(), serde_json::json!(value));
2133 }
2134 if let Some(value) = symbols {
2135 payload.insert("symbols".to_string(), serde_json::json!(value));
2136 }
2137 if let Some(value) = time_zone {
2138 payload.insert("timeZone".to_string(), serde_json::json!(value));
2139 }
2140 if let Some(value) = r#type {
2141 payload.insert("type".to_string(), serde_json::json!(value));
2142 }
2143 if let Some(value) = symbol_status {
2144 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
2145 }
2146 let payload = remove_empty_value(payload);
2147
2148 self.websocket_api_base
2149 .send_message::<Vec<models::TickerTradingDayResponseResultInner>>(
2150 "/ticker.tradingDay".trim_start_matches('/'),
2151 payload,
2152 WebsocketMessageSendOptions::new(),
2153 )
2154 .await
2155 .map_err(anyhow::Error::from)?
2156 .into_iter()
2157 .next()
2158 .ok_or(WebsocketError::NoResponse)
2159 .map_err(anyhow::Error::from)
2160 }
2161
2162 async fn trades_aggregate(
2163 &self,
2164 params: TradesAggregateParams,
2165 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesAggregateResponseResultInner>>> {
2166 let TradesAggregateParams {
2167 symbol,
2168 id,
2169 from_id,
2170 start_time,
2171 end_time,
2172 limit,
2173 } = params;
2174
2175 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2176 payload.insert("symbol".to_string(), serde_json::json!(symbol));
2177 if let Some(value) = id {
2178 payload.insert("id".to_string(), serde_json::json!(value));
2179 }
2180 if let Some(value) = from_id {
2181 payload.insert("fromId".to_string(), serde_json::json!(value));
2182 }
2183 if let Some(value) = start_time {
2184 payload.insert("startTime".to_string(), serde_json::json!(value));
2185 }
2186 if let Some(value) = end_time {
2187 payload.insert("endTime".to_string(), serde_json::json!(value));
2188 }
2189 if let Some(value) = limit {
2190 payload.insert("limit".to_string(), serde_json::json!(value));
2191 }
2192 let payload = remove_empty_value(payload);
2193
2194 self.websocket_api_base
2195 .send_message::<Vec<models::TradesAggregateResponseResultInner>>(
2196 "/trades.aggregate".trim_start_matches('/'),
2197 payload,
2198 WebsocketMessageSendOptions::new(),
2199 )
2200 .await
2201 .map_err(anyhow::Error::from)?
2202 .into_iter()
2203 .next()
2204 .ok_or(WebsocketError::NoResponse)
2205 .map_err(anyhow::Error::from)
2206 }
2207
2208 async fn trades_historical(
2209 &self,
2210 params: TradesHistoricalParams,
2211 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesHistoricalResponseResultInner>>>
2212 {
2213 let TradesHistoricalParams {
2214 symbol,
2215 id,
2216 from_id,
2217 limit,
2218 } = params;
2219
2220 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2221 payload.insert("symbol".to_string(), serde_json::json!(symbol));
2222 if let Some(value) = id {
2223 payload.insert("id".to_string(), serde_json::json!(value));
2224 }
2225 if let Some(value) = from_id {
2226 payload.insert("fromId".to_string(), serde_json::json!(value));
2227 }
2228 if let Some(value) = limit {
2229 payload.insert("limit".to_string(), serde_json::json!(value));
2230 }
2231 let payload = remove_empty_value(payload);
2232
2233 self.websocket_api_base
2234 .send_message::<Vec<models::TradesHistoricalResponseResultInner>>(
2235 "/trades.historical".trim_start_matches('/'),
2236 payload,
2237 WebsocketMessageSendOptions::new(),
2238 )
2239 .await
2240 .map_err(anyhow::Error::from)?
2241 .into_iter()
2242 .next()
2243 .ok_or(WebsocketError::NoResponse)
2244 .map_err(anyhow::Error::from)
2245 }
2246
2247 async fn trades_recent(
2248 &self,
2249 params: TradesRecentParams,
2250 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::TradesRecentResponseResultInner>>> {
2251 let TradesRecentParams { symbol, id, limit } = params;
2252
2253 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2254 payload.insert("symbol".to_string(), serde_json::json!(symbol));
2255 if let Some(value) = id {
2256 payload.insert("id".to_string(), serde_json::json!(value));
2257 }
2258 if let Some(value) = limit {
2259 payload.insert("limit".to_string(), serde_json::json!(value));
2260 }
2261 let payload = remove_empty_value(payload);
2262
2263 self.websocket_api_base
2264 .send_message::<Vec<models::TradesRecentResponseResultInner>>(
2265 "/trades.recent".trim_start_matches('/'),
2266 payload,
2267 WebsocketMessageSendOptions::new(),
2268 )
2269 .await
2270 .map_err(anyhow::Error::from)?
2271 .into_iter()
2272 .next()
2273 .ok_or(WebsocketError::NoResponse)
2274 .map_err(anyhow::Error::from)
2275 }
2276
2277 async fn ui_klines(
2278 &self,
2279 params: UiKlinesParams,
2280 ) -> anyhow::Result<WebsocketApiResponse<Vec<Vec<models::KlinesItemInner>>>> {
2281 let UiKlinesParams {
2282 symbol,
2283 interval,
2284 id,
2285 start_time,
2286 end_time,
2287 time_zone,
2288 limit,
2289 } = params;
2290
2291 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
2292 payload.insert("symbol".to_string(), serde_json::json!(symbol));
2293 payload.insert("interval".to_string(), serde_json::json!(interval));
2294 if let Some(value) = id {
2295 payload.insert("id".to_string(), serde_json::json!(value));
2296 }
2297 if let Some(value) = start_time {
2298 payload.insert("startTime".to_string(), serde_json::json!(value));
2299 }
2300 if let Some(value) = end_time {
2301 payload.insert("endTime".to_string(), serde_json::json!(value));
2302 }
2303 if let Some(value) = time_zone {
2304 payload.insert("timeZone".to_string(), serde_json::json!(value));
2305 }
2306 if let Some(value) = limit {
2307 payload.insert("limit".to_string(), serde_json::json!(value));
2308 }
2309 let payload = remove_empty_value(payload);
2310
2311 self.websocket_api_base
2312 .send_message::<Vec<Vec<models::KlinesItemInner>>>(
2313 "/uiKlines".trim_start_matches('/'),
2314 payload,
2315 WebsocketMessageSendOptions::new(),
2316 )
2317 .await
2318 .map_err(anyhow::Error::from)?
2319 .into_iter()
2320 .next()
2321 .ok_or(WebsocketError::NoResponse)
2322 .map_err(anyhow::Error::from)
2323 }
2324}
2325
2326#[cfg(all(test, feature = "spot"))]
2327mod tests {
2328 use super::*;
2329 use crate::TOKIO_SHARED_RT;
2330 use crate::common::websocket::{WebsocketApi, WebsocketConnection, WebsocketHandler};
2331 use crate::config::ConfigurationWebsocketApi;
2332 use crate::errors::WebsocketError;
2333 use crate::models::WebsocketApiRateLimit;
2334 use serde_json::{Value, json};
2335 use tokio::spawn;
2336 use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
2337 use tokio::time::{Duration, timeout};
2338 use tokio_tungstenite::tungstenite::Message;
2339
2340 async fn setup() -> (
2341 Arc<WebsocketApi>,
2342 Arc<WebsocketConnection>,
2343 UnboundedReceiver<Message>,
2344 ) {
2345 let conn = WebsocketConnection::new("test-conn");
2346 let (tx, rx) = unbounded_channel::<Message>();
2347 {
2348 let mut conn_state = conn.state.lock().await;
2349 conn_state.ws_write_tx = Some(tx);
2350 }
2351
2352 let config = ConfigurationWebsocketApi::builder()
2353 .api_key("key")
2354 .api_secret("secret")
2355 .build()
2356 .expect("Failed to build configuration");
2357 let ws_api = WebsocketApi::new(config, vec![conn.clone()]);
2358 conn.set_handler(ws_api.clone() as Arc<dyn WebsocketHandler>)
2359 .await;
2360 ws_api.clone().connect().await.unwrap();
2361
2362 (ws_api, conn, rx)
2363 }
2364
2365 #[test]
2366 fn avg_price_success() {
2367 TOKIO_SHARED_RT.block_on(async {
2368 let (ws_api, conn, mut rx) = setup().await;
2369 let client = MarketApiClient::new(ws_api.clone());
2370
2371 let handle = spawn(async move {
2372 let params = AvgPriceParams::builder("BNBUSDT".to_string(),).build().unwrap();
2373 client.avg_price(params).await
2374 });
2375
2376 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2377 let Message::Text(text) = sent else { panic!() };
2378 let v: Value = serde_json::from_str(&text).unwrap();
2379 let id = v["id"].as_str().unwrap();
2380 assert_eq!(v["method"], "/avgPrice".trim_start_matches('/'));
2381
2382 let mut resp_json: Value = serde_json::from_str(r#"{"id":"ddbfb65f-9ebf-42ec-8240-8f0f91de0867","status":200,"result":{"mins":5,"price":"9.35751834","closeTime":1694061154503},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
2383 resp_json["id"] = id.into();
2384
2385 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2386 let expected_data: Box<models::AvgPriceResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2387 let empty_array = Value::Array(vec![]);
2388 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2389 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2390 match raw_rate_limits.as_array() {
2391 Some(arr) if arr.is_empty() => None,
2392 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2393 None => None,
2394 };
2395
2396 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2397
2398 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2399
2400
2401 let response_rate_limits = response.rate_limits.clone();
2402 let response_data = response.data().expect("deserialize data");
2403
2404 assert_eq!(response_rate_limits, expected_rate_limits);
2405 assert_eq!(response_data, expected_data);
2406 });
2407 }
2408
2409 #[test]
2410 fn avg_price_error_response() {
2411 TOKIO_SHARED_RT.block_on(async {
2412 let (ws_api, conn, mut rx) = setup().await;
2413 let client = MarketApiClient::new(ws_api.clone());
2414
2415 let handle = tokio::spawn(async move {
2416 let params = AvgPriceParams::builder("BNBUSDT".to_string(),).build().unwrap();
2417 client.avg_price(params).await
2418 });
2419
2420 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2421 let Message::Text(text) = sent else { panic!() };
2422 let v: Value = serde_json::from_str(&text).unwrap();
2423 let id = v["id"].as_str().unwrap().to_string();
2424
2425 let resp_json = json!({
2426 "id": id,
2427 "status": 400,
2428 "error": {
2429 "code": -2010,
2430 "msg": "Account has insufficient balance for requested action.",
2431 },
2432 "rateLimits": [
2433 {
2434 "rateLimitType": "ORDERS",
2435 "interval": "SECOND",
2436 "intervalNum": 10,
2437 "limit": 50,
2438 "count": 13
2439 },
2440 ],
2441 });
2442 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2443
2444 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2445 match join {
2446 Ok(Err(e)) => {
2447 let msg = e.to_string();
2448 assert!(
2449 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2450 "Expected error msg to contain server error, got: {msg}"
2451 );
2452 }
2453 Ok(Ok(_)) => panic!("Expected error"),
2454 Err(_) => panic!("Task panicked"),
2455 }
2456 });
2457 }
2458
2459 #[test]
2460 fn avg_price_request_timeout() {
2461 TOKIO_SHARED_RT.block_on(async {
2462 let (ws_api, _conn, mut rx) = setup().await;
2463 let client = MarketApiClient::new(ws_api.clone());
2464
2465 let handle = spawn(async move {
2466 let params = AvgPriceParams::builder("BNBUSDT".to_string())
2467 .build()
2468 .unwrap();
2469 client.avg_price(params).await
2470 });
2471
2472 let sent = timeout(Duration::from_secs(1), rx.recv())
2473 .await
2474 .expect("send should occur")
2475 .expect("channel closed");
2476 let Message::Text(text) = sent else {
2477 panic!("expected Message Text")
2478 };
2479
2480 let _: Value = serde_json::from_str(&text).unwrap();
2481
2482 let result = handle.await.expect("task completed");
2483 match result {
2484 Err(e) => {
2485 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2486 assert!(matches!(inner, WebsocketError::Timeout));
2487 } else {
2488 panic!("Unexpected error type: {:?}", e);
2489 }
2490 }
2491 Ok(_) => panic!("Expected timeout error"),
2492 }
2493 });
2494 }
2495
2496 #[test]
2497 fn block_trades_historical_success() {
2498 TOKIO_SHARED_RT.block_on(async {
2499 let (ws_api, conn, mut rx) = setup().await;
2500 let client = MarketApiClient::new(ws_api.clone());
2501
2502 let handle = spawn(async move {
2503 let params = BlockTradesHistoricalParams::builder("BNBUSDT".to_string(),1,).build().unwrap();
2504 client.block_trades_historical(params).await
2505 });
2506
2507 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2508 let Message::Text(text) = sent else { panic!() };
2509 let v: Value = serde_json::from_str(&text).unwrap();
2510 let id = v["id"].as_str().unwrap();
2511 assert_eq!(v["method"], "/blockTrades.historical".trim_start_matches('/'));
2512
2513 let mut resp_json: Value = serde_json::from_str(r#"{"id":"cffc9c7d-4efc-4ce0-b587-6b87448f052a","status":200,"result":[{"id":582,"price":"0.052","qty":"5838","quoteQty":"303.576","time":1772506983321,"isBuyerMaker":true}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":10}]}"#).unwrap();
2514 resp_json["id"] = id.into();
2515
2516 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2517 let expected_data: Vec<models::BlockTradesHistoricalResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2518 let empty_array = Value::Array(vec![]);
2519 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2520 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2521 match raw_rate_limits.as_array() {
2522 Some(arr) if arr.is_empty() => None,
2523 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2524 None => None,
2525 };
2526
2527 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2528
2529 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2530
2531
2532 let response_rate_limits = response.rate_limits.clone();
2533 let response_data = response.data().expect("deserialize data");
2534
2535 assert_eq!(response_rate_limits, expected_rate_limits);
2536 assert_eq!(response_data, expected_data);
2537 });
2538 }
2539
2540 #[test]
2541 fn block_trades_historical_error_response() {
2542 TOKIO_SHARED_RT.block_on(async {
2543 let (ws_api, conn, mut rx) = setup().await;
2544 let client = MarketApiClient::new(ws_api.clone());
2545
2546 let handle = tokio::spawn(async move {
2547 let params = BlockTradesHistoricalParams::builder("BNBUSDT".to_string(),1,).build().unwrap();
2548 client.block_trades_historical(params).await
2549 });
2550
2551 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2552 let Message::Text(text) = sent else { panic!() };
2553 let v: Value = serde_json::from_str(&text).unwrap();
2554 let id = v["id"].as_str().unwrap().to_string();
2555
2556 let resp_json = json!({
2557 "id": id,
2558 "status": 400,
2559 "error": {
2560 "code": -2010,
2561 "msg": "Account has insufficient balance for requested action.",
2562 },
2563 "rateLimits": [
2564 {
2565 "rateLimitType": "ORDERS",
2566 "interval": "SECOND",
2567 "intervalNum": 10,
2568 "limit": 50,
2569 "count": 13
2570 },
2571 ],
2572 });
2573 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2574
2575 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2576 match join {
2577 Ok(Err(e)) => {
2578 let msg = e.to_string();
2579 assert!(
2580 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2581 "Expected error msg to contain server error, got: {msg}"
2582 );
2583 }
2584 Ok(Ok(_)) => panic!("Expected error"),
2585 Err(_) => panic!("Task panicked"),
2586 }
2587 });
2588 }
2589
2590 #[test]
2591 fn block_trades_historical_request_timeout() {
2592 TOKIO_SHARED_RT.block_on(async {
2593 let (ws_api, _conn, mut rx) = setup().await;
2594 let client = MarketApiClient::new(ws_api.clone());
2595
2596 let handle = spawn(async move {
2597 let params = BlockTradesHistoricalParams::builder("BNBUSDT".to_string(), 1)
2598 .build()
2599 .unwrap();
2600 client.block_trades_historical(params).await
2601 });
2602
2603 let sent = timeout(Duration::from_secs(1), rx.recv())
2604 .await
2605 .expect("send should occur")
2606 .expect("channel closed");
2607 let Message::Text(text) = sent else {
2608 panic!("expected Message Text")
2609 };
2610
2611 let _: Value = serde_json::from_str(&text).unwrap();
2612
2613 let result = handle.await.expect("task completed");
2614 match result {
2615 Err(e) => {
2616 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2617 assert!(matches!(inner, WebsocketError::Timeout));
2618 } else {
2619 panic!("Unexpected error type: {:?}", e);
2620 }
2621 }
2622 Ok(_) => panic!("Expected timeout error"),
2623 }
2624 });
2625 }
2626
2627 #[test]
2628 fn depth_success() {
2629 TOKIO_SHARED_RT.block_on(async {
2630 let (ws_api, conn, mut rx) = setup().await;
2631 let client = MarketApiClient::new(ws_api.clone());
2632
2633 let handle = spawn(async move {
2634 let params = DepthParams::builder("BNBUSDT".to_string(),).build().unwrap();
2635 client.depth(params).await
2636 });
2637
2638 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2639 let Message::Text(text) = sent else { panic!() };
2640 let v: Value = serde_json::from_str(&text).unwrap();
2641 let id = v["id"].as_str().unwrap();
2642 assert_eq!(v["method"], "/depth".trim_start_matches('/'));
2643
2644 let mut resp_json: Value = serde_json::from_str(r#"{"id":"51e2affb-0aba-4821-ba75-f2625006eb43","status":200,"result":{"lastUpdateId":2731179239,"bids":[["0.01379900","3.43200000"],["0.01379800","3.24300000"],["0.01379700","10.45500000"],["0.01379600","3.82100000"],["0.01379500","10.26200000"]],"asks":[["0.01380000","5.91700000"],["0.01380100","6.01400000"],["0.01380200","0.26800000"],["0.01380300","0.33800000"],["0.01380400","0.26800000"]]},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
2645 resp_json["id"] = id.into();
2646
2647 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2648 let expected_data: Box<models::DepthResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2649 let empty_array = Value::Array(vec![]);
2650 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2651 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2652 match raw_rate_limits.as_array() {
2653 Some(arr) if arr.is_empty() => None,
2654 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2655 None => None,
2656 };
2657
2658 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2659
2660 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2661
2662
2663 let response_rate_limits = response.rate_limits.clone();
2664 let response_data = response.data().expect("deserialize data");
2665
2666 assert_eq!(response_rate_limits, expected_rate_limits);
2667 assert_eq!(response_data, expected_data);
2668 });
2669 }
2670
2671 #[test]
2672 fn depth_error_response() {
2673 TOKIO_SHARED_RT.block_on(async {
2674 let (ws_api, conn, mut rx) = setup().await;
2675 let client = MarketApiClient::new(ws_api.clone());
2676
2677 let handle = tokio::spawn(async move {
2678 let params = DepthParams::builder("BNBUSDT".to_string(),).build().unwrap();
2679 client.depth(params).await
2680 });
2681
2682 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2683 let Message::Text(text) = sent else { panic!() };
2684 let v: Value = serde_json::from_str(&text).unwrap();
2685 let id = v["id"].as_str().unwrap().to_string();
2686
2687 let resp_json = json!({
2688 "id": id,
2689 "status": 400,
2690 "error": {
2691 "code": -2010,
2692 "msg": "Account has insufficient balance for requested action.",
2693 },
2694 "rateLimits": [
2695 {
2696 "rateLimitType": "ORDERS",
2697 "interval": "SECOND",
2698 "intervalNum": 10,
2699 "limit": 50,
2700 "count": 13
2701 },
2702 ],
2703 });
2704 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2705
2706 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2707 match join {
2708 Ok(Err(e)) => {
2709 let msg = e.to_string();
2710 assert!(
2711 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2712 "Expected error msg to contain server error, got: {msg}"
2713 );
2714 }
2715 Ok(Ok(_)) => panic!("Expected error"),
2716 Err(_) => panic!("Task panicked"),
2717 }
2718 });
2719 }
2720
2721 #[test]
2722 fn depth_request_timeout() {
2723 TOKIO_SHARED_RT.block_on(async {
2724 let (ws_api, _conn, mut rx) = setup().await;
2725 let client = MarketApiClient::new(ws_api.clone());
2726
2727 let handle = spawn(async move {
2728 let params = DepthParams::builder("BNBUSDT".to_string()).build().unwrap();
2729 client.depth(params).await
2730 });
2731
2732 let sent = timeout(Duration::from_secs(1), rx.recv())
2733 .await
2734 .expect("send should occur")
2735 .expect("channel closed");
2736 let Message::Text(text) = sent else {
2737 panic!("expected Message Text")
2738 };
2739
2740 let _: Value = serde_json::from_str(&text).unwrap();
2741
2742 let result = handle.await.expect("task completed");
2743 match result {
2744 Err(e) => {
2745 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2746 assert!(matches!(inner, WebsocketError::Timeout));
2747 } else {
2748 panic!("Unexpected error type: {:?}", e);
2749 }
2750 }
2751 Ok(_) => panic!("Expected timeout error"),
2752 }
2753 });
2754 }
2755
2756 #[test]
2757 fn klines_success() {
2758 TOKIO_SHARED_RT.block_on(async {
2759 let (ws_api, conn, mut rx) = setup().await;
2760 let client = MarketApiClient::new(ws_api.clone());
2761
2762 let handle = spawn(async move {
2763 let params = KlinesParams::builder("BNBUSDT".to_string(),KlinesIntervalEnum::Interval1s,).build().unwrap();
2764 client.klines(params).await
2765 });
2766
2767 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2768 let Message::Text(text) = sent else { panic!() };
2769 let v: Value = serde_json::from_str(&text).unwrap();
2770 let id = v["id"].as_str().unwrap();
2771 assert_eq!(v["method"], "/klines".trim_start_matches('/'));
2772
2773 let mut resp_json: Value = serde_json::from_str(r#"{"id":"1dbbeb56-8eea-466a-8f6e-86bdcfa2fc0b","status":200,"result":[[1655971200000,"0.01086000","0.01086600","0.01083600","0.01083800","2290.53800000",1655974799999,"24.85074442",2283,"1171.64000000","12.71225884","0"]],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
2774 resp_json["id"] = id.into();
2775
2776 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2777 let expected_data: Vec<Vec<models::KlinesItemInner>> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2778 let empty_array = Value::Array(vec![]);
2779 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2780 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2781 match raw_rate_limits.as_array() {
2782 Some(arr) if arr.is_empty() => None,
2783 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2784 None => None,
2785 };
2786
2787 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2788
2789 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2790
2791
2792 let response_rate_limits = response.rate_limits.clone();
2793 let response_data = response.data().expect("deserialize data");
2794
2795 assert_eq!(response_rate_limits, expected_rate_limits);
2796 assert_eq!(response_data, expected_data);
2797 });
2798 }
2799
2800 #[test]
2801 fn klines_error_response() {
2802 TOKIO_SHARED_RT.block_on(async {
2803 let (ws_api, conn, mut rx) = setup().await;
2804 let client = MarketApiClient::new(ws_api.clone());
2805
2806 let handle = tokio::spawn(async move {
2807 let params = KlinesParams::builder("BNBUSDT".to_string(),KlinesIntervalEnum::Interval1s,).build().unwrap();
2808 client.klines(params).await
2809 });
2810
2811 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2812 let Message::Text(text) = sent else { panic!() };
2813 let v: Value = serde_json::from_str(&text).unwrap();
2814 let id = v["id"].as_str().unwrap().to_string();
2815
2816 let resp_json = json!({
2817 "id": id,
2818 "status": 400,
2819 "error": {
2820 "code": -2010,
2821 "msg": "Account has insufficient balance for requested action.",
2822 },
2823 "rateLimits": [
2824 {
2825 "rateLimitType": "ORDERS",
2826 "interval": "SECOND",
2827 "intervalNum": 10,
2828 "limit": 50,
2829 "count": 13
2830 },
2831 ],
2832 });
2833 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2834
2835 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2836 match join {
2837 Ok(Err(e)) => {
2838 let msg = e.to_string();
2839 assert!(
2840 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2841 "Expected error msg to contain server error, got: {msg}"
2842 );
2843 }
2844 Ok(Ok(_)) => panic!("Expected error"),
2845 Err(_) => panic!("Task panicked"),
2846 }
2847 });
2848 }
2849
2850 #[test]
2851 fn klines_request_timeout() {
2852 TOKIO_SHARED_RT.block_on(async {
2853 let (ws_api, _conn, mut rx) = setup().await;
2854 let client = MarketApiClient::new(ws_api.clone());
2855
2856 let handle = spawn(async move {
2857 let params =
2858 KlinesParams::builder("BNBUSDT".to_string(), KlinesIntervalEnum::Interval1s)
2859 .build()
2860 .unwrap();
2861 client.klines(params).await
2862 });
2863
2864 let sent = timeout(Duration::from_secs(1), rx.recv())
2865 .await
2866 .expect("send should occur")
2867 .expect("channel closed");
2868 let Message::Text(text) = sent else {
2869 panic!("expected Message Text")
2870 };
2871
2872 let _: Value = serde_json::from_str(&text).unwrap();
2873
2874 let result = handle.await.expect("task completed");
2875 match result {
2876 Err(e) => {
2877 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2878 assert!(matches!(inner, WebsocketError::Timeout));
2879 } else {
2880 panic!("Unexpected error type: {:?}", e);
2881 }
2882 }
2883 Ok(_) => panic!("Expected timeout error"),
2884 }
2885 });
2886 }
2887
2888 #[test]
2889 fn reference_price_success() {
2890 TOKIO_SHARED_RT.block_on(async {
2891 let (ws_api, conn, mut rx) = setup().await;
2892 let client = MarketApiClient::new(ws_api.clone());
2893
2894 let handle = spawn(async move {
2895 let params = ReferencePriceParams::builder("BNBUSDT".to_string(),).build().unwrap();
2896 client.reference_price(params).await
2897 });
2898
2899 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2900 let Message::Text(text) = sent else { panic!() };
2901 let v: Value = serde_json::from_str(&text).unwrap();
2902 let id = v["id"].as_str().unwrap();
2903 assert_eq!(v["method"], "/referencePrice".trim_start_matches('/'));
2904
2905 let mut resp_json: Value = serde_json::from_str(r#"{"id":"5132affa-0aba-4831-b475-f262504556b41","status":200,"result":{"symbol":"BAZUSD","referencePrice":"0.00501900","timestamp":1770946889251,"code":-2043,"msg":"This symbol doesn't have a reference price."}}"#).unwrap();
2906 resp_json["id"] = id.into();
2907
2908 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2909 let expected_data: Box<models::ReferencePriceResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2910 let empty_array = Value::Array(vec![]);
2911 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2912 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2913 match raw_rate_limits.as_array() {
2914 Some(arr) if arr.is_empty() => None,
2915 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2916 None => None,
2917 };
2918
2919 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2920
2921 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2922
2923
2924 let response_rate_limits = response.rate_limits.clone();
2925 let response_data = response.data().expect("deserialize data");
2926
2927 assert_eq!(response_rate_limits, expected_rate_limits);
2928 assert_eq!(response_data, expected_data);
2929 });
2930 }
2931
2932 #[test]
2933 fn reference_price_error_response() {
2934 TOKIO_SHARED_RT.block_on(async {
2935 let (ws_api, conn, mut rx) = setup().await;
2936 let client = MarketApiClient::new(ws_api.clone());
2937
2938 let handle = tokio::spawn(async move {
2939 let params = ReferencePriceParams::builder("BNBUSDT".to_string(),).build().unwrap();
2940 client.reference_price(params).await
2941 });
2942
2943 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2944 let Message::Text(text) = sent else { panic!() };
2945 let v: Value = serde_json::from_str(&text).unwrap();
2946 let id = v["id"].as_str().unwrap().to_string();
2947
2948 let resp_json = json!({
2949 "id": id,
2950 "status": 400,
2951 "error": {
2952 "code": -2010,
2953 "msg": "Account has insufficient balance for requested action.",
2954 },
2955 "rateLimits": [
2956 {
2957 "rateLimitType": "ORDERS",
2958 "interval": "SECOND",
2959 "intervalNum": 10,
2960 "limit": 50,
2961 "count": 13
2962 },
2963 ],
2964 });
2965 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2966
2967 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2968 match join {
2969 Ok(Err(e)) => {
2970 let msg = e.to_string();
2971 assert!(
2972 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2973 "Expected error msg to contain server error, got: {msg}"
2974 );
2975 }
2976 Ok(Ok(_)) => panic!("Expected error"),
2977 Err(_) => panic!("Task panicked"),
2978 }
2979 });
2980 }
2981
2982 #[test]
2983 fn reference_price_request_timeout() {
2984 TOKIO_SHARED_RT.block_on(async {
2985 let (ws_api, _conn, mut rx) = setup().await;
2986 let client = MarketApiClient::new(ws_api.clone());
2987
2988 let handle = spawn(async move {
2989 let params = ReferencePriceParams::builder("BNBUSDT".to_string())
2990 .build()
2991 .unwrap();
2992 client.reference_price(params).await
2993 });
2994
2995 let sent = timeout(Duration::from_secs(1), rx.recv())
2996 .await
2997 .expect("send should occur")
2998 .expect("channel closed");
2999 let Message::Text(text) = sent else {
3000 panic!("expected Message Text")
3001 };
3002
3003 let _: Value = serde_json::from_str(&text).unwrap();
3004
3005 let result = handle.await.expect("task completed");
3006 match result {
3007 Err(e) => {
3008 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3009 assert!(matches!(inner, WebsocketError::Timeout));
3010 } else {
3011 panic!("Unexpected error type: {:?}", e);
3012 }
3013 }
3014 Ok(_) => panic!("Expected timeout error"),
3015 }
3016 });
3017 }
3018
3019 #[test]
3020 fn reference_price_calculation_success() {
3021 TOKIO_SHARED_RT.block_on(async {
3022 let (ws_api, conn, mut rx) = setup().await;
3023 let client = MarketApiClient::new(ws_api.clone());
3024
3025 let handle = spawn(async move {
3026 let params = ReferencePriceCalculationParams::builder("BNBUSDT".to_string(),).build().unwrap();
3027 client.reference_price_calculation(params).await
3028 });
3029
3030 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3031 let Message::Text(text) = sent else { panic!() };
3032 let v: Value = serde_json::from_str(&text).unwrap();
3033 let id = v["id"].as_str().unwrap();
3034 assert_eq!(v["method"], "/referencePrice.calculation".trim_start_matches('/'));
3035
3036 let mut resp_json: Value = serde_json::from_str(r#"{"id":"5132affa-0aba-4831-b475-f262504556b41","status":200,"result":{"symbol":"BAZUSD","calculationType":"EXTERNAL","bucketCount":10,"bucketWidthMs":1000,"externalCalculationId":42}}"#).unwrap();
3037 resp_json["id"] = id.into();
3038
3039 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3040 let expected_data: Box<models::ReferencePriceCalculationResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3041 let empty_array = Value::Array(vec![]);
3042 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3043 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3044 match raw_rate_limits.as_array() {
3045 Some(arr) if arr.is_empty() => None,
3046 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3047 None => None,
3048 };
3049
3050 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3051
3052 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3053
3054
3055 let response_rate_limits = response.rate_limits.clone();
3056 let response_data = response.data().expect("deserialize data");
3057
3058 assert_eq!(response_rate_limits, expected_rate_limits);
3059 assert_eq!(response_data, expected_data);
3060 });
3061 }
3062
3063 #[test]
3064 fn reference_price_calculation_error_response() {
3065 TOKIO_SHARED_RT.block_on(async {
3066 let (ws_api, conn, mut rx) = setup().await;
3067 let client = MarketApiClient::new(ws_api.clone());
3068
3069 let handle = tokio::spawn(async move {
3070 let params = ReferencePriceCalculationParams::builder("BNBUSDT".to_string(),).build().unwrap();
3071 client.reference_price_calculation(params).await
3072 });
3073
3074 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3075 let Message::Text(text) = sent else { panic!() };
3076 let v: Value = serde_json::from_str(&text).unwrap();
3077 let id = v["id"].as_str().unwrap().to_string();
3078
3079 let resp_json = json!({
3080 "id": id,
3081 "status": 400,
3082 "error": {
3083 "code": -2010,
3084 "msg": "Account has insufficient balance for requested action.",
3085 },
3086 "rateLimits": [
3087 {
3088 "rateLimitType": "ORDERS",
3089 "interval": "SECOND",
3090 "intervalNum": 10,
3091 "limit": 50,
3092 "count": 13
3093 },
3094 ],
3095 });
3096 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3097
3098 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3099 match join {
3100 Ok(Err(e)) => {
3101 let msg = e.to_string();
3102 assert!(
3103 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3104 "Expected error msg to contain server error, got: {msg}"
3105 );
3106 }
3107 Ok(Ok(_)) => panic!("Expected error"),
3108 Err(_) => panic!("Task panicked"),
3109 }
3110 });
3111 }
3112
3113 #[test]
3114 fn reference_price_calculation_request_timeout() {
3115 TOKIO_SHARED_RT.block_on(async {
3116 let (ws_api, _conn, mut rx) = setup().await;
3117 let client = MarketApiClient::new(ws_api.clone());
3118
3119 let handle = spawn(async move {
3120 let params = ReferencePriceCalculationParams::builder("BNBUSDT".to_string())
3121 .build()
3122 .unwrap();
3123 client.reference_price_calculation(params).await
3124 });
3125
3126 let sent = timeout(Duration::from_secs(1), rx.recv())
3127 .await
3128 .expect("send should occur")
3129 .expect("channel closed");
3130 let Message::Text(text) = sent else {
3131 panic!("expected Message Text")
3132 };
3133
3134 let _: Value = serde_json::from_str(&text).unwrap();
3135
3136 let result = handle.await.expect("task completed");
3137 match result {
3138 Err(e) => {
3139 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3140 assert!(matches!(inner, WebsocketError::Timeout));
3141 } else {
3142 panic!("Unexpected error type: {:?}", e);
3143 }
3144 }
3145 Ok(_) => panic!("Expected timeout error"),
3146 }
3147 });
3148 }
3149
3150 #[test]
3151 fn ticker_success() {
3152 TOKIO_SHARED_RT.block_on(async {
3153 let (ws_api, conn, mut rx) = setup().await;
3154 let client = MarketApiClient::new(ws_api.clone());
3155
3156 let handle = spawn(async move {
3157 let params = TickerParams::builder().build().unwrap();
3158 client.ticker(params).await
3159 });
3160
3161 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3162 let Message::Text(text) = sent else { panic!() };
3163 let v: Value = serde_json::from_str(&text).unwrap();
3164 let id = v["id"].as_str().unwrap();
3165 assert_eq!(v["method"], "/ticker".trim_start_matches('/'));
3166
3167 let mut resp_json: Value = serde_json::from_str(r#"{"id":"bdb7c503-542c-495c-b797-4d2ee2e91173","status":200,"result":{"symbol":"BNBBTC","priceChange":"0.00061500","priceChangePercent":"4.735","weightedAvgPrice":"0.01368242","openPrice":"0.01298900","highPrice":"0.01418800","lowPrice":"0.01296000","lastPrice":"0.01360400","volume":"587179.23900000","quoteVolume":"8034.03382165","openTime":1659580020000,"closeTime":1660184865291,"firstId":192977765,"lastId":195365758,"count":2387994},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":4}]}"#).unwrap();
3168 resp_json["id"] = id.into();
3169
3170 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3171 let expected_data: models::TickerResponse = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3172 let empty_array = Value::Array(vec![]);
3173 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3174 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3175 match raw_rate_limits.as_array() {
3176 Some(arr) if arr.is_empty() => None,
3177 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3178 None => None,
3179 };
3180
3181 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3182
3183 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3184
3185
3186 let response_rate_limits = response.rate_limits.clone();
3187 let response_data = response.data().expect("deserialize data");
3188
3189 assert_eq!(response_rate_limits, expected_rate_limits);
3190 assert_eq!(response_data, expected_data);
3191 });
3192 }
3193
3194 #[test]
3195 fn ticker_error_response() {
3196 TOKIO_SHARED_RT.block_on(async {
3197 let (ws_api, conn, mut rx) = setup().await;
3198 let client = MarketApiClient::new(ws_api.clone());
3199
3200 let handle = tokio::spawn(async move {
3201 let params = TickerParams::builder().build().unwrap();
3202 client.ticker(params).await
3203 });
3204
3205 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3206 let Message::Text(text) = sent else { panic!() };
3207 let v: Value = serde_json::from_str(&text).unwrap();
3208 let id = v["id"].as_str().unwrap().to_string();
3209
3210 let resp_json = json!({
3211 "id": id,
3212 "status": 400,
3213 "error": {
3214 "code": -2010,
3215 "msg": "Account has insufficient balance for requested action.",
3216 },
3217 "rateLimits": [
3218 {
3219 "rateLimitType": "ORDERS",
3220 "interval": "SECOND",
3221 "intervalNum": 10,
3222 "limit": 50,
3223 "count": 13
3224 },
3225 ],
3226 });
3227 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3228
3229 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3230 match join {
3231 Ok(Err(e)) => {
3232 let msg = e.to_string();
3233 assert!(
3234 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3235 "Expected error msg to contain server error, got: {msg}"
3236 );
3237 }
3238 Ok(Ok(_)) => panic!("Expected error"),
3239 Err(_) => panic!("Task panicked"),
3240 }
3241 });
3242 }
3243
3244 #[test]
3245 fn ticker_request_timeout() {
3246 TOKIO_SHARED_RT.block_on(async {
3247 let (ws_api, _conn, mut rx) = setup().await;
3248 let client = MarketApiClient::new(ws_api.clone());
3249
3250 let handle = spawn(async move {
3251 let params = TickerParams::builder().build().unwrap();
3252 client.ticker(params).await
3253 });
3254
3255 let sent = timeout(Duration::from_secs(1), rx.recv())
3256 .await
3257 .expect("send should occur")
3258 .expect("channel closed");
3259 let Message::Text(text) = sent else {
3260 panic!("expected Message Text")
3261 };
3262
3263 let _: Value = serde_json::from_str(&text).unwrap();
3264
3265 let result = handle.await.expect("task completed");
3266 match result {
3267 Err(e) => {
3268 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3269 assert!(matches!(inner, WebsocketError::Timeout));
3270 } else {
3271 panic!("Unexpected error type: {:?}", e);
3272 }
3273 }
3274 Ok(_) => panic!("Expected timeout error"),
3275 }
3276 });
3277 }
3278
3279 #[test]
3280 fn ticker24hr_success() {
3281 TOKIO_SHARED_RT.block_on(async {
3282 let (ws_api, conn, mut rx) = setup().await;
3283 let client = MarketApiClient::new(ws_api.clone());
3284
3285 let handle = spawn(async move {
3286 let params = Ticker24hrParams::builder().build().unwrap();
3287 client.ticker24hr(params).await
3288 });
3289
3290 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3291 let Message::Text(text) = sent else { panic!() };
3292 let v: Value = serde_json::from_str(&text).unwrap();
3293 let id = v["id"].as_str().unwrap();
3294 assert_eq!(v["method"], "/ticker.24hr".trim_start_matches('/'));
3295
3296 let mut resp_json: Value = serde_json::from_str(r#"{"id":"9fa2a91b-3fca-4ed7-a9ad-58e3b67483de","status":200,"result":{"symbol":"BNBBTC","priceChange":"0.00013900","priceChangePercent":"1.020","weightedAvgPrice":"0.01382453","prevClosePrice":"0.01362800","lastPrice":"0.01376700","lastQty":"1.78800000","bidPrice":"0.01376700","bidQty":"4.64600000","askPrice":"0.01376800","askQty":"14.31400000","openPrice":"0.01362800","highPrice":"0.01414900","lowPrice":"0.01346600","volume":"69412.40500000","quoteVolume":"959.59411487","openTime":1660014164909,"closeTime":1660100564909,"firstId":194696115,"lastId":194968287,"count":272173},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
3297 resp_json["id"] = id.into();
3298
3299 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3300 let expected_data: models::Ticker24hrResponse = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3301 let empty_array = Value::Array(vec![]);
3302 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3303 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3304 match raw_rate_limits.as_array() {
3305 Some(arr) if arr.is_empty() => None,
3306 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3307 None => None,
3308 };
3309
3310 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3311
3312 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3313
3314
3315 let response_rate_limits = response.rate_limits.clone();
3316 let response_data = response.data().expect("deserialize data");
3317
3318 assert_eq!(response_rate_limits, expected_rate_limits);
3319 assert_eq!(response_data, expected_data);
3320 });
3321 }
3322
3323 #[test]
3324 fn ticker24hr_error_response() {
3325 TOKIO_SHARED_RT.block_on(async {
3326 let (ws_api, conn, mut rx) = setup().await;
3327 let client = MarketApiClient::new(ws_api.clone());
3328
3329 let handle = tokio::spawn(async move {
3330 let params = Ticker24hrParams::builder().build().unwrap();
3331 client.ticker24hr(params).await
3332 });
3333
3334 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3335 let Message::Text(text) = sent else { panic!() };
3336 let v: Value = serde_json::from_str(&text).unwrap();
3337 let id = v["id"].as_str().unwrap().to_string();
3338
3339 let resp_json = json!({
3340 "id": id,
3341 "status": 400,
3342 "error": {
3343 "code": -2010,
3344 "msg": "Account has insufficient balance for requested action.",
3345 },
3346 "rateLimits": [
3347 {
3348 "rateLimitType": "ORDERS",
3349 "interval": "SECOND",
3350 "intervalNum": 10,
3351 "limit": 50,
3352 "count": 13
3353 },
3354 ],
3355 });
3356 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3357
3358 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3359 match join {
3360 Ok(Err(e)) => {
3361 let msg = e.to_string();
3362 assert!(
3363 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3364 "Expected error msg to contain server error, got: {msg}"
3365 );
3366 }
3367 Ok(Ok(_)) => panic!("Expected error"),
3368 Err(_) => panic!("Task panicked"),
3369 }
3370 });
3371 }
3372
3373 #[test]
3374 fn ticker24hr_request_timeout() {
3375 TOKIO_SHARED_RT.block_on(async {
3376 let (ws_api, _conn, mut rx) = setup().await;
3377 let client = MarketApiClient::new(ws_api.clone());
3378
3379 let handle = spawn(async move {
3380 let params = Ticker24hrParams::builder().build().unwrap();
3381 client.ticker24hr(params).await
3382 });
3383
3384 let sent = timeout(Duration::from_secs(1), rx.recv())
3385 .await
3386 .expect("send should occur")
3387 .expect("channel closed");
3388 let Message::Text(text) = sent else {
3389 panic!("expected Message Text")
3390 };
3391
3392 let _: Value = serde_json::from_str(&text).unwrap();
3393
3394 let result = handle.await.expect("task completed");
3395 match result {
3396 Err(e) => {
3397 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3398 assert!(matches!(inner, WebsocketError::Timeout));
3399 } else {
3400 panic!("Unexpected error type: {:?}", e);
3401 }
3402 }
3403 Ok(_) => panic!("Expected timeout error"),
3404 }
3405 });
3406 }
3407
3408 #[test]
3409 fn ticker_book_success() {
3410 TOKIO_SHARED_RT.block_on(async {
3411 let (ws_api, conn, mut rx) = setup().await;
3412 let client = MarketApiClient::new(ws_api.clone());
3413
3414 let handle = spawn(async move {
3415 let params = TickerBookParams::builder().build().unwrap();
3416 client.ticker_book(params).await
3417 });
3418
3419 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3420 let Message::Text(text) = sent else { panic!() };
3421 let v: Value = serde_json::from_str(&text).unwrap();
3422 let id = v["id"].as_str().unwrap();
3423 assert_eq!(v["method"], "/ticker.book".trim_start_matches('/'));
3424
3425 let mut resp_json: Value = serde_json::from_str(r#"{"id":"9d32157c-a556-4d27-9866-66760a174b57","status":200,"result":{"symbol":"BNBBTC","bidPrice":"0.01358000","bidQty":"12.53400000","askPrice":"0.01358100","askQty":"17.83700000"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
3426 resp_json["id"] = id.into();
3427
3428 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3429 let expected_data: models::TickerBookResponse = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3430 let empty_array = Value::Array(vec![]);
3431 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3432 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3433 match raw_rate_limits.as_array() {
3434 Some(arr) if arr.is_empty() => None,
3435 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3436 None => None,
3437 };
3438
3439 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3440
3441 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3442
3443
3444 let response_rate_limits = response.rate_limits.clone();
3445 let response_data = response.data().expect("deserialize data");
3446
3447 assert_eq!(response_rate_limits, expected_rate_limits);
3448 assert_eq!(response_data, expected_data);
3449 });
3450 }
3451
3452 #[test]
3453 fn ticker_book_error_response() {
3454 TOKIO_SHARED_RT.block_on(async {
3455 let (ws_api, conn, mut rx) = setup().await;
3456 let client = MarketApiClient::new(ws_api.clone());
3457
3458 let handle = tokio::spawn(async move {
3459 let params = TickerBookParams::builder().build().unwrap();
3460 client.ticker_book(params).await
3461 });
3462
3463 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3464 let Message::Text(text) = sent else { panic!() };
3465 let v: Value = serde_json::from_str(&text).unwrap();
3466 let id = v["id"].as_str().unwrap().to_string();
3467
3468 let resp_json = json!({
3469 "id": id,
3470 "status": 400,
3471 "error": {
3472 "code": -2010,
3473 "msg": "Account has insufficient balance for requested action.",
3474 },
3475 "rateLimits": [
3476 {
3477 "rateLimitType": "ORDERS",
3478 "interval": "SECOND",
3479 "intervalNum": 10,
3480 "limit": 50,
3481 "count": 13
3482 },
3483 ],
3484 });
3485 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3486
3487 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3488 match join {
3489 Ok(Err(e)) => {
3490 let msg = e.to_string();
3491 assert!(
3492 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3493 "Expected error msg to contain server error, got: {msg}"
3494 );
3495 }
3496 Ok(Ok(_)) => panic!("Expected error"),
3497 Err(_) => panic!("Task panicked"),
3498 }
3499 });
3500 }
3501
3502 #[test]
3503 fn ticker_book_request_timeout() {
3504 TOKIO_SHARED_RT.block_on(async {
3505 let (ws_api, _conn, mut rx) = setup().await;
3506 let client = MarketApiClient::new(ws_api.clone());
3507
3508 let handle = spawn(async move {
3509 let params = TickerBookParams::builder().build().unwrap();
3510 client.ticker_book(params).await
3511 });
3512
3513 let sent = timeout(Duration::from_secs(1), rx.recv())
3514 .await
3515 .expect("send should occur")
3516 .expect("channel closed");
3517 let Message::Text(text) = sent else {
3518 panic!("expected Message Text")
3519 };
3520
3521 let _: Value = serde_json::from_str(&text).unwrap();
3522
3523 let result = handle.await.expect("task completed");
3524 match result {
3525 Err(e) => {
3526 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3527 assert!(matches!(inner, WebsocketError::Timeout));
3528 } else {
3529 panic!("Unexpected error type: {:?}", e);
3530 }
3531 }
3532 Ok(_) => panic!("Expected timeout error"),
3533 }
3534 });
3535 }
3536
3537 #[test]
3538 fn ticker_price_success() {
3539 TOKIO_SHARED_RT.block_on(async {
3540 let (ws_api, conn, mut rx) = setup().await;
3541 let client = MarketApiClient::new(ws_api.clone());
3542
3543 let handle = spawn(async move {
3544 let params = TickerPriceParams::builder().build().unwrap();
3545 client.ticker_price(params).await
3546 });
3547
3548 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3549 let Message::Text(text) = sent else { panic!() };
3550 let v: Value = serde_json::from_str(&text).unwrap();
3551 let id = v["id"].as_str().unwrap();
3552 assert_eq!(v["method"], "/ticker.price".trim_start_matches('/'));
3553
3554 let mut resp_json: Value = serde_json::from_str(r#"{"id":"043a7cf2-bde3-4888-9604-c8ac41fcba4d","status":200,"result":{"symbol":"BNBBTC","price":"0.01361900"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
3555 resp_json["id"] = id.into();
3556
3557 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3558 let expected_data: models::TickerPriceResponse = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3559 let empty_array = Value::Array(vec![]);
3560 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3561 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3562 match raw_rate_limits.as_array() {
3563 Some(arr) if arr.is_empty() => None,
3564 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3565 None => None,
3566 };
3567
3568 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3569
3570 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3571
3572
3573 let response_rate_limits = response.rate_limits.clone();
3574 let response_data = response.data().expect("deserialize data");
3575
3576 assert_eq!(response_rate_limits, expected_rate_limits);
3577 assert_eq!(response_data, expected_data);
3578 });
3579 }
3580
3581 #[test]
3582 fn ticker_price_error_response() {
3583 TOKIO_SHARED_RT.block_on(async {
3584 let (ws_api, conn, mut rx) = setup().await;
3585 let client = MarketApiClient::new(ws_api.clone());
3586
3587 let handle = tokio::spawn(async move {
3588 let params = TickerPriceParams::builder().build().unwrap();
3589 client.ticker_price(params).await
3590 });
3591
3592 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3593 let Message::Text(text) = sent else { panic!() };
3594 let v: Value = serde_json::from_str(&text).unwrap();
3595 let id = v["id"].as_str().unwrap().to_string();
3596
3597 let resp_json = json!({
3598 "id": id,
3599 "status": 400,
3600 "error": {
3601 "code": -2010,
3602 "msg": "Account has insufficient balance for requested action.",
3603 },
3604 "rateLimits": [
3605 {
3606 "rateLimitType": "ORDERS",
3607 "interval": "SECOND",
3608 "intervalNum": 10,
3609 "limit": 50,
3610 "count": 13
3611 },
3612 ],
3613 });
3614 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3615
3616 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3617 match join {
3618 Ok(Err(e)) => {
3619 let msg = e.to_string();
3620 assert!(
3621 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3622 "Expected error msg to contain server error, got: {msg}"
3623 );
3624 }
3625 Ok(Ok(_)) => panic!("Expected error"),
3626 Err(_) => panic!("Task panicked"),
3627 }
3628 });
3629 }
3630
3631 #[test]
3632 fn ticker_price_request_timeout() {
3633 TOKIO_SHARED_RT.block_on(async {
3634 let (ws_api, _conn, mut rx) = setup().await;
3635 let client = MarketApiClient::new(ws_api.clone());
3636
3637 let handle = spawn(async move {
3638 let params = TickerPriceParams::builder().build().unwrap();
3639 client.ticker_price(params).await
3640 });
3641
3642 let sent = timeout(Duration::from_secs(1), rx.recv())
3643 .await
3644 .expect("send should occur")
3645 .expect("channel closed");
3646 let Message::Text(text) = sent else {
3647 panic!("expected Message Text")
3648 };
3649
3650 let _: Value = serde_json::from_str(&text).unwrap();
3651
3652 let result = handle.await.expect("task completed");
3653 match result {
3654 Err(e) => {
3655 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3656 assert!(matches!(inner, WebsocketError::Timeout));
3657 } else {
3658 panic!("Unexpected error type: {:?}", e);
3659 }
3660 }
3661 Ok(_) => panic!("Expected timeout error"),
3662 }
3663 });
3664 }
3665
3666 #[test]
3667 fn ticker_trading_day_success() {
3668 TOKIO_SHARED_RT.block_on(async {
3669 let (ws_api, conn, mut rx) = setup().await;
3670 let client = MarketApiClient::new(ws_api.clone());
3671
3672 let handle = spawn(async move {
3673 let params = TickerTradingDayParams::builder().build().unwrap();
3674 client.ticker_trading_day(params).await
3675 });
3676
3677 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3678 let Message::Text(text) = sent else { panic!() };
3679 let v: Value = serde_json::from_str(&text).unwrap();
3680 let id = v["id"].as_str().unwrap();
3681 assert_eq!(v["method"], "/ticker.tradingDay".trim_start_matches('/'));
3682
3683 let mut resp_json: Value = serde_json::from_str(r#"{"id":"f4b3b507-c8f2-442a-81a6-b2f12daa030f","status":200,"result":[{"symbol":"BNBUSDT","priceChange":"2.60000000","priceChangePercent":"1.238","weightedAvgPrice":"211.92276958","openPrice":"210.00000000","highPrice":"213.70000000","lowPrice":"209.70000000","lastPrice":"212.60000000","volume":"280709.58900000","quoteVolume":"59488753.54750000","openTime":1695686400000,"closeTime":1695772799999,"firstId":672397461,"lastId":672496158,"count":98698},{"symbol":"BTCUSDT","priceChange":"-83.13000000","priceChangePercent":"-0.317","weightedAvgPrice":"26234.58803036","openPrice":"26304.80000000","highPrice":"26397.46000000","lowPrice":"26088.34000000","lastPrice":"26221.67000000","volume":"18495.35066000","quoteVolume":"485217905.04210480","openTime":1695686400000,"closeTime":1695772799999,"firstId":3220151555,"lastId":3220849281,"count":697727}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":8}]}"#).unwrap();
3684 resp_json["id"] = id.into();
3685
3686 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3687 let expected_data: Vec<models::TickerTradingDayResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3688 let empty_array = Value::Array(vec![]);
3689 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3690 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3691 match raw_rate_limits.as_array() {
3692 Some(arr) if arr.is_empty() => None,
3693 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3694 None => None,
3695 };
3696
3697 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3698
3699 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3700
3701
3702 let response_rate_limits = response.rate_limits.clone();
3703 let response_data = response.data().expect("deserialize data");
3704
3705 assert_eq!(response_rate_limits, expected_rate_limits);
3706 assert_eq!(response_data, expected_data);
3707 });
3708 }
3709
3710 #[test]
3711 fn ticker_trading_day_error_response() {
3712 TOKIO_SHARED_RT.block_on(async {
3713 let (ws_api, conn, mut rx) = setup().await;
3714 let client = MarketApiClient::new(ws_api.clone());
3715
3716 let handle = tokio::spawn(async move {
3717 let params = TickerTradingDayParams::builder().build().unwrap();
3718 client.ticker_trading_day(params).await
3719 });
3720
3721 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3722 let Message::Text(text) = sent else { panic!() };
3723 let v: Value = serde_json::from_str(&text).unwrap();
3724 let id = v["id"].as_str().unwrap().to_string();
3725
3726 let resp_json = json!({
3727 "id": id,
3728 "status": 400,
3729 "error": {
3730 "code": -2010,
3731 "msg": "Account has insufficient balance for requested action.",
3732 },
3733 "rateLimits": [
3734 {
3735 "rateLimitType": "ORDERS",
3736 "interval": "SECOND",
3737 "intervalNum": 10,
3738 "limit": 50,
3739 "count": 13
3740 },
3741 ],
3742 });
3743 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3744
3745 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3746 match join {
3747 Ok(Err(e)) => {
3748 let msg = e.to_string();
3749 assert!(
3750 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3751 "Expected error msg to contain server error, got: {msg}"
3752 );
3753 }
3754 Ok(Ok(_)) => panic!("Expected error"),
3755 Err(_) => panic!("Task panicked"),
3756 }
3757 });
3758 }
3759
3760 #[test]
3761 fn ticker_trading_day_request_timeout() {
3762 TOKIO_SHARED_RT.block_on(async {
3763 let (ws_api, _conn, mut rx) = setup().await;
3764 let client = MarketApiClient::new(ws_api.clone());
3765
3766 let handle = spawn(async move {
3767 let params = TickerTradingDayParams::builder().build().unwrap();
3768 client.ticker_trading_day(params).await
3769 });
3770
3771 let sent = timeout(Duration::from_secs(1), rx.recv())
3772 .await
3773 .expect("send should occur")
3774 .expect("channel closed");
3775 let Message::Text(text) = sent else {
3776 panic!("expected Message Text")
3777 };
3778
3779 let _: Value = serde_json::from_str(&text).unwrap();
3780
3781 let result = handle.await.expect("task completed");
3782 match result {
3783 Err(e) => {
3784 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3785 assert!(matches!(inner, WebsocketError::Timeout));
3786 } else {
3787 panic!("Unexpected error type: {:?}", e);
3788 }
3789 }
3790 Ok(_) => panic!("Expected timeout error"),
3791 }
3792 });
3793 }
3794
3795 #[test]
3796 fn trades_aggregate_success() {
3797 TOKIO_SHARED_RT.block_on(async {
3798 let (ws_api, conn, mut rx) = setup().await;
3799 let client = MarketApiClient::new(ws_api.clone());
3800
3801 let handle = spawn(async move {
3802 let params = TradesAggregateParams::builder("BNBUSDT".to_string(),).build().unwrap();
3803 client.trades_aggregate(params).await
3804 });
3805
3806 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3807 let Message::Text(text) = sent else { panic!() };
3808 let v: Value = serde_json::from_str(&text).unwrap();
3809 let id = v["id"].as_str().unwrap();
3810 assert_eq!(v["method"], "/trades.aggregate".trim_start_matches('/'));
3811
3812 let mut resp_json: Value = serde_json::from_str(r#"{"id":"189da436-d4bd-48ca-9f95-9f613d621717","status":200,"result":[{"a":50000000,"p":"0.00274100","q":"57.19000000","f":59120167,"l":59120170,"T":1565877971222,"m":true,"M":true}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
3813 resp_json["id"] = id.into();
3814
3815 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3816 let expected_data: Vec<models::TradesAggregateResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3817 let empty_array = Value::Array(vec![]);
3818 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3819 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3820 match raw_rate_limits.as_array() {
3821 Some(arr) if arr.is_empty() => None,
3822 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3823 None => None,
3824 };
3825
3826 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3827
3828 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3829
3830
3831 let response_rate_limits = response.rate_limits.clone();
3832 let response_data = response.data().expect("deserialize data");
3833
3834 assert_eq!(response_rate_limits, expected_rate_limits);
3835 assert_eq!(response_data, expected_data);
3836 });
3837 }
3838
3839 #[test]
3840 fn trades_aggregate_error_response() {
3841 TOKIO_SHARED_RT.block_on(async {
3842 let (ws_api, conn, mut rx) = setup().await;
3843 let client = MarketApiClient::new(ws_api.clone());
3844
3845 let handle = tokio::spawn(async move {
3846 let params = TradesAggregateParams::builder("BNBUSDT".to_string(),).build().unwrap();
3847 client.trades_aggregate(params).await
3848 });
3849
3850 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3851 let Message::Text(text) = sent else { panic!() };
3852 let v: Value = serde_json::from_str(&text).unwrap();
3853 let id = v["id"].as_str().unwrap().to_string();
3854
3855 let resp_json = json!({
3856 "id": id,
3857 "status": 400,
3858 "error": {
3859 "code": -2010,
3860 "msg": "Account has insufficient balance for requested action.",
3861 },
3862 "rateLimits": [
3863 {
3864 "rateLimitType": "ORDERS",
3865 "interval": "SECOND",
3866 "intervalNum": 10,
3867 "limit": 50,
3868 "count": 13
3869 },
3870 ],
3871 });
3872 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3873
3874 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3875 match join {
3876 Ok(Err(e)) => {
3877 let msg = e.to_string();
3878 assert!(
3879 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3880 "Expected error msg to contain server error, got: {msg}"
3881 );
3882 }
3883 Ok(Ok(_)) => panic!("Expected error"),
3884 Err(_) => panic!("Task panicked"),
3885 }
3886 });
3887 }
3888
3889 #[test]
3890 fn trades_aggregate_request_timeout() {
3891 TOKIO_SHARED_RT.block_on(async {
3892 let (ws_api, _conn, mut rx) = setup().await;
3893 let client = MarketApiClient::new(ws_api.clone());
3894
3895 let handle = spawn(async move {
3896 let params = TradesAggregateParams::builder("BNBUSDT".to_string())
3897 .build()
3898 .unwrap();
3899 client.trades_aggregate(params).await
3900 });
3901
3902 let sent = timeout(Duration::from_secs(1), rx.recv())
3903 .await
3904 .expect("send should occur")
3905 .expect("channel closed");
3906 let Message::Text(text) = sent else {
3907 panic!("expected Message Text")
3908 };
3909
3910 let _: Value = serde_json::from_str(&text).unwrap();
3911
3912 let result = handle.await.expect("task completed");
3913 match result {
3914 Err(e) => {
3915 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3916 assert!(matches!(inner, WebsocketError::Timeout));
3917 } else {
3918 panic!("Unexpected error type: {:?}", e);
3919 }
3920 }
3921 Ok(_) => panic!("Expected timeout error"),
3922 }
3923 });
3924 }
3925
3926 #[test]
3927 fn trades_historical_success() {
3928 TOKIO_SHARED_RT.block_on(async {
3929 let (ws_api, conn, mut rx) = setup().await;
3930 let client = MarketApiClient::new(ws_api.clone());
3931
3932 let handle = spawn(async move {
3933 let params = TradesHistoricalParams::builder("BNBUSDT".to_string(),).build().unwrap();
3934 client.trades_historical(params).await
3935 });
3936
3937 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3938 let Message::Text(text) = sent else { panic!() };
3939 let v: Value = serde_json::from_str(&text).unwrap();
3940 let id = v["id"].as_str().unwrap();
3941 assert_eq!(v["method"], "/trades.historical".trim_start_matches('/'));
3942
3943 let mut resp_json: Value = serde_json::from_str(r#"{"id":"cffc9c7d-4efc-4ce0-b587-6b87448f052a","status":200,"result":[{"id":0,"price":"0.00005000","qty":"40.00000000","quoteQty":"0.00200000","time":1500004800376,"isBuyerMaker":true,"isBestMatch":true}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":10}]}"#).unwrap();
3944 resp_json["id"] = id.into();
3945
3946 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3947 let expected_data: Vec<models::TradesHistoricalResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3948 let empty_array = Value::Array(vec![]);
3949 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3950 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3951 match raw_rate_limits.as_array() {
3952 Some(arr) if arr.is_empty() => None,
3953 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3954 None => None,
3955 };
3956
3957 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3958
3959 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3960
3961
3962 let response_rate_limits = response.rate_limits.clone();
3963 let response_data = response.data().expect("deserialize data");
3964
3965 assert_eq!(response_rate_limits, expected_rate_limits);
3966 assert_eq!(response_data, expected_data);
3967 });
3968 }
3969
3970 #[test]
3971 fn trades_historical_error_response() {
3972 TOKIO_SHARED_RT.block_on(async {
3973 let (ws_api, conn, mut rx) = setup().await;
3974 let client = MarketApiClient::new(ws_api.clone());
3975
3976 let handle = tokio::spawn(async move {
3977 let params = TradesHistoricalParams::builder("BNBUSDT".to_string(),).build().unwrap();
3978 client.trades_historical(params).await
3979 });
3980
3981 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3982 let Message::Text(text) = sent else { panic!() };
3983 let v: Value = serde_json::from_str(&text).unwrap();
3984 let id = v["id"].as_str().unwrap().to_string();
3985
3986 let resp_json = json!({
3987 "id": id,
3988 "status": 400,
3989 "error": {
3990 "code": -2010,
3991 "msg": "Account has insufficient balance for requested action.",
3992 },
3993 "rateLimits": [
3994 {
3995 "rateLimitType": "ORDERS",
3996 "interval": "SECOND",
3997 "intervalNum": 10,
3998 "limit": 50,
3999 "count": 13
4000 },
4001 ],
4002 });
4003 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
4004
4005 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
4006 match join {
4007 Ok(Err(e)) => {
4008 let msg = e.to_string();
4009 assert!(
4010 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
4011 "Expected error msg to contain server error, got: {msg}"
4012 );
4013 }
4014 Ok(Ok(_)) => panic!("Expected error"),
4015 Err(_) => panic!("Task panicked"),
4016 }
4017 });
4018 }
4019
4020 #[test]
4021 fn trades_historical_request_timeout() {
4022 TOKIO_SHARED_RT.block_on(async {
4023 let (ws_api, _conn, mut rx) = setup().await;
4024 let client = MarketApiClient::new(ws_api.clone());
4025
4026 let handle = spawn(async move {
4027 let params = TradesHistoricalParams::builder("BNBUSDT".to_string())
4028 .build()
4029 .unwrap();
4030 client.trades_historical(params).await
4031 });
4032
4033 let sent = timeout(Duration::from_secs(1), rx.recv())
4034 .await
4035 .expect("send should occur")
4036 .expect("channel closed");
4037 let Message::Text(text) = sent else {
4038 panic!("expected Message Text")
4039 };
4040
4041 let _: Value = serde_json::from_str(&text).unwrap();
4042
4043 let result = handle.await.expect("task completed");
4044 match result {
4045 Err(e) => {
4046 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
4047 assert!(matches!(inner, WebsocketError::Timeout));
4048 } else {
4049 panic!("Unexpected error type: {:?}", e);
4050 }
4051 }
4052 Ok(_) => panic!("Expected timeout error"),
4053 }
4054 });
4055 }
4056
4057 #[test]
4058 fn trades_recent_success() {
4059 TOKIO_SHARED_RT.block_on(async {
4060 let (ws_api, conn, mut rx) = setup().await;
4061 let client = MarketApiClient::new(ws_api.clone());
4062
4063 let handle = spawn(async move {
4064 let params = TradesRecentParams::builder("BNBUSDT".to_string(),).build().unwrap();
4065 client.trades_recent(params).await
4066 });
4067
4068 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
4069 let Message::Text(text) = sent else { panic!() };
4070 let v: Value = serde_json::from_str(&text).unwrap();
4071 let id = v["id"].as_str().unwrap();
4072 assert_eq!(v["method"], "/trades.recent".trim_start_matches('/'));
4073
4074 let mut resp_json: Value = serde_json::from_str(r#"{"id":"409a20bd-253d-41db-a6dd-687862a5882f","status":200,"result":[{"id":194686783,"price":"0.01361000","qty":"0.01400000","quoteQty":"0.00019054","time":1660009530807,"isBuyerMaker":true,"isBestMatch":true}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
4075 resp_json["id"] = id.into();
4076
4077 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
4078 let expected_data: Vec<models::TradesRecentResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
4079 let empty_array = Value::Array(vec![]);
4080 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
4081 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
4082 match raw_rate_limits.as_array() {
4083 Some(arr) if arr.is_empty() => None,
4084 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
4085 None => None,
4086 };
4087
4088 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
4089
4090 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
4091
4092
4093 let response_rate_limits = response.rate_limits.clone();
4094 let response_data = response.data().expect("deserialize data");
4095
4096 assert_eq!(response_rate_limits, expected_rate_limits);
4097 assert_eq!(response_data, expected_data);
4098 });
4099 }
4100
4101 #[test]
4102 fn trades_recent_error_response() {
4103 TOKIO_SHARED_RT.block_on(async {
4104 let (ws_api, conn, mut rx) = setup().await;
4105 let client = MarketApiClient::new(ws_api.clone());
4106
4107 let handle = tokio::spawn(async move {
4108 let params = TradesRecentParams::builder("BNBUSDT".to_string(),).build().unwrap();
4109 client.trades_recent(params).await
4110 });
4111
4112 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
4113 let Message::Text(text) = sent else { panic!() };
4114 let v: Value = serde_json::from_str(&text).unwrap();
4115 let id = v["id"].as_str().unwrap().to_string();
4116
4117 let resp_json = json!({
4118 "id": id,
4119 "status": 400,
4120 "error": {
4121 "code": -2010,
4122 "msg": "Account has insufficient balance for requested action.",
4123 },
4124 "rateLimits": [
4125 {
4126 "rateLimitType": "ORDERS",
4127 "interval": "SECOND",
4128 "intervalNum": 10,
4129 "limit": 50,
4130 "count": 13
4131 },
4132 ],
4133 });
4134 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
4135
4136 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
4137 match join {
4138 Ok(Err(e)) => {
4139 let msg = e.to_string();
4140 assert!(
4141 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
4142 "Expected error msg to contain server error, got: {msg}"
4143 );
4144 }
4145 Ok(Ok(_)) => panic!("Expected error"),
4146 Err(_) => panic!("Task panicked"),
4147 }
4148 });
4149 }
4150
4151 #[test]
4152 fn trades_recent_request_timeout() {
4153 TOKIO_SHARED_RT.block_on(async {
4154 let (ws_api, _conn, mut rx) = setup().await;
4155 let client = MarketApiClient::new(ws_api.clone());
4156
4157 let handle = spawn(async move {
4158 let params = TradesRecentParams::builder("BNBUSDT".to_string())
4159 .build()
4160 .unwrap();
4161 client.trades_recent(params).await
4162 });
4163
4164 let sent = timeout(Duration::from_secs(1), rx.recv())
4165 .await
4166 .expect("send should occur")
4167 .expect("channel closed");
4168 let Message::Text(text) = sent else {
4169 panic!("expected Message Text")
4170 };
4171
4172 let _: Value = serde_json::from_str(&text).unwrap();
4173
4174 let result = handle.await.expect("task completed");
4175 match result {
4176 Err(e) => {
4177 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
4178 assert!(matches!(inner, WebsocketError::Timeout));
4179 } else {
4180 panic!("Unexpected error type: {:?}", e);
4181 }
4182 }
4183 Ok(_) => panic!("Expected timeout error"),
4184 }
4185 });
4186 }
4187
4188 #[test]
4189 fn ui_klines_success() {
4190 TOKIO_SHARED_RT.block_on(async {
4191 let (ws_api, conn, mut rx) = setup().await;
4192 let client = MarketApiClient::new(ws_api.clone());
4193
4194 let handle = spawn(async move {
4195 let params = UiKlinesParams::builder("BNBUSDT".to_string(),UiKlinesIntervalEnum::Interval1s,).build().unwrap();
4196 client.ui_klines(params).await
4197 });
4198
4199 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
4200 let Message::Text(text) = sent else { panic!() };
4201 let v: Value = serde_json::from_str(&text).unwrap();
4202 let id = v["id"].as_str().unwrap();
4203 assert_eq!(v["method"], "/uiKlines".trim_start_matches('/'));
4204
4205 let mut resp_json: Value = serde_json::from_str(r#"{"id":"b137468a-fb20-4c06-bd6b-625148eec958","status":200,"result":[[1655971200000,"0.01086000","0.01086600","0.01083600","0.01083800","2290.53800000",1655974799999,"24.85074442",2283,"1171.64000000","12.71225884","0"]],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":2}]}"#).unwrap();
4206 resp_json["id"] = id.into();
4207
4208 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
4209 let expected_data: Vec<Vec<models::KlinesItemInner>> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
4210 let empty_array = Value::Array(vec![]);
4211 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
4212 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
4213 match raw_rate_limits.as_array() {
4214 Some(arr) if arr.is_empty() => None,
4215 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
4216 None => None,
4217 };
4218
4219 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
4220
4221 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
4222
4223
4224 let response_rate_limits = response.rate_limits.clone();
4225 let response_data = response.data().expect("deserialize data");
4226
4227 assert_eq!(response_rate_limits, expected_rate_limits);
4228 assert_eq!(response_data, expected_data);
4229 });
4230 }
4231
4232 #[test]
4233 fn ui_klines_error_response() {
4234 TOKIO_SHARED_RT.block_on(async {
4235 let (ws_api, conn, mut rx) = setup().await;
4236 let client = MarketApiClient::new(ws_api.clone());
4237
4238 let handle = tokio::spawn(async move {
4239 let params = UiKlinesParams::builder("BNBUSDT".to_string(),UiKlinesIntervalEnum::Interval1s,).build().unwrap();
4240 client.ui_klines(params).await
4241 });
4242
4243 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
4244 let Message::Text(text) = sent else { panic!() };
4245 let v: Value = serde_json::from_str(&text).unwrap();
4246 let id = v["id"].as_str().unwrap().to_string();
4247
4248 let resp_json = json!({
4249 "id": id,
4250 "status": 400,
4251 "error": {
4252 "code": -2010,
4253 "msg": "Account has insufficient balance for requested action.",
4254 },
4255 "rateLimits": [
4256 {
4257 "rateLimitType": "ORDERS",
4258 "interval": "SECOND",
4259 "intervalNum": 10,
4260 "limit": 50,
4261 "count": 13
4262 },
4263 ],
4264 });
4265 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
4266
4267 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
4268 match join {
4269 Ok(Err(e)) => {
4270 let msg = e.to_string();
4271 assert!(
4272 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
4273 "Expected error msg to contain server error, got: {msg}"
4274 );
4275 }
4276 Ok(Ok(_)) => panic!("Expected error"),
4277 Err(_) => panic!("Task panicked"),
4278 }
4279 });
4280 }
4281
4282 #[test]
4283 fn ui_klines_request_timeout() {
4284 TOKIO_SHARED_RT.block_on(async {
4285 let (ws_api, _conn, mut rx) = setup().await;
4286 let client = MarketApiClient::new(ws_api.clone());
4287
4288 let handle = spawn(async move {
4289 let params = UiKlinesParams::builder(
4290 "BNBUSDT".to_string(),
4291 UiKlinesIntervalEnum::Interval1s,
4292 )
4293 .build()
4294 .unwrap();
4295 client.ui_klines(params).await
4296 });
4297
4298 let sent = timeout(Duration::from_secs(1), rx.recv())
4299 .await
4300 .expect("send should occur")
4301 .expect("channel closed");
4302 let Message::Text(text) = sent else {
4303 panic!("expected Message Text")
4304 };
4305
4306 let _: Value = serde_json::from_str(&text).unwrap();
4307
4308 let result = handle.await.expect("task completed");
4309 match result {
4310 Err(e) => {
4311 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
4312 assert!(matches!(inner, WebsocketError::Timeout));
4313 } else {
4314 panic!("Unexpected error type: {:?}", e);
4315 }
4316 }
4317 Ok(_) => panic!("Expected timeout error"),
4318 }
4319 });
4320 }
4321}