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