1use std::fmt::Debug;
21
22use anyhow::Context;
23use nautilus_core::{UUID4, nanos::UnixNanos, string::secret::SecretString};
24use nautilus_model::{
25 enums::AccountType,
26 events::AccountState,
27 identifiers::AccountId,
28 types::{AccountBalance, Currency, Money},
29};
30use rust_decimal::Decimal;
31use zeroize::{Zeroize, ZeroizeOnDrop};
32
33use crate::{
34 common::{
35 enums::{
36 BinanceOrderStatus, BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce,
37 },
38 parse::{account_balance_from_free_locked, parse_micros_or_init, sbe_mantissa_to_decimal},
39 },
40 spot::sbe::spot::{
41 contingency_type::ContingencyType, list_order_status::ListOrderStatus,
42 list_status_type::ListStatusType, order_side::OrderSide, order_status::OrderStatus,
43 order_type::OrderType, self_trade_prevention_mode::SelfTradePreventionMode,
44 time_in_force::TimeInForce,
45 },
46};
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct BinancePriceLevel {
51 pub price_mantissa: i64,
53 pub qty_mantissa: i64,
55}
56
57#[derive(Debug, Clone, PartialEq)]
59pub struct BinanceDepth {
60 pub last_update_id: i64,
62 pub price_exponent: i8,
64 pub qty_exponent: i8,
66 pub bids: Vec<BinancePriceLevel>,
68 pub asks: Vec<BinancePriceLevel>,
70}
71
72#[derive(Debug, Clone, PartialEq)]
74pub struct BinanceTrade {
75 pub id: i64,
77 pub price_mantissa: i64,
79 pub qty_mantissa: i64,
81 pub quote_qty_mantissa: i64,
83 pub time: i64,
85 pub is_buyer_maker: bool,
87 pub is_best_match: bool,
89}
90
91#[derive(Debug, Clone, PartialEq)]
93pub struct BinanceTrades {
94 pub price_exponent: i8,
96 pub qty_exponent: i8,
98 pub trades: Vec<BinanceTrade>,
100}
101
102#[derive(Debug, Clone, PartialEq)]
104pub struct BinanceAggTrade {
105 pub id: i64,
107 pub price_mantissa: i64,
109 pub qty_mantissa: i64,
111 pub first_trade_id: i64,
113 pub last_trade_id: i64,
115 pub time: i64,
117 pub is_buyer_maker: bool,
119 pub is_best_match: bool,
121}
122
123#[derive(Debug, Clone, PartialEq)]
125pub struct BinanceAggTrades {
126 pub price_exponent: i8,
128 pub qty_exponent: i8,
130 pub trades: Vec<BinanceAggTrade>,
132}
133
134#[derive(Debug, Clone, PartialEq)]
136pub struct BinanceOrderFill {
137 pub price_mantissa: i64,
139 pub qty_mantissa: i64,
141 pub commission_mantissa: i64,
143 pub commission_exponent: i8,
145 pub commission_asset: String,
147 pub trade_id: Option<i64>,
149}
150
151#[derive(Debug, Clone, PartialEq)]
153pub struct BinanceNewOrderResponse {
154 pub price_exponent: i8,
156 pub qty_exponent: i8,
158 pub order_id: i64,
160 pub order_list_id: Option<i64>,
162 pub transact_time: i64,
164 pub price_mantissa: i64,
166 pub orig_qty_mantissa: i64,
168 pub executed_qty_mantissa: i64,
170 pub cummulative_quote_qty_mantissa: i64,
172 pub status: OrderStatus,
174 pub time_in_force: TimeInForce,
176 pub order_type: OrderType,
178 pub side: OrderSide,
180 pub stop_price_mantissa: Option<i64>,
182 pub working_time: Option<i64>,
184 pub self_trade_prevention_mode: SelfTradePreventionMode,
186 pub client_order_id: String,
188 pub symbol: String,
190 pub fills: Vec<BinanceOrderFill>,
192 pub expiry_reason: Option<u8>,
194}
195
196#[derive(Debug, Clone, PartialEq)]
198pub struct BinanceCancelOrderResponse {
199 pub price_exponent: i8,
201 pub qty_exponent: i8,
203 pub order_id: i64,
205 pub order_list_id: Option<i64>,
207 pub transact_time: i64,
209 pub price_mantissa: i64,
211 pub orig_qty_mantissa: i64,
213 pub executed_qty_mantissa: i64,
215 pub cummulative_quote_qty_mantissa: i64,
217 pub status: OrderStatus,
219 pub time_in_force: TimeInForce,
221 pub order_type: OrderType,
223 pub side: OrderSide,
225 pub self_trade_prevention_mode: SelfTradePreventionMode,
227 pub client_order_id: String,
229 pub orig_client_order_id: String,
231 pub symbol: String,
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub struct BinanceCancelOrderListOrder {
238 pub symbol: String,
240 pub order_id: i64,
242 pub client_order_id: String,
244}
245
246#[derive(Debug, Clone, PartialEq)]
248pub struct BinanceCancelOrderListResponse {
249 pub order_list_id: i64,
251 pub contingency_type: ContingencyType,
253 pub list_status_type: ListStatusType,
255 pub list_order_status: ListOrderStatus,
257 pub transaction_time: i64,
259 pub list_client_order_id: String,
261 pub symbol: String,
263 pub orders: Vec<BinanceCancelOrderListOrder>,
265 pub order_reports: Vec<BinanceCancelOrderResponse>,
267}
268
269#[derive(Debug, Clone, PartialEq)]
271pub enum BinanceCancelOpenOrdersResponse {
272 Order(BinanceCancelOrderResponse),
274 OrderList(BinanceCancelOrderListResponse),
276}
277
278#[derive(Debug, Clone, PartialEq)]
280pub struct BinanceOrderResponse {
281 pub price_exponent: i8,
283 pub qty_exponent: i8,
285 pub order_id: i64,
287 pub order_list_id: Option<i64>,
289 pub price_mantissa: i64,
291 pub orig_qty_mantissa: i64,
293 pub executed_qty_mantissa: i64,
295 pub cummulative_quote_qty_mantissa: i64,
297 pub status: OrderStatus,
299 pub time_in_force: TimeInForce,
301 pub order_type: OrderType,
303 pub side: OrderSide,
305 pub stop_price_mantissa: Option<i64>,
307 pub iceberg_qty_mantissa: Option<i64>,
309 pub time: i64,
311 pub update_time: i64,
313 pub is_working: bool,
315 pub working_time: Option<i64>,
317 pub orig_quote_order_qty_mantissa: i64,
319 pub self_trade_prevention_mode: SelfTradePreventionMode,
321 pub client_order_id: String,
323 pub symbol: String,
325 pub expiry_reason: Option<u8>,
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub struct BinanceBalance {
332 pub asset: String,
334 pub free_mantissa: i64,
336 pub locked_mantissa: i64,
338 pub exponent: i8,
340}
341
342#[derive(Debug, Clone, PartialEq)]
344pub struct BinanceAccountInfo {
345 pub commission_exponent: i8,
347 pub maker_commission_mantissa: i64,
349 pub taker_commission_mantissa: i64,
351 pub buyer_commission_mantissa: i64,
353 pub seller_commission_mantissa: i64,
355 pub can_trade: bool,
357 pub can_withdraw: bool,
359 pub can_deposit: bool,
361 pub require_self_trade_prevention: bool,
363 pub prevent_sor: bool,
365 pub update_time: i64,
367 pub account_type: String,
369 pub balances: Vec<BinanceBalance>,
371}
372
373impl BinanceAccountInfo {
374 pub fn to_account_state(
384 &self,
385 account_id: AccountId,
386 ts_init: UnixNanos,
387 ) -> anyhow::Result<AccountState> {
388 let mut balances = Vec::with_capacity(self.balances.len());
389
390 for asset in &self.balances {
391 let free = sbe_mantissa_to_decimal(asset.free_mantissa, asset.exponent)
392 .with_context(|| format!("failed to convert free balance for {}", asset.asset))?;
393 let locked = sbe_mantissa_to_decimal(asset.locked_mantissa, asset.exponent)
394 .with_context(|| format!("failed to convert locked balance for {}", asset.asset))?;
395 let currency =
396 Currency::get_or_create_crypto_with_context(&asset.asset, Some("spot balance"));
397 let balance = account_balance_from_free_locked(&asset.asset, free, locked, currency)?;
398 balances.push(balance);
399 }
400
401 if balances.is_empty() {
403 let zero_currency = Currency::USDT();
404 let zero_money = Money::zero(zero_currency);
405 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
406 balances.push(zero_balance);
407 }
408
409 let ts_event =
410 parse_micros_or_init(self.update_time, "Spot SBE account update time", ts_init);
411
412 Ok(AccountState::new(
413 account_id,
414 AccountType::Cash,
415 balances,
416 vec![], true, UUID4::new(),
419 ts_event,
420 ts_init,
421 None, ))
423 }
424}
425
426#[derive(Debug, Clone, PartialEq)]
428pub struct BinancePriceFilterSbe {
429 pub price_exponent: i8,
431 pub min_price: i64,
433 pub max_price: i64,
435 pub tick_size: i64,
437}
438
439#[derive(Debug, Clone, PartialEq)]
441pub struct BinanceLotSizeFilterSbe {
442 pub qty_exponent: i8,
444 pub min_qty: i64,
446 pub max_qty: i64,
448 pub step_size: i64,
450}
451
452#[derive(Debug, Clone, PartialEq)]
454pub struct BinanceNotionalFilter {
455 pub min: Decimal,
457 pub max: Option<Decimal>,
459 pub apply_min_to_market: bool,
461 pub apply_max_to_market: bool,
463 pub avg_price_mins: u32,
465}
466
467#[derive(Debug, Clone, Default, PartialEq)]
469pub struct BinanceSymbolFiltersSbe {
470 pub price_filter: Option<BinancePriceFilterSbe>,
472 pub lot_size_filter: Option<BinanceLotSizeFilterSbe>,
474 pub notional_filters: Vec<BinanceNotionalFilter>,
476}
477
478#[derive(Debug, Clone, PartialEq)]
480pub struct BinanceSymbolSbe {
481 pub symbol: String,
483 pub base_asset: String,
485 pub quote_asset: String,
487 pub base_asset_precision: u8,
489 pub quote_asset_precision: u8,
491 pub status: u8,
493 pub order_types: u16,
495 pub iceberg_allowed: bool,
497 pub oco_allowed: bool,
499 pub oto_allowed: bool,
501 pub quote_order_qty_market_allowed: bool,
503 pub allow_trailing_stop: bool,
505 pub cancel_replace_allowed: bool,
507 pub amend_allowed: bool,
509 pub is_spot_trading_allowed: bool,
511 pub is_margin_trading_allowed: bool,
513 pub filters: BinanceSymbolFiltersSbe,
515 pub permissions: Vec<Vec<String>>,
517}
518
519#[derive(Debug, Clone, PartialEq)]
521pub struct BinanceExchangeInfoSbe {
522 pub symbols: Vec<BinanceSymbolSbe>,
524}
525
526#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
528pub struct BinanceExchangeInfoJson {
529 pub symbols: Vec<BinanceSymbolJson>,
531}
532
533#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct BinanceSymbolJson {
537 pub symbol: String,
539 pub status: String,
541 pub base_asset: String,
543 pub quote_asset: String,
545 pub base_asset_precision: u8,
547 pub quote_asset_precision: u8,
549 pub filters: Vec<BinanceSymbolFilterJson>,
551}
552
553#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct BinanceSymbolFilterJson {
557 pub filter_type: String,
559 pub min_price: Option<String>,
561 pub max_price: Option<String>,
563 pub tick_size: Option<String>,
565 pub min_qty: Option<String>,
567 pub max_qty: Option<String>,
569 pub step_size: Option<String>,
571 pub min_notional: Option<String>,
573 pub max_notional: Option<String>,
575 pub apply_to_market: Option<bool>,
577 pub apply_min_to_market: Option<bool>,
579 pub apply_max_to_market: Option<bool>,
581 pub avg_price_mins: Option<u32>,
583}
584
585#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct BinanceAccountCommission {
589 pub symbol: String,
591 pub standard_commission: BinanceCommissionRates,
593}
594
595#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
597pub struct BinanceCommissionRates {
598 pub maker: String,
600 pub taker: String,
602}
603
604#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct BinanceAccountRatesJson {
608 pub commission_rates: BinanceCommissionRates,
610}
611
612#[derive(Debug, Clone, PartialEq)]
614pub struct BinanceAccountTrade {
615 pub price_exponent: i8,
617 pub qty_exponent: i8,
619 pub commission_exponent: i8,
621 pub id: i64,
623 pub order_id: i64,
625 pub order_list_id: Option<i64>,
627 pub price_mantissa: i64,
629 pub qty_mantissa: i64,
631 pub quote_qty_mantissa: i64,
633 pub commission_mantissa: i64,
635 pub time: i64,
637 pub is_buyer: bool,
639 pub is_maker: bool,
641 pub is_best_match: bool,
643 pub symbol: String,
645 pub commission_asset: String,
647}
648
649#[derive(Debug, Clone, PartialEq)]
651pub struct BinanceKlines {
652 pub price_exponent: i8,
654 pub qty_exponent: i8,
656 pub klines: Vec<BinanceKline>,
658}
659
660#[derive(Debug, Clone, PartialEq, serde::Deserialize, Zeroize, ZeroizeOnDrop)]
662#[serde(rename_all = "camelCase")]
663pub struct ListenKeyResponse {
664 pub listen_key: SecretString,
666}
667
668impl ListenKeyResponse {
669 #[must_use]
671 pub fn into_listen_key(mut self) -> SecretString {
672 std::mem::take(&mut self.listen_key)
673 }
674}
675
676#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct Ticker24hr {
680 pub symbol: String,
682 pub price_change: String,
684 pub price_change_percent: String,
686 pub weighted_avg_price: String,
688 pub prev_close_price: String,
690 pub last_price: String,
692 pub last_qty: String,
694 pub bid_price: String,
696 pub bid_qty: String,
698 pub ask_price: String,
700 pub ask_qty: String,
702 pub open_price: String,
704 pub high_price: String,
706 pub low_price: String,
708 pub volume: String,
710 pub quote_volume: String,
712 pub open_time: i64,
714 pub close_time: i64,
716 pub first_id: i64,
718 pub last_id: i64,
720 pub count: i64,
722}
723
724#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
726pub struct TickerPrice {
727 pub symbol: String,
729 pub price: String,
731}
732
733#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
735#[serde(rename_all = "camelCase")]
736pub struct BookTicker {
737 pub symbol: String,
739 pub bid_price: String,
741 pub bid_qty: String,
743 pub ask_price: String,
745 pub ask_qty: String,
747}
748
749#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
751pub struct AvgPrice {
752 pub mins: i64,
754 pub price: String,
756 #[serde(rename = "closeTime")]
758 pub close_time: i64,
759}
760
761#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct TradeFee {
765 pub symbol: String,
767 pub maker_commission: String,
769 pub taker_commission: String,
771}
772
773#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
775#[serde(rename_all = "camelCase")]
776pub struct NewOcoOrderListResponse {
777 pub order_list_id: i64,
779 pub contingency_type: String,
781 pub list_status_type: String,
783 pub list_order_status: String,
785 pub list_client_order_id: String,
787 pub transaction_time: i64,
789 pub symbol: String,
791 pub orders: Vec<OrderListOrder>,
793}
794
795#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub struct OrderListOrder {
799 pub symbol: String,
801 pub order_id: i64,
803 pub client_order_id: String,
805}
806
807#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
811#[serde(untagged)]
812pub enum BatchOrderResult {
813 Success(Box<BatchOrderSuccess>),
815 Error(BatchOrderError),
817}
818
819#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
821#[serde(rename_all = "camelCase")]
822pub struct BatchOrderSuccess {
823 pub symbol: String,
825 pub order_id: i64,
827 #[serde(default)]
829 pub order_list_id: Option<i64>,
830 pub client_order_id: String,
832 pub transact_time: i64,
834 pub price: String,
836 pub orig_qty: String,
838 pub executed_qty: String,
840 #[serde(rename = "cummulativeQuoteQty")]
842 pub cummulative_quote_qty: String,
843 pub status: BinanceOrderStatus,
845 pub time_in_force: BinanceTimeInForce,
847 #[serde(rename = "type")]
849 pub order_type: String,
850 pub side: BinanceSide,
852 #[serde(default)]
854 pub working_time: Option<i64>,
855 #[serde(default)]
857 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
858}
859
860#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
862pub struct BatchOrderError {
863 pub code: i64,
865 pub msg: String,
867}
868
869#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
871#[serde(untagged)]
872pub enum BatchCancelResult {
873 Success(Box<BatchCancelSuccess>),
875 Error(BatchOrderError),
877}
878
879#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct BatchCancelSuccess {
883 pub symbol: String,
885 pub orig_client_order_id: String,
887 pub order_id: i64,
889 #[serde(default)]
891 pub order_list_id: Option<i64>,
892 pub client_order_id: String,
894 #[serde(default)]
896 pub transact_time: Option<i64>,
897 pub price: String,
899 pub orig_qty: String,
901 pub executed_qty: String,
903 #[serde(rename = "cummulativeQuoteQty")]
905 pub cummulative_quote_qty: String,
906 pub status: BinanceOrderStatus,
908 pub time_in_force: BinanceTimeInForce,
910 #[serde(rename = "type")]
912 pub order_type: String,
913 pub side: BinanceSide,
915 #[serde(default)]
917 pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
918}
919
920#[derive(Debug, Clone, PartialEq)]
922pub struct BinanceKline {
923 pub open_time: i64,
925 pub open_price: i64,
927 pub high_price: i64,
929 pub low_price: i64,
931 pub close_price: i64,
933 pub volume: [u8; 16],
935 pub close_time: i64,
937 pub quote_volume: [u8; 16],
939 pub num_trades: i64,
941 pub taker_buy_base_volume: [u8; 16],
943 pub taker_buy_quote_volume: [u8; 16],
945}
946
947#[cfg(test)]
948mod tests {
949 use rstest::rstest;
950 use zeroize::Zeroize;
951
952 use super::*;
953 use crate::common::testing::load_fixture_string;
954
955 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
956
957 #[rstest]
958 fn test_listen_key_response_deserialize() {
959 assert_zeroize_on_drop::<ListenKeyResponse>();
960
961 let json = r#"{"listenKey": "abc123xyz"}"#;
962 let mut response: ListenKeyResponse = serde_json::from_str(json).unwrap();
963
964 let debug = format!("{response:?}");
965 assert_eq!(response.listen_key.expose_secret(), "abc123xyz");
966 assert_eq!(debug, "ListenKeyResponse { listen_key: <redacted> }");
967 assert!(!debug.contains(response.listen_key.expose_secret()));
968
969 response.zeroize();
970 assert!(response.listen_key.expose_secret().is_empty());
971 }
972
973 #[rstest]
974 fn test_ticker_price_deserialize() {
975 let json = load_fixture_string("spot/http_json/ticker_price_response.json");
976 let response: TickerPrice = serde_json::from_str(&json).unwrap();
977 assert_eq!(response.symbol, "LTCBTC");
978 assert_eq!(response.price, "4.00000200");
979 }
980
981 #[rstest]
982 fn test_book_ticker_deserialize() {
983 let json = load_fixture_string("spot/http_json/book_ticker_response.json");
984 let response: BookTicker = serde_json::from_str(&json).unwrap();
985 assert_eq!(response.symbol, "LTCBTC");
986 assert_eq!(response.bid_price, "4.00000000");
987 assert_eq!(response.ask_price, "4.00000200");
988 }
989
990 #[rstest]
991 fn test_avg_price_deserialize() {
992 let json = load_fixture_string("spot/http_json/avg_price_response.json");
993 let response: AvgPrice = serde_json::from_str(&json).unwrap();
994 assert_eq!(response.mins, 5);
995 assert_eq!(response.price, "9.35751834");
996 assert_eq!(response.close_time, 1694061154503);
997 }
998
999 #[rstest]
1000 fn test_trade_fee_deserialize() {
1001 let json = r#"{
1002 "symbol": "BTCUSDT",
1003 "makerCommission": "0.001",
1004 "takerCommission": "0.001"
1005 }"#;
1006 let response: TradeFee = serde_json::from_str(json).unwrap();
1007 assert_eq!(response.symbol, "BTCUSDT");
1008 assert_eq!(response.maker_commission, "0.001");
1009 assert_eq!(response.taker_commission, "0.001");
1010 }
1011
1012 #[rstest]
1013 fn test_batch_order_result_success() {
1014 let json = load_fixture_string("spot/http_json/new_order_full_response.json");
1015 let result: BatchOrderResult = serde_json::from_str(&json).unwrap();
1016 match result {
1017 BatchOrderResult::Success(order) => {
1018 assert_eq!(order.symbol, "BTCUSDT");
1019 assert_eq!(order.order_id, 28);
1020 assert_eq!(order.status, BinanceOrderStatus::Filled);
1021 assert_eq!(order.time_in_force, BinanceTimeInForce::Gtc);
1022 assert_eq!(order.order_type, "MARKET");
1023 assert_eq!(order.side, BinanceSide::Sell);
1024 assert_eq!(
1025 order.self_trade_prevention_mode,
1026 Some(BinanceSelfTradePreventionMode::None)
1027 );
1028 }
1029 BatchOrderResult::Error(_) => panic!("Expected Success"),
1030 }
1031 }
1032
1033 #[rstest]
1034 fn test_batch_order_result_error() {
1035 let json = r#"{"code": -1013, "msg": "Invalid quantity."}"#;
1036 let result: BatchOrderResult = serde_json::from_str(json).unwrap();
1037 match result {
1038 BatchOrderResult::Success(_) => panic!("Expected Error"),
1039 BatchOrderResult::Error(error) => {
1040 assert_eq!(error.code, -1013);
1041 assert_eq!(error.msg, "Invalid quantity.");
1042 }
1043 }
1044 }
1045
1046 #[rstest]
1047 fn test_batch_cancel_result_success() {
1048 let json = load_fixture_string("spot/http_json/cancel_order_response.json");
1049 let result: BatchCancelResult = serde_json::from_str(&json).unwrap();
1050 match result {
1051 BatchCancelResult::Success(cancel) => {
1052 assert_eq!(cancel.symbol, "LTCBTC");
1053 assert_eq!(cancel.order_id, 4);
1054 assert_eq!(cancel.status, BinanceOrderStatus::Canceled);
1055 assert_eq!(cancel.time_in_force, BinanceTimeInForce::Gtc);
1056 assert_eq!(cancel.order_type, "LIMIT");
1057 assert_eq!(cancel.side, BinanceSide::Buy);
1058 assert_eq!(
1059 cancel.self_trade_prevention_mode,
1060 Some(BinanceSelfTradePreventionMode::None)
1061 );
1062 }
1063 BatchCancelResult::Error(_) => panic!("Expected Success"),
1064 }
1065 }
1066
1067 #[rstest]
1068 fn test_batch_cancel_result_error() {
1069 let json = r#"{"code": -2011, "msg": "Unknown order sent."}"#;
1070 let result: BatchCancelResult = serde_json::from_str(json).unwrap();
1071 match result {
1072 BatchCancelResult::Success(_) => panic!("Expected Error"),
1073 BatchCancelResult::Error(error) => {
1074 assert_eq!(error.code, -2011);
1075 assert_eq!(error.msg, "Unknown order sent.");
1076 }
1077 }
1078 }
1079
1080 #[rstest]
1081 fn test_ticker_24hr_deserialize() {
1082 let json = load_fixture_string("spot/http_json/ticker_24hr_response.json");
1083 let response: Ticker24hr = serde_json::from_str(&json).unwrap();
1084 assert_eq!(response.symbol, "BNBBTC");
1085 assert_eq!(response.last_price, "4.00000200");
1086 assert_eq!(response.count, 76);
1087 }
1088
1089 fn account_info(balances: Vec<BinanceBalance>) -> BinanceAccountInfo {
1090 BinanceAccountInfo {
1091 commission_exponent: -8,
1092 maker_commission_mantissa: 10,
1093 taker_commission_mantissa: 10,
1094 buyer_commission_mantissa: 0,
1095 seller_commission_mantissa: 0,
1096 can_trade: true,
1097 can_withdraw: true,
1098 can_deposit: true,
1099 require_self_trade_prevention: false,
1100 prevent_sor: false,
1101 update_time: 1_700_000_000_000_000,
1102 account_type: "SPOT".to_string(),
1103 balances,
1104 }
1105 }
1106
1107 fn balance(asset: &str, exponent: i8, free: i64, locked: i64) -> BinanceBalance {
1108 BinanceBalance {
1109 asset: asset.to_string(),
1110 free_mantissa: free,
1111 locked_mantissa: locked,
1112 exponent,
1113 }
1114 }
1115
1116 #[rstest]
1117 fn test_account_info_to_account_state_keeps_exact_balance() {
1118 let info = account_info(vec![balance("BTC", -8, 100_000_000, 50_000_000)]);
1119
1120 let state = info
1121 .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1122 .unwrap();
1123
1124 assert_eq!(state.balances.len(), 1);
1125 assert_eq!(
1126 state.balances[0].total.as_decimal(),
1127 Decimal::from_str_exact("1.5").unwrap()
1128 );
1129 assert_eq!(
1130 state.balances[0].locked.as_decimal(),
1131 Decimal::from_str_exact("0.5").unwrap()
1132 );
1133 }
1134
1135 #[rstest]
1136 fn test_account_info_to_account_state_positive_exponent_is_exact() {
1137 let info = account_info(vec![balance("BTC", 2, 42, 7)]);
1138
1139 let state = info
1140 .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1141 .unwrap();
1142
1143 assert_eq!(state.balances[0].total.as_decimal(), Decimal::from(4_900));
1144 assert_eq!(state.balances[0].locked.as_decimal(), Decimal::from(700));
1145 }
1146
1147 #[rstest]
1148 #[case::scale_above_max(-29_i8, 1_i64)]
1149 #[case::min_exponent(i8::MIN, 1_i64)]
1150 #[case::positive_overflow(29_i8, 1_i64)]
1151 #[case::product_overflow(18_i8, i64::MAX)]
1152 fn test_account_info_to_account_state_rejects_unrepresentable_balance(
1153 #[case] exponent: i8,
1154 #[case] mantissa: i64,
1155 ) {
1156 let info = account_info(vec![
1157 balance("BTC", -8, 100_000_000, 0),
1158 balance("ETH", exponent, mantissa, mantissa),
1159 ]);
1160
1161 let err = info
1162 .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1163 .unwrap_err();
1164 let message = format!("{err:#}");
1165
1166 assert!(message.contains("ETH"), "{message}");
1167 assert!(message.contains(&exponent.to_string()), "{message}");
1168 }
1169
1170 #[rstest]
1171 #[case::free_overflow(i64::MAX, 1_i64)]
1172 #[case::locked_overflow(1_i64, i64::MAX)]
1173 fn test_account_info_to_account_state_rejects_one_sided_overflow(
1174 #[case] free: i64,
1175 #[case] locked: i64,
1176 ) {
1177 let info = account_info(vec![balance("ETH", 10, free, locked)]);
1178
1179 let err = info
1180 .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1181 .unwrap_err();
1182 let message = format!("{err:#}");
1183
1184 assert!(message.contains("ETH"), "{message}");
1185 assert!(message.contains("10"), "{message}");
1186 }
1187
1188 #[rstest]
1189 fn test_account_info_to_account_state_empty_list_uses_zero_usdt() {
1190 let info = account_info(vec![]);
1191
1192 let state = info
1193 .to_account_state(AccountId::from("BINANCE-001"), UnixNanos::default())
1194 .unwrap();
1195
1196 assert_eq!(state.balances.len(), 1);
1197 assert_eq!(state.balances[0].currency, Currency::USDT());
1198 assert!(state.balances[0].total.is_zero());
1199 }
1200}