nautilus_binance/spot/http/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! SBE decode functions for Binance Spot HTTP responses.
17//!
18//! Each function decodes raw SBE bytes into domain types, validating the
19//! message header (schema ID, version, template ID) before extracting fields.
20
21use super::{
22    error::SbeDecodeError,
23    models::{
24        BinanceAccountInfo, BinanceAccountTrade, BinanceBalance, BinanceCancelOrderResponse,
25        BinanceDepth, BinanceExchangeInfoSbe, BinanceNewOrderResponse, BinanceOrderFill,
26        BinanceOrderResponse, BinancePriceLevel, BinanceSymbolSbe, BinanceTrade, BinanceTrades,
27    },
28};
29use crate::common::sbe::{
30    cursor::SbeCursor,
31    spot::{
32        SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
33        account_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TEMPLATE_ID,
34        account_trades_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TRADES_TEMPLATE_ID,
35        account_type::AccountType, bool_enum::BoolEnum,
36        cancel_open_orders_response_codec::SBE_TEMPLATE_ID as CANCEL_OPEN_ORDERS_TEMPLATE_ID,
37        cancel_order_response_codec::SBE_TEMPLATE_ID as CANCEL_ORDER_TEMPLATE_ID,
38        depth_response_codec::SBE_TEMPLATE_ID as DEPTH_TEMPLATE_ID,
39        exchange_info_response_codec::SBE_TEMPLATE_ID as EXCHANGE_INFO_TEMPLATE_ID,
40        message_header_codec::ENCODED_LENGTH as HEADER_LENGTH,
41        new_order_full_response_codec::SBE_TEMPLATE_ID as NEW_ORDER_FULL_TEMPLATE_ID,
42        order_response_codec::SBE_TEMPLATE_ID as ORDER_TEMPLATE_ID,
43        orders_response_codec::SBE_TEMPLATE_ID as ORDERS_TEMPLATE_ID,
44        ping_response_codec::SBE_TEMPLATE_ID as PING_TEMPLATE_ID,
45        server_time_response_codec::SBE_TEMPLATE_ID as SERVER_TIME_TEMPLATE_ID,
46        trades_response_codec::SBE_TEMPLATE_ID as TRADES_TEMPLATE_ID,
47    },
48};
49
50/// SBE message header.
51#[derive(Debug, Clone, Copy)]
52struct MessageHeader {
53    #[allow(dead_code)]
54    block_length: u16,
55    template_id: u16,
56    schema_id: u16,
57    version: u16,
58}
59
60impl MessageHeader {
61    /// Decode message header using cursor.
62    fn decode_cursor(cursor: &mut SbeCursor<'_>) -> Result<Self, SbeDecodeError> {
63        cursor.require(HEADER_LENGTH)?;
64        Ok(Self {
65            block_length: cursor.read_u16_le()?,
66            template_id: cursor.read_u16_le()?,
67            schema_id: cursor.read_u16_le()?,
68            version: cursor.read_u16_le()?,
69        })
70    }
71
72    /// Validate schema ID and version.
73    fn validate(&self) -> Result<(), SbeDecodeError> {
74        if self.schema_id != SBE_SCHEMA_ID {
75            return Err(SbeDecodeError::SchemaMismatch {
76                expected: SBE_SCHEMA_ID,
77                actual: self.schema_id,
78            });
79        }
80        if self.version != SBE_SCHEMA_VERSION {
81            return Err(SbeDecodeError::VersionMismatch {
82                expected: SBE_SCHEMA_VERSION,
83                actual: self.version,
84            });
85        }
86        Ok(())
87    }
88}
89
90/// Decode a ping response.
91///
92/// Ping response has no body (block_length = 0), just validates the header.
93///
94/// # Errors
95///
96/// Returns error if buffer is too short or schema mismatch.
97pub fn decode_ping(buf: &[u8]) -> Result<(), SbeDecodeError> {
98    let mut cursor = SbeCursor::new(buf);
99    let header = MessageHeader::decode_cursor(&mut cursor)?;
100    header.validate()?;
101
102    if header.template_id != PING_TEMPLATE_ID {
103        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
104    }
105
106    Ok(())
107}
108
109/// Decode a server time response.
110///
111/// Returns the server time as **microseconds** since epoch (SBE provides
112/// microsecond precision vs JSON's milliseconds).
113///
114/// # Errors
115///
116/// Returns error if buffer is too short or schema mismatch.
117pub fn decode_server_time(buf: &[u8]) -> Result<i64, SbeDecodeError> {
118    let mut cursor = SbeCursor::new(buf);
119    let header = MessageHeader::decode_cursor(&mut cursor)?;
120    header.validate()?;
121
122    if header.template_id != SERVER_TIME_TEMPLATE_ID {
123        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
124    }
125
126    cursor.read_i64_le()
127}
128
129/// Decode a depth response.
130///
131/// Returns the order book depth with bids and asks.
132///
133/// # Errors
134///
135/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
136pub fn decode_depth(buf: &[u8]) -> Result<BinanceDepth, SbeDecodeError> {
137    let mut cursor = SbeCursor::new(buf);
138    let header = MessageHeader::decode_cursor(&mut cursor)?;
139    header.validate()?;
140
141    if header.template_id != DEPTH_TEMPLATE_ID {
142        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
143    }
144
145    let last_update_id = cursor.read_i64_le()?;
146    let price_exponent = cursor.read_i8()?;
147    let qty_exponent = cursor.read_i8()?;
148
149    let (block_len, count) = cursor.read_group_header()?;
150    let bids = cursor.read_group(block_len, count, |c| {
151        Ok(BinancePriceLevel {
152            price_mantissa: c.read_i64_le()?,
153            qty_mantissa: c.read_i64_le()?,
154        })
155    })?;
156
157    let (block_len, count) = cursor.read_group_header()?;
158    let asks = cursor.read_group(block_len, count, |c| {
159        Ok(BinancePriceLevel {
160            price_mantissa: c.read_i64_le()?,
161            qty_mantissa: c.read_i64_le()?,
162        })
163    })?;
164
165    Ok(BinanceDepth {
166        last_update_id,
167        price_exponent,
168        qty_exponent,
169        bids,
170        asks,
171    })
172}
173
174/// Decode a trades response.
175///
176/// Returns the list of trades.
177///
178/// # Errors
179///
180/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
181pub fn decode_trades(buf: &[u8]) -> Result<BinanceTrades, SbeDecodeError> {
182    let mut cursor = SbeCursor::new(buf);
183    let header = MessageHeader::decode_cursor(&mut cursor)?;
184    header.validate()?;
185
186    if header.template_id != TRADES_TEMPLATE_ID {
187        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
188    }
189
190    let price_exponent = cursor.read_i8()?;
191    let qty_exponent = cursor.read_i8()?;
192
193    let (block_len, count) = cursor.read_group_header()?;
194    let trades = cursor.read_group(block_len, count, |c| {
195        Ok(BinanceTrade {
196            id: c.read_i64_le()?,
197            price_mantissa: c.read_i64_le()?,
198            qty_mantissa: c.read_i64_le()?,
199            quote_qty_mantissa: c.read_i64_le()?,
200            time: c.read_i64_le()?,
201            is_buyer_maker: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
202            is_best_match: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
203        })
204    })?;
205
206    Ok(BinanceTrades {
207        price_exponent,
208        qty_exponent,
209        trades,
210    })
211}
212
213/// Block length for new order full response.
214const NEW_ORDER_FULL_BLOCK_LENGTH: usize = 153;
215
216/// Block length for cancel order response.
217const CANCEL_ORDER_BLOCK_LENGTH: usize = 137;
218
219/// Block length for order response (query).
220const ORDER_BLOCK_LENGTH: usize = 153;
221
222/// Decode a new order full response.
223///
224/// # Errors
225///
226/// Returns error if buffer is too short, schema mismatch, or decode error.
227#[allow(dead_code)]
228pub fn decode_new_order_full(buf: &[u8]) -> Result<BinanceNewOrderResponse, SbeDecodeError> {
229    let mut cursor = SbeCursor::new(buf);
230    let header = MessageHeader::decode_cursor(&mut cursor)?;
231    header.validate()?;
232
233    if header.template_id != NEW_ORDER_FULL_TEMPLATE_ID {
234        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
235    }
236
237    cursor.require(NEW_ORDER_FULL_BLOCK_LENGTH)?;
238
239    let price_exponent = cursor.read_i8()?;
240    let qty_exponent = cursor.read_i8()?;
241    let order_id = cursor.read_i64_le()?;
242    let order_list_id = cursor.read_optional_i64_le()?;
243    let transact_time = cursor.read_i64_le()?;
244    let price_mantissa = cursor.read_i64_le()?;
245    let orig_qty_mantissa = cursor.read_i64_le()?;
246    let executed_qty_mantissa = cursor.read_i64_le()?;
247    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
248    let status = cursor.read_u8()?.into();
249    let time_in_force = cursor.read_u8()?.into();
250    let order_type = cursor.read_u8()?.into();
251    let side = cursor.read_u8()?.into();
252    let stop_price_mantissa = cursor.read_optional_i64_le()?;
253
254    cursor.advance(16)?; // Skip trailing_delta (8) + trailing_time (8)
255    let working_time = cursor.read_optional_i64_le()?;
256
257    cursor.advance(23)?; // Skip iceberg to used_sor
258    let self_trade_prevention_mode = cursor.read_u8()?.into();
259
260    cursor.advance(16)?; // Skip trade_group_id + prevented_quantity
261    let _commission_exponent = cursor.read_i8()?;
262
263    cursor.advance(18)?; // Skip to end of fixed block
264
265    let fills = decode_fills_cursor(&mut cursor)?;
266
267    // Skip prevented matches group
268    let (block_len, count) = cursor.read_group_header()?;
269    cursor.advance(block_len as usize * count as usize)?;
270
271    let symbol = cursor.read_var_string8()?;
272    let client_order_id = cursor.read_var_string8()?;
273
274    Ok(BinanceNewOrderResponse {
275        price_exponent,
276        qty_exponent,
277        order_id,
278        order_list_id,
279        transact_time,
280        price_mantissa,
281        orig_qty_mantissa,
282        executed_qty_mantissa,
283        cummulative_quote_qty_mantissa,
284        status,
285        time_in_force,
286        order_type,
287        side,
288        stop_price_mantissa,
289        working_time,
290        self_trade_prevention_mode,
291        client_order_id,
292        symbol,
293        fills,
294    })
295}
296
297/// Decode a cancel order response.
298///
299/// # Errors
300///
301/// Returns error if buffer is too short, schema mismatch, or decode error.
302#[allow(dead_code)]
303pub fn decode_cancel_order(buf: &[u8]) -> Result<BinanceCancelOrderResponse, SbeDecodeError> {
304    let mut cursor = SbeCursor::new(buf);
305    let header = MessageHeader::decode_cursor(&mut cursor)?;
306    header.validate()?;
307
308    if header.template_id != CANCEL_ORDER_TEMPLATE_ID {
309        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
310    }
311
312    cursor.require(CANCEL_ORDER_BLOCK_LENGTH)?;
313
314    let price_exponent = cursor.read_i8()?;
315    let qty_exponent = cursor.read_i8()?;
316    let order_id = cursor.read_i64_le()?;
317    let order_list_id = cursor.read_optional_i64_le()?;
318    let transact_time = cursor.read_i64_le()?;
319    let price_mantissa = cursor.read_i64_le()?;
320    let orig_qty_mantissa = cursor.read_i64_le()?;
321    let executed_qty_mantissa = cursor.read_i64_le()?;
322    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
323    let status = cursor.read_u8()?.into();
324    let time_in_force = cursor.read_u8()?.into();
325    let order_type = cursor.read_u8()?.into();
326    let side = cursor.read_u8()?.into();
327    let self_trade_prevention_mode = cursor.read_u8()?.into();
328
329    cursor.advance(CANCEL_ORDER_BLOCK_LENGTH - 63)?; // Skip to end of fixed block
330
331    let symbol = cursor.read_var_string8()?;
332    let orig_client_order_id = cursor.read_var_string8()?;
333    let client_order_id = cursor.read_var_string8()?;
334
335    Ok(BinanceCancelOrderResponse {
336        price_exponent,
337        qty_exponent,
338        order_id,
339        order_list_id,
340        transact_time,
341        price_mantissa,
342        orig_qty_mantissa,
343        executed_qty_mantissa,
344        cummulative_quote_qty_mantissa,
345        status,
346        time_in_force,
347        order_type,
348        side,
349        self_trade_prevention_mode,
350        client_order_id,
351        orig_client_order_id,
352        symbol,
353    })
354}
355
356/// Decode an order query response.
357///
358/// # Errors
359///
360/// Returns error if buffer is too short, schema mismatch, or decode error.
361#[allow(dead_code)]
362pub fn decode_order(buf: &[u8]) -> Result<BinanceOrderResponse, SbeDecodeError> {
363    let mut cursor = SbeCursor::new(buf);
364    let header = MessageHeader::decode_cursor(&mut cursor)?;
365    header.validate()?;
366
367    if header.template_id != ORDER_TEMPLATE_ID {
368        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
369    }
370
371    cursor.require(ORDER_BLOCK_LENGTH)?;
372
373    let price_exponent = cursor.read_i8()?;
374    let qty_exponent = cursor.read_i8()?;
375    let order_id = cursor.read_i64_le()?;
376    let order_list_id = cursor.read_optional_i64_le()?;
377    let price_mantissa = cursor.read_i64_le()?;
378    let orig_qty_mantissa = cursor.read_i64_le()?;
379    let executed_qty_mantissa = cursor.read_i64_le()?;
380    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
381    let status = cursor.read_u8()?.into();
382    let time_in_force = cursor.read_u8()?.into();
383    let order_type = cursor.read_u8()?.into();
384    let side = cursor.read_u8()?.into();
385    let stop_price_mantissa = cursor.read_optional_i64_le()?;
386    let iceberg_qty_mantissa = cursor.read_optional_i64_le()?;
387    let time = cursor.read_i64_le()?;
388    let update_time = cursor.read_i64_le()?;
389    let is_working = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
390    let working_time = cursor.read_optional_i64_le()?;
391    let orig_quote_order_qty_mantissa = cursor.read_i64_le()?;
392    let self_trade_prevention_mode = cursor.read_u8()?.into();
393
394    cursor.advance(ORDER_BLOCK_LENGTH - 104)?; // Skip to end of fixed block
395
396    let symbol = cursor.read_var_string8()?;
397    let client_order_id = cursor.read_var_string8()?;
398
399    Ok(BinanceOrderResponse {
400        price_exponent,
401        qty_exponent,
402        order_id,
403        order_list_id,
404        price_mantissa,
405        orig_qty_mantissa,
406        executed_qty_mantissa,
407        cummulative_quote_qty_mantissa,
408        status,
409        time_in_force,
410        order_type,
411        side,
412        stop_price_mantissa,
413        iceberg_qty_mantissa,
414        time,
415        update_time,
416        is_working,
417        working_time,
418        orig_quote_order_qty_mantissa,
419        self_trade_prevention_mode,
420        client_order_id,
421        symbol,
422    })
423}
424
425/// Block length for orders group item.
426const ORDERS_GROUP_BLOCK_LENGTH: usize = 162;
427
428/// Decode multiple orders response.
429///
430/// # Errors
431///
432/// Returns error if buffer is too short, schema mismatch, or decode error.
433#[allow(dead_code)]
434pub fn decode_orders(buf: &[u8]) -> Result<Vec<BinanceOrderResponse>, SbeDecodeError> {
435    let mut cursor = SbeCursor::new(buf);
436    let header = MessageHeader::decode_cursor(&mut cursor)?;
437    header.validate()?;
438
439    if header.template_id != ORDERS_TEMPLATE_ID {
440        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
441    }
442
443    let (block_length, count) = cursor.read_group_header()?;
444
445    if count == 0 {
446        return Ok(Vec::new());
447    }
448
449    if block_length as usize != ORDERS_GROUP_BLOCK_LENGTH {
450        return Err(SbeDecodeError::InvalidBlockLength {
451            expected: ORDERS_GROUP_BLOCK_LENGTH as u16,
452            actual: block_length,
453        });
454    }
455
456    let mut orders = Vec::with_capacity(count as usize);
457
458    for _ in 0..count {
459        cursor.require(ORDERS_GROUP_BLOCK_LENGTH)?;
460
461        let price_exponent = cursor.read_i8()?;
462        let qty_exponent = cursor.read_i8()?;
463        let order_id = cursor.read_i64_le()?;
464        let order_list_id = cursor.read_optional_i64_le()?;
465        let price_mantissa = cursor.read_i64_le()?;
466        let orig_qty_mantissa = cursor.read_i64_le()?;
467        let executed_qty_mantissa = cursor.read_i64_le()?;
468        let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
469        let status = cursor.read_u8()?.into();
470        let time_in_force = cursor.read_u8()?.into();
471        let order_type = cursor.read_u8()?.into();
472        let side = cursor.read_u8()?.into();
473        let stop_price_mantissa = cursor.read_optional_i64_le()?;
474
475        cursor.advance(16)?; // Skip trailing_delta + trailing_time
476        let iceberg_qty_mantissa = cursor.read_optional_i64_le()?;
477        let time = cursor.read_i64_le()?;
478        let update_time = cursor.read_i64_le()?;
479        let is_working = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
480        let working_time = cursor.read_optional_i64_le()?;
481        let orig_quote_order_qty_mantissa = cursor.read_i64_le()?;
482
483        cursor.advance(14)?; // Skip strategy_id to working_floor
484        let self_trade_prevention_mode = cursor.read_u8()?.into();
485
486        cursor.advance(28)?; // Skip to end of fixed block
487
488        let symbol = cursor.read_var_string8()?;
489        let client_order_id = cursor.read_var_string8()?;
490
491        orders.push(BinanceOrderResponse {
492            price_exponent,
493            qty_exponent,
494            order_id,
495            order_list_id,
496            price_mantissa,
497            orig_qty_mantissa,
498            executed_qty_mantissa,
499            cummulative_quote_qty_mantissa,
500            status,
501            time_in_force,
502            order_type,
503            side,
504            stop_price_mantissa,
505            iceberg_qty_mantissa,
506            time,
507            update_time,
508            is_working,
509            working_time,
510            orig_quote_order_qty_mantissa,
511            self_trade_prevention_mode,
512            client_order_id,
513            symbol,
514        });
515    }
516
517    Ok(orders)
518}
519
520/// Decode cancel open orders response.
521///
522/// Each item in the response group contains an embedded cancel_order_response SBE message.
523///
524/// # Errors
525///
526/// Returns error if buffer is too short, schema mismatch, or decode error.
527#[allow(dead_code)]
528pub fn decode_cancel_open_orders(
529    buf: &[u8],
530) -> Result<Vec<BinanceCancelOrderResponse>, SbeDecodeError> {
531    let mut cursor = SbeCursor::new(buf);
532    let header = MessageHeader::decode_cursor(&mut cursor)?;
533    header.validate()?;
534
535    if header.template_id != CANCEL_OPEN_ORDERS_TEMPLATE_ID {
536        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
537    }
538
539    let (_block_length, count) = cursor.read_group_header()?;
540
541    if count == 0 {
542        return Ok(Vec::new());
543    }
544
545    let mut responses = Vec::with_capacity(count as usize);
546
547    // Each group item has block_length=0, followed by u16 length + embedded SBE message
548    for _ in 0..count {
549        let response_len = cursor.read_u16_le()? as usize;
550        let embedded_bytes = cursor.read_bytes(response_len)?;
551        let cancel_response = decode_cancel_order(embedded_bytes)?;
552        responses.push(cancel_response);
553    }
554
555    Ok(responses)
556}
557
558/// Account response block length (from SBE codec).
559const ACCOUNT_BLOCK_LENGTH: usize = 64;
560
561/// Balance group item block length (from SBE codec).
562const BALANCE_BLOCK_LENGTH: u16 = 17;
563
564/// Decode account information response.
565///
566/// # Errors
567///
568/// Returns error if buffer is too short, schema mismatch, or decode error.
569#[allow(dead_code)]
570pub fn decode_account(buf: &[u8]) -> Result<BinanceAccountInfo, SbeDecodeError> {
571    let mut cursor = SbeCursor::new(buf);
572    let header = MessageHeader::decode_cursor(&mut cursor)?;
573    header.validate()?;
574
575    if header.template_id != ACCOUNT_TEMPLATE_ID {
576        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
577    }
578
579    cursor.require(ACCOUNT_BLOCK_LENGTH)?;
580
581    let commission_exponent = cursor.read_i8()?;
582    let maker_commission_mantissa = cursor.read_i64_le()?;
583    let taker_commission_mantissa = cursor.read_i64_le()?;
584    let buyer_commission_mantissa = cursor.read_i64_le()?;
585    let seller_commission_mantissa = cursor.read_i64_le()?;
586    let can_trade = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
587    let can_withdraw = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
588    let can_deposit = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
589    cursor.advance(1)?; // Skip brokered
590    let require_self_trade_prevention = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
591    let prevent_sor = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
592    let update_time = cursor.read_i64_le()?;
593    let account_type_enum = AccountType::from(cursor.read_u8()?);
594    cursor.advance(16)?; // Skip tradeGroupId + uid
595
596    let account_type = account_type_enum.to_string();
597
598    let (block_length, balance_count) = cursor.read_group_header()?;
599
600    if block_length != BALANCE_BLOCK_LENGTH {
601        return Err(SbeDecodeError::InvalidBlockLength {
602            expected: BALANCE_BLOCK_LENGTH,
603            actual: block_length,
604        });
605    }
606
607    let mut balances = Vec::with_capacity(balance_count as usize);
608
609    for _ in 0..balance_count {
610        cursor.require(block_length as usize)?;
611
612        let exponent = cursor.read_i8()?;
613        let free_mantissa = cursor.read_i64_le()?;
614        let locked_mantissa = cursor.read_i64_le()?;
615
616        let asset = cursor.read_var_string8()?;
617
618        balances.push(BinanceBalance {
619            asset,
620            free_mantissa,
621            locked_mantissa,
622            exponent,
623        });
624    }
625
626    Ok(BinanceAccountInfo {
627        commission_exponent,
628        maker_commission_mantissa,
629        taker_commission_mantissa,
630        buyer_commission_mantissa,
631        seller_commission_mantissa,
632        can_trade,
633        can_withdraw,
634        can_deposit,
635        require_self_trade_prevention,
636        prevent_sor,
637        update_time,
638        account_type,
639        balances,
640    })
641}
642
643/// Account trade group item block length (from SBE codec).
644const ACCOUNT_TRADE_BLOCK_LENGTH: u16 = 70;
645
646/// Decode account trades response.
647///
648/// # Errors
649///
650/// Returns error if buffer is too short, schema mismatch, or decode error.
651#[allow(dead_code)]
652pub fn decode_account_trades(buf: &[u8]) -> Result<Vec<BinanceAccountTrade>, SbeDecodeError> {
653    let mut cursor = SbeCursor::new(buf);
654    let header = MessageHeader::decode_cursor(&mut cursor)?;
655    header.validate()?;
656
657    if header.template_id != ACCOUNT_TRADES_TEMPLATE_ID {
658        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
659    }
660
661    let (block_length, trade_count) = cursor.read_group_header()?;
662
663    if block_length != ACCOUNT_TRADE_BLOCK_LENGTH {
664        return Err(SbeDecodeError::InvalidBlockLength {
665            expected: ACCOUNT_TRADE_BLOCK_LENGTH,
666            actual: block_length,
667        });
668    }
669
670    let mut trades = Vec::with_capacity(trade_count as usize);
671
672    for _ in 0..trade_count {
673        cursor.require(block_length as usize)?;
674
675        let price_exponent = cursor.read_i8()?;
676        let qty_exponent = cursor.read_i8()?;
677        let commission_exponent = cursor.read_i8()?;
678        let id = cursor.read_i64_le()?;
679        let order_id = cursor.read_i64_le()?;
680        let order_list_id = cursor.read_optional_i64_le()?;
681        let price_mantissa = cursor.read_i64_le()?;
682        let qty_mantissa = cursor.read_i64_le()?;
683        let quote_qty_mantissa = cursor.read_i64_le()?;
684        let commission_mantissa = cursor.read_i64_le()?;
685        let time = cursor.read_i64_le()?;
686        let is_buyer = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
687        let is_maker = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
688        let is_best_match = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
689
690        let symbol = cursor.read_var_string8()?;
691        let commission_asset = cursor.read_var_string8()?;
692
693        trades.push(BinanceAccountTrade {
694            price_exponent,
695            qty_exponent,
696            commission_exponent,
697            id,
698            order_id,
699            order_list_id,
700            price_mantissa,
701            qty_mantissa,
702            quote_qty_mantissa,
703            commission_mantissa,
704            time,
705            is_buyer,
706            is_maker,
707            is_best_match,
708            symbol,
709            commission_asset,
710        });
711    }
712
713    Ok(trades)
714}
715
716/// Fills group item block length (from SBE codec).
717const FILLS_BLOCK_LENGTH: u16 = 42;
718
719/// Decode order fills using cursor.
720fn decode_fills_cursor(
721    cursor: &mut SbeCursor<'_>,
722) -> Result<Vec<BinanceOrderFill>, SbeDecodeError> {
723    let (block_length, count) = cursor.read_group_header()?;
724
725    if block_length != FILLS_BLOCK_LENGTH {
726        return Err(SbeDecodeError::InvalidBlockLength {
727            expected: FILLS_BLOCK_LENGTH,
728            actual: block_length,
729        });
730    }
731
732    let mut fills = Vec::with_capacity(count as usize);
733
734    for _ in 0..count {
735        cursor.require(block_length as usize)?;
736
737        let commission_exponent = cursor.read_i8()?;
738        cursor.advance(1)?; // Skip matchType
739        let price_mantissa = cursor.read_i64_le()?;
740        let qty_mantissa = cursor.read_i64_le()?;
741        let commission_mantissa = cursor.read_i64_le()?;
742        let trade_id = cursor.read_optional_i64_le()?;
743        cursor.advance(8)?; // Skip allocId
744
745        let commission_asset = cursor.read_var_string8()?;
746
747        fills.push(BinanceOrderFill {
748            price_mantissa,
749            qty_mantissa,
750            commission_mantissa,
751            commission_exponent,
752            commission_asset,
753            trade_id,
754        });
755    }
756
757    Ok(fills)
758}
759
760/// Symbols group block length (from SBE codec).
761const SYMBOL_BLOCK_LENGTH: usize = 19;
762
763/// Decode exchange info response.
764///
765/// ExchangeInfo response contains rate limits, exchange filters, symbols, and SOR info.
766/// We only decode the symbols array which contains instrument definitions.
767///
768/// # Errors
769///
770/// Returns error if buffer is too short, schema mismatch, or template ID mismatch.
771pub fn decode_exchange_info(buf: &[u8]) -> Result<BinanceExchangeInfoSbe, SbeDecodeError> {
772    let mut cursor = SbeCursor::new(buf);
773    let header = MessageHeader::decode_cursor(&mut cursor)?;
774    header.validate()?;
775
776    if header.template_id != EXCHANGE_INFO_TEMPLATE_ID {
777        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
778    }
779
780    // Skip rate_limits group
781    let (rate_limits_block_len, rate_limits_count) = cursor.read_group_header()?;
782    cursor.advance(rate_limits_block_len as usize * rate_limits_count as usize)?;
783
784    // Skip exchange_filters group
785    let (_exchange_filters_block_len, exchange_filters_count) = cursor.read_group_header()?;
786    for _ in 0..exchange_filters_count {
787        // Each filter is a varString8
788        cursor.read_var_string8()?;
789    }
790
791    // Decode symbols group
792    let (symbols_block_len, symbols_count) = cursor.read_group_header()?;
793
794    if symbols_block_len != SYMBOL_BLOCK_LENGTH as u16 {
795        return Err(SbeDecodeError::InvalidBlockLength {
796            expected: SYMBOL_BLOCK_LENGTH as u16,
797            actual: symbols_block_len,
798        });
799    }
800
801    let mut symbols = Vec::with_capacity(symbols_count as usize);
802
803    for _ in 0..symbols_count {
804        cursor.require(SYMBOL_BLOCK_LENGTH)?;
805
806        // Fixed fields (19 bytes)
807        let status = cursor.read_u8()?;
808        let base_asset_precision = cursor.read_u8()?;
809        let quote_asset_precision = cursor.read_u8()?;
810        let _base_commission_precision = cursor.read_u8()?;
811        let _quote_commission_precision = cursor.read_u8()?;
812        let order_types = cursor.read_u16_le()?;
813        let iceberg_allowed = cursor.read_u8()? == BoolEnum::True as u8;
814        let oco_allowed = cursor.read_u8()? == BoolEnum::True as u8;
815        let oto_allowed = cursor.read_u8()? == BoolEnum::True as u8;
816        let quote_order_qty_market_allowed = cursor.read_u8()? == BoolEnum::True as u8;
817        let allow_trailing_stop = cursor.read_u8()? == BoolEnum::True as u8;
818        let cancel_replace_allowed = cursor.read_u8()? == BoolEnum::True as u8;
819        let amend_allowed = cursor.read_u8()? == BoolEnum::True as u8;
820        let is_spot_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
821        let is_margin_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
822        let _default_self_trade_prevention_mode = cursor.read_u8()?;
823        let _allowed_self_trade_prevention_modes = cursor.read_u8()?;
824        let _peg_instructions_allowed = cursor.read_u8()?;
825
826        // Filters nested group (JSON blobs)
827        let (_filters_block_len, filters_count) = cursor.read_group_header()?;
828        let mut filters = Vec::with_capacity(filters_count as usize);
829        for _ in 0..filters_count {
830            let filter_json = cursor.read_var_string8()?;
831            if let Ok(value) = serde_json::from_str(&filter_json) {
832                filters.push(value);
833            }
834        }
835
836        // Permission sets nested group
837        let (_perm_sets_block_len, perm_sets_count) = cursor.read_group_header()?;
838        let mut permissions = Vec::with_capacity(perm_sets_count as usize);
839        for _ in 0..perm_sets_count {
840            // Permissions nested group
841            let (_perms_block_len, perms_count) = cursor.read_group_header()?;
842            let mut perm_set = Vec::with_capacity(perms_count as usize);
843            for _ in 0..perms_count {
844                let perm = cursor.read_var_string8()?;
845                perm_set.push(perm);
846            }
847            permissions.push(perm_set);
848        }
849
850        // Variable-length strings
851        let symbol = cursor.read_var_string8()?;
852        let base_asset = cursor.read_var_string8()?;
853        let quote_asset = cursor.read_var_string8()?;
854
855        symbols.push(BinanceSymbolSbe {
856            symbol,
857            base_asset,
858            quote_asset,
859            base_asset_precision,
860            quote_asset_precision,
861            status,
862            order_types,
863            iceberg_allowed,
864            oco_allowed,
865            oto_allowed,
866            quote_order_qty_market_allowed,
867            allow_trailing_stop,
868            cancel_replace_allowed,
869            amend_allowed,
870            is_spot_trading_allowed,
871            is_margin_trading_allowed,
872            filters,
873            permissions,
874        });
875    }
876
877    // Skip SOR group (we don't need it)
878
879    Ok(BinanceExchangeInfoSbe { symbols })
880}
881
882#[cfg(test)]
883mod tests {
884    use rstest::rstest;
885
886    use super::*;
887
888    fn create_header(block_length: u16, template_id: u16, schema_id: u16, version: u16) -> [u8; 8] {
889        let mut buf = [0u8; 8];
890        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
891        buf[2..4].copy_from_slice(&template_id.to_le_bytes());
892        buf[4..6].copy_from_slice(&schema_id.to_le_bytes());
893        buf[6..8].copy_from_slice(&version.to_le_bytes());
894        buf
895    }
896
897    #[rstest]
898    fn test_decode_ping_valid() {
899        // Ping: block_length=0, template_id=101, schema_id=3, version=1
900        let buf = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
901        assert!(decode_ping(&buf).is_ok());
902    }
903
904    #[rstest]
905    fn test_decode_ping_buffer_too_short() {
906        let buf = [0u8; 4];
907        let err = decode_ping(&buf).unwrap_err();
908        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
909    }
910
911    #[rstest]
912    fn test_decode_ping_schema_mismatch() {
913        let buf = create_header(0, PING_TEMPLATE_ID, 99, SBE_SCHEMA_VERSION);
914        let err = decode_ping(&buf).unwrap_err();
915        assert!(matches!(err, SbeDecodeError::SchemaMismatch { .. }));
916    }
917
918    #[rstest]
919    fn test_decode_ping_wrong_template() {
920        let buf = create_header(0, 999, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
921        let err = decode_ping(&buf).unwrap_err();
922        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(999)));
923    }
924
925    #[rstest]
926    fn test_decode_server_time_valid() {
927        // ServerTime: block_length=8, template_id=102, schema_id=3, version=1
928        let header = create_header(
929            8,
930            SERVER_TIME_TEMPLATE_ID,
931            SBE_SCHEMA_ID,
932            SBE_SCHEMA_VERSION,
933        );
934        let timestamp: i64 = 1734300000000; // Example timestamp
935
936        let mut buf = Vec::with_capacity(16);
937        buf.extend_from_slice(&header);
938        buf.extend_from_slice(&timestamp.to_le_bytes());
939
940        let result = decode_server_time(&buf).unwrap();
941        assert_eq!(result, timestamp);
942    }
943
944    #[rstest]
945    fn test_decode_server_time_buffer_too_short() {
946        // Header only, missing body
947        let buf = create_header(
948            8,
949            SERVER_TIME_TEMPLATE_ID,
950            SBE_SCHEMA_ID,
951            SBE_SCHEMA_VERSION,
952        );
953        let err = decode_server_time(&buf).unwrap_err();
954        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
955    }
956
957    #[rstest]
958    fn test_decode_server_time_wrong_template() {
959        let header = create_header(8, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
960        let mut buf = Vec::with_capacity(16);
961        buf.extend_from_slice(&header);
962        buf.extend_from_slice(&0i64.to_le_bytes());
963
964        let err = decode_server_time(&buf).unwrap_err();
965        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
966    }
967
968    #[rstest]
969    fn test_decode_server_time_version_mismatch() {
970        let header = create_header(8, SERVER_TIME_TEMPLATE_ID, SBE_SCHEMA_ID, 99);
971        let mut buf = Vec::with_capacity(16);
972        buf.extend_from_slice(&header);
973        buf.extend_from_slice(&0i64.to_le_bytes());
974
975        let err = decode_server_time(&buf).unwrap_err();
976        assert!(matches!(err, SbeDecodeError::VersionMismatch { .. }));
977    }
978
979    fn create_group_header(block_length: u16, count: u32) -> [u8; 6] {
980        let mut buf = [0u8; 6];
981        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
982        buf[2..6].copy_from_slice(&count.to_le_bytes());
983        buf
984    }
985
986    #[rstest]
987    fn test_decode_depth_valid() {
988        // Depth: block_length=10, template_id=200
989        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
990
991        let mut buf = Vec::new();
992        buf.extend_from_slice(&header);
993
994        // Block: last_update_id (8) + price_exponent (1) + qty_exponent (1)
995        let last_update_id: i64 = 123456789;
996        let price_exponent: i8 = -8;
997        let qty_exponent: i8 = -8;
998        buf.extend_from_slice(&last_update_id.to_le_bytes());
999        buf.push(price_exponent as u8);
1000        buf.push(qty_exponent as u8);
1001
1002        // Bids group: 2 levels
1003        buf.extend_from_slice(&create_group_header(16, 2));
1004        // Bid 1: price=100000000000, qty=50000000
1005        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes());
1006        buf.extend_from_slice(&50_000_000i64.to_le_bytes());
1007        // Bid 2: price=99900000000, qty=30000000
1008        buf.extend_from_slice(&99_900_000_000i64.to_le_bytes());
1009        buf.extend_from_slice(&30_000_000i64.to_le_bytes());
1010
1011        // Asks group: 1 level
1012        buf.extend_from_slice(&create_group_header(16, 1));
1013        // Ask 1: price=100100000000, qty=25000000
1014        buf.extend_from_slice(&100_100_000_000i64.to_le_bytes());
1015        buf.extend_from_slice(&25_000_000i64.to_le_bytes());
1016
1017        let depth = decode_depth(&buf).unwrap();
1018
1019        assert_eq!(depth.last_update_id, 123456789);
1020        assert_eq!(depth.price_exponent, -8);
1021        assert_eq!(depth.qty_exponent, -8);
1022        assert_eq!(depth.bids.len(), 2);
1023        assert_eq!(depth.asks.len(), 1);
1024        assert_eq!(depth.bids[0].price_mantissa, 100_000_000_000);
1025        assert_eq!(depth.bids[0].qty_mantissa, 50_000_000);
1026        assert_eq!(depth.asks[0].price_mantissa, 100_100_000_000);
1027    }
1028
1029    #[rstest]
1030    fn test_decode_depth_empty_book() {
1031        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1032
1033        let mut buf = Vec::new();
1034        buf.extend_from_slice(&header);
1035        buf.extend_from_slice(&0i64.to_le_bytes()); // last_update_id
1036        buf.push(0); // price_exponent
1037        buf.push(0); // qty_exponent
1038
1039        // Empty bids
1040        buf.extend_from_slice(&create_group_header(16, 0));
1041        // Empty asks
1042        buf.extend_from_slice(&create_group_header(16, 0));
1043
1044        let depth = decode_depth(&buf).unwrap();
1045
1046        assert!(depth.bids.is_empty());
1047        assert!(depth.asks.is_empty());
1048    }
1049
1050    #[rstest]
1051    fn test_decode_trades_valid() {
1052        // Trades: block_length=2, template_id=201
1053        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1054
1055        let mut buf = Vec::new();
1056        buf.extend_from_slice(&header);
1057
1058        // Block: price_exponent (1) + qty_exponent (1)
1059        let price_exponent: i8 = -8;
1060        let qty_exponent: i8 = -8;
1061        buf.push(price_exponent as u8);
1062        buf.push(qty_exponent as u8);
1063
1064        // Trades group: 1 trade (42 bytes each)
1065        buf.extend_from_slice(&create_group_header(42, 1));
1066
1067        // Trade: id(8) + price(8) + qty(8) + quoteQty(8) + time(8) + isBuyerMaker(1) + isBestMatch(1)
1068        let trade_id: i64 = 999;
1069        let price: i64 = 100_000_000_000;
1070        let qty: i64 = 10_000_000;
1071        let quote_qty: i64 = 1_000_000_000_000;
1072        let time: i64 = 1734300000000;
1073        let is_buyer_maker: u8 = 1; // true
1074        let is_best_match: u8 = 1; // true
1075
1076        buf.extend_from_slice(&trade_id.to_le_bytes());
1077        buf.extend_from_slice(&price.to_le_bytes());
1078        buf.extend_from_slice(&qty.to_le_bytes());
1079        buf.extend_from_slice(&quote_qty.to_le_bytes());
1080        buf.extend_from_slice(&time.to_le_bytes());
1081        buf.push(is_buyer_maker);
1082        buf.push(is_best_match);
1083
1084        let trades = decode_trades(&buf).unwrap();
1085
1086        assert_eq!(trades.price_exponent, -8);
1087        assert_eq!(trades.qty_exponent, -8);
1088        assert_eq!(trades.trades.len(), 1);
1089        assert_eq!(trades.trades[0].id, 999);
1090        assert_eq!(trades.trades[0].price_mantissa, 100_000_000_000);
1091        assert!(trades.trades[0].is_buyer_maker);
1092        assert!(trades.trades[0].is_best_match);
1093    }
1094
1095    #[rstest]
1096    fn test_decode_trades_empty() {
1097        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1098
1099        let mut buf = Vec::new();
1100        buf.extend_from_slice(&header);
1101        buf.push(0); // price_exponent
1102        buf.push(0); // qty_exponent
1103
1104        // Empty trades group
1105        buf.extend_from_slice(&create_group_header(42, 0));
1106
1107        let trades = decode_trades(&buf).unwrap();
1108
1109        assert!(trades.trades.is_empty());
1110    }
1111
1112    #[rstest]
1113    fn test_decode_depth_wrong_template() {
1114        let header = create_header(10, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1115
1116        let mut buf = Vec::new();
1117        buf.extend_from_slice(&header);
1118        buf.extend_from_slice(&[0u8; 10]); // dummy block
1119
1120        let err = decode_depth(&buf).unwrap_err();
1121        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1122    }
1123
1124    #[rstest]
1125    fn test_decode_trades_wrong_template() {
1126        let header = create_header(2, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1127
1128        let mut buf = Vec::new();
1129        buf.extend_from_slice(&header);
1130        buf.extend_from_slice(&[0u8; 2]); // dummy block
1131
1132        let err = decode_trades(&buf).unwrap_err();
1133        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1134    }
1135
1136    fn write_var_string(buf: &mut Vec<u8>, s: &str) {
1137        buf.push(s.len() as u8);
1138        buf.extend_from_slice(s.as_bytes());
1139    }
1140
1141    #[rstest]
1142    fn test_decode_order_valid() {
1143        let header = create_header(
1144            ORDER_BLOCK_LENGTH as u16,
1145            ORDER_TEMPLATE_ID,
1146            SBE_SCHEMA_ID,
1147            SBE_SCHEMA_VERSION,
1148        );
1149
1150        let mut buf = Vec::new();
1151        buf.extend_from_slice(&header);
1152
1153        // Fixed block (153 bytes)
1154        buf.push((-8i8) as u8); // price_exponent
1155        buf.push((-8i8) as u8); // qty_exponent
1156        buf.extend_from_slice(&12345i64.to_le_bytes()); // order_id
1157        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1158        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1159        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty_mantissa
1160        buf.extend_from_slice(&5_000_000i64.to_le_bytes()); // executed_qty_mantissa
1161        buf.extend_from_slice(&500_000_000i64.to_le_bytes()); // cummulative_quote_qty_mantissa
1162        buf.push(1); // status (NEW)
1163        buf.push(1); // time_in_force (GTC)
1164        buf.push(1); // order_type (LIMIT)
1165        buf.push(1); // side (BUY)
1166        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1167        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1168        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1169        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1170        buf.push(1); // is_working (true)
1171        buf.extend_from_slice(&1734300000500i64.to_le_bytes()); // working_time
1172        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty_mantissa
1173        buf.push(0); // self_trade_prevention_mode
1174
1175        // Pad to 153 bytes
1176        while buf.len() < 8 + ORDER_BLOCK_LENGTH {
1177            buf.push(0);
1178        }
1179
1180        write_var_string(&mut buf, "BTCUSDT");
1181        write_var_string(&mut buf, "my-order-123");
1182
1183        let order = decode_order(&buf).unwrap();
1184
1185        assert_eq!(order.order_id, 12345);
1186        assert!(order.order_list_id.is_none());
1187        assert_eq!(order.price_exponent, -8);
1188        assert_eq!(order.price_mantissa, 100_000_000_000);
1189        assert!(order.stop_price_mantissa.is_none());
1190        assert!(order.iceberg_qty_mantissa.is_none());
1191        assert!(order.is_working);
1192        assert_eq!(order.working_time, Some(1734300000500));
1193        assert_eq!(order.symbol, "BTCUSDT");
1194        assert_eq!(order.client_order_id, "my-order-123");
1195    }
1196
1197    #[rstest]
1198    fn test_decode_orders_multiple() {
1199        // This test verifies cursor advances correctly through multiple orders
1200        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1201
1202        let mut buf = Vec::new();
1203        buf.extend_from_slice(&header);
1204
1205        // Group header: block_length=162, count=2
1206        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 2));
1207
1208        // Order 1
1209        let order1_start = buf.len();
1210        buf.push((-8i8) as u8); // price_exponent
1211        buf.push((-8i8) as u8); // qty_exponent
1212        buf.extend_from_slice(&1001i64.to_le_bytes()); // order_id
1213        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1214        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1215        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1216        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1217        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1218        buf.push(1); // status
1219        buf.push(1); // time_in_force
1220        buf.push(1); // order_type
1221        buf.push(1); // side
1222        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1223        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1224        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1225        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1226        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
1227        buf.push(1); // is_working
1228        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // working_time
1229        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1230
1231        // Pad to 162 bytes from order start
1232        while buf.len() - order1_start < ORDERS_GROUP_BLOCK_LENGTH {
1233            buf.push(0);
1234        }
1235        write_var_string(&mut buf, "BTCUSDT");
1236        write_var_string(&mut buf, "order-1");
1237
1238        // Order 2
1239        let order2_start = buf.len();
1240        buf.push((-8i8) as u8); // price_exponent
1241        buf.push((-8i8) as u8); // qty_exponent
1242        buf.extend_from_slice(&2002i64.to_le_bytes()); // order_id
1243        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1244        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes()); // price_mantissa
1245        buf.extend_from_slice(&20_000_000i64.to_le_bytes()); // orig_qty
1246        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1247        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1248        buf.push(1); // status
1249        buf.push(1); // time_in_force
1250        buf.push(1); // order_type
1251        buf.push(2); // side (SELL)
1252        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1253        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1254        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1255        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // time
1256        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1257        buf.push(1); // is_working
1258        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // working_time
1259        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1260
1261        while buf.len() - order2_start < ORDERS_GROUP_BLOCK_LENGTH {
1262            buf.push(0);
1263        }
1264        write_var_string(&mut buf, "ETHUSDT");
1265        write_var_string(&mut buf, "order-2");
1266
1267        let orders = decode_orders(&buf).unwrap();
1268
1269        assert_eq!(orders.len(), 2);
1270        assert_eq!(orders[0].order_id, 1001);
1271        assert_eq!(orders[0].symbol, "BTCUSDT");
1272        assert_eq!(orders[0].client_order_id, "order-1");
1273        assert_eq!(orders[0].price_mantissa, 100_000_000_000);
1274
1275        assert_eq!(orders[1].order_id, 2002);
1276        assert_eq!(orders[1].symbol, "ETHUSDT");
1277        assert_eq!(orders[1].client_order_id, "order-2");
1278        assert_eq!(orders[1].price_mantissa, 200_000_000_000);
1279    }
1280
1281    #[rstest]
1282    fn test_decode_orders_empty() {
1283        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1284
1285        let mut buf = Vec::new();
1286        buf.extend_from_slice(&header);
1287        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 0));
1288
1289        let orders = decode_orders(&buf).unwrap();
1290        assert!(orders.is_empty());
1291    }
1292
1293    #[rstest]
1294    fn test_decode_orders_truncated_var_string() {
1295        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1296
1297        let mut buf = Vec::new();
1298        buf.extend_from_slice(&header);
1299        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 1));
1300
1301        // Pad fixed block to 162 bytes
1302        buf.extend_from_slice(&[0u8; ORDERS_GROUP_BLOCK_LENGTH]);
1303
1304        // Symbol length says 7 bytes but we only provide 3
1305        buf.push(7); // Length prefix claims "BTCUSDT" (7 chars)
1306        buf.extend_from_slice(b"BTC"); // Only 3 bytes - truncated
1307
1308        let err = decode_orders(&buf).unwrap_err();
1309        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1310    }
1311
1312    #[rstest]
1313    fn test_decode_orders_invalid_utf8() {
1314        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1315
1316        let mut buf = Vec::new();
1317        buf.extend_from_slice(&header);
1318        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 1));
1319
1320        buf.extend_from_slice(&[0u8; ORDERS_GROUP_BLOCK_LENGTH]);
1321
1322        // Invalid UTF-8 sequence
1323        buf.push(4);
1324        buf.extend_from_slice(&[0xFF, 0xFE, 0x00, 0x01]);
1325
1326        let err = decode_orders(&buf).unwrap_err();
1327        assert!(matches!(err, SbeDecodeError::InvalidUtf8));
1328    }
1329
1330    #[rstest]
1331    fn test_decode_cancel_order_valid() {
1332        let header = create_header(
1333            CANCEL_ORDER_BLOCK_LENGTH as u16,
1334            CANCEL_ORDER_TEMPLATE_ID,
1335            SBE_SCHEMA_ID,
1336            SBE_SCHEMA_VERSION,
1337        );
1338
1339        let mut buf = Vec::new();
1340        buf.extend_from_slice(&header);
1341
1342        buf.push((-8i8) as u8); // price_exponent
1343        buf.push((-8i8) as u8); // qty_exponent
1344        buf.extend_from_slice(&99999i64.to_le_bytes()); // order_id
1345        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1346        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // transact_time
1347        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1348        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1349        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // executed_qty
1350        buf.extend_from_slice(&1_000_000_000i64.to_le_bytes()); // cummulative_quote_qty
1351        buf.push(4); // status (CANCELED)
1352        buf.push(1); // time_in_force
1353        buf.push(1); // order_type
1354        buf.push(1); // side
1355        buf.push(0); // self_trade_prevention_mode
1356
1357        // Pad to block length
1358        while buf.len() < 8 + CANCEL_ORDER_BLOCK_LENGTH {
1359            buf.push(0);
1360        }
1361
1362        write_var_string(&mut buf, "BTCUSDT");
1363        write_var_string(&mut buf, "orig-client-id");
1364        write_var_string(&mut buf, "new-client-id");
1365
1366        let cancel = decode_cancel_order(&buf).unwrap();
1367
1368        assert_eq!(cancel.order_id, 99999);
1369        assert!(cancel.order_list_id.is_none());
1370        assert_eq!(cancel.symbol, "BTCUSDT");
1371        assert_eq!(cancel.orig_client_order_id, "orig-client-id");
1372        assert_eq!(cancel.client_order_id, "new-client-id");
1373    }
1374
1375    #[rstest]
1376    fn test_decode_account_with_balances() {
1377        let header = create_header(
1378            ACCOUNT_BLOCK_LENGTH as u16,
1379            ACCOUNT_TEMPLATE_ID,
1380            SBE_SCHEMA_ID,
1381            SBE_SCHEMA_VERSION,
1382        );
1383
1384        let mut buf = Vec::new();
1385        buf.extend_from_slice(&header);
1386
1387        // Fixed block (64 bytes)
1388        buf.push((-8i8) as u8); // commission_exponent
1389        buf.extend_from_slice(&100_000i64.to_le_bytes()); // maker_commission
1390        buf.extend_from_slice(&100_000i64.to_le_bytes()); // taker_commission
1391        buf.extend_from_slice(&0i64.to_le_bytes()); // buyer_commission
1392        buf.extend_from_slice(&0i64.to_le_bytes()); // seller_commission
1393        buf.push(1); // can_trade
1394        buf.push(1); // can_withdraw
1395        buf.push(1); // can_deposit
1396        buf.push(0); // brokered
1397        buf.push(0); // require_self_trade_prevention
1398        buf.push(0); // prevent_sor
1399        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
1400        buf.push(1); // account_type (SPOT)
1401
1402        // Pad to 64 bytes
1403        while buf.len() < 8 + ACCOUNT_BLOCK_LENGTH {
1404            buf.push(0);
1405        }
1406
1407        // Balances group: 2 balances
1408        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 2));
1409
1410        // Balance 1: BTC
1411        buf.push((-8i8) as u8); // exponent
1412        buf.extend_from_slice(&100_000_000i64.to_le_bytes()); // free (1.0 BTC)
1413        buf.extend_from_slice(&50_000_000i64.to_le_bytes()); // locked (0.5 BTC)
1414        write_var_string(&mut buf, "BTC");
1415
1416        // Balance 2: USDT
1417        buf.push((-8i8) as u8); // exponent
1418        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // free (10000 USDT)
1419        buf.extend_from_slice(&0i64.to_le_bytes()); // locked
1420        write_var_string(&mut buf, "USDT");
1421
1422        let account = decode_account(&buf).unwrap();
1423
1424        assert!(account.can_trade);
1425        assert!(account.can_withdraw);
1426        assert!(account.can_deposit);
1427        assert_eq!(account.balances.len(), 2);
1428        assert_eq!(account.balances[0].asset, "BTC");
1429        assert_eq!(account.balances[0].free_mantissa, 100_000_000);
1430        assert_eq!(account.balances[0].locked_mantissa, 50_000_000);
1431        assert_eq!(account.balances[1].asset, "USDT");
1432        assert_eq!(account.balances[1].free_mantissa, 1_000_000_000_000);
1433    }
1434
1435    #[rstest]
1436    fn test_decode_account_empty_balances() {
1437        let header = create_header(
1438            ACCOUNT_BLOCK_LENGTH as u16,
1439            ACCOUNT_TEMPLATE_ID,
1440            SBE_SCHEMA_ID,
1441            SBE_SCHEMA_VERSION,
1442        );
1443
1444        let mut buf = Vec::new();
1445        buf.extend_from_slice(&header);
1446
1447        // Minimal fixed block
1448        buf.push((-8i8) as u8);
1449        buf.extend_from_slice(&[0u8; 63]); // Rest of fixed block
1450
1451        // Empty balances group
1452        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 0));
1453
1454        let account = decode_account(&buf).unwrap();
1455        assert!(account.balances.is_empty());
1456    }
1457
1458    #[rstest]
1459    fn test_decode_account_trades_multiple() {
1460        let header = create_header(
1461            0,
1462            ACCOUNT_TRADES_TEMPLATE_ID,
1463            SBE_SCHEMA_ID,
1464            SBE_SCHEMA_VERSION,
1465        );
1466
1467        let mut buf = Vec::new();
1468        buf.extend_from_slice(&header);
1469
1470        // Group header: 2 trades
1471        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 2));
1472
1473        // Trade 1
1474        buf.push((-8i8) as u8); // price_exponent
1475        buf.push((-8i8) as u8); // qty_exponent
1476        buf.push((-8i8) as u8); // commission_exponent
1477        buf.extend_from_slice(&1001i64.to_le_bytes()); // id
1478        buf.extend_from_slice(&5001i64.to_le_bytes()); // order_id
1479        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1480        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price
1481        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // qty
1482        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // quote_qty
1483        buf.extend_from_slice(&100_000i64.to_le_bytes()); // commission
1484        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1485        buf.push(1); // is_buyer
1486        buf.push(0); // is_maker
1487        buf.push(1); // is_best_match
1488        write_var_string(&mut buf, "BTCUSDT");
1489        write_var_string(&mut buf, "BNB");
1490
1491        // Trade 2
1492        buf.push((-8i8) as u8);
1493        buf.push((-8i8) as u8);
1494        buf.push((-8i8) as u8);
1495        buf.extend_from_slice(&1002i64.to_le_bytes());
1496        buf.extend_from_slice(&5002i64.to_le_bytes());
1497        buf.extend_from_slice(&i64::MIN.to_le_bytes());
1498        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes());
1499        buf.extend_from_slice(&5_000_000i64.to_le_bytes());
1500        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes());
1501        buf.extend_from_slice(&50_000i64.to_le_bytes());
1502        buf.extend_from_slice(&1734300001000i64.to_le_bytes());
1503        buf.push(0); // is_buyer (false = seller)
1504        buf.push(1); // is_maker
1505        buf.push(1); // is_best_match
1506        write_var_string(&mut buf, "ETHUSDT");
1507        write_var_string(&mut buf, "USDT");
1508
1509        let trades = decode_account_trades(&buf).unwrap();
1510
1511        assert_eq!(trades.len(), 2);
1512        assert_eq!(trades[0].id, 1001);
1513        assert_eq!(trades[0].order_id, 5001);
1514        assert!(trades[0].order_list_id.is_none());
1515        assert_eq!(trades[0].symbol, "BTCUSDT");
1516        assert_eq!(trades[0].commission_asset, "BNB");
1517        assert!(trades[0].is_buyer);
1518        assert!(!trades[0].is_maker);
1519
1520        assert_eq!(trades[1].id, 1002);
1521        assert_eq!(trades[1].symbol, "ETHUSDT");
1522        assert_eq!(trades[1].commission_asset, "USDT");
1523        assert!(!trades[1].is_buyer);
1524        assert!(trades[1].is_maker);
1525    }
1526
1527    #[rstest]
1528    fn test_decode_account_trades_empty() {
1529        let header = create_header(
1530            0,
1531            ACCOUNT_TRADES_TEMPLATE_ID,
1532            SBE_SCHEMA_ID,
1533            SBE_SCHEMA_VERSION,
1534        );
1535
1536        let mut buf = Vec::new();
1537        buf.extend_from_slice(&header);
1538        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 0));
1539
1540        let trades = decode_account_trades(&buf).unwrap();
1541        assert!(trades.is_empty());
1542    }
1543
1544    #[rstest]
1545    fn test_decode_exchange_info_single_symbol() {
1546        let header = create_header(
1547            0,
1548            EXCHANGE_INFO_TEMPLATE_ID,
1549            SBE_SCHEMA_ID,
1550            SBE_SCHEMA_VERSION,
1551        );
1552
1553        let mut buf = Vec::new();
1554        buf.extend_from_slice(&header);
1555
1556        // Empty rate_limits group
1557        buf.extend_from_slice(&create_group_header(11, 0));
1558
1559        // Empty exchange_filters group
1560        buf.extend_from_slice(&create_group_header(0, 0));
1561
1562        // Symbols group: 1 symbol with block_length=19
1563        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 1));
1564
1565        // Fixed block (19 bytes)
1566        buf.push(0); // status (Trading)
1567        buf.push(8); // base_asset_precision
1568        buf.push(8); // quote_asset_precision
1569        buf.push(8); // base_commission_precision
1570        buf.push(8); // quote_commission_precision
1571        buf.extend_from_slice(&0b0000_0111u16.to_le_bytes()); // order_types (MARKET|LIMIT|STOP_LOSS)
1572        buf.push(1); // iceberg_allowed (True)
1573        buf.push(1); // oco_allowed (True)
1574        buf.push(0); // oto_allowed (False)
1575        buf.push(1); // quote_order_qty_market_allowed (True)
1576        buf.push(1); // allow_trailing_stop (True)
1577        buf.push(1); // cancel_replace_allowed (True)
1578        buf.push(0); // amend_allowed (False)
1579        buf.push(1); // is_spot_trading_allowed (True)
1580        buf.push(0); // is_margin_trading_allowed (False)
1581        buf.push(0); // default_self_trade_prevention_mode
1582        buf.push(0); // allowed_self_trade_prevention_modes
1583        buf.push(0); // peg_instructions_allowed
1584
1585        // Filters nested group: 1 filter
1586        buf.extend_from_slice(&create_group_header(0, 1));
1587        let filter_json = r#"{"filterType":"PRICE_FILTER","minPrice":"0.01","maxPrice":"100000","tickSize":"0.01"}"#;
1588        write_var_string(&mut buf, filter_json);
1589
1590        // Permission sets nested group: 1 set with 1 permission
1591        buf.extend_from_slice(&create_group_header(0, 1));
1592        buf.extend_from_slice(&create_group_header(0, 1));
1593        write_var_string(&mut buf, "SPOT");
1594
1595        // Variable-length strings
1596        write_var_string(&mut buf, "BTCUSDT");
1597        write_var_string(&mut buf, "BTC");
1598        write_var_string(&mut buf, "USDT");
1599
1600        let info = decode_exchange_info(&buf).unwrap();
1601
1602        assert_eq!(info.symbols.len(), 1);
1603        let symbol = &info.symbols[0];
1604        assert_eq!(symbol.symbol, "BTCUSDT");
1605        assert_eq!(symbol.base_asset, "BTC");
1606        assert_eq!(symbol.quote_asset, "USDT");
1607        assert_eq!(symbol.base_asset_precision, 8);
1608        assert_eq!(symbol.quote_asset_precision, 8);
1609        assert_eq!(symbol.status, 0); // Trading
1610        assert_eq!(symbol.order_types, 0b0000_0111);
1611        assert!(symbol.iceberg_allowed);
1612        assert!(symbol.oco_allowed);
1613        assert!(!symbol.oto_allowed);
1614        assert!(symbol.quote_order_qty_market_allowed);
1615        assert!(symbol.allow_trailing_stop);
1616        assert!(symbol.cancel_replace_allowed);
1617        assert!(!symbol.amend_allowed);
1618        assert!(symbol.is_spot_trading_allowed);
1619        assert!(!symbol.is_margin_trading_allowed);
1620        assert_eq!(symbol.filters.len(), 1);
1621        assert_eq!(symbol.permissions.len(), 1);
1622        assert_eq!(symbol.permissions[0], vec!["SPOT"]);
1623    }
1624
1625    #[rstest]
1626    fn test_decode_exchange_info_empty() {
1627        let header = create_header(
1628            0,
1629            EXCHANGE_INFO_TEMPLATE_ID,
1630            SBE_SCHEMA_ID,
1631            SBE_SCHEMA_VERSION,
1632        );
1633
1634        let mut buf = Vec::new();
1635        buf.extend_from_slice(&header);
1636
1637        // Empty rate_limits group
1638        buf.extend_from_slice(&create_group_header(11, 0));
1639
1640        // Empty exchange_filters group
1641        buf.extend_from_slice(&create_group_header(0, 0));
1642
1643        // Empty symbols group
1644        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 0));
1645
1646        let info = decode_exchange_info(&buf).unwrap();
1647        assert!(info.symbols.is_empty());
1648    }
1649
1650    #[rstest]
1651    fn test_decode_exchange_info_wrong_template() {
1652        let header = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1653
1654        let mut buf = Vec::new();
1655        buf.extend_from_slice(&header);
1656
1657        let err = decode_exchange_info(&buf).unwrap_err();
1658        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1659    }
1660
1661    #[rstest]
1662    fn test_decode_exchange_info_multiple_symbols() {
1663        let header = create_header(
1664            0,
1665            EXCHANGE_INFO_TEMPLATE_ID,
1666            SBE_SCHEMA_ID,
1667            SBE_SCHEMA_VERSION,
1668        );
1669
1670        let mut buf = Vec::new();
1671        buf.extend_from_slice(&header);
1672
1673        // Empty rate_limits group
1674        buf.extend_from_slice(&create_group_header(11, 0));
1675
1676        // Empty exchange_filters group
1677        buf.extend_from_slice(&create_group_header(0, 0));
1678
1679        // Symbols group: 2 symbols
1680        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 2));
1681
1682        // Symbol 1: BTCUSDT
1683        buf.push(0); // status
1684        buf.push(8); // base_asset_precision
1685        buf.push(8); // quote_asset_precision
1686        buf.push(8); // base_commission_precision
1687        buf.push(8); // quote_commission_precision
1688        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
1689        buf.push(1); // iceberg_allowed
1690        buf.push(1); // oco_allowed
1691        buf.push(0); // oto_allowed
1692        buf.push(1); // quote_order_qty_market_allowed
1693        buf.push(1); // allow_trailing_stop
1694        buf.push(1); // cancel_replace_allowed
1695        buf.push(0); // amend_allowed
1696        buf.push(1); // is_spot_trading_allowed
1697        buf.push(0); // is_margin_trading_allowed
1698        buf.push(0); // default_self_trade_prevention_mode
1699        buf.push(0); // allowed_self_trade_prevention_modes
1700        buf.push(0); // peg_instructions_allowed
1701        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
1702        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
1703        write_var_string(&mut buf, "BTCUSDT");
1704        write_var_string(&mut buf, "BTC");
1705        write_var_string(&mut buf, "USDT");
1706
1707        // Symbol 2: ETHUSDT
1708        buf.push(0); // status
1709        buf.push(8); // base_asset_precision
1710        buf.push(8); // quote_asset_precision
1711        buf.push(8); // base_commission_precision
1712        buf.push(8); // quote_commission_precision
1713        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
1714        buf.push(1); // iceberg_allowed
1715        buf.push(1); // oco_allowed
1716        buf.push(0); // oto_allowed
1717        buf.push(1); // quote_order_qty_market_allowed
1718        buf.push(1); // allow_trailing_stop
1719        buf.push(1); // cancel_replace_allowed
1720        buf.push(0); // amend_allowed
1721        buf.push(1); // is_spot_trading_allowed
1722        buf.push(1); // is_margin_trading_allowed
1723        buf.push(0); // default_self_trade_prevention_mode
1724        buf.push(0); // allowed_self_trade_prevention_modes
1725        buf.push(0); // peg_instructions_allowed
1726        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
1727        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
1728        write_var_string(&mut buf, "ETHUSDT");
1729        write_var_string(&mut buf, "ETH");
1730        write_var_string(&mut buf, "USDT");
1731
1732        let info = decode_exchange_info(&buf).unwrap();
1733
1734        assert_eq!(info.symbols.len(), 2);
1735        assert_eq!(info.symbols[0].symbol, "BTCUSDT");
1736        assert_eq!(info.symbols[0].base_asset, "BTC");
1737        assert!(!info.symbols[0].is_margin_trading_allowed);
1738
1739        assert_eq!(info.symbols[1].symbol, "ETHUSDT");
1740        assert_eq!(info.symbols[1].base_asset, "ETH");
1741        assert!(info.symbols[1].is_margin_trading_allowed);
1742    }
1743}