Skip to main content

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 and template ID) before extracting fields.
20
21use rust_decimal::Decimal;
22
23use super::{
24    error::SbeDecodeError,
25    models::{
26        BinanceAccountInfo, BinanceAccountTrade, BinanceAggTrade, BinanceAggTrades, BinanceBalance,
27        BinanceCancelOpenOrdersResponse, BinanceCancelOrderListOrder,
28        BinanceCancelOrderListResponse, BinanceCancelOrderResponse, BinanceDepth,
29        BinanceExchangeInfoSbe, BinanceKline, BinanceKlines, BinanceLotSizeFilterSbe,
30        BinanceNewOrderResponse, BinanceNotionalFilter, BinanceOrderFill, BinanceOrderResponse,
31        BinancePriceFilterSbe, BinancePriceLevel, BinanceSymbolFiltersSbe, BinanceSymbolSbe,
32        BinanceTrade, BinanceTrades,
33    },
34};
35use crate::spot::sbe::{
36    cursor::SbeCursor,
37    spot::{
38        SBE_SCHEMA_ID, account_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TEMPLATE_ID,
39        account_trades_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TRADES_TEMPLATE_ID,
40        account_type::AccountType,
41        agg_trades_response_codec::SBE_TEMPLATE_ID as AGG_TRADES_TEMPLATE_ID, bool_enum::BoolEnum,
42        cancel_open_orders_response_codec::SBE_TEMPLATE_ID as CANCEL_OPEN_ORDERS_TEMPLATE_ID,
43        cancel_order_list_response_codec::SBE_TEMPLATE_ID as CANCEL_ORDER_LIST_TEMPLATE_ID,
44        cancel_order_response_codec::SBE_TEMPLATE_ID as CANCEL_ORDER_TEMPLATE_ID,
45        cancel_replace_order_response_codec::SBE_TEMPLATE_ID as CANCEL_REPLACE_TEMPLATE_ID,
46        cancel_replace_status::CancelReplaceStatus,
47        depth_response_codec::SBE_TEMPLATE_ID as DEPTH_TEMPLATE_ID,
48        exchange_info_response_codec::SBE_TEMPLATE_ID as EXCHANGE_INFO_TEMPLATE_ID,
49        klines_response_codec::SBE_TEMPLATE_ID as KLINES_TEMPLATE_ID,
50        lot_size_filter_codec::SBE_TEMPLATE_ID as LOT_SIZE_FILTER_TEMPLATE_ID,
51        message_header_codec::ENCODED_LENGTH as HEADER_LENGTH,
52        min_notional_filter_codec::SBE_TEMPLATE_ID as MIN_NOTIONAL_FILTER_TEMPLATE_ID,
53        new_order_full_response_codec::SBE_TEMPLATE_ID as NEW_ORDER_FULL_TEMPLATE_ID,
54        notional_filter_codec::SBE_TEMPLATE_ID as NOTIONAL_FILTER_TEMPLATE_ID,
55        order_response_codec::SBE_TEMPLATE_ID as ORDER_TEMPLATE_ID,
56        orders_response_codec::SBE_TEMPLATE_ID as ORDERS_TEMPLATE_ID,
57        ping_response_codec::SBE_TEMPLATE_ID as PING_TEMPLATE_ID,
58        price_filter_codec::SBE_TEMPLATE_ID as PRICE_FILTER_TEMPLATE_ID,
59        server_time_response_codec::SBE_TEMPLATE_ID as SERVER_TIME_TEMPLATE_ID,
60        trades_response_codec::SBE_TEMPLATE_ID as TRADES_TEMPLATE_ID,
61    },
62};
63
64/// SBE message header.
65#[derive(Debug, Clone, Copy)]
66struct MessageHeader {
67    block_length: u16,
68    template_id: u16,
69    schema_id: u16,
70    version: u16,
71}
72
73impl MessageHeader {
74    /// Decode message header using cursor.
75    fn decode_cursor(cursor: &mut SbeCursor<'_>) -> Result<Self, SbeDecodeError> {
76        cursor.require(HEADER_LENGTH)?;
77        let block_length = cursor.read_u16_le()?;
78        let template_id = cursor.read_u16_le()?;
79        let schema_id = cursor.read_u16_le()?;
80        let version = cursor.read_u16_le()?;
81        Ok(Self {
82            block_length,
83            template_id,
84            schema_id,
85            version,
86        })
87    }
88
89    /// Validate the message schema ID.
90    ///
91    /// The exact schema version is intentionally not enforced, matching the
92    /// WebSocket SBE path. Binance evolves the schema additively within a schema ID
93    /// and rolls new versions out gradually, so a single client sees both the current
94    /// and next version during a rollout: 3:4 and 3:5 share identical block layouts,
95    /// differing only by an added `symbolStatus` enum value, and unknown enum values
96    /// decode to their null variant. Enforcing an exact version would hard-fail
97    /// instrument loading on a server-side bump. A different schema ID is a breaking
98    /// change and is still rejected.
99    ///
100    /// The decoders assume block layouts are stable within a schema ID.
101    /// `decode_exchange_info` verifies the symbol block length and fails loudly on a
102    /// mismatch; the market-data decoders read groups at a fixed offset, so a future
103    /// version that adds fixed-block fields to those messages would need them updated
104    /// to advance past the added bytes via `block_length`.
105    fn validate(&self) -> Result<(), SbeDecodeError> {
106        if self.schema_id != SBE_SCHEMA_ID {
107            return Err(SbeDecodeError::SchemaMismatch {
108                expected: SBE_SCHEMA_ID,
109                actual: self.schema_id,
110            });
111        }
112        Ok(())
113    }
114}
115
116/// Decode a ping response.
117///
118/// Ping response has no body (block_length = 0), just validates the header.
119///
120/// # Errors
121///
122/// Returns error if buffer is too short or schema mismatch.
123pub fn decode_ping(buf: &[u8]) -> Result<(), SbeDecodeError> {
124    let mut cursor = SbeCursor::new(buf);
125    let header = MessageHeader::decode_cursor(&mut cursor)?;
126    header.validate()?;
127
128    if header.template_id != PING_TEMPLATE_ID {
129        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
130    }
131
132    Ok(())
133}
134
135/// Decode a server time response.
136///
137/// Returns the server time as **microseconds** since epoch (SBE provides
138/// microsecond precision vs JSON's milliseconds).
139///
140/// # Errors
141///
142/// Returns error if buffer is too short or schema mismatch.
143pub fn decode_server_time(buf: &[u8]) -> Result<i64, SbeDecodeError> {
144    let mut cursor = SbeCursor::new(buf);
145    let header = MessageHeader::decode_cursor(&mut cursor)?;
146    header.validate()?;
147
148    if header.template_id != SERVER_TIME_TEMPLATE_ID {
149        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
150    }
151
152    cursor.read_i64_le()
153}
154
155/// Decode a depth response.
156///
157/// Returns the order book depth with bids and asks.
158///
159/// # Errors
160///
161/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
162pub fn decode_depth(buf: &[u8]) -> Result<BinanceDepth, SbeDecodeError> {
163    let mut cursor = SbeCursor::new(buf);
164    let header = MessageHeader::decode_cursor(&mut cursor)?;
165    header.validate()?;
166
167    if header.template_id != DEPTH_TEMPLATE_ID {
168        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
169    }
170
171    let last_update_id = cursor.read_i64_le()?;
172    let price_exponent = cursor.read_i8()?;
173    let qty_exponent = cursor.read_i8()?;
174
175    let (block_len, count) = cursor.read_group_header()?;
176    let bids = cursor.read_group(block_len, count, |c| {
177        Ok(BinancePriceLevel {
178            price_mantissa: c.read_i64_le()?,
179            qty_mantissa: c.read_i64_le()?,
180        })
181    })?;
182
183    let (block_len, count) = cursor.read_group_header()?;
184    let asks = cursor.read_group(block_len, count, |c| {
185        Ok(BinancePriceLevel {
186            price_mantissa: c.read_i64_le()?,
187            qty_mantissa: c.read_i64_le()?,
188        })
189    })?;
190
191    Ok(BinanceDepth {
192        last_update_id,
193        price_exponent,
194        qty_exponent,
195        bids,
196        asks,
197    })
198}
199
200/// Decode a trades response.
201///
202/// Returns the list of trades.
203///
204/// # Errors
205///
206/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
207pub fn decode_trades(buf: &[u8]) -> Result<BinanceTrades, SbeDecodeError> {
208    let mut cursor = SbeCursor::new(buf);
209    let header = MessageHeader::decode_cursor(&mut cursor)?;
210    header.validate()?;
211
212    if header.template_id != TRADES_TEMPLATE_ID {
213        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
214    }
215
216    let price_exponent = cursor.read_i8()?;
217    let qty_exponent = cursor.read_i8()?;
218
219    let (block_len, count) = cursor.read_group_header()?;
220    let trades = cursor.read_group(block_len, count, |c| {
221        Ok(BinanceTrade {
222            id: c.read_i64_le()?,
223            price_mantissa: c.read_i64_le()?,
224            qty_mantissa: c.read_i64_le()?,
225            quote_qty_mantissa: c.read_i64_le()?,
226            time: c.read_i64_le()?,
227            is_buyer_maker: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
228            is_best_match: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
229        })
230    })?;
231
232    Ok(BinanceTrades {
233        price_exponent,
234        qty_exponent,
235        trades,
236    })
237}
238
239/// Decodes an aggregate trades response.
240///
241/// # Errors
242///
243/// Returns an error for an invalid header, template, or group payload.
244pub fn decode_agg_trades(buf: &[u8]) -> Result<BinanceAggTrades, SbeDecodeError> {
245    let mut cursor = SbeCursor::new(buf);
246    let header = MessageHeader::decode_cursor(&mut cursor)?;
247    header.validate()?;
248
249    if header.template_id != AGG_TRADES_TEMPLATE_ID {
250        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
251    }
252
253    let price_exponent = cursor.read_i8()?;
254    let qty_exponent = cursor.read_i8()?;
255    let (block_len, count) = cursor.read_group_header()?;
256    let trades = cursor.read_group(block_len, count, |c| {
257        Ok(BinanceAggTrade {
258            id: c.read_i64_le()?,
259            price_mantissa: c.read_i64_le()?,
260            qty_mantissa: c.read_i64_le()?,
261            first_trade_id: c.read_i64_le()?,
262            last_trade_id: c.read_i64_le()?,
263            time: c.read_i64_le()?,
264            is_buyer_maker: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
265            is_best_match: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
266        })
267    })?;
268
269    Ok(BinanceAggTrades {
270        price_exponent,
271        qty_exponent,
272        trades,
273    })
274}
275
276/// Klines group item block length (from SBE codec).
277const KLINES_BLOCK_LENGTH: u16 = 120;
278
279/// Decode a klines (candlestick) response.
280///
281/// Returns the list of klines with their price and quantity exponents.
282///
283/// # Errors
284///
285/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
286pub fn decode_klines(buf: &[u8]) -> Result<BinanceKlines, SbeDecodeError> {
287    let mut cursor = SbeCursor::new(buf);
288    let header = MessageHeader::decode_cursor(&mut cursor)?;
289    header.validate()?;
290
291    if header.template_id != KLINES_TEMPLATE_ID {
292        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
293    }
294
295    let price_exponent = cursor.read_i8()?;
296    let qty_exponent = cursor.read_i8()?;
297
298    let (block_len, count) = cursor.read_group_header()?;
299
300    if block_len != KLINES_BLOCK_LENGTH {
301        return Err(SbeDecodeError::InvalidBlockLength {
302            expected: KLINES_BLOCK_LENGTH,
303            actual: block_len,
304        });
305    }
306
307    let mut klines = Vec::with_capacity(count as usize);
308
309    for _ in 0..count {
310        cursor.require(KLINES_BLOCK_LENGTH as usize)?;
311
312        let open_time = cursor.read_i64_le()?;
313        let open_price = cursor.read_i64_le()?;
314        let high_price = cursor.read_i64_le()?;
315        let low_price = cursor.read_i64_le()?;
316        let close_price = cursor.read_i64_le()?;
317
318        let volume_slice = cursor.read_bytes(16)?;
319        let mut volume = [0u8; 16];
320        volume.copy_from_slice(volume_slice);
321
322        let close_time = cursor.read_i64_le()?;
323
324        let quote_volume_slice = cursor.read_bytes(16)?;
325        let mut quote_volume = [0u8; 16];
326        quote_volume.copy_from_slice(quote_volume_slice);
327
328        let num_trades = cursor.read_i64_le()?;
329
330        let taker_buy_base_volume_slice = cursor.read_bytes(16)?;
331        let mut taker_buy_base_volume = [0u8; 16];
332        taker_buy_base_volume.copy_from_slice(taker_buy_base_volume_slice);
333
334        let taker_buy_quote_volume_slice = cursor.read_bytes(16)?;
335        let mut taker_buy_quote_volume = [0u8; 16];
336        taker_buy_quote_volume.copy_from_slice(taker_buy_quote_volume_slice);
337
338        klines.push(BinanceKline {
339            open_time,
340            open_price,
341            high_price,
342            low_price,
343            close_price,
344            volume,
345            close_time,
346            quote_volume,
347            num_trades,
348            taker_buy_base_volume,
349            taker_buy_quote_volume,
350        });
351    }
352
353    Ok(BinanceKlines {
354        price_exponent,
355        qty_exponent,
356        klines,
357    })
358}
359
360/// Bytes consumed from the fixed block before the end-of-block skip in each decoder.
361/// These represent explicit reads and advances up to the last field we extract.
362const NEW_ORDER_FULL_FIELDS_END: usize = 117;
363const CANCEL_ORDER_FIELDS_END: usize = 109;
364const CANCEL_ORDER_LIST_FIELDS_END: usize = 21;
365const CANCEL_ORDER_LIST_ORDER_FIELDS_END: usize = 8;
366const CANCEL_ORDER_LIST_REPORT_FIELDS_END: usize = 107;
367const CANCEL_ORDER_LIST_REPORT_V0_BLOCK_LENGTH: u16 = 124;
368const CANCEL_ORDER_LIST_REPORT_V1_BLOCK_LENGTH: u16 = 135;
369const ORDER_FIELDS_END: usize = 134;
370
371/// Minimum fixed-block length of an order in `orderResponse` and the
372/// `ordersResponse` group (schema 3:3 baseline).
373const ORDER_MIN_BLOCK_LENGTH: u16 = 162;
374
375/// Sentinel value for a null `expiryReason` in schema 3:4.
376const EXPIRY_REASON_NULL: u8 = 0xff;
377
378/// Schema-3:4 byte offsets of the `expiryReason` field within each fixed block.
379/// Sourced from the SBE codecs: `newOrderFullResponse` puts it at 153,
380/// `orderResponse` / `ordersResponse` at 162.
381const NEW_ORDER_FULL_EXPIRY_REASON_OFFSET: usize = 153;
382const ORDER_EXPIRY_REASON_OFFSET: usize = 162;
383
384/// Reads the schema-3:4 `expiryReason` byte from the fixed block when present.
385///
386/// `fields_end` is the cursor position (in bytes from the start of the block)
387/// after the last field the caller has explicitly parsed. `expiry_reason_offset`
388/// is the field's encoded offset within the block per the SBE schema.
389/// Returns `Ok(None)` when the runtime `block_length` does not span the
390/// `expiryReason` byte (schema 3:3 layouts) or when the byte holds the SBE
391/// null sentinel. In every case the cursor is advanced to the end of the
392/// fixed block.
393fn read_trailing_expiry_reason(
394    cursor: &mut SbeCursor<'_>,
395    block_length: usize,
396    fields_end: usize,
397    expiry_reason_offset: usize,
398) -> Result<Option<u8>, SbeDecodeError> {
399    debug_assert!(fields_end <= expiry_reason_offset);
400    if block_length < fields_end {
401        return Ok(None);
402    }
403    let trailer = block_length - fields_end;
404    if trailer == 0 {
405        return Ok(None);
406    }
407    // Pre-3:4 block: no expiryReason byte at this offset, skip remaining bytes.
408    if block_length <= expiry_reason_offset {
409        cursor.advance(trailer)?;
410        return Ok(None);
411    }
412    let pre = expiry_reason_offset - fields_end;
413    cursor.advance(pre)?;
414    let byte = cursor.read_u8()?;
415    let post = trailer - pre - 1;
416    if post > 0 {
417        cursor.advance(post)?;
418    }
419    Ok((byte != EXPIRY_REASON_NULL).then_some(byte))
420}
421
422/// Decode a new order full response.
423///
424/// # Errors
425///
426/// Returns error if buffer is too short, schema mismatch, or decode error.
427#[allow(dead_code)]
428pub fn decode_new_order_full(buf: &[u8]) -> Result<BinanceNewOrderResponse, SbeDecodeError> {
429    let mut cursor = SbeCursor::new(buf);
430    let header = MessageHeader::decode_cursor(&mut cursor)?;
431    header.validate()?;
432
433    if header.template_id != NEW_ORDER_FULL_TEMPLATE_ID {
434        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
435    }
436
437    if usize::from(header.block_length) < NEW_ORDER_FULL_FIELDS_END {
438        return Err(SbeDecodeError::InvalidBlockLength {
439            expected: NEW_ORDER_FULL_FIELDS_END as u16,
440            actual: header.block_length,
441        });
442    }
443
444    cursor.require(header.block_length as usize)?;
445
446    let price_exponent = cursor.read_i8()?;
447    let qty_exponent = cursor.read_i8()?;
448    let order_id = cursor.read_i64_le()?;
449    let order_list_id = cursor.read_optional_i64_le()?;
450    let transact_time = cursor.read_i64_le()?;
451    let price_mantissa = cursor.read_i64_le()?;
452    let orig_qty_mantissa = cursor.read_i64_le()?;
453    let executed_qty_mantissa = cursor.read_i64_le()?;
454    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
455    let status = cursor.read_u8()?.into();
456    let time_in_force = cursor.read_u8()?.into();
457    let order_type = cursor.read_u8()?.into();
458    let side = cursor.read_u8()?.into();
459    let stop_price_mantissa = cursor.read_optional_i64_le()?;
460
461    cursor.advance(16)?; // Skip trailing_delta (8) + trailing_time (8)
462    let working_time = cursor.read_optional_i64_le()?;
463
464    cursor.advance(22)?; // Skip iceberg_qty to working_floor
465    let self_trade_prevention_mode = cursor.read_u8()?.into();
466
467    let expiry_reason = read_trailing_expiry_reason(
468        &mut cursor,
469        header.block_length as usize,
470        NEW_ORDER_FULL_FIELDS_END,
471        NEW_ORDER_FULL_EXPIRY_REASON_OFFSET,
472    )?;
473
474    let fills = decode_fills_cursor(&mut cursor)?;
475
476    // Skip prevented matches group
477    let (block_len, count) = cursor.read_group_header()?;
478    cursor.advance(block_len as usize * count as usize)?;
479
480    let symbol = cursor.read_var_string8()?;
481    let client_order_id = cursor.read_var_string8()?;
482
483    Ok(BinanceNewOrderResponse {
484        price_exponent,
485        qty_exponent,
486        order_id,
487        order_list_id,
488        transact_time,
489        price_mantissa,
490        orig_qty_mantissa,
491        executed_qty_mantissa,
492        cummulative_quote_qty_mantissa,
493        status,
494        time_in_force,
495        order_type,
496        side,
497        stop_price_mantissa,
498        working_time,
499        self_trade_prevention_mode,
500        client_order_id,
501        symbol,
502        fills,
503        expiry_reason,
504    })
505}
506
507/// Decodes the replacement order from a successful cancel-replace response.
508///
509/// # Errors
510///
511/// Returns an error for a malformed wrapper or an unsuccessful replacement.
512pub fn decode_cancel_replace(buf: &[u8]) -> Result<BinanceNewOrderResponse, SbeDecodeError> {
513    let (_, replacement) = decode_cancel_replace_payloads(buf)?;
514    decode_new_order_full(replacement)
515}
516
517pub(crate) fn decode_cancel_replace_orders(
518    buf: &[u8],
519) -> Result<(BinanceCancelOrderResponse, BinanceNewOrderResponse), SbeDecodeError> {
520    let (cancellation, replacement) = decode_cancel_replace_payloads(buf)?;
521    Ok((
522        decode_cancel_order(cancellation)?,
523        decode_new_order_full(replacement)?,
524    ))
525}
526
527fn decode_cancel_replace_payloads(buf: &[u8]) -> Result<(&[u8], &[u8]), SbeDecodeError> {
528    let mut cursor = SbeCursor::new(buf);
529    let header = MessageHeader::decode_cursor(&mut cursor)?;
530    header.validate()?;
531    if header.template_id != CANCEL_REPLACE_TEMPLATE_ID {
532        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
533    }
534
535    if header.block_length < 2 {
536        return Err(SbeDecodeError::InvalidBlockLength {
537            expected: 2,
538            actual: header.block_length,
539        });
540    }
541    let cancel_result = cursor.read_u8()?;
542    let new_order_result = cursor.read_u8()?;
543    cursor.advance(usize::from(header.block_length) - 2)?;
544
545    if cancel_result != CancelReplaceStatus::Success as u8
546        || new_order_result != CancelReplaceStatus::Success as u8
547    {
548        return Err(SbeDecodeError::InvalidValue {
549            field: "cancel-replace result",
550        });
551    }
552    let cancel_len = usize::from(cursor.read_u16_le()?);
553    let cancellation = cursor.read_bytes(cancel_len)?;
554    let new_order_len = cursor.read_u32_le()? as usize;
555    let replacement = cursor.read_bytes(new_order_len)?;
556    Ok((cancellation, replacement))
557}
558
559/// Decode a cancel order response.
560///
561/// # Errors
562///
563/// Returns error if buffer is too short, schema mismatch, or decode error.
564#[allow(dead_code)]
565pub fn decode_cancel_order(buf: &[u8]) -> Result<BinanceCancelOrderResponse, SbeDecodeError> {
566    let mut cursor = SbeCursor::new(buf);
567    let header = MessageHeader::decode_cursor(&mut cursor)?;
568    header.validate()?;
569
570    if header.template_id != CANCEL_ORDER_TEMPLATE_ID {
571        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
572    }
573
574    if usize::from(header.block_length) < CANCEL_ORDER_FIELDS_END {
575        return Err(SbeDecodeError::InvalidBlockLength {
576            expected: CANCEL_ORDER_FIELDS_END as u16,
577            actual: header.block_length,
578        });
579    }
580
581    cursor.require(header.block_length as usize)?;
582
583    let price_exponent = cursor.read_i8()?;
584    let qty_exponent = cursor.read_i8()?;
585    let order_id = cursor.read_i64_le()?;
586    let order_list_id = cursor.read_optional_i64_le()?;
587    let transact_time = cursor.read_i64_le()?;
588    let price_mantissa = cursor.read_i64_le()?;
589    let orig_qty_mantissa = cursor.read_i64_le()?;
590    let executed_qty_mantissa = cursor.read_i64_le()?;
591    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
592    let status = cursor.read_u8()?.into();
593    let time_in_force = cursor.read_u8()?.into();
594    let order_type = cursor.read_u8()?.into();
595    let side = cursor.read_u8()?.into();
596
597    cursor.advance(46)?; // Skip stop_price to working_floor
598    let self_trade_prevention_mode = cursor.read_u8()?.into();
599
600    cursor.advance(header.block_length as usize - CANCEL_ORDER_FIELDS_END)?;
601
602    let symbol = cursor.read_var_string8()?;
603    let orig_client_order_id = cursor.read_var_string8()?;
604    let client_order_id = cursor.read_var_string8()?;
605
606    Ok(BinanceCancelOrderResponse {
607        price_exponent,
608        qty_exponent,
609        order_id,
610        order_list_id,
611        transact_time,
612        price_mantissa,
613        orig_qty_mantissa,
614        executed_qty_mantissa,
615        cummulative_quote_qty_mantissa,
616        status,
617        time_in_force,
618        order_type,
619        side,
620        self_trade_prevention_mode,
621        client_order_id,
622        orig_client_order_id,
623        symbol,
624    })
625}
626
627fn decode_cancel_order_list(buf: &[u8]) -> Result<BinanceCancelOrderListResponse, SbeDecodeError> {
628    let mut cursor = SbeCursor::new(buf);
629    let header = MessageHeader::decode_cursor(&mut cursor)?;
630    header.validate()?;
631
632    if header.template_id != CANCEL_ORDER_LIST_TEMPLATE_ID {
633        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
634    }
635
636    if usize::from(header.block_length) < CANCEL_ORDER_LIST_FIELDS_END {
637        return Err(SbeDecodeError::InvalidBlockLength {
638            expected: CANCEL_ORDER_LIST_FIELDS_END as u16,
639            actual: header.block_length,
640        });
641    }
642
643    cursor.require(header.block_length as usize)?;
644    let order_list_id = cursor.read_i64_le()?;
645    let contingency_type = cursor.read_u8()?.into();
646    let list_status_type = cursor.read_u8()?.into();
647    let list_order_status = cursor.read_u8()?.into();
648    let transaction_time = cursor.read_i64_le()?;
649    let price_exponent = cursor.read_i8()?;
650    let qty_exponent = cursor.read_i8()?;
651    cursor.advance(usize::from(header.block_length) - CANCEL_ORDER_LIST_FIELDS_END)?;
652
653    let (order_block_length, order_count) = cursor.read_group_header_16()?;
654    if usize::from(order_block_length) < CANCEL_ORDER_LIST_ORDER_FIELDS_END {
655        return Err(SbeDecodeError::InvalidBlockLength {
656            expected: CANCEL_ORDER_LIST_ORDER_FIELDS_END as u16,
657            actual: order_block_length,
658        });
659    }
660
661    let mut orders = Vec::with_capacity(order_count as usize);
662    for _ in 0..order_count {
663        cursor.require(order_block_length as usize)?;
664        let order_id = cursor.read_i64_le()?;
665        cursor.advance(usize::from(order_block_length) - CANCEL_ORDER_LIST_ORDER_FIELDS_END)?;
666        let symbol = cursor.read_var_string8()?;
667        let client_order_id = cursor.read_var_string8()?;
668        orders.push(BinanceCancelOrderListOrder {
669            symbol,
670            order_id,
671            client_order_id,
672        });
673    }
674
675    let (report_block_length, report_count) = cursor.read_group_header_16()?;
676    let report_min_block_length = if header.version == 0 {
677        CANCEL_ORDER_LIST_REPORT_V0_BLOCK_LENGTH
678    } else {
679        CANCEL_ORDER_LIST_REPORT_V1_BLOCK_LENGTH
680    };
681
682    if report_block_length < report_min_block_length {
683        return Err(SbeDecodeError::InvalidBlockLength {
684            expected: report_min_block_length,
685            actual: report_block_length,
686        });
687    }
688
689    let mut order_reports = Vec::with_capacity(report_count as usize);
690
691    for _ in 0..report_count {
692        cursor.require(report_block_length as usize)?;
693        let order_id = cursor.read_i64_le()?;
694        let order_list_id = cursor.read_optional_i64_le()?;
695        let transact_time = cursor.read_i64_le()?;
696        let price_mantissa = cursor.read_i64_le()?;
697        let orig_qty_mantissa = cursor.read_i64_le()?;
698        let executed_qty_mantissa = cursor.read_i64_le()?;
699        let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
700        let status = cursor.read_u8()?.into();
701        let time_in_force = cursor.read_u8()?.into();
702        let order_type = cursor.read_u8()?.into();
703        let side = cursor.read_u8()?.into();
704        cursor.advance(46)?;
705        let self_trade_prevention_mode = cursor.read_u8()?.into();
706        cursor.advance(usize::from(report_block_length) - CANCEL_ORDER_LIST_REPORT_FIELDS_END)?;
707        let symbol = cursor.read_var_string8()?;
708        let orig_client_order_id = cursor.read_var_string8()?;
709        let client_order_id = cursor.read_var_string8()?;
710        order_reports.push(BinanceCancelOrderResponse {
711            price_exponent,
712            qty_exponent,
713            order_id,
714            order_list_id,
715            transact_time,
716            price_mantissa,
717            orig_qty_mantissa,
718            executed_qty_mantissa,
719            cummulative_quote_qty_mantissa,
720            status,
721            time_in_force,
722            order_type,
723            side,
724            self_trade_prevention_mode,
725            client_order_id,
726            orig_client_order_id,
727            symbol,
728        });
729    }
730
731    let list_client_order_id = cursor.read_var_string8()?;
732    let symbol = cursor.read_var_string8()?;
733
734    Ok(BinanceCancelOrderListResponse {
735        order_list_id,
736        contingency_type,
737        list_status_type,
738        list_order_status,
739        transaction_time,
740        list_client_order_id,
741        symbol,
742        orders,
743        order_reports,
744    })
745}
746
747/// Decode an order query response.
748///
749/// # Errors
750///
751/// Returns error if buffer is too short, schema mismatch, or decode error.
752#[allow(dead_code)]
753pub fn decode_order(buf: &[u8]) -> Result<BinanceOrderResponse, SbeDecodeError> {
754    let mut cursor = SbeCursor::new(buf);
755    let header = MessageHeader::decode_cursor(&mut cursor)?;
756    header.validate()?;
757
758    if header.template_id != ORDER_TEMPLATE_ID {
759        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
760    }
761
762    decode_order_block(&mut cursor, header.block_length)
763}
764
765/// Decode multiple orders response.
766///
767/// # Errors
768///
769/// Returns error if buffer is too short, schema mismatch, or decode error.
770#[allow(dead_code)]
771pub fn decode_orders(buf: &[u8]) -> Result<Vec<BinanceOrderResponse>, 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 != ORDERS_TEMPLATE_ID {
777        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
778    }
779
780    let (block_length, count) = cursor.read_group_header()?;
781    let mut orders = Vec::with_capacity(count as usize);
782
783    for _ in 0..count {
784        orders.push(decode_order_block(&mut cursor, block_length)?);
785    }
786
787    Ok(orders)
788}
789
790/// Decodes one order fixed block plus its trailing var-strings, shared by the
791/// `orderResponse` message and each `ordersResponse` group item.
792fn decode_order_block(
793    cursor: &mut SbeCursor<'_>,
794    block_length: u16,
795) -> Result<BinanceOrderResponse, SbeDecodeError> {
796    if block_length < ORDER_MIN_BLOCK_LENGTH {
797        return Err(SbeDecodeError::InvalidBlockLength {
798            expected: ORDER_MIN_BLOCK_LENGTH,
799            actual: block_length,
800        });
801    }
802
803    cursor.require(block_length as usize)?;
804
805    let price_exponent = cursor.read_i8()?;
806    let qty_exponent = cursor.read_i8()?;
807    let order_id = cursor.read_i64_le()?;
808    let order_list_id = cursor.read_optional_i64_le()?;
809    let price_mantissa = cursor.read_i64_le()?;
810    let orig_qty_mantissa = cursor.read_i64_le()?;
811    let executed_qty_mantissa = cursor.read_i64_le()?;
812    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
813    let status = cursor.read_u8()?.into();
814    let time_in_force = cursor.read_u8()?.into();
815    let order_type = cursor.read_u8()?.into();
816    let side = cursor.read_u8()?.into();
817    let stop_price_mantissa = cursor.read_optional_i64_le()?;
818
819    cursor.advance(16)?; // Skip trailing_delta + trailing_time
820    let iceberg_qty_mantissa = cursor.read_optional_i64_le()?;
821    let time = cursor.read_i64_le()?;
822    let update_time = cursor.read_i64_le()?;
823    let is_working = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
824    let working_time = cursor.read_optional_i64_le()?;
825    let orig_quote_order_qty_mantissa = cursor.read_i64_le()?;
826
827    cursor.advance(14)?; // Skip strategy_id to working_floor
828    let self_trade_prevention_mode = cursor.read_u8()?.into();
829
830    let expiry_reason = read_trailing_expiry_reason(
831        cursor,
832        block_length as usize,
833        ORDER_FIELDS_END,
834        ORDER_EXPIRY_REASON_OFFSET,
835    )?;
836
837    let symbol = cursor.read_var_string8()?;
838    let client_order_id = cursor.read_var_string8()?;
839
840    Ok(BinanceOrderResponse {
841        price_exponent,
842        qty_exponent,
843        order_id,
844        order_list_id,
845        price_mantissa,
846        orig_qty_mantissa,
847        executed_qty_mantissa,
848        cummulative_quote_qty_mantissa,
849        status,
850        time_in_force,
851        order_type,
852        side,
853        stop_price_mantissa,
854        iceberg_qty_mantissa,
855        time,
856        update_time,
857        is_working,
858        working_time,
859        orig_quote_order_qty_mantissa,
860        self_trade_prevention_mode,
861        client_order_id,
862        symbol,
863        expiry_reason,
864    })
865}
866
867/// Decode cancel open orders response.
868///
869/// Each item contains an embedded cancel-order or cancel-order-list SBE message.
870///
871/// # Errors
872///
873/// Returns error if buffer is too short, schema mismatch, or decode error.
874#[allow(dead_code)]
875pub fn decode_cancel_open_orders(
876    buf: &[u8],
877) -> Result<Vec<BinanceCancelOpenOrdersResponse>, SbeDecodeError> {
878    let mut cursor = SbeCursor::new(buf);
879    let header = MessageHeader::decode_cursor(&mut cursor)?;
880    header.validate()?;
881
882    if header.template_id != CANCEL_OPEN_ORDERS_TEMPLATE_ID {
883        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
884    }
885
886    if header.block_length != 0 {
887        return Err(SbeDecodeError::InvalidBlockLength {
888            expected: 0,
889            actual: header.block_length,
890        });
891    }
892
893    let (block_length, count) = cursor.read_group_header()?;
894    if block_length != 0 {
895        return Err(SbeDecodeError::InvalidBlockLength {
896            expected: 0,
897            actual: block_length,
898        });
899    }
900
901    if count == 0 {
902        return Ok(Vec::new());
903    }
904
905    let mut responses = Vec::with_capacity(count as usize);
906
907    // Each group item has block_length=0, followed by u16 length + embedded SBE message
908    for _ in 0..count {
909        let response_len = cursor.read_u16_le()? as usize;
910        let embedded_bytes = cursor.read_bytes(response_len)?;
911        let mut embedded_cursor = SbeCursor::new(embedded_bytes);
912        let embedded_header = MessageHeader::decode_cursor(&mut embedded_cursor)?;
913        embedded_header.validate()?;
914        let response = match embedded_header.template_id {
915            CANCEL_ORDER_TEMPLATE_ID => {
916                BinanceCancelOpenOrdersResponse::Order(decode_cancel_order(embedded_bytes)?)
917            }
918            CANCEL_ORDER_LIST_TEMPLATE_ID => BinanceCancelOpenOrdersResponse::OrderList(
919                decode_cancel_order_list(embedded_bytes)?,
920            ),
921            template_id => return Err(SbeDecodeError::UnknownTemplateId(template_id)),
922        };
923        responses.push(response);
924    }
925
926    Ok(responses)
927}
928
929/// Account response block length (from SBE codec).
930const ACCOUNT_BLOCK_LENGTH: usize = 64;
931
932/// Balance group item block length (from SBE codec).
933const BALANCE_BLOCK_LENGTH: u16 = 17;
934
935/// Decode account information response.
936///
937/// # Errors
938///
939/// Returns error if buffer is too short, schema mismatch, or decode error.
940#[allow(dead_code)]
941pub fn decode_account(buf: &[u8]) -> Result<BinanceAccountInfo, SbeDecodeError> {
942    let mut cursor = SbeCursor::new(buf);
943    let header = MessageHeader::decode_cursor(&mut cursor)?;
944    header.validate()?;
945
946    if header.template_id != ACCOUNT_TEMPLATE_ID {
947        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
948    }
949
950    cursor.require(ACCOUNT_BLOCK_LENGTH)?;
951
952    let commission_exponent = cursor.read_i8()?;
953    let maker_commission_mantissa = cursor.read_i64_le()?;
954    let taker_commission_mantissa = cursor.read_i64_le()?;
955    let buyer_commission_mantissa = cursor.read_i64_le()?;
956    let seller_commission_mantissa = cursor.read_i64_le()?;
957    let can_trade = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
958    let can_withdraw = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
959    let can_deposit = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
960    cursor.advance(1)?; // Skip brokered
961    let require_self_trade_prevention = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
962    let prevent_sor = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
963    let update_time = cursor.read_i64_le()?;
964    let account_type_enum = AccountType::from(cursor.read_u8()?);
965    cursor.advance(16)?; // Skip tradeGroupId + uid
966
967    let account_type = account_type_enum.to_string();
968
969    let (block_length, balance_count) = cursor.read_group_header()?;
970
971    if block_length != BALANCE_BLOCK_LENGTH {
972        return Err(SbeDecodeError::InvalidBlockLength {
973            expected: BALANCE_BLOCK_LENGTH,
974            actual: block_length,
975        });
976    }
977
978    let mut balances = Vec::with_capacity(balance_count as usize);
979
980    for _ in 0..balance_count {
981        cursor.require(block_length as usize)?;
982
983        let exponent = cursor.read_i8()?;
984        let free_mantissa = cursor.read_i64_le()?;
985        let locked_mantissa = cursor.read_i64_le()?;
986
987        let asset = cursor.read_var_string8()?;
988
989        balances.push(BinanceBalance {
990            asset,
991            free_mantissa,
992            locked_mantissa,
993            exponent,
994        });
995    }
996
997    Ok(BinanceAccountInfo {
998        commission_exponent,
999        maker_commission_mantissa,
1000        taker_commission_mantissa,
1001        buyer_commission_mantissa,
1002        seller_commission_mantissa,
1003        can_trade,
1004        can_withdraw,
1005        can_deposit,
1006        require_self_trade_prevention,
1007        prevent_sor,
1008        update_time,
1009        account_type,
1010        balances,
1011    })
1012}
1013
1014/// Account trade group item block length (from SBE codec).
1015const ACCOUNT_TRADE_BLOCK_LENGTH: u16 = 70;
1016
1017/// Decode account trades response.
1018///
1019/// # Errors
1020///
1021/// Returns error if buffer is too short, schema mismatch, or decode error.
1022#[allow(dead_code)]
1023pub fn decode_account_trades(buf: &[u8]) -> Result<Vec<BinanceAccountTrade>, SbeDecodeError> {
1024    let mut cursor = SbeCursor::new(buf);
1025    let header = MessageHeader::decode_cursor(&mut cursor)?;
1026    header.validate()?;
1027
1028    if header.template_id != ACCOUNT_TRADES_TEMPLATE_ID {
1029        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
1030    }
1031
1032    let (block_length, trade_count) = cursor.read_group_header()?;
1033
1034    if block_length != ACCOUNT_TRADE_BLOCK_LENGTH {
1035        return Err(SbeDecodeError::InvalidBlockLength {
1036            expected: ACCOUNT_TRADE_BLOCK_LENGTH,
1037            actual: block_length,
1038        });
1039    }
1040
1041    let mut trades = Vec::with_capacity(trade_count as usize);
1042
1043    for _ in 0..trade_count {
1044        cursor.require(block_length as usize)?;
1045
1046        let price_exponent = cursor.read_i8()?;
1047        let qty_exponent = cursor.read_i8()?;
1048        let commission_exponent = cursor.read_i8()?;
1049        let id = cursor.read_i64_le()?;
1050        let order_id = cursor.read_i64_le()?;
1051        let order_list_id = cursor.read_optional_i64_le()?;
1052        let price_mantissa = cursor.read_i64_le()?;
1053        let qty_mantissa = cursor.read_i64_le()?;
1054        let quote_qty_mantissa = cursor.read_i64_le()?;
1055        let commission_mantissa = cursor.read_i64_le()?;
1056        let time = cursor.read_i64_le()?;
1057        let is_buyer = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
1058        let is_maker = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
1059        let is_best_match = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
1060
1061        let symbol = cursor.read_var_string8()?;
1062        let commission_asset = cursor.read_var_string8()?;
1063
1064        trades.push(BinanceAccountTrade {
1065            price_exponent,
1066            qty_exponent,
1067            commission_exponent,
1068            id,
1069            order_id,
1070            order_list_id,
1071            price_mantissa,
1072            qty_mantissa,
1073            quote_qty_mantissa,
1074            commission_mantissa,
1075            time,
1076            is_buyer,
1077            is_maker,
1078            is_best_match,
1079            symbol,
1080            commission_asset,
1081        });
1082    }
1083
1084    Ok(trades)
1085}
1086
1087/// Fills group item block length (from SBE codec).
1088const FILLS_BLOCK_LENGTH: u16 = 42;
1089
1090/// Decode order fills using cursor.
1091fn decode_fills_cursor(
1092    cursor: &mut SbeCursor<'_>,
1093) -> Result<Vec<BinanceOrderFill>, SbeDecodeError> {
1094    let (block_length, count) = cursor.read_group_header()?;
1095
1096    if block_length != FILLS_BLOCK_LENGTH {
1097        return Err(SbeDecodeError::InvalidBlockLength {
1098            expected: FILLS_BLOCK_LENGTH,
1099            actual: block_length,
1100        });
1101    }
1102
1103    let mut fills = Vec::with_capacity(count as usize);
1104
1105    for _ in 0..count {
1106        cursor.require(block_length as usize)?;
1107
1108        let commission_exponent = cursor.read_i8()?;
1109        cursor.advance(1)?; // Skip matchType
1110        let price_mantissa = cursor.read_i64_le()?;
1111        let qty_mantissa = cursor.read_i64_le()?;
1112        let commission_mantissa = cursor.read_i64_le()?;
1113        let trade_id = cursor.read_optional_i64_le()?;
1114        cursor.advance(8)?; // Skip allocId
1115
1116        let commission_asset = cursor.read_var_string8()?;
1117
1118        fills.push(BinanceOrderFill {
1119            price_mantissa,
1120            qty_mantissa,
1121            commission_mantissa,
1122            commission_exponent,
1123            commission_asset,
1124            trade_id,
1125        });
1126    }
1127
1128    Ok(fills)
1129}
1130
1131/// Symbols group block length (from SBE codec).
1132const SYMBOL_BLOCK_LENGTH: usize = 19;
1133
1134/// Decode exchange info response.
1135///
1136/// ExchangeInfo response contains rate limits, exchange filters, symbols, and SOR info.
1137/// We only decode the symbols array which contains instrument definitions.
1138///
1139/// # Errors
1140///
1141/// Returns error if buffer is too short, schema mismatch, or template ID mismatch.
1142///
1143/// # Panics
1144///
1145/// This function will panic if filter byte slices cannot be converted to fixed-size arrays,
1146/// which should not occur if the SBE data is well-formed.
1147pub fn decode_exchange_info(buf: &[u8]) -> Result<BinanceExchangeInfoSbe, SbeDecodeError> {
1148    let mut cursor = SbeCursor::new(buf);
1149    let header = MessageHeader::decode_cursor(&mut cursor)?;
1150    header.validate()?;
1151
1152    if header.template_id != EXCHANGE_INFO_TEMPLATE_ID {
1153        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
1154    }
1155
1156    // Skip rate_limits group
1157    let (rate_limits_block_len, rate_limits_count) = cursor.read_group_header()?;
1158    cursor.advance(rate_limits_block_len as usize * rate_limits_count as usize)?;
1159
1160    // Skip exchange_filters group
1161    let (_exchange_filters_block_len, exchange_filters_count) = cursor.read_group_header()?;
1162    for _ in 0..exchange_filters_count {
1163        // Each filter is a varString8
1164        cursor.read_var_string8()?;
1165    }
1166
1167    // Decode symbols group
1168    let (symbols_block_len, symbols_count) = cursor.read_group_header()?;
1169
1170    if symbols_block_len != SYMBOL_BLOCK_LENGTH as u16 {
1171        return Err(SbeDecodeError::InvalidBlockLength {
1172            expected: SYMBOL_BLOCK_LENGTH as u16,
1173            actual: symbols_block_len,
1174        });
1175    }
1176
1177    let mut symbols = Vec::with_capacity(symbols_count as usize);
1178
1179    for _ in 0..symbols_count {
1180        cursor.require(SYMBOL_BLOCK_LENGTH)?;
1181
1182        // Fixed fields (19 bytes)
1183        let status = cursor.read_u8()?;
1184        let base_asset_precision = cursor.read_u8()?;
1185        let quote_asset_precision = cursor.read_u8()?;
1186        let _base_commission_precision = cursor.read_u8()?;
1187        let _quote_commission_precision = cursor.read_u8()?;
1188        let order_types = cursor.read_u16_le()?;
1189        let iceberg_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1190        let oco_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1191        let oto_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1192        let quote_order_qty_market_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1193        let allow_trailing_stop = cursor.read_u8()? == BoolEnum::True as u8;
1194        let cancel_replace_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1195        let amend_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1196        let is_spot_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1197        let is_margin_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
1198        let _default_self_trade_prevention_mode = cursor.read_u8()?;
1199        let _allowed_self_trade_prevention_modes = cursor.read_u8()?;
1200        let _peg_instructions_allowed = cursor.read_u8()?;
1201
1202        let (_filters_block_len, filters_count) = cursor.read_group_header()?;
1203        let mut filters = BinanceSymbolFiltersSbe::default();
1204
1205        for _ in 0..filters_count {
1206            let filter_bytes = cursor.read_var_bytes8()?;
1207
1208            // Filters can have header (8 bytes) or be raw body only,
1209            // detect format by checking if bytes [2..4] contain a valid template_id
1210            let (template_id, offset) = if filter_bytes.len() >= HEADER_LENGTH + 2 {
1211                let potential_template = u16::from_le_bytes([filter_bytes[2], filter_bytes[3]]);
1212                if potential_template == PRICE_FILTER_TEMPLATE_ID
1213                    || potential_template == LOT_SIZE_FILTER_TEMPLATE_ID
1214                    || potential_template == MIN_NOTIONAL_FILTER_TEMPLATE_ID
1215                    || potential_template == NOTIONAL_FILTER_TEMPLATE_ID
1216                {
1217                    (potential_template, HEADER_LENGTH)
1218                } else {
1219                    let raw_template = u16::from_le_bytes([filter_bytes[0], filter_bytes[1]]);
1220                    (raw_template, 2)
1221                }
1222            } else if filter_bytes.len() >= 2 {
1223                let raw_template = u16::from_le_bytes([filter_bytes[0], filter_bytes[1]]);
1224                (raw_template, 2)
1225            } else {
1226                continue;
1227            };
1228
1229            // Filter body layout: exponent(1) + min(8) + max(8) + size(8) = 25 bytes
1230            match template_id {
1231                MIN_NOTIONAL_FILTER_TEMPLATE_ID | NOTIONAL_FILTER_TEMPLATE_ID => {
1232                    let mut filter = SbeCursor::new(&filter_bytes[offset..]);
1233                    let exponent = filter.read_i8()?;
1234                    let min = decode_notional_amount(&mut filter, exponent)?;
1235                    let apply_min_to_market = decode_notional_market_flag(&mut filter)?;
1236                    let (max, apply_max_to_market) = if template_id == NOTIONAL_FILTER_TEMPLATE_ID {
1237                        (
1238                            Some(decode_notional_amount(&mut filter, exponent)?),
1239                            decode_notional_market_flag(&mut filter)?,
1240                        )
1241                    } else {
1242                        (None, false)
1243                    };
1244
1245                    let avg_price_mins = u32::try_from(filter.read_i32_le()?).map_err(|_| {
1246                        SbeDecodeError::InvalidValue {
1247                            field: "avgPriceMins",
1248                        }
1249                    })?;
1250
1251                    filters.notional_filters.push(BinanceNotionalFilter {
1252                        min,
1253                        max,
1254                        apply_min_to_market,
1255                        apply_max_to_market,
1256                        avg_price_mins,
1257                    });
1258                }
1259                PRICE_FILTER_TEMPLATE_ID if filter_bytes.len() >= offset + 25 => {
1260                    let price_exp = filter_bytes[offset] as i8;
1261                    let min_price = i64::from_le_bytes(
1262                        filter_bytes[offset + 1..offset + 9].try_into().unwrap(),
1263                    );
1264                    let max_price = i64::from_le_bytes(
1265                        filter_bytes[offset + 9..offset + 17].try_into().unwrap(),
1266                    );
1267                    let tick_size = i64::from_le_bytes(
1268                        filter_bytes[offset + 17..offset + 25].try_into().unwrap(),
1269                    );
1270                    filters.price_filter = Some(BinancePriceFilterSbe {
1271                        price_exponent: price_exp,
1272                        min_price,
1273                        max_price,
1274                        tick_size,
1275                    });
1276                }
1277                LOT_SIZE_FILTER_TEMPLATE_ID if filter_bytes.len() >= offset + 25 => {
1278                    let qty_exp = filter_bytes[offset] as i8;
1279                    let min_qty = i64::from_le_bytes(
1280                        filter_bytes[offset + 1..offset + 9].try_into().unwrap(),
1281                    );
1282                    let max_qty = i64::from_le_bytes(
1283                        filter_bytes[offset + 9..offset + 17].try_into().unwrap(),
1284                    );
1285                    let step_size = i64::from_le_bytes(
1286                        filter_bytes[offset + 17..offset + 25].try_into().unwrap(),
1287                    );
1288                    filters.lot_size_filter = Some(BinanceLotSizeFilterSbe {
1289                        qty_exponent: qty_exp,
1290                        min_qty,
1291                        max_qty,
1292                        step_size,
1293                    });
1294                }
1295                _ => {}
1296            }
1297        }
1298
1299        // Permission sets nested group
1300        let (_perm_sets_block_len, perm_sets_count) = cursor.read_group_header()?;
1301        let mut permissions = Vec::with_capacity(perm_sets_count as usize);
1302        for _ in 0..perm_sets_count {
1303            // Permissions nested group
1304            let (_perms_block_len, perms_count) = cursor.read_group_header()?;
1305            let mut perm_set = Vec::with_capacity(perms_count as usize);
1306            for _ in 0..perms_count {
1307                let perm = cursor.read_var_string8()?;
1308                perm_set.push(perm);
1309            }
1310            permissions.push(perm_set);
1311        }
1312
1313        // Variable-length strings
1314        let symbol = cursor.read_var_string8()?;
1315        let base_asset = cursor.read_var_string8()?;
1316        let quote_asset = cursor.read_var_string8()?;
1317
1318        symbols.push(BinanceSymbolSbe {
1319            symbol,
1320            base_asset,
1321            quote_asset,
1322            base_asset_precision,
1323            quote_asset_precision,
1324            status,
1325            order_types,
1326            iceberg_allowed,
1327            oco_allowed,
1328            oto_allowed,
1329            quote_order_qty_market_allowed,
1330            allow_trailing_stop,
1331            cancel_replace_allowed,
1332            amend_allowed,
1333            is_spot_trading_allowed,
1334            is_margin_trading_allowed,
1335            filters,
1336            permissions,
1337        });
1338    }
1339
1340    // Skip SOR group (we don't need it)
1341
1342    Ok(BinanceExchangeInfoSbe { symbols })
1343}
1344
1345fn decode_notional_amount(
1346    cursor: &mut SbeCursor<'_>,
1347    exponent: i8,
1348) -> Result<Decimal, SbeDecodeError> {
1349    let mantissa = cursor.read_i64_le()?;
1350    if exponent == i8::MIN || mantissa < 0 {
1351        return Err(SbeDecodeError::InvalidValue { field: "notional" });
1352    }
1353    Decimal::from_scientific(&format!("{mantissa}e{exponent}"))
1354        .map_err(|_| SbeDecodeError::InvalidValue { field: "notional" })
1355}
1356
1357fn decode_notional_market_flag(cursor: &mut SbeCursor<'_>) -> Result<bool, SbeDecodeError> {
1358    match cursor.read_u8()? {
1359        value if value == BoolEnum::False as u8 => Ok(false),
1360        value if value == BoolEnum::True as u8 => Ok(true),
1361        _ => Err(SbeDecodeError::InvalidValue {
1362            field: "notional market flag",
1363        }),
1364    }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369    use rstest::rstest;
1370
1371    use super::*;
1372    use crate::spot::sbe::spot::{
1373        SBE_SCHEMA_VERSION, WriteBuf,
1374        cancel_order_response_codec::CancelOrderResponseEncoder,
1375        expiry_reason::ExpiryReason,
1376        floor::Floor,
1377        match_type::MatchType,
1378        new_order_full_response_codec::{
1379            NewOrderFullResponseEncoder,
1380            encoder::{FillsEncoder, PreventedMatchesEncoder},
1381        },
1382        order_capacity::OrderCapacity,
1383        order_response_codec::OrderResponseEncoder,
1384        order_side::OrderSide,
1385        order_status::OrderStatus,
1386        order_type::OrderType,
1387        orders_response_codec::{OrdersResponseEncoder, encoder::OrdersEncoder},
1388        self_trade_prevention_mode::SelfTradePreventionMode,
1389        time_in_force::TimeInForce,
1390    };
1391
1392    fn create_header(block_length: u16, template_id: u16, schema_id: u16, version: u16) -> [u8; 8] {
1393        let mut buf = [0u8; 8];
1394        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
1395        buf[2..4].copy_from_slice(&template_id.to_le_bytes());
1396        buf[4..6].copy_from_slice(&schema_id.to_le_bytes());
1397        buf[6..8].copy_from_slice(&version.to_le_bytes());
1398        buf
1399    }
1400
1401    #[rstest]
1402    fn test_decode_ping_valid() {
1403        // Ping: block_length=0, template_id=101, schema_id=3, version=1
1404        let buf = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1405        decode_ping(&buf).unwrap();
1406    }
1407
1408    #[rstest]
1409    fn test_decode_ping_buffer_too_short() {
1410        let buf = [0u8; 4];
1411        let err = decode_ping(&buf).unwrap_err();
1412        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1413    }
1414
1415    #[rstest]
1416    fn test_decode_ping_schema_mismatch() {
1417        let buf = create_header(0, PING_TEMPLATE_ID, 99, SBE_SCHEMA_VERSION);
1418        let err = decode_ping(&buf).unwrap_err();
1419        assert!(matches!(err, SbeDecodeError::SchemaMismatch { .. }));
1420    }
1421
1422    #[rstest]
1423    fn test_decode_ping_wrong_template() {
1424        let buf = create_header(0, 999, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1425        let err = decode_ping(&buf).unwrap_err();
1426        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(999)));
1427    }
1428
1429    #[rstest]
1430    fn test_decode_server_time_valid() {
1431        // ServerTime: block_length=8, template_id=102, schema_id=3, version=1
1432        let header = create_header(
1433            8,
1434            SERVER_TIME_TEMPLATE_ID,
1435            SBE_SCHEMA_ID,
1436            SBE_SCHEMA_VERSION,
1437        );
1438        let timestamp: i64 = 1734300000000; // Example timestamp
1439
1440        let mut buf = Vec::with_capacity(16);
1441        buf.extend_from_slice(&header);
1442        buf.extend_from_slice(&timestamp.to_le_bytes());
1443
1444        let result = decode_server_time(&buf).unwrap();
1445        assert_eq!(result, timestamp);
1446    }
1447
1448    #[rstest]
1449    fn test_decode_server_time_buffer_too_short() {
1450        // Header only, missing body
1451        let buf = create_header(
1452            8,
1453            SERVER_TIME_TEMPLATE_ID,
1454            SBE_SCHEMA_ID,
1455            SBE_SCHEMA_VERSION,
1456        );
1457        let err = decode_server_time(&buf).unwrap_err();
1458        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1459    }
1460
1461    #[rstest]
1462    fn test_decode_server_time_wrong_template() {
1463        let header = create_header(8, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1464        let mut buf = Vec::with_capacity(16);
1465        buf.extend_from_slice(&header);
1466        buf.extend_from_slice(&0i64.to_le_bytes());
1467
1468        let err = decode_server_time(&buf).unwrap_err();
1469        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1470    }
1471
1472    // Any version within the schema ID must decode. During a rollout one client sees
1473    // both the older version (un-migrated server) and the newer version; future
1474    // additive bumps must keep decoding too.
1475    #[rstest]
1476    #[case(SBE_SCHEMA_VERSION - 1)]
1477    #[case(SBE_SCHEMA_VERSION)]
1478    #[case(SBE_SCHEMA_VERSION + 1)]
1479    #[case(99)]
1480    fn test_decode_server_time_accepts_any_version(#[case] version: u16) {
1481        let header = create_header(8, SERVER_TIME_TEMPLATE_ID, SBE_SCHEMA_ID, version);
1482        let mut buf = Vec::with_capacity(16);
1483        buf.extend_from_slice(&header);
1484        buf.extend_from_slice(&1_700_000_000_000i64.to_le_bytes());
1485
1486        let result = decode_server_time(&buf).unwrap();
1487        assert_eq!(result, 1_700_000_000_000);
1488    }
1489
1490    fn create_group_header(block_length: u16, count: u32) -> [u8; 6] {
1491        let mut buf = [0u8; 6];
1492        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
1493        buf[2..6].copy_from_slice(&count.to_le_bytes());
1494        buf
1495    }
1496
1497    #[rstest]
1498    fn test_decode_depth_valid() {
1499        // Depth: block_length=10, template_id=200
1500        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1501
1502        let mut buf = Vec::new();
1503        buf.extend_from_slice(&header);
1504
1505        // Block: last_update_id (8) + price_exponent (1) + qty_exponent (1)
1506        let last_update_id: i64 = 123456789;
1507        let price_exponent: i8 = -8;
1508        let qty_exponent: i8 = -8;
1509        buf.extend_from_slice(&last_update_id.to_le_bytes());
1510        buf.push(price_exponent as u8);
1511        buf.push(qty_exponent as u8);
1512
1513        // Bids group: 2 levels
1514        buf.extend_from_slice(&create_group_header(16, 2));
1515        // Bid 1: price=100000000000, qty=50000000
1516        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes());
1517        buf.extend_from_slice(&50_000_000i64.to_le_bytes());
1518        // Bid 2: price=99900000000, qty=30000000
1519        buf.extend_from_slice(&99_900_000_000i64.to_le_bytes());
1520        buf.extend_from_slice(&30_000_000i64.to_le_bytes());
1521
1522        // Asks group: 1 level
1523        buf.extend_from_slice(&create_group_header(16, 1));
1524        // Ask 1: price=100100000000, qty=25000000
1525        buf.extend_from_slice(&100_100_000_000i64.to_le_bytes());
1526        buf.extend_from_slice(&25_000_000i64.to_le_bytes());
1527
1528        let depth = decode_depth(&buf).unwrap();
1529
1530        assert_eq!(depth.last_update_id, 123456789);
1531        assert_eq!(depth.price_exponent, -8);
1532        assert_eq!(depth.qty_exponent, -8);
1533        assert_eq!(depth.bids.len(), 2);
1534        assert_eq!(depth.asks.len(), 1);
1535        assert_eq!(depth.bids[0].price_mantissa, 100_000_000_000);
1536        assert_eq!(depth.bids[0].qty_mantissa, 50_000_000);
1537        assert_eq!(depth.asks[0].price_mantissa, 100_100_000_000);
1538    }
1539
1540    #[rstest]
1541    fn test_decode_depth_empty_book() {
1542        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1543
1544        let mut buf = Vec::new();
1545        buf.extend_from_slice(&header);
1546        buf.extend_from_slice(&0i64.to_le_bytes()); // last_update_id
1547        buf.push(0); // price_exponent
1548        buf.push(0); // qty_exponent
1549
1550        // Empty bids
1551        buf.extend_from_slice(&create_group_header(16, 0));
1552        // Empty asks
1553        buf.extend_from_slice(&create_group_header(16, 0));
1554
1555        let depth = decode_depth(&buf).unwrap();
1556
1557        assert!(depth.bids.is_empty());
1558        assert!(depth.asks.is_empty());
1559    }
1560
1561    #[rstest]
1562    fn test_decode_trades_valid() {
1563        // Trades: block_length=2, template_id=201
1564        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1565
1566        let mut buf = Vec::new();
1567        buf.extend_from_slice(&header);
1568
1569        // Block: price_exponent (1) + qty_exponent (1)
1570        let price_exponent: i8 = -8;
1571        let qty_exponent: i8 = -8;
1572        buf.push(price_exponent as u8);
1573        buf.push(qty_exponent as u8);
1574
1575        // Trades group: 1 trade (42 bytes each)
1576        buf.extend_from_slice(&create_group_header(42, 1));
1577
1578        // Trade: id(8) + price(8) + qty(8) + quoteQty(8) + time(8) + isBuyerMaker(1) + isBestMatch(1)
1579        let trade_id: i64 = 999;
1580        let price: i64 = 100_000_000_000;
1581        let qty: i64 = 10_000_000;
1582        let quote_qty: i64 = 1_000_000_000_000;
1583        let time: i64 = 1734300000000;
1584        let is_buyer_maker: u8 = 1; // true
1585        let is_best_match: u8 = 1; // true
1586
1587        buf.extend_from_slice(&trade_id.to_le_bytes());
1588        buf.extend_from_slice(&price.to_le_bytes());
1589        buf.extend_from_slice(&qty.to_le_bytes());
1590        buf.extend_from_slice(&quote_qty.to_le_bytes());
1591        buf.extend_from_slice(&time.to_le_bytes());
1592        buf.push(is_buyer_maker);
1593        buf.push(is_best_match);
1594
1595        let trades = decode_trades(&buf).unwrap();
1596
1597        assert_eq!(trades.price_exponent, -8);
1598        assert_eq!(trades.qty_exponent, -8);
1599        assert_eq!(trades.trades.len(), 1);
1600        assert_eq!(trades.trades[0].id, 999);
1601        assert_eq!(trades.trades[0].price_mantissa, 100_000_000_000);
1602        assert!(trades.trades[0].is_buyer_maker);
1603        assert!(trades.trades[0].is_best_match);
1604    }
1605
1606    #[rstest]
1607    fn test_decode_trades_empty() {
1608        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1609
1610        let mut buf = Vec::new();
1611        buf.extend_from_slice(&header);
1612        buf.push(0); // price_exponent
1613        buf.push(0); // qty_exponent
1614
1615        // Empty trades group
1616        buf.extend_from_slice(&create_group_header(42, 0));
1617
1618        let trades = decode_trades(&buf).unwrap();
1619
1620        assert!(trades.trades.is_empty());
1621    }
1622
1623    #[rstest]
1624    fn test_decode_agg_trades_preserves_all_fields() {
1625        let header = create_header(2, AGG_TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1626        let mut buf = Vec::new();
1627        buf.extend_from_slice(&header);
1628        buf.push((-7_i8) as u8);
1629        buf.push((-5_i8) as u8);
1630        buf.extend_from_slice(&create_group_header(50, 1));
1631        buf.extend_from_slice(&101_i64.to_le_bytes());
1632        buf.extend_from_slice(&123_456_789_i64.to_le_bytes());
1633        buf.extend_from_slice(&765_432_i64.to_le_bytes());
1634        buf.extend_from_slice(&201_i64.to_le_bytes());
1635        buf.extend_from_slice(&207_i64.to_le_bytes());
1636        buf.extend_from_slice(&1_700_000_000_123_i64.to_le_bytes());
1637        buf.push(1);
1638        buf.push(0);
1639
1640        let trades = decode_agg_trades(&buf).unwrap();
1641
1642        assert_eq!(trades.price_exponent, -7);
1643        assert_eq!(trades.qty_exponent, -5);
1644        assert_eq!(trades.trades.len(), 1);
1645        assert_eq!(trades.trades[0].id, 101);
1646        assert_eq!(trades.trades[0].price_mantissa, 123_456_789);
1647        assert_eq!(trades.trades[0].qty_mantissa, 765_432);
1648        assert_eq!(trades.trades[0].first_trade_id, 201);
1649        assert_eq!(trades.trades[0].last_trade_id, 207);
1650        assert_eq!(trades.trades[0].time, 1_700_000_000_123);
1651        assert!(trades.trades[0].is_buyer_maker);
1652        assert!(!trades.trades[0].is_best_match);
1653    }
1654
1655    #[rstest]
1656    fn test_decode_agg_trades_rejects_wrong_template() {
1657        let header = create_header(2, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1658        let mut buf = Vec::new();
1659        buf.extend_from_slice(&header);
1660        buf.extend_from_slice(&[0_u8; 2]);
1661
1662        let error = decode_agg_trades(&buf).unwrap_err();
1663
1664        assert!(matches!(error, SbeDecodeError::UnknownTemplateId(101)));
1665    }
1666
1667    #[rstest]
1668    fn test_decode_depth_wrong_template() {
1669        let header = create_header(10, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1670
1671        let mut buf = Vec::new();
1672        buf.extend_from_slice(&header);
1673        buf.extend_from_slice(&[0u8; 10]); // dummy block
1674
1675        let err = decode_depth(&buf).unwrap_err();
1676        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1677    }
1678
1679    #[rstest]
1680    fn test_decode_trades_wrong_template() {
1681        let header = create_header(2, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1682
1683        let mut buf = Vec::new();
1684        buf.extend_from_slice(&header);
1685        buf.extend_from_slice(&[0u8; 2]); // dummy block
1686
1687        let err = decode_trades(&buf).unwrap_err();
1688        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1689    }
1690
1691    fn write_var_string(buf: &mut Vec<u8>, s: &str) {
1692        buf.push(s.len() as u8);
1693        buf.extend_from_slice(s.as_bytes());
1694    }
1695
1696    fn resize_fixed_block(buf: &mut Vec<u8>, block_length: usize) {
1697        let current = usize::from(u16::from_le_bytes([buf[0], buf[1]]));
1698        let block_end = HEADER_LENGTH + current;
1699        let new_end = HEADER_LENGTH + block_length;
1700
1701        if new_end < block_end {
1702            buf.drain(new_end..block_end);
1703        } else {
1704            buf.splice(block_end..block_end, vec![0u8; new_end - block_end]);
1705        }
1706
1707        buf[0..2].copy_from_slice(&(block_length as u16).to_le_bytes());
1708    }
1709
1710    fn build_order_response_buffer(expiry_reason: ExpiryReason) -> Vec<u8> {
1711        let mut bytes = vec![0u8; 256];
1712        let enc = OrderResponseEncoder::default().wrap(WriteBuf::new(&mut bytes), HEADER_LENGTH);
1713        let mut header = enc.header(0);
1714        let mut enc = header.parent().unwrap();
1715        enc.price_exponent(-2);
1716        enc.qty_exponent(-4);
1717        enc.order_id(12345);
1718        enc.order_list_id(99);
1719        enc.price(12_345);
1720        enc.orig_qty(25_000);
1721        enc.executed_qty(10_000);
1722        enc.cummulative_quote_qty(123_450_000);
1723        enc.status(OrderStatus::PartiallyFilled);
1724        enc.time_in_force(TimeInForce::Gtc);
1725        enc.order_type(OrderType::StopLossLimit);
1726        enc.side(OrderSide::Sell);
1727        enc.stop_price(12_000);
1728        enc.trailing_delta(100);
1729        enc.trailing_time(1_700_000_000_000_100);
1730        enc.iceberg_qty(5_000);
1731        enc.time(1_700_000_000_000_000);
1732        enc.update_time(1_700_000_000_000_200);
1733        enc.is_working(BoolEnum::True);
1734        enc.working_time(1_700_000_000_000_300);
1735        enc.orig_quote_order_qty(7_000);
1736        enc.strategy_id(42);
1737        enc.strategy_type(7);
1738        enc.order_capacity(OrderCapacity::Principal);
1739        enc.working_floor(Floor::Sor);
1740        enc.self_trade_prevention_mode(SelfTradePreventionMode::ExpireMaker);
1741        enc.prevented_match_id(77);
1742        enc.prevented_quantity(3);
1743        enc.used_sor(BoolEnum::True);
1744        enc.expiry_reason(expiry_reason);
1745        enc.symbol("BTCUSDT");
1746        enc.client_order_id("my-order-123");
1747        let len = HEADER_LENGTH + enc.encoded_length();
1748        bytes.truncate(len);
1749        bytes
1750    }
1751
1752    fn expected_order_response(expiry_reason: Option<u8>) -> BinanceOrderResponse {
1753        BinanceOrderResponse {
1754            price_exponent: -2,
1755            qty_exponent: -4,
1756            order_id: 12345,
1757            order_list_id: Some(99),
1758            price_mantissa: 12_345,
1759            orig_qty_mantissa: 25_000,
1760            executed_qty_mantissa: 10_000,
1761            cummulative_quote_qty_mantissa: 123_450_000,
1762            status: OrderStatus::PartiallyFilled,
1763            time_in_force: TimeInForce::Gtc,
1764            order_type: OrderType::StopLossLimit,
1765            side: OrderSide::Sell,
1766            stop_price_mantissa: Some(12_000),
1767            iceberg_qty_mantissa: Some(5_000),
1768            time: 1_700_000_000_000_000,
1769            update_time: 1_700_000_000_000_200,
1770            is_working: true,
1771            working_time: Some(1_700_000_000_000_300),
1772            orig_quote_order_qty_mantissa: 7_000,
1773            self_trade_prevention_mode: SelfTradePreventionMode::ExpireMaker,
1774            client_order_id: "my-order-123".to_string(),
1775            symbol: "BTCUSDT".to_string(),
1776            expiry_reason,
1777        }
1778    }
1779
1780    #[rstest]
1781    #[case::pre_v4_no_expiry_reason(162, ExpiryReason::NullVal, None)]
1782    #[case::v4_null_sentinel(163, ExpiryReason::NullVal, None)]
1783    #[case::v4_captures_value(163, ExpiryReason::UnfilledIocQuantityExpired, Some(0x05))]
1784    #[case::future_block_length(167, ExpiryReason::UnfilledIocQuantityExpired, Some(0x05))]
1785    fn test_decode_order_matches_codec(
1786        #[case] block_length: usize,
1787        #[case] expiry_reason: ExpiryReason,
1788        #[case] expected: Option<u8>,
1789    ) {
1790        let mut buf = build_order_response_buffer(expiry_reason);
1791        resize_fixed_block(&mut buf, block_length);
1792
1793        let order = decode_order(&buf).unwrap();
1794
1795        assert_eq!(order, expected_order_response(expected));
1796    }
1797
1798    #[rstest]
1799    fn test_decode_order_rejects_short_block() {
1800        let mut buf = build_order_response_buffer(ExpiryReason::NullVal);
1801        resize_fixed_block(&mut buf, 161);
1802
1803        let error = decode_order(&buf).unwrap_err();
1804
1805        assert_eq!(
1806            error,
1807            SbeDecodeError::InvalidBlockLength {
1808                expected: 162,
1809                actual: 161,
1810            }
1811        );
1812    }
1813
1814    fn build_orders_response_buffer(orders: &[(i64, &str)]) -> Vec<u8> {
1815        let mut bytes = vec![0u8; 512];
1816        let enc = OrdersResponseEncoder::default().wrap(WriteBuf::new(&mut bytes), HEADER_LENGTH);
1817        let mut header = enc.header(0);
1818        let enc = header.parent().unwrap();
1819        let mut group = enc.orders_encoder(orders.len() as u32, OrdersEncoder::default());
1820
1821        for (order_id, client_order_id) in orders {
1822            group.advance().unwrap();
1823            group.order_id(*order_id);
1824            group.iceberg_qty(5_000);
1825            group.time(order_id * 10);
1826            group.update_time(order_id * 10 + 1);
1827            group.is_working(BoolEnum::True);
1828            group.working_time(order_id * 10 + 2);
1829            group.self_trade_prevention_mode(SelfTradePreventionMode::ExpireTaker);
1830            group.expiry_reason(ExpiryReason::NullVal);
1831            group.symbol("BTCUSDT");
1832            group.client_order_id(client_order_id);
1833        }
1834
1835        let enc = group.parent().unwrap();
1836        let len = HEADER_LENGTH + enc.encoded_length();
1837        bytes.truncate(len);
1838        bytes
1839    }
1840
1841    #[rstest]
1842    fn test_decode_orders_matches_codec() {
1843        let expected = [(111, "order-1"), (222, "order-2")];
1844        let buf = build_orders_response_buffer(&expected);
1845
1846        let orders = decode_orders(&buf).unwrap();
1847
1848        assert_eq!(orders.len(), 2);
1849
1850        for (order, (order_id, client_order_id)) in orders.iter().zip(expected) {
1851            assert_eq!(order.order_id, order_id);
1852            assert_eq!(order.iceberg_qty_mantissa, Some(5_000));
1853            assert_eq!(order.time, order_id * 10);
1854            assert_eq!(order.update_time, order_id * 10 + 1);
1855            assert!(order.is_working);
1856            assert_eq!(order.working_time, Some(order_id * 10 + 2));
1857            assert_eq!(
1858                order.self_trade_prevention_mode,
1859                SelfTradePreventionMode::ExpireTaker
1860            );
1861            assert_eq!(order.expiry_reason, None);
1862            assert_eq!(order.symbol, "BTCUSDT");
1863            assert_eq!(order.client_order_id, client_order_id);
1864        }
1865    }
1866
1867    #[rstest]
1868    fn test_decode_orders_multiple() {
1869        // This test verifies cursor advances correctly through multiple orders
1870        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1871
1872        let mut buf = Vec::new();
1873        buf.extend_from_slice(&header);
1874
1875        // Group header: block_length=162, count=2
1876        buf.extend_from_slice(&create_group_header(ORDER_MIN_BLOCK_LENGTH, 2));
1877
1878        // Order 1
1879        let order1_start = buf.len();
1880        buf.push((-8i8) as u8); // price_exponent
1881        buf.push((-8i8) as u8); // qty_exponent
1882        buf.extend_from_slice(&1001i64.to_le_bytes()); // order_id
1883        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1884        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1885        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1886        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1887        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1888        buf.push(1); // status
1889        buf.push(1); // time_in_force
1890        buf.push(1); // order_type
1891        buf.push(1); // side
1892        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1893        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1894        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1895        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1896        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
1897        buf.push(1); // is_working
1898        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // working_time
1899        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1900
1901        // Pad to 162 bytes from order start
1902        while buf.len() - order1_start < ORDER_MIN_BLOCK_LENGTH as usize {
1903            buf.push(0);
1904        }
1905        write_var_string(&mut buf, "BTCUSDT");
1906        write_var_string(&mut buf, "order-1");
1907
1908        // Order 2
1909        let order2_start = buf.len();
1910        buf.push((-8i8) as u8); // price_exponent
1911        buf.push((-8i8) as u8); // qty_exponent
1912        buf.extend_from_slice(&2002i64.to_le_bytes()); // order_id
1913        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1914        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes()); // price_mantissa
1915        buf.extend_from_slice(&20_000_000i64.to_le_bytes()); // orig_qty
1916        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1917        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1918        buf.push(1); // status
1919        buf.push(1); // time_in_force
1920        buf.push(1); // order_type
1921        buf.push(2); // side (SELL)
1922        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1923        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1924        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1925        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // time
1926        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1927        buf.push(1); // is_working
1928        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // working_time
1929        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1930
1931        while buf.len() - order2_start < ORDER_MIN_BLOCK_LENGTH as usize {
1932            buf.push(0);
1933        }
1934        write_var_string(&mut buf, "ETHUSDT");
1935        write_var_string(&mut buf, "order-2");
1936
1937        let orders = decode_orders(&buf).unwrap();
1938
1939        assert_eq!(orders.len(), 2);
1940        assert_eq!(orders[0].order_id, 1001);
1941        assert_eq!(orders[0].symbol, "BTCUSDT");
1942        assert_eq!(orders[0].client_order_id, "order-1");
1943        assert_eq!(orders[0].price_mantissa, 100_000_000_000);
1944
1945        assert_eq!(orders[1].order_id, 2002);
1946        assert_eq!(orders[1].symbol, "ETHUSDT");
1947        assert_eq!(orders[1].client_order_id, "order-2");
1948        assert_eq!(orders[1].price_mantissa, 200_000_000_000);
1949    }
1950
1951    #[rstest]
1952    fn test_decode_orders_v4_trailing_expiry_reason() {
1953        // Schema 3:4 appends a 1-byte expiryReason to the orders group fixed block,
1954        // bumping its length 162 -> 163. The decoder must read it via the runtime
1955        // group block_length so the symbol var-string starts at the right offset.
1956        const V4_BLOCK_LENGTH: u16 = 163;
1957        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1958
1959        let mut buf = Vec::new();
1960        buf.extend_from_slice(&header);
1961        buf.extend_from_slice(&create_group_header(V4_BLOCK_LENGTH, 1));
1962
1963        let order_start = buf.len();
1964        buf.extend_from_slice(&[0u8; ORDER_MIN_BLOCK_LENGTH as usize]);
1965        buf.push(0xFF); // Sentinel for the new expiryReason byte (null/absent)
1966        assert_eq!(buf.len() - order_start, V4_BLOCK_LENGTH as usize);
1967
1968        write_var_string(&mut buf, "BTCUSDT");
1969        write_var_string(&mut buf, "v4-order");
1970
1971        let orders = decode_orders(&buf).unwrap();
1972
1973        assert_eq!(orders.len(), 1);
1974        assert_eq!(orders[0].symbol, "BTCUSDT");
1975        assert_eq!(orders[0].client_order_id, "v4-order");
1976        assert!(orders[0].expiry_reason.is_none());
1977    }
1978
1979    #[rstest]
1980    fn test_decode_orders_pre_v4_block_returns_no_expiry_reason() {
1981        // Schema 3:3 block_length is 162 (no expiryReason byte). The decoder
1982        // must surface `expiry_reason = None` regardless of the trailing
1983        // padding bytes inside the fixed block.
1984        const PRE_V4_BLOCK_LENGTH: u16 = ORDER_MIN_BLOCK_LENGTH;
1985        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1986
1987        let mut buf = Vec::new();
1988        buf.extend_from_slice(&header);
1989        buf.extend_from_slice(&create_group_header(PRE_V4_BLOCK_LENGTH, 1));
1990
1991        // Fill the fixed block with non-zero, non-0xff padding so the previous
1992        // bug (returning that last byte as expiry_reason) would surface.
1993        buf.extend_from_slice(&[0xAAu8; PRE_V4_BLOCK_LENGTH as usize]);
1994
1995        write_var_string(&mut buf, "BTCUSDT");
1996        write_var_string(&mut buf, "pre-v4-order");
1997
1998        let orders = decode_orders(&buf).unwrap();
1999        assert!(orders[0].expiry_reason.is_none());
2000    }
2001
2002    #[rstest]
2003    fn test_decode_orders_v4_captures_expiry_reason_value() {
2004        // Same layout as the null case, but the trailing byte carries a real
2005        // expiryReason value (0x05 = UnfilledIocQuantityExpired). The decoder
2006        // must surface it on the parsed `BinanceOrderResponse`.
2007        const V4_BLOCK_LENGTH: u16 = 163;
2008        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2009
2010        let mut buf = Vec::new();
2011        buf.extend_from_slice(&header);
2012        buf.extend_from_slice(&create_group_header(V4_BLOCK_LENGTH, 1));
2013
2014        buf.extend_from_slice(&[0u8; ORDER_MIN_BLOCK_LENGTH as usize]);
2015        buf.push(0x05);
2016
2017        write_var_string(&mut buf, "BTCUSDT");
2018        write_var_string(&mut buf, "v4-expired");
2019
2020        let orders = decode_orders(&buf).unwrap();
2021        assert_eq!(orders[0].expiry_reason, Some(0x05));
2022    }
2023
2024    #[rstest]
2025    fn test_decode_orders_empty() {
2026        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2027
2028        let mut buf = Vec::new();
2029        buf.extend_from_slice(&header);
2030        buf.extend_from_slice(&create_group_header(ORDER_MIN_BLOCK_LENGTH, 0));
2031
2032        let orders = decode_orders(&buf).unwrap();
2033        assert!(orders.is_empty());
2034    }
2035
2036    #[rstest]
2037    fn test_decode_orders_truncated_var_string() {
2038        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2039
2040        let mut buf = Vec::new();
2041        buf.extend_from_slice(&header);
2042        buf.extend_from_slice(&create_group_header(ORDER_MIN_BLOCK_LENGTH, 1));
2043
2044        // Pad fixed block to 162 bytes
2045        buf.extend_from_slice(&[0u8; ORDER_MIN_BLOCK_LENGTH as usize]);
2046
2047        // Symbol length says 7 bytes but we only provide 3
2048        buf.push(7); // Length prefix claims "BTCUSDT" (7 chars)
2049        buf.extend_from_slice(b"BTC"); // Only 3 bytes - truncated
2050
2051        let err = decode_orders(&buf).unwrap_err();
2052        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
2053    }
2054
2055    #[rstest]
2056    fn test_decode_orders_invalid_utf8() {
2057        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2058
2059        let mut buf = Vec::new();
2060        buf.extend_from_slice(&header);
2061        buf.extend_from_slice(&create_group_header(ORDER_MIN_BLOCK_LENGTH, 1));
2062
2063        buf.extend_from_slice(&[0u8; ORDER_MIN_BLOCK_LENGTH as usize]);
2064
2065        // Invalid UTF-8 sequence
2066        buf.push(4);
2067        buf.extend_from_slice(&[0xFF, 0xFE, 0x00, 0x01]);
2068
2069        let err = decode_orders(&buf).unwrap_err();
2070        assert!(matches!(err, SbeDecodeError::InvalidUtf8));
2071    }
2072
2073    #[rstest]
2074    #[case::current(137)]
2075    #[case::future_block_length(141)]
2076    fn test_decode_cancel_order_matches_codec(#[case] block_length: usize) {
2077        let mut buf = create_cancel_order_response_buffer(99999, "BTCUSDT", "orig-id", "new-id");
2078        resize_fixed_block(&mut buf, block_length);
2079
2080        let cancel = decode_cancel_order(&buf).unwrap();
2081
2082        assert_eq!(cancel.order_id, 99999);
2083        assert!(cancel.order_list_id.is_none());
2084        assert_eq!(cancel.transact_time, 1_700_000_000_000_000);
2085        assert_eq!(cancel.status, OrderStatus::Canceled);
2086        assert_eq!(
2087            cancel.self_trade_prevention_mode,
2088            SelfTradePreventionMode::ExpireTaker
2089        );
2090        assert_eq!(cancel.symbol, "BTCUSDT");
2091        assert_eq!(cancel.orig_client_order_id, "orig-id");
2092        assert_eq!(cancel.client_order_id, "new-id");
2093    }
2094
2095    #[rstest]
2096    fn test_decode_account_with_balances() {
2097        let header = create_header(
2098            ACCOUNT_BLOCK_LENGTH as u16,
2099            ACCOUNT_TEMPLATE_ID,
2100            SBE_SCHEMA_ID,
2101            SBE_SCHEMA_VERSION,
2102        );
2103
2104        let mut buf = Vec::new();
2105        buf.extend_from_slice(&header);
2106
2107        // Fixed block (64 bytes)
2108        buf.push((-8i8) as u8); // commission_exponent
2109        buf.extend_from_slice(&100_000i64.to_le_bytes()); // maker_commission
2110        buf.extend_from_slice(&100_000i64.to_le_bytes()); // taker_commission
2111        buf.extend_from_slice(&0i64.to_le_bytes()); // buyer_commission
2112        buf.extend_from_slice(&0i64.to_le_bytes()); // seller_commission
2113        buf.push(1); // can_trade
2114        buf.push(1); // can_withdraw
2115        buf.push(1); // can_deposit
2116        buf.push(0); // brokered
2117        buf.push(0); // require_self_trade_prevention
2118        buf.push(0); // prevent_sor
2119        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
2120        buf.push(1); // account_type (SPOT)
2121
2122        // Pad to 64 bytes
2123        while buf.len() < 8 + ACCOUNT_BLOCK_LENGTH {
2124            buf.push(0);
2125        }
2126
2127        // Balances group: 2 balances
2128        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 2));
2129
2130        // Balance 1: BTC
2131        buf.push((-8i8) as u8); // exponent
2132        buf.extend_from_slice(&100_000_000i64.to_le_bytes()); // free (1.0 BTC)
2133        buf.extend_from_slice(&50_000_000i64.to_le_bytes()); // locked (0.5 BTC)
2134        write_var_string(&mut buf, "BTC");
2135
2136        // Balance 2: USDT
2137        buf.push((-8i8) as u8); // exponent
2138        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // free (10000 USDT)
2139        buf.extend_from_slice(&0i64.to_le_bytes()); // locked
2140        write_var_string(&mut buf, "USDT");
2141
2142        let account = decode_account(&buf).unwrap();
2143
2144        assert!(account.can_trade);
2145        assert!(account.can_withdraw);
2146        assert!(account.can_deposit);
2147        assert_eq!(account.balances.len(), 2);
2148        assert_eq!(account.balances[0].asset, "BTC");
2149        assert_eq!(account.balances[0].free_mantissa, 100_000_000);
2150        assert_eq!(account.balances[0].locked_mantissa, 50_000_000);
2151        assert_eq!(account.balances[1].asset, "USDT");
2152        assert_eq!(account.balances[1].free_mantissa, 1_000_000_000_000);
2153    }
2154
2155    #[rstest]
2156    fn test_decode_account_empty_balances() {
2157        let header = create_header(
2158            ACCOUNT_BLOCK_LENGTH as u16,
2159            ACCOUNT_TEMPLATE_ID,
2160            SBE_SCHEMA_ID,
2161            SBE_SCHEMA_VERSION,
2162        );
2163
2164        let mut buf = Vec::new();
2165        buf.extend_from_slice(&header);
2166
2167        // Minimal fixed block
2168        buf.push((-8i8) as u8);
2169        buf.extend_from_slice(&[0u8; 63]); // Rest of fixed block
2170
2171        // Empty balances group
2172        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 0));
2173
2174        let account = decode_account(&buf).unwrap();
2175        assert!(account.balances.is_empty());
2176    }
2177
2178    #[rstest]
2179    fn test_decode_account_trades_multiple() {
2180        let header = create_header(
2181            0,
2182            ACCOUNT_TRADES_TEMPLATE_ID,
2183            SBE_SCHEMA_ID,
2184            SBE_SCHEMA_VERSION,
2185        );
2186
2187        let mut buf = Vec::new();
2188        buf.extend_from_slice(&header);
2189
2190        // Group header: 2 trades
2191        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 2));
2192
2193        // Trade 1
2194        buf.push((-8i8) as u8); // price_exponent
2195        buf.push((-8i8) as u8); // qty_exponent
2196        buf.push((-8i8) as u8); // commission_exponent
2197        buf.extend_from_slice(&1001i64.to_le_bytes()); // id
2198        buf.extend_from_slice(&5001i64.to_le_bytes()); // order_id
2199        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
2200        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price
2201        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // qty
2202        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // quote_qty
2203        buf.extend_from_slice(&100_000i64.to_le_bytes()); // commission
2204        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
2205        buf.push(1); // is_buyer
2206        buf.push(0); // is_maker
2207        buf.push(1); // is_best_match
2208        write_var_string(&mut buf, "BTCUSDT");
2209        write_var_string(&mut buf, "BNB");
2210
2211        // Trade 2
2212        buf.push((-8i8) as u8);
2213        buf.push((-8i8) as u8);
2214        buf.push((-8i8) as u8);
2215        buf.extend_from_slice(&1002i64.to_le_bytes());
2216        buf.extend_from_slice(&5002i64.to_le_bytes());
2217        buf.extend_from_slice(&i64::MIN.to_le_bytes());
2218        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes());
2219        buf.extend_from_slice(&5_000_000i64.to_le_bytes());
2220        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes());
2221        buf.extend_from_slice(&50_000i64.to_le_bytes());
2222        buf.extend_from_slice(&1734300001000i64.to_le_bytes());
2223        buf.push(0); // is_buyer (false = seller)
2224        buf.push(1); // is_maker
2225        buf.push(1); // is_best_match
2226        write_var_string(&mut buf, "ETHUSDT");
2227        write_var_string(&mut buf, "USDT");
2228
2229        let trades = decode_account_trades(&buf).unwrap();
2230
2231        assert_eq!(trades.len(), 2);
2232        assert_eq!(trades[0].id, 1001);
2233        assert_eq!(trades[0].order_id, 5001);
2234        assert!(trades[0].order_list_id.is_none());
2235        assert_eq!(trades[0].symbol, "BTCUSDT");
2236        assert_eq!(trades[0].commission_asset, "BNB");
2237        assert!(trades[0].is_buyer);
2238        assert!(!trades[0].is_maker);
2239
2240        assert_eq!(trades[1].id, 1002);
2241        assert_eq!(trades[1].symbol, "ETHUSDT");
2242        assert_eq!(trades[1].commission_asset, "USDT");
2243        assert!(!trades[1].is_buyer);
2244        assert!(trades[1].is_maker);
2245    }
2246
2247    #[rstest]
2248    fn test_decode_account_trades_empty() {
2249        let header = create_header(
2250            0,
2251            ACCOUNT_TRADES_TEMPLATE_ID,
2252            SBE_SCHEMA_ID,
2253            SBE_SCHEMA_VERSION,
2254        );
2255
2256        let mut buf = Vec::new();
2257        buf.extend_from_slice(&header);
2258        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 0));
2259
2260        let trades = decode_account_trades(&buf).unwrap();
2261        assert!(trades.is_empty());
2262    }
2263
2264    #[rstest]
2265    fn test_decode_exchange_info_single_symbol() {
2266        let header = create_header(
2267            0,
2268            EXCHANGE_INFO_TEMPLATE_ID,
2269            SBE_SCHEMA_ID,
2270            SBE_SCHEMA_VERSION,
2271        );
2272
2273        let mut buf = Vec::new();
2274        buf.extend_from_slice(&header);
2275
2276        // Empty rate_limits group
2277        buf.extend_from_slice(&create_group_header(11, 0));
2278
2279        // Empty exchange_filters group
2280        buf.extend_from_slice(&create_group_header(0, 0));
2281
2282        // Symbols group: 1 symbol with block_length=19
2283        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 1));
2284
2285        // Fixed block (19 bytes)
2286        buf.push(0); // status (Trading)
2287        buf.push(8); // base_asset_precision
2288        buf.push(8); // quote_asset_precision
2289        buf.push(8); // base_commission_precision
2290        buf.push(8); // quote_commission_precision
2291        buf.extend_from_slice(&0b0000_0111u16.to_le_bytes()); // order_types (MARKET|LIMIT|STOP_LOSS)
2292        buf.push(1); // iceberg_allowed (True)
2293        buf.push(1); // oco_allowed (True)
2294        buf.push(0); // oto_allowed (False)
2295        buf.push(1); // quote_order_qty_market_allowed (True)
2296        buf.push(1); // allow_trailing_stop (True)
2297        buf.push(1); // cancel_replace_allowed (True)
2298        buf.push(0); // amend_allowed (False)
2299        buf.push(1); // is_spot_trading_allowed (True)
2300        buf.push(0); // is_margin_trading_allowed (False)
2301        buf.push(0); // default_self_trade_prevention_mode
2302        buf.push(0); // allowed_self_trade_prevention_modes
2303        buf.push(0); // peg_instructions_allowed
2304
2305        // Filters nested group: 0 filters (SBE binary filters are skipped)
2306        buf.extend_from_slice(&create_group_header(0, 0));
2307
2308        // Permission sets nested group: 1 set with 1 permission
2309        buf.extend_from_slice(&create_group_header(0, 1));
2310        buf.extend_from_slice(&create_group_header(0, 1));
2311        write_var_string(&mut buf, "SPOT");
2312
2313        // Variable-length strings
2314        write_var_string(&mut buf, "BTCUSDT");
2315        write_var_string(&mut buf, "BTC");
2316        write_var_string(&mut buf, "USDT");
2317
2318        let info = decode_exchange_info(&buf).unwrap();
2319
2320        assert_eq!(info.symbols.len(), 1);
2321        let symbol = &info.symbols[0];
2322        assert_eq!(symbol.symbol, "BTCUSDT");
2323        assert_eq!(symbol.base_asset, "BTC");
2324        assert_eq!(symbol.quote_asset, "USDT");
2325        assert_eq!(symbol.base_asset_precision, 8);
2326        assert_eq!(symbol.quote_asset_precision, 8);
2327        assert_eq!(symbol.status, 0); // Trading
2328        assert_eq!(symbol.order_types, 0b0000_0111);
2329        assert!(symbol.iceberg_allowed);
2330        assert!(symbol.oco_allowed);
2331        assert!(!symbol.oto_allowed);
2332        assert!(symbol.quote_order_qty_market_allowed);
2333        assert!(symbol.allow_trailing_stop);
2334        assert!(symbol.cancel_replace_allowed);
2335        assert!(!symbol.amend_allowed);
2336        assert!(symbol.is_spot_trading_allowed);
2337        assert!(!symbol.is_margin_trading_allowed);
2338        assert!(symbol.filters.price_filter.is_none()); // No filters in test data
2339        assert!(symbol.filters.lot_size_filter.is_none());
2340        assert_eq!(symbol.permissions.len(), 1);
2341        assert_eq!(symbol.permissions[0], vec!["SPOT"]);
2342    }
2343
2344    #[rstest]
2345    #[case::exponent(0, 128, "notional")]
2346    #[case::minimum(8, 255, "notional")]
2347    #[case::min_flag(9, 255, "notional market flag")]
2348    #[case::maximum(17, 255, "notional")]
2349    #[case::max_flag(18, 255, "notional market flag")]
2350    #[case::average(22, 255, "avgPriceMins")]
2351    fn test_spot_notional_sbe_invalid_fields(
2352        #[case] offset: usize,
2353        #[case] value: u8,
2354        #[case] field: &'static str,
2355    ) {
2356        let mut wire =
2357            include_bytes!("../../../test_data/spot/http_sbe/notional_range.sbe").to_vec();
2358        let header = [23, 0, 6, 0, 3, 0, 5, 0];
2359        let start = wire
2360            .windows(header.len())
2361            .position(|bytes| bytes == header)
2362            .unwrap();
2363        wire[start + header.len() + offset] = value;
2364        assert!(
2365            matches!(decode_exchange_info(&wire), Err(SbeDecodeError::InvalidValue { field: actual }) if actual == field)
2366        );
2367    }
2368
2369    #[rstest]
2370    fn test_spot_notional_sbe_truncated_filter() {
2371        let mut wire =
2372            include_bytes!("../../../test_data/spot/http_sbe/notional_range.sbe").to_vec();
2373        let header = [23, 0, 6, 0, 3, 0, 5, 0];
2374        let start = wire
2375            .windows(header.len())
2376            .position(|bytes| bytes == header)
2377            .unwrap();
2378        wire[start - 1] -= 1;
2379        assert!(matches!(
2380            decode_exchange_info(&wire),
2381            Err(SbeDecodeError::BufferTooShort {
2382                expected: 23,
2383                actual: 22
2384            })
2385        ));
2386    }
2387
2388    #[rstest]
2389    fn test_decode_exchange_info_empty() {
2390        let header = create_header(
2391            0,
2392            EXCHANGE_INFO_TEMPLATE_ID,
2393            SBE_SCHEMA_ID,
2394            SBE_SCHEMA_VERSION,
2395        );
2396
2397        let mut buf = Vec::new();
2398        buf.extend_from_slice(&header);
2399
2400        // Empty rate_limits group
2401        buf.extend_from_slice(&create_group_header(11, 0));
2402
2403        // Empty exchange_filters group
2404        buf.extend_from_slice(&create_group_header(0, 0));
2405
2406        // Empty symbols group
2407        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 0));
2408
2409        let info = decode_exchange_info(&buf).unwrap();
2410        assert!(info.symbols.is_empty());
2411    }
2412
2413    #[rstest]
2414    fn test_decode_exchange_info_wrong_template() {
2415        let header = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2416
2417        let mut buf = Vec::new();
2418        buf.extend_from_slice(&header);
2419
2420        let err = decode_exchange_info(&buf).unwrap_err();
2421        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
2422    }
2423
2424    #[rstest]
2425    fn test_decode_exchange_info_multiple_symbols() {
2426        let header = create_header(
2427            0,
2428            EXCHANGE_INFO_TEMPLATE_ID,
2429            SBE_SCHEMA_ID,
2430            SBE_SCHEMA_VERSION,
2431        );
2432
2433        let mut buf = Vec::new();
2434        buf.extend_from_slice(&header);
2435
2436        // Empty rate_limits group
2437        buf.extend_from_slice(&create_group_header(11, 0));
2438
2439        // Empty exchange_filters group
2440        buf.extend_from_slice(&create_group_header(0, 0));
2441
2442        // Symbols group: 2 symbols
2443        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 2));
2444
2445        // Symbol 1: BTCUSDT
2446        buf.push(0); // status
2447        buf.push(8); // base_asset_precision
2448        buf.push(8); // quote_asset_precision
2449        buf.push(8); // base_commission_precision
2450        buf.push(8); // quote_commission_precision
2451        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
2452        buf.push(1); // iceberg_allowed
2453        buf.push(1); // oco_allowed
2454        buf.push(0); // oto_allowed
2455        buf.push(1); // quote_order_qty_market_allowed
2456        buf.push(1); // allow_trailing_stop
2457        buf.push(1); // cancel_replace_allowed
2458        buf.push(0); // amend_allowed
2459        buf.push(1); // is_spot_trading_allowed
2460        buf.push(0); // is_margin_trading_allowed
2461        buf.push(0); // default_self_trade_prevention_mode
2462        buf.push(0); // allowed_self_trade_prevention_modes
2463        buf.push(0); // peg_instructions_allowed
2464        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
2465        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
2466        write_var_string(&mut buf, "BTCUSDT");
2467        write_var_string(&mut buf, "BTC");
2468        write_var_string(&mut buf, "USDT");
2469
2470        // Symbol 2: ETHUSDT
2471        buf.push(0); // status
2472        buf.push(8); // base_asset_precision
2473        buf.push(8); // quote_asset_precision
2474        buf.push(8); // base_commission_precision
2475        buf.push(8); // quote_commission_precision
2476        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
2477        buf.push(1); // iceberg_allowed
2478        buf.push(1); // oco_allowed
2479        buf.push(0); // oto_allowed
2480        buf.push(1); // quote_order_qty_market_allowed
2481        buf.push(1); // allow_trailing_stop
2482        buf.push(1); // cancel_replace_allowed
2483        buf.push(0); // amend_allowed
2484        buf.push(1); // is_spot_trading_allowed
2485        buf.push(1); // is_margin_trading_allowed
2486        buf.push(0); // default_self_trade_prevention_mode
2487        buf.push(0); // allowed_self_trade_prevention_modes
2488        buf.push(0); // peg_instructions_allowed
2489        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
2490        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
2491        write_var_string(&mut buf, "ETHUSDT");
2492        write_var_string(&mut buf, "ETH");
2493        write_var_string(&mut buf, "USDT");
2494
2495        let info = decode_exchange_info(&buf).unwrap();
2496
2497        assert_eq!(info.symbols.len(), 2);
2498        assert_eq!(info.symbols[0].symbol, "BTCUSDT");
2499        assert_eq!(info.symbols[0].base_asset, "BTC");
2500        assert!(!info.symbols[0].is_margin_trading_allowed);
2501
2502        assert_eq!(info.symbols[1].symbol, "ETHUSDT");
2503        assert_eq!(info.symbols[1].base_asset, "ETH");
2504        assert!(info.symbols[1].is_margin_trading_allowed);
2505    }
2506
2507    #[rstest]
2508    fn test_decode_klines_valid() {
2509        let header = create_header(2, KLINES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
2510
2511        let mut buf = Vec::new();
2512        buf.extend_from_slice(&header);
2513        buf.push((-2i8) as u8); // price_exponent
2514        buf.push((-4i8) as u8); // qty_exponent
2515        buf.extend_from_slice(&create_group_header(KLINES_BLOCK_LENGTH, 1));
2516        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes()); // open_time (micros)
2517        buf.extend_from_slice(&12_000i64.to_le_bytes()); // open_price
2518        buf.extend_from_slice(&12_500i64.to_le_bytes()); // high_price
2519        buf.extend_from_slice(&11_900i64.to_le_bytes()); // low_price
2520        buf.extend_from_slice(&12_345i64.to_le_bytes()); // close_price
2521        buf.extend_from_slice(&1_234_500i128.to_le_bytes()); // volume
2522        buf.extend_from_slice(&1_700_000_059_999_000i64.to_le_bytes()); // close_time (micros)
2523        buf.extend_from_slice(&2_345_600i128.to_le_bytes()); // quote_volume
2524        buf.extend_from_slice(&100i64.to_le_bytes()); // num_trades
2525        buf.extend_from_slice(&600_000i128.to_le_bytes()); // taker_buy_base_volume
2526        buf.extend_from_slice(&1_200_000i128.to_le_bytes()); // taker_buy_quote_volume
2527
2528        let klines = decode_klines(&buf).unwrap();
2529
2530        assert_eq!(klines.price_exponent, -2);
2531        assert_eq!(klines.qty_exponent, -4);
2532        assert_eq!(klines.klines.len(), 1);
2533        assert_eq!(klines.klines[0].open_time, 1_700_000_000_000_000);
2534        assert_eq!(klines.klines[0].close_price, 12_345);
2535        assert_eq!(i128::from_le_bytes(klines.klines[0].volume), 1_234_500);
2536        assert_eq!(klines.klines[0].num_trades, 100);
2537    }
2538
2539    fn build_new_order_full_buffer(expiry_reason: ExpiryReason) -> Vec<u8> {
2540        let mut bytes = vec![0u8; 512];
2541        let enc =
2542            NewOrderFullResponseEncoder::default().wrap(WriteBuf::new(&mut bytes), HEADER_LENGTH);
2543        let mut header = enc.header(0);
2544        let mut enc = header.parent().unwrap();
2545        enc.price_exponent(-2);
2546        enc.qty_exponent(-4);
2547        enc.order_id(12345);
2548        enc.order_list_id(99);
2549        enc.transact_time(1_700_000_000_000_000);
2550        enc.price(12_345);
2551        enc.orig_qty(25_000);
2552        enc.executed_qty(10_000);
2553        enc.cummulative_quote_qty(123_450_000);
2554        enc.status(OrderStatus::PartiallyFilled);
2555        enc.time_in_force(TimeInForce::Gtc);
2556        enc.order_type(OrderType::Limit);
2557        enc.side(OrderSide::Buy);
2558        enc.stop_price(12_000);
2559        enc.working_time(1_700_000_000_000_500);
2560        enc.iceberg_qty(5_000);
2561        enc.order_capacity(OrderCapacity::Principal);
2562        enc.working_floor(Floor::Sor);
2563        enc.self_trade_prevention_mode(SelfTradePreventionMode::ExpireMaker);
2564        enc.trade_group_id(1);
2565        enc.used_sor(BoolEnum::True);
2566        enc.expiry_reason(expiry_reason);
2567        let mut fills = enc.fills_encoder(1, FillsEncoder::default());
2568        fills.advance().unwrap();
2569        fills.commission_exponent(-8);
2570        fills.match_type(MatchType::AutoMatch);
2571        fills.price(12_345);
2572        fills.qty(10_000);
2573        fills.commission(10_000);
2574        fills.trade_id(555);
2575        fills.alloc_id(i64::MIN);
2576        fills.commission_asset("USDT");
2577        let enc = fills.parent().unwrap();
2578        let mut prevented = enc.prevented_matches_encoder(0, PreventedMatchesEncoder::default());
2579        let mut enc = prevented.parent().unwrap();
2580        enc.symbol("ETHUSDT");
2581        enc.client_order_id("client-456");
2582        let len = HEADER_LENGTH + enc.encoded_length();
2583        bytes.truncate(len);
2584        bytes
2585    }
2586
2587    #[rstest]
2588    #[case::pre_v4_no_expiry_reason(153, ExpiryReason::NullVal, None)]
2589    #[case::v4_null_sentinel(154, ExpiryReason::NullVal, None)]
2590    #[case::v4_captures_value(154, ExpiryReason::UnfilledIocQuantityExpired, Some(0x05))]
2591    #[case::future_block_length(158, ExpiryReason::UnfilledIocQuantityExpired, Some(0x05))]
2592    fn test_decode_new_order_full_matches_codec(
2593        #[case] block_length: usize,
2594        #[case] expiry_reason: ExpiryReason,
2595        #[case] expected: Option<u8>,
2596    ) {
2597        let mut buf = build_new_order_full_buffer(expiry_reason);
2598        resize_fixed_block(&mut buf, block_length);
2599
2600        let response = decode_new_order_full(&buf).unwrap();
2601
2602        assert_eq!(response.order_id, 12345);
2603        assert_eq!(response.order_list_id, Some(99));
2604        assert_eq!(response.transact_time, 1_700_000_000_000_000);
2605        assert_eq!(response.price_mantissa, 12_345);
2606        assert_eq!(response.orig_qty_mantissa, 25_000);
2607        assert_eq!(response.executed_qty_mantissa, 10_000);
2608        assert_eq!(response.status, OrderStatus::PartiallyFilled);
2609        assert_eq!(response.stop_price_mantissa, Some(12_000));
2610        assert_eq!(response.working_time, Some(1_700_000_000_000_500));
2611        assert_eq!(
2612            response.self_trade_prevention_mode,
2613            SelfTradePreventionMode::ExpireMaker
2614        );
2615        assert_eq!(response.expiry_reason, expected);
2616        assert_eq!(response.symbol, "ETHUSDT");
2617        assert_eq!(response.client_order_id, "client-456");
2618        assert_eq!(response.fills.len(), 1);
2619        assert_eq!(response.fills[0].price_mantissa, 12_345);
2620        assert_eq!(response.fills[0].qty_mantissa, 10_000);
2621        assert_eq!(response.fills[0].trade_id, Some(555));
2622        assert_eq!(response.fills[0].commission_asset, "USDT");
2623    }
2624
2625    #[rstest]
2626    fn test_decode_new_order_full_rejects_short_block() {
2627        let mut buf = build_new_order_full_buffer(ExpiryReason::NullVal);
2628        resize_fixed_block(&mut buf, 116);
2629
2630        let error = decode_new_order_full(&buf).unwrap_err();
2631
2632        assert_eq!(
2633            error,
2634            SbeDecodeError::InvalidBlockLength {
2635                expected: 117,
2636                actual: 116,
2637            }
2638        );
2639    }
2640
2641    #[rstest]
2642    #[case::current(2)]
2643    #[case::extended(5)]
2644    fn test_decode_cancel_replace_nested_order(#[case] block_length: u16) {
2645        let nested = build_new_order_full_buffer(ExpiryReason::NullVal);
2646        let mut buf = create_header(
2647            block_length,
2648            CANCEL_REPLACE_TEMPLATE_ID,
2649            SBE_SCHEMA_ID,
2650            SBE_SCHEMA_VERSION,
2651        )
2652        .to_vec();
2653        buf.resize(HEADER_LENGTH + usize::from(block_length), 0);
2654        buf.extend_from_slice(&3_u16.to_le_bytes());
2655        buf.extend_from_slice(&[11, 22, 33]);
2656        buf.extend_from_slice(&(nested.len() as u32).to_le_bytes());
2657        buf.extend_from_slice(&nested);
2658
2659        let response = decode_cancel_replace(&buf).unwrap();
2660        let expected = decode_new_order_full(&nested).unwrap();
2661        assert_eq!(response, expected);
2662
2663        for end in 0..buf.len() {
2664            assert!(
2665                decode_cancel_replace(&buf[..end]).is_err(),
2666                "Accepted truncated response at {end}"
2667            );
2668        }
2669        buf[HEADER_LENGTH + 1] = CancelReplaceStatus::Failure as u8;
2670        assert!(matches!(
2671            decode_cancel_replace(&buf),
2672            Err(SbeDecodeError::InvalidValue {
2673                field: "cancel-replace result"
2674            })
2675        ));
2676    }
2677
2678    #[rstest]
2679    fn test_decode_cancel_open_orders_valid() {
2680        let response_one = create_cancel_order_response_buffer(111, "ETHUSDT", "orig-1", "new-1");
2681        let response_two = create_cancel_order_response_buffer(222, "BTCUSDT", "orig-2", "new-2");
2682        let buf = create_cancel_open_orders_buffer(&[response_one, response_two]);
2683        let responses = decode_cancel_open_orders(&buf).unwrap();
2684
2685        assert_eq!(responses.len(), 2);
2686        let BinanceCancelOpenOrdersResponse::Order(first) = &responses[0] else {
2687            panic!("Expected ordinary cancel response");
2688        };
2689        assert_eq!(first.order_id, 111);
2690        assert_eq!(first.symbol, "ETHUSDT");
2691        assert_eq!(first.orig_client_order_id, "orig-1");
2692        assert_eq!(first.client_order_id, "new-1");
2693        let BinanceCancelOpenOrdersResponse::Order(second) = &responses[1] else {
2694            panic!("Expected ordinary cancel response");
2695        };
2696        assert_eq!(second.order_id, 222);
2697        assert_eq!(second.symbol, "BTCUSDT");
2698        assert_eq!(second.orig_client_order_id, "orig-2");
2699        assert_eq!(second.client_order_id, "new-2");
2700    }
2701
2702    #[rstest]
2703    fn test_decode_cancel_open_orders_empty() {
2704        let buf = create_cancel_open_orders_buffer(&[]);
2705
2706        let responses = decode_cancel_open_orders(&buf).unwrap();
2707
2708        assert!(responses.is_empty());
2709    }
2710
2711    #[rstest]
2712    fn test_decode_cancel_open_orders_order_list() {
2713        let order_list = create_cancel_order_list_response_buffer();
2714        let buf = create_cancel_open_orders_buffer(&[order_list]);
2715
2716        let responses = decode_cancel_open_orders(&buf).unwrap();
2717
2718        let BinanceCancelOpenOrdersResponse::OrderList(response) = &responses[0] else {
2719            panic!("Expected order-list cancel response");
2720        };
2721        assert_eq!(response.order_list_id, 44);
2722        assert_eq!(response.symbol, "BTCUSDT");
2723        assert_eq!(response.list_client_order_id, "list-44");
2724        assert_eq!(response.orders.len(), 2);
2725        assert_eq!(response.orders[0].order_id, 111);
2726        assert_eq!(response.orders[0].client_order_id, "orig-1");
2727        assert_eq!(response.orders[1].order_id, 222);
2728        assert_eq!(response.orders[1].client_order_id, "orig-2");
2729        assert_eq!(response.order_reports.len(), 2);
2730        assert_eq!(response.order_reports[0].order_id, 111);
2731        assert_eq!(response.order_reports[0].order_list_id, Some(44));
2732        assert_eq!(response.order_reports[0].orig_client_order_id, "orig-1");
2733        assert_eq!(response.order_reports[1].order_id, 222);
2734        assert_eq!(response.order_reports[1].orig_client_order_id, "orig-2");
2735    }
2736
2737    #[rstest]
2738    fn test_decode_cancel_open_orders_mixed() {
2739        let order = create_cancel_order_response_buffer(333, "BTCUSDT", "orig-3", "new-3");
2740        let order_list = create_cancel_order_list_response_buffer();
2741        let buf = create_cancel_open_orders_buffer(&[order, order_list]);
2742
2743        let responses = decode_cancel_open_orders(&buf).unwrap();
2744
2745        assert!(matches!(
2746            responses[0],
2747            BinanceCancelOpenOrdersResponse::Order(_)
2748        ));
2749        assert!(matches!(
2750            responses[1],
2751            BinanceCancelOpenOrdersResponse::OrderList(_)
2752        ));
2753    }
2754
2755    #[rstest]
2756    fn test_decode_cancel_open_orders_unknown_embedded_template() {
2757        let unknown = create_header(0, 999, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION).to_vec();
2758        let buf = create_cancel_open_orders_buffer(&[unknown]);
2759
2760        let error = decode_cancel_open_orders(&buf).unwrap_err();
2761
2762        assert_eq!(error, SbeDecodeError::UnknownTemplateId(999));
2763    }
2764
2765    #[rstest]
2766    fn test_decode_cancel_open_orders_truncated_item() {
2767        let mut order_list = create_cancel_order_list_response_buffer();
2768        order_list.pop();
2769        let buf = create_cancel_open_orders_buffer(&[order_list]);
2770
2771        let error = decode_cancel_open_orders(&buf).unwrap_err();
2772
2773        assert!(matches!(error, SbeDecodeError::BufferTooShort { .. }));
2774    }
2775
2776    #[rstest]
2777    fn test_decode_cancel_open_orders_rejects_group_item_block() {
2778        let mut buf = create_cancel_open_orders_buffer(&[]);
2779        buf[8..10].copy_from_slice(&1u16.to_le_bytes());
2780
2781        let error = decode_cancel_open_orders(&buf).unwrap_err();
2782
2783        assert_eq!(
2784            error,
2785            SbeDecodeError::InvalidBlockLength {
2786                expected: 0,
2787                actual: 1,
2788            }
2789        );
2790    }
2791
2792    #[rstest]
2793    fn test_decode_cancel_open_orders_rejects_short_order_list_block() {
2794        let mut order_list = create_cancel_order_list_response_buffer();
2795        order_list[0..2].copy_from_slice(&20u16.to_le_bytes());
2796        let buf = create_cancel_open_orders_buffer(&[order_list]);
2797
2798        let error = decode_cancel_open_orders(&buf).unwrap_err();
2799
2800        assert_eq!(
2801            error,
2802            SbeDecodeError::InvalidBlockLength {
2803                expected: 21,
2804                actual: 20,
2805            }
2806        );
2807    }
2808
2809    #[rstest]
2810    fn test_decode_cancel_open_orders_rejects_short_order_block() {
2811        let mut order = create_cancel_order_response_buffer(111, "BTCUSDT", "orig-1", "new-1");
2812        order[0..2].copy_from_slice(&108u16.to_le_bytes());
2813        let buf = create_cancel_open_orders_buffer(&[order]);
2814
2815        let error = decode_cancel_open_orders(&buf).unwrap_err();
2816
2817        assert_eq!(
2818            error,
2819            SbeDecodeError::InvalidBlockLength {
2820                expected: 109,
2821                actual: 108,
2822            }
2823        );
2824    }
2825
2826    #[rstest]
2827    fn test_decode_cancel_open_orders_rejects_short_order_report_block() {
2828        let mut order_list = create_cancel_order_list_response_buffer();
2829        let report_header = order_list
2830            .windows(4)
2831            .position(|window| window == [135, 0, 2, 0])
2832            .unwrap();
2833        order_list[report_header..report_header + 2].copy_from_slice(&134u16.to_le_bytes());
2834        let buf = create_cancel_open_orders_buffer(&[order_list]);
2835
2836        let error = decode_cancel_open_orders(&buf).unwrap_err();
2837
2838        assert_eq!(
2839            error,
2840            SbeDecodeError::InvalidBlockLength {
2841                expected: 135,
2842                actual: 134,
2843            }
2844        );
2845    }
2846
2847    #[rstest]
2848    fn test_decode_cancel_open_orders_rejects_invalid_utf8() {
2849        let mut order_list = create_cancel_order_list_response_buffer();
2850        let symbol_start = order_list
2851            .windows(b"BTCUSDT".len())
2852            .position(|window| window == b"BTCUSDT")
2853            .unwrap();
2854        order_list[symbol_start] = 0xff;
2855        let buf = create_cancel_open_orders_buffer(&[order_list]);
2856
2857        let error = decode_cancel_open_orders(&buf).unwrap_err();
2858
2859        assert_eq!(error, SbeDecodeError::InvalidUtf8);
2860    }
2861
2862    fn create_cancel_order_response_buffer(
2863        order_id: i64,
2864        symbol: &str,
2865        orig_client_order_id: &str,
2866        client_order_id: &str,
2867    ) -> Vec<u8> {
2868        let mut bytes = vec![0u8; 256];
2869        let enc =
2870            CancelOrderResponseEncoder::default().wrap(WriteBuf::new(&mut bytes), HEADER_LENGTH);
2871        let mut header = enc.header(0);
2872        let mut enc = header.parent().unwrap();
2873        enc.price_exponent(-8);
2874        enc.qty_exponent(-8);
2875        enc.order_id(order_id);
2876        enc.order_list_id(i64::MIN);
2877        enc.transact_time(1_700_000_000_000_000);
2878        enc.price(100_000_000_000);
2879        enc.orig_qty(10_000_000);
2880        enc.executed_qty(10_000_000);
2881        enc.cummulative_quote_qty(1_000_000_000);
2882        enc.status(OrderStatus::Canceled);
2883        enc.time_in_force(TimeInForce::Gtc);
2884        enc.order_type(OrderType::Limit);
2885        enc.side(OrderSide::Buy);
2886        enc.stop_price(3);
2887        enc.self_trade_prevention_mode(SelfTradePreventionMode::ExpireTaker);
2888        enc.symbol(symbol);
2889        enc.orig_client_order_id(orig_client_order_id);
2890        enc.client_order_id(client_order_id);
2891        let len = HEADER_LENGTH + enc.encoded_length();
2892        bytes.truncate(len);
2893        bytes
2894    }
2895
2896    fn create_cancel_order_list_response_buffer() -> Vec<u8> {
2897        const REPORT_BLOCK_LENGTH: usize = 135;
2898
2899        let mut buf = Vec::new();
2900        buf.extend_from_slice(&create_header(
2901            CANCEL_ORDER_LIST_FIELDS_END as u16,
2902            CANCEL_ORDER_LIST_TEMPLATE_ID,
2903            SBE_SCHEMA_ID,
2904            SBE_SCHEMA_VERSION,
2905        ));
2906        buf.extend_from_slice(&44i64.to_le_bytes());
2907        buf.push(1); // contingency_type (OCO)
2908        buf.push(2); // list_status_type (ALL_DONE)
2909        buf.push(2); // list_order_status (ALL_DONE)
2910        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes());
2911        buf.push((-8i8) as u8);
2912        buf.push((-8i8) as u8);
2913
2914        buf.extend_from_slice(&8u16.to_le_bytes());
2915        buf.extend_from_slice(&2u16.to_le_bytes());
2916        for (order_id, client_order_id) in [(111i64, "orig-1"), (222, "orig-2")] {
2917            buf.extend_from_slice(&order_id.to_le_bytes());
2918            write_var_string(&mut buf, "BTCUSDT");
2919            write_var_string(&mut buf, client_order_id);
2920        }
2921
2922        buf.extend_from_slice(&(REPORT_BLOCK_LENGTH as u16).to_le_bytes());
2923        buf.extend_from_slice(&2u16.to_le_bytes());
2924        for (order_id, orig_client_order_id) in [(111i64, "orig-1"), (222, "orig-2")] {
2925            let report_start = buf.len();
2926            buf.extend_from_slice(&order_id.to_le_bytes());
2927            buf.extend_from_slice(&44i64.to_le_bytes());
2928            buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes());
2929            buf.extend_from_slice(&100_000_000_000i64.to_le_bytes());
2930            buf.extend_from_slice(&10_000_000i64.to_le_bytes());
2931            buf.extend_from_slice(&0i64.to_le_bytes());
2932            buf.extend_from_slice(&0i64.to_le_bytes());
2933            buf.push(3); // status (CANCELED)
2934            buf.push(0); // time_in_force (GTC)
2935            buf.push(1); // order_type (LIMIT)
2936            buf.push(1); // side (SELL)
2937            while buf.len() - report_start < REPORT_BLOCK_LENGTH {
2938                buf.push(0);
2939            }
2940            write_var_string(&mut buf, "BTCUSDT");
2941            write_var_string(&mut buf, orig_client_order_id);
2942            write_var_string(&mut buf, "cancel-44");
2943        }
2944
2945        write_var_string(&mut buf, "list-44");
2946        write_var_string(&mut buf, "BTCUSDT");
2947        buf
2948    }
2949
2950    fn create_cancel_open_orders_buffer(responses: &[Vec<u8>]) -> Vec<u8> {
2951        let mut buf = Vec::new();
2952        buf.extend_from_slice(&create_header(
2953            0,
2954            CANCEL_OPEN_ORDERS_TEMPLATE_ID,
2955            SBE_SCHEMA_ID,
2956            SBE_SCHEMA_VERSION,
2957        ));
2958        buf.extend_from_slice(&create_group_header(0, responses.len() as u32));
2959        for response in responses {
2960            buf.extend_from_slice(&(response.len() as u16).to_le_bytes());
2961            buf.extend_from_slice(response);
2962        }
2963        buf
2964    }
2965}