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, version, template ID) before extracting fields.
20
21use super::{
22    error::SbeDecodeError,
23    models::{
24        BinanceAccountInfo, BinanceAccountTrade, BinanceBalance, BinanceCancelOrderResponse,
25        BinanceDepth, BinanceExchangeInfoSbe, BinanceKline, BinanceKlines, BinanceLotSizeFilterSbe,
26        BinanceNewOrderResponse, BinanceOrderFill, BinanceOrderResponse, BinancePriceFilterSbe,
27        BinancePriceLevel, BinanceSymbolFiltersSbe, BinanceSymbolSbe, BinanceTrade, BinanceTrades,
28    },
29};
30use crate::spot::sbe::{
31    cursor::SbeCursor,
32    spot::{
33        SBE_SCHEMA_ID, SBE_SCHEMA_VERSION,
34        account_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TEMPLATE_ID,
35        account_trades_response_codec::SBE_TEMPLATE_ID as ACCOUNT_TRADES_TEMPLATE_ID,
36        account_type::AccountType, bool_enum::BoolEnum,
37        cancel_open_orders_response_codec::SBE_TEMPLATE_ID as CANCEL_OPEN_ORDERS_TEMPLATE_ID,
38        cancel_order_response_codec::SBE_TEMPLATE_ID as CANCEL_ORDER_TEMPLATE_ID,
39        depth_response_codec::SBE_TEMPLATE_ID as DEPTH_TEMPLATE_ID,
40        exchange_info_response_codec::SBE_TEMPLATE_ID as EXCHANGE_INFO_TEMPLATE_ID,
41        klines_response_codec::SBE_TEMPLATE_ID as KLINES_TEMPLATE_ID,
42        lot_size_filter_codec::SBE_TEMPLATE_ID as LOT_SIZE_FILTER_TEMPLATE_ID,
43        message_header_codec::ENCODED_LENGTH as HEADER_LENGTH,
44        new_order_full_response_codec::SBE_TEMPLATE_ID as NEW_ORDER_FULL_TEMPLATE_ID,
45        order_response_codec::SBE_TEMPLATE_ID as ORDER_TEMPLATE_ID,
46        orders_response_codec::SBE_TEMPLATE_ID as ORDERS_TEMPLATE_ID,
47        ping_response_codec::SBE_TEMPLATE_ID as PING_TEMPLATE_ID,
48        price_filter_codec::SBE_TEMPLATE_ID as PRICE_FILTER_TEMPLATE_ID,
49        server_time_response_codec::SBE_TEMPLATE_ID as SERVER_TIME_TEMPLATE_ID,
50        trades_response_codec::SBE_TEMPLATE_ID as TRADES_TEMPLATE_ID,
51    },
52};
53
54/// SBE message header.
55#[derive(Debug, Clone, Copy)]
56struct MessageHeader {
57    block_length: u16,
58    template_id: u16,
59    schema_id: u16,
60    version: u16,
61}
62
63impl MessageHeader {
64    /// Decode message header using cursor.
65    fn decode_cursor(cursor: &mut SbeCursor<'_>) -> Result<Self, SbeDecodeError> {
66        cursor.require(HEADER_LENGTH)?;
67        Ok(Self {
68            block_length: cursor.read_u16_le()?,
69            template_id: cursor.read_u16_le()?,
70            schema_id: cursor.read_u16_le()?,
71            version: cursor.read_u16_le()?,
72        })
73    }
74
75    /// Validate schema ID and version.
76    fn validate(&self) -> Result<(), SbeDecodeError> {
77        if self.schema_id != SBE_SCHEMA_ID {
78            return Err(SbeDecodeError::SchemaMismatch {
79                expected: SBE_SCHEMA_ID,
80                actual: self.schema_id,
81            });
82        }
83
84        if self.version != SBE_SCHEMA_VERSION {
85            return Err(SbeDecodeError::VersionMismatch {
86                expected: SBE_SCHEMA_VERSION,
87                actual: self.version,
88            });
89        }
90        Ok(())
91    }
92}
93
94/// Decode a ping response.
95///
96/// Ping response has no body (block_length = 0), just validates the header.
97///
98/// # Errors
99///
100/// Returns error if buffer is too short or schema mismatch.
101pub fn decode_ping(buf: &[u8]) -> Result<(), SbeDecodeError> {
102    let mut cursor = SbeCursor::new(buf);
103    let header = MessageHeader::decode_cursor(&mut cursor)?;
104    header.validate()?;
105
106    if header.template_id != PING_TEMPLATE_ID {
107        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
108    }
109
110    Ok(())
111}
112
113/// Decode a server time response.
114///
115/// Returns the server time as **microseconds** since epoch (SBE provides
116/// microsecond precision vs JSON's milliseconds).
117///
118/// # Errors
119///
120/// Returns error if buffer is too short or schema mismatch.
121pub fn decode_server_time(buf: &[u8]) -> Result<i64, SbeDecodeError> {
122    let mut cursor = SbeCursor::new(buf);
123    let header = MessageHeader::decode_cursor(&mut cursor)?;
124    header.validate()?;
125
126    if header.template_id != SERVER_TIME_TEMPLATE_ID {
127        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
128    }
129
130    cursor.read_i64_le()
131}
132
133/// Decode a depth response.
134///
135/// Returns the order book depth with bids and asks.
136///
137/// # Errors
138///
139/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
140pub fn decode_depth(buf: &[u8]) -> Result<BinanceDepth, SbeDecodeError> {
141    let mut cursor = SbeCursor::new(buf);
142    let header = MessageHeader::decode_cursor(&mut cursor)?;
143    header.validate()?;
144
145    if header.template_id != DEPTH_TEMPLATE_ID {
146        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
147    }
148
149    let last_update_id = cursor.read_i64_le()?;
150    let price_exponent = cursor.read_i8()?;
151    let qty_exponent = cursor.read_i8()?;
152
153    let (block_len, count) = cursor.read_group_header()?;
154    let bids = cursor.read_group(block_len, count, |c| {
155        Ok(BinancePriceLevel {
156            price_mantissa: c.read_i64_le()?,
157            qty_mantissa: c.read_i64_le()?,
158        })
159    })?;
160
161    let (block_len, count) = cursor.read_group_header()?;
162    let asks = cursor.read_group(block_len, count, |c| {
163        Ok(BinancePriceLevel {
164            price_mantissa: c.read_i64_le()?,
165            qty_mantissa: c.read_i64_le()?,
166        })
167    })?;
168
169    Ok(BinanceDepth {
170        last_update_id,
171        price_exponent,
172        qty_exponent,
173        bids,
174        asks,
175    })
176}
177
178/// Decode a trades response.
179///
180/// Returns the list of trades.
181///
182/// # Errors
183///
184/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
185pub fn decode_trades(buf: &[u8]) -> Result<BinanceTrades, SbeDecodeError> {
186    let mut cursor = SbeCursor::new(buf);
187    let header = MessageHeader::decode_cursor(&mut cursor)?;
188    header.validate()?;
189
190    if header.template_id != TRADES_TEMPLATE_ID {
191        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
192    }
193
194    let price_exponent = cursor.read_i8()?;
195    let qty_exponent = cursor.read_i8()?;
196
197    let (block_len, count) = cursor.read_group_header()?;
198    let trades = cursor.read_group(block_len, count, |c| {
199        Ok(BinanceTrade {
200            id: c.read_i64_le()?,
201            price_mantissa: c.read_i64_le()?,
202            qty_mantissa: c.read_i64_le()?,
203            quote_qty_mantissa: c.read_i64_le()?,
204            time: c.read_i64_le()?,
205            is_buyer_maker: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
206            is_best_match: BoolEnum::from(c.read_u8()?) == BoolEnum::True,
207        })
208    })?;
209
210    Ok(BinanceTrades {
211        price_exponent,
212        qty_exponent,
213        trades,
214    })
215}
216
217/// Klines group item block length (from SBE codec).
218const KLINES_BLOCK_LENGTH: u16 = 120;
219
220/// Decode a klines (candlestick) response.
221///
222/// Returns the list of klines with their price and quantity exponents.
223///
224/// # Errors
225///
226/// Returns error if buffer is too short, schema mismatch, or group size exceeded.
227pub fn decode_klines(buf: &[u8]) -> Result<BinanceKlines, SbeDecodeError> {
228    let mut cursor = SbeCursor::new(buf);
229    let header = MessageHeader::decode_cursor(&mut cursor)?;
230    header.validate()?;
231
232    if header.template_id != KLINES_TEMPLATE_ID {
233        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
234    }
235
236    let price_exponent = cursor.read_i8()?;
237    let qty_exponent = cursor.read_i8()?;
238
239    let (block_len, count) = cursor.read_group_header()?;
240
241    if block_len != KLINES_BLOCK_LENGTH {
242        return Err(SbeDecodeError::InvalidBlockLength {
243            expected: KLINES_BLOCK_LENGTH,
244            actual: block_len,
245        });
246    }
247
248    let mut klines = Vec::with_capacity(count as usize);
249
250    for _ in 0..count {
251        cursor.require(KLINES_BLOCK_LENGTH as usize)?;
252
253        let open_time = cursor.read_i64_le()?;
254        let open_price = cursor.read_i64_le()?;
255        let high_price = cursor.read_i64_le()?;
256        let low_price = cursor.read_i64_le()?;
257        let close_price = cursor.read_i64_le()?;
258
259        let volume_slice = cursor.read_bytes(16)?;
260        let mut volume = [0u8; 16];
261        volume.copy_from_slice(volume_slice);
262
263        let close_time = cursor.read_i64_le()?;
264
265        let quote_volume_slice = cursor.read_bytes(16)?;
266        let mut quote_volume = [0u8; 16];
267        quote_volume.copy_from_slice(quote_volume_slice);
268
269        let num_trades = cursor.read_i64_le()?;
270
271        let taker_buy_base_volume_slice = cursor.read_bytes(16)?;
272        let mut taker_buy_base_volume = [0u8; 16];
273        taker_buy_base_volume.copy_from_slice(taker_buy_base_volume_slice);
274
275        let taker_buy_quote_volume_slice = cursor.read_bytes(16)?;
276        let mut taker_buy_quote_volume = [0u8; 16];
277        taker_buy_quote_volume.copy_from_slice(taker_buy_quote_volume_slice);
278
279        klines.push(BinanceKline {
280            open_time,
281            open_price,
282            high_price,
283            low_price,
284            close_price,
285            volume,
286            close_time,
287            quote_volume,
288            num_trades,
289            taker_buy_base_volume,
290            taker_buy_quote_volume,
291        });
292    }
293
294    Ok(BinanceKlines {
295        price_exponent,
296        qty_exponent,
297        klines,
298    })
299}
300
301/// Bytes consumed from the fixed block before the end-of-block skip in each decoder.
302/// These represent explicit reads and advances up to the last field we extract.
303const NEW_ORDER_FULL_FIELDS_END: usize = 135;
304const CANCEL_ORDER_FIELDS_END: usize = 63;
305const ORDER_FIELDS_END: usize = 104;
306
307/// Decode a new order full response.
308///
309/// # Errors
310///
311/// Returns error if buffer is too short, schema mismatch, or decode error.
312#[allow(dead_code)]
313pub fn decode_new_order_full(buf: &[u8]) -> Result<BinanceNewOrderResponse, SbeDecodeError> {
314    let mut cursor = SbeCursor::new(buf);
315    let header = MessageHeader::decode_cursor(&mut cursor)?;
316    header.validate()?;
317
318    if header.template_id != NEW_ORDER_FULL_TEMPLATE_ID {
319        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
320    }
321
322    cursor.require(header.block_length as usize)?;
323
324    let price_exponent = cursor.read_i8()?;
325    let qty_exponent = cursor.read_i8()?;
326    let order_id = cursor.read_i64_le()?;
327    let order_list_id = cursor.read_optional_i64_le()?;
328    let transact_time = cursor.read_i64_le()?;
329    let price_mantissa = cursor.read_i64_le()?;
330    let orig_qty_mantissa = cursor.read_i64_le()?;
331    let executed_qty_mantissa = cursor.read_i64_le()?;
332    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
333    let status = cursor.read_u8()?.into();
334    let time_in_force = cursor.read_u8()?.into();
335    let order_type = cursor.read_u8()?.into();
336    let side = cursor.read_u8()?.into();
337    let stop_price_mantissa = cursor.read_optional_i64_le()?;
338
339    cursor.advance(16)?; // Skip trailing_delta (8) + trailing_time (8)
340    let working_time = cursor.read_optional_i64_le()?;
341
342    cursor.advance(23)?; // Skip iceberg to used_sor
343    let self_trade_prevention_mode = cursor.read_u8()?.into();
344
345    cursor.advance(16)?; // Skip trade_group_id + prevented_quantity
346    let _commission_exponent = cursor.read_i8()?;
347
348    cursor.advance(header.block_length as usize - NEW_ORDER_FULL_FIELDS_END)?;
349
350    let fills = decode_fills_cursor(&mut cursor)?;
351
352    // Skip prevented matches group
353    let (block_len, count) = cursor.read_group_header()?;
354    cursor.advance(block_len as usize * count as usize)?;
355
356    let symbol = cursor.read_var_string8()?;
357    let client_order_id = cursor.read_var_string8()?;
358
359    Ok(BinanceNewOrderResponse {
360        price_exponent,
361        qty_exponent,
362        order_id,
363        order_list_id,
364        transact_time,
365        price_mantissa,
366        orig_qty_mantissa,
367        executed_qty_mantissa,
368        cummulative_quote_qty_mantissa,
369        status,
370        time_in_force,
371        order_type,
372        side,
373        stop_price_mantissa,
374        working_time,
375        self_trade_prevention_mode,
376        client_order_id,
377        symbol,
378        fills,
379    })
380}
381
382/// Decode a cancel order response.
383///
384/// # Errors
385///
386/// Returns error if buffer is too short, schema mismatch, or decode error.
387#[allow(dead_code)]
388pub fn decode_cancel_order(buf: &[u8]) -> Result<BinanceCancelOrderResponse, SbeDecodeError> {
389    let mut cursor = SbeCursor::new(buf);
390    let header = MessageHeader::decode_cursor(&mut cursor)?;
391    header.validate()?;
392
393    if header.template_id != CANCEL_ORDER_TEMPLATE_ID {
394        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
395    }
396
397    cursor.require(header.block_length as usize)?;
398
399    let price_exponent = cursor.read_i8()?;
400    let qty_exponent = cursor.read_i8()?;
401    let order_id = cursor.read_i64_le()?;
402    let order_list_id = cursor.read_optional_i64_le()?;
403    let transact_time = cursor.read_i64_le()?;
404    let price_mantissa = cursor.read_i64_le()?;
405    let orig_qty_mantissa = cursor.read_i64_le()?;
406    let executed_qty_mantissa = cursor.read_i64_le()?;
407    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
408    let status = cursor.read_u8()?.into();
409    let time_in_force = cursor.read_u8()?.into();
410    let order_type = cursor.read_u8()?.into();
411    let side = cursor.read_u8()?.into();
412    let self_trade_prevention_mode = cursor.read_u8()?.into();
413
414    cursor.advance(header.block_length as usize - CANCEL_ORDER_FIELDS_END)?;
415
416    let symbol = cursor.read_var_string8()?;
417    let orig_client_order_id = cursor.read_var_string8()?;
418    let client_order_id = cursor.read_var_string8()?;
419
420    Ok(BinanceCancelOrderResponse {
421        price_exponent,
422        qty_exponent,
423        order_id,
424        order_list_id,
425        transact_time,
426        price_mantissa,
427        orig_qty_mantissa,
428        executed_qty_mantissa,
429        cummulative_quote_qty_mantissa,
430        status,
431        time_in_force,
432        order_type,
433        side,
434        self_trade_prevention_mode,
435        client_order_id,
436        orig_client_order_id,
437        symbol,
438    })
439}
440
441/// Decode an order query response.
442///
443/// # Errors
444///
445/// Returns error if buffer is too short, schema mismatch, or decode error.
446#[allow(dead_code)]
447pub fn decode_order(buf: &[u8]) -> Result<BinanceOrderResponse, SbeDecodeError> {
448    let mut cursor = SbeCursor::new(buf);
449    let header = MessageHeader::decode_cursor(&mut cursor)?;
450    header.validate()?;
451
452    if header.template_id != ORDER_TEMPLATE_ID {
453        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
454    }
455
456    cursor.require(header.block_length as usize)?;
457
458    let price_exponent = cursor.read_i8()?;
459    let qty_exponent = cursor.read_i8()?;
460    let order_id = cursor.read_i64_le()?;
461    let order_list_id = cursor.read_optional_i64_le()?;
462    let price_mantissa = cursor.read_i64_le()?;
463    let orig_qty_mantissa = cursor.read_i64_le()?;
464    let executed_qty_mantissa = cursor.read_i64_le()?;
465    let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
466    let status = cursor.read_u8()?.into();
467    let time_in_force = cursor.read_u8()?.into();
468    let order_type = cursor.read_u8()?.into();
469    let side = cursor.read_u8()?.into();
470    let stop_price_mantissa = cursor.read_optional_i64_le()?;
471    let iceberg_qty_mantissa = cursor.read_optional_i64_le()?;
472    let time = cursor.read_i64_le()?;
473    let update_time = cursor.read_i64_le()?;
474    let is_working = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
475    let working_time = cursor.read_optional_i64_le()?;
476    let orig_quote_order_qty_mantissa = cursor.read_i64_le()?;
477    let self_trade_prevention_mode = cursor.read_u8()?.into();
478
479    cursor.advance(header.block_length as usize - ORDER_FIELDS_END)?;
480
481    let symbol = cursor.read_var_string8()?;
482    let client_order_id = cursor.read_var_string8()?;
483
484    Ok(BinanceOrderResponse {
485        price_exponent,
486        qty_exponent,
487        order_id,
488        order_list_id,
489        price_mantissa,
490        orig_qty_mantissa,
491        executed_qty_mantissa,
492        cummulative_quote_qty_mantissa,
493        status,
494        time_in_force,
495        order_type,
496        side,
497        stop_price_mantissa,
498        iceberg_qty_mantissa,
499        time,
500        update_time,
501        is_working,
502        working_time,
503        orig_quote_order_qty_mantissa,
504        self_trade_prevention_mode,
505        client_order_id,
506        symbol,
507    })
508}
509
510/// Block length for orders group item.
511const ORDERS_GROUP_BLOCK_LENGTH: usize = 162;
512
513/// Decode multiple orders response.
514///
515/// # Errors
516///
517/// Returns error if buffer is too short, schema mismatch, or decode error.
518#[allow(dead_code)]
519pub fn decode_orders(buf: &[u8]) -> Result<Vec<BinanceOrderResponse>, SbeDecodeError> {
520    let mut cursor = SbeCursor::new(buf);
521    let header = MessageHeader::decode_cursor(&mut cursor)?;
522    header.validate()?;
523
524    if header.template_id != ORDERS_TEMPLATE_ID {
525        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
526    }
527
528    let (block_length, count) = cursor.read_group_header()?;
529
530    if count == 0 {
531        return Ok(Vec::new());
532    }
533
534    if block_length as usize != ORDERS_GROUP_BLOCK_LENGTH {
535        return Err(SbeDecodeError::InvalidBlockLength {
536            expected: ORDERS_GROUP_BLOCK_LENGTH as u16,
537            actual: block_length,
538        });
539    }
540
541    let mut orders = Vec::with_capacity(count as usize);
542
543    for _ in 0..count {
544        cursor.require(ORDERS_GROUP_BLOCK_LENGTH)?;
545
546        let price_exponent = cursor.read_i8()?;
547        let qty_exponent = cursor.read_i8()?;
548        let order_id = cursor.read_i64_le()?;
549        let order_list_id = cursor.read_optional_i64_le()?;
550        let price_mantissa = cursor.read_i64_le()?;
551        let orig_qty_mantissa = cursor.read_i64_le()?;
552        let executed_qty_mantissa = cursor.read_i64_le()?;
553        let cummulative_quote_qty_mantissa = cursor.read_i64_le()?;
554        let status = cursor.read_u8()?.into();
555        let time_in_force = cursor.read_u8()?.into();
556        let order_type = cursor.read_u8()?.into();
557        let side = cursor.read_u8()?.into();
558        let stop_price_mantissa = cursor.read_optional_i64_le()?;
559
560        cursor.advance(16)?; // Skip trailing_delta + trailing_time
561        let iceberg_qty_mantissa = cursor.read_optional_i64_le()?;
562        let time = cursor.read_i64_le()?;
563        let update_time = cursor.read_i64_le()?;
564        let is_working = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
565        let working_time = cursor.read_optional_i64_le()?;
566        let orig_quote_order_qty_mantissa = cursor.read_i64_le()?;
567
568        cursor.advance(14)?; // Skip strategy_id to working_floor
569        let self_trade_prevention_mode = cursor.read_u8()?.into();
570
571        cursor.advance(28)?; // Skip to end of fixed block
572
573        let symbol = cursor.read_var_string8()?;
574        let client_order_id = cursor.read_var_string8()?;
575
576        orders.push(BinanceOrderResponse {
577            price_exponent,
578            qty_exponent,
579            order_id,
580            order_list_id,
581            price_mantissa,
582            orig_qty_mantissa,
583            executed_qty_mantissa,
584            cummulative_quote_qty_mantissa,
585            status,
586            time_in_force,
587            order_type,
588            side,
589            stop_price_mantissa,
590            iceberg_qty_mantissa,
591            time,
592            update_time,
593            is_working,
594            working_time,
595            orig_quote_order_qty_mantissa,
596            self_trade_prevention_mode,
597            client_order_id,
598            symbol,
599        });
600    }
601
602    Ok(orders)
603}
604
605/// Decode cancel open orders response.
606///
607/// Each item in the response group contains an embedded cancel_order_response SBE message.
608///
609/// # Errors
610///
611/// Returns error if buffer is too short, schema mismatch, or decode error.
612#[allow(dead_code)]
613pub fn decode_cancel_open_orders(
614    buf: &[u8],
615) -> Result<Vec<BinanceCancelOrderResponse>, SbeDecodeError> {
616    let mut cursor = SbeCursor::new(buf);
617    let header = MessageHeader::decode_cursor(&mut cursor)?;
618    header.validate()?;
619
620    if header.template_id != CANCEL_OPEN_ORDERS_TEMPLATE_ID {
621        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
622    }
623
624    let (_block_length, count) = cursor.read_group_header()?;
625
626    if count == 0 {
627        return Ok(Vec::new());
628    }
629
630    let mut responses = Vec::with_capacity(count as usize);
631
632    // Each group item has block_length=0, followed by u16 length + embedded SBE message
633    for _ in 0..count {
634        let response_len = cursor.read_u16_le()? as usize;
635        let embedded_bytes = cursor.read_bytes(response_len)?;
636        let cancel_response = decode_cancel_order(embedded_bytes)?;
637        responses.push(cancel_response);
638    }
639
640    Ok(responses)
641}
642
643/// Account response block length (from SBE codec).
644const ACCOUNT_BLOCK_LENGTH: usize = 64;
645
646/// Balance group item block length (from SBE codec).
647const BALANCE_BLOCK_LENGTH: u16 = 17;
648
649/// Decode account information response.
650///
651/// # Errors
652///
653/// Returns error if buffer is too short, schema mismatch, or decode error.
654#[allow(dead_code)]
655pub fn decode_account(buf: &[u8]) -> Result<BinanceAccountInfo, SbeDecodeError> {
656    let mut cursor = SbeCursor::new(buf);
657    let header = MessageHeader::decode_cursor(&mut cursor)?;
658    header.validate()?;
659
660    if header.template_id != ACCOUNT_TEMPLATE_ID {
661        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
662    }
663
664    cursor.require(ACCOUNT_BLOCK_LENGTH)?;
665
666    let commission_exponent = cursor.read_i8()?;
667    let maker_commission_mantissa = cursor.read_i64_le()?;
668    let taker_commission_mantissa = cursor.read_i64_le()?;
669    let buyer_commission_mantissa = cursor.read_i64_le()?;
670    let seller_commission_mantissa = cursor.read_i64_le()?;
671    let can_trade = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
672    let can_withdraw = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
673    let can_deposit = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
674    cursor.advance(1)?; // Skip brokered
675    let require_self_trade_prevention = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
676    let prevent_sor = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
677    let update_time = cursor.read_i64_le()?;
678    let account_type_enum = AccountType::from(cursor.read_u8()?);
679    cursor.advance(16)?; // Skip tradeGroupId + uid
680
681    let account_type = account_type_enum.to_string();
682
683    let (block_length, balance_count) = cursor.read_group_header()?;
684
685    if block_length != BALANCE_BLOCK_LENGTH {
686        return Err(SbeDecodeError::InvalidBlockLength {
687            expected: BALANCE_BLOCK_LENGTH,
688            actual: block_length,
689        });
690    }
691
692    let mut balances = Vec::with_capacity(balance_count as usize);
693
694    for _ in 0..balance_count {
695        cursor.require(block_length as usize)?;
696
697        let exponent = cursor.read_i8()?;
698        let free_mantissa = cursor.read_i64_le()?;
699        let locked_mantissa = cursor.read_i64_le()?;
700
701        let asset = cursor.read_var_string8()?;
702
703        balances.push(BinanceBalance {
704            asset,
705            free_mantissa,
706            locked_mantissa,
707            exponent,
708        });
709    }
710
711    Ok(BinanceAccountInfo {
712        commission_exponent,
713        maker_commission_mantissa,
714        taker_commission_mantissa,
715        buyer_commission_mantissa,
716        seller_commission_mantissa,
717        can_trade,
718        can_withdraw,
719        can_deposit,
720        require_self_trade_prevention,
721        prevent_sor,
722        update_time,
723        account_type,
724        balances,
725    })
726}
727
728/// Account trade group item block length (from SBE codec).
729const ACCOUNT_TRADE_BLOCK_LENGTH: u16 = 70;
730
731/// Decode account trades response.
732///
733/// # Errors
734///
735/// Returns error if buffer is too short, schema mismatch, or decode error.
736#[allow(dead_code)]
737pub fn decode_account_trades(buf: &[u8]) -> Result<Vec<BinanceAccountTrade>, SbeDecodeError> {
738    let mut cursor = SbeCursor::new(buf);
739    let header = MessageHeader::decode_cursor(&mut cursor)?;
740    header.validate()?;
741
742    if header.template_id != ACCOUNT_TRADES_TEMPLATE_ID {
743        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
744    }
745
746    let (block_length, trade_count) = cursor.read_group_header()?;
747
748    if block_length != ACCOUNT_TRADE_BLOCK_LENGTH {
749        return Err(SbeDecodeError::InvalidBlockLength {
750            expected: ACCOUNT_TRADE_BLOCK_LENGTH,
751            actual: block_length,
752        });
753    }
754
755    let mut trades = Vec::with_capacity(trade_count as usize);
756
757    for _ in 0..trade_count {
758        cursor.require(block_length as usize)?;
759
760        let price_exponent = cursor.read_i8()?;
761        let qty_exponent = cursor.read_i8()?;
762        let commission_exponent = cursor.read_i8()?;
763        let id = cursor.read_i64_le()?;
764        let order_id = cursor.read_i64_le()?;
765        let order_list_id = cursor.read_optional_i64_le()?;
766        let price_mantissa = cursor.read_i64_le()?;
767        let qty_mantissa = cursor.read_i64_le()?;
768        let quote_qty_mantissa = cursor.read_i64_le()?;
769        let commission_mantissa = cursor.read_i64_le()?;
770        let time = cursor.read_i64_le()?;
771        let is_buyer = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
772        let is_maker = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
773        let is_best_match = BoolEnum::from(cursor.read_u8()?) == BoolEnum::True;
774
775        let symbol = cursor.read_var_string8()?;
776        let commission_asset = cursor.read_var_string8()?;
777
778        trades.push(BinanceAccountTrade {
779            price_exponent,
780            qty_exponent,
781            commission_exponent,
782            id,
783            order_id,
784            order_list_id,
785            price_mantissa,
786            qty_mantissa,
787            quote_qty_mantissa,
788            commission_mantissa,
789            time,
790            is_buyer,
791            is_maker,
792            is_best_match,
793            symbol,
794            commission_asset,
795        });
796    }
797
798    Ok(trades)
799}
800
801/// Fills group item block length (from SBE codec).
802const FILLS_BLOCK_LENGTH: u16 = 42;
803
804/// Decode order fills using cursor.
805fn decode_fills_cursor(
806    cursor: &mut SbeCursor<'_>,
807) -> Result<Vec<BinanceOrderFill>, SbeDecodeError> {
808    let (block_length, count) = cursor.read_group_header()?;
809
810    if block_length != FILLS_BLOCK_LENGTH {
811        return Err(SbeDecodeError::InvalidBlockLength {
812            expected: FILLS_BLOCK_LENGTH,
813            actual: block_length,
814        });
815    }
816
817    let mut fills = Vec::with_capacity(count as usize);
818
819    for _ in 0..count {
820        cursor.require(block_length as usize)?;
821
822        let commission_exponent = cursor.read_i8()?;
823        cursor.advance(1)?; // Skip matchType
824        let price_mantissa = cursor.read_i64_le()?;
825        let qty_mantissa = cursor.read_i64_le()?;
826        let commission_mantissa = cursor.read_i64_le()?;
827        let trade_id = cursor.read_optional_i64_le()?;
828        cursor.advance(8)?; // Skip allocId
829
830        let commission_asset = cursor.read_var_string8()?;
831
832        fills.push(BinanceOrderFill {
833            price_mantissa,
834            qty_mantissa,
835            commission_mantissa,
836            commission_exponent,
837            commission_asset,
838            trade_id,
839        });
840    }
841
842    Ok(fills)
843}
844
845/// Symbols group block length (from SBE codec).
846const SYMBOL_BLOCK_LENGTH: usize = 19;
847
848/// Decode exchange info response.
849///
850/// ExchangeInfo response contains rate limits, exchange filters, symbols, and SOR info.
851/// We only decode the symbols array which contains instrument definitions.
852///
853/// # Errors
854///
855/// Returns error if buffer is too short, schema mismatch, or template ID mismatch.
856///
857/// # Panics
858///
859/// This function will panic if filter byte slices cannot be converted to fixed-size arrays,
860/// which should not occur if the SBE data is well-formed.
861pub fn decode_exchange_info(buf: &[u8]) -> Result<BinanceExchangeInfoSbe, SbeDecodeError> {
862    let mut cursor = SbeCursor::new(buf);
863    let header = MessageHeader::decode_cursor(&mut cursor)?;
864    header.validate()?;
865
866    if header.template_id != EXCHANGE_INFO_TEMPLATE_ID {
867        return Err(SbeDecodeError::UnknownTemplateId(header.template_id));
868    }
869
870    // Skip rate_limits group
871    let (rate_limits_block_len, rate_limits_count) = cursor.read_group_header()?;
872    cursor.advance(rate_limits_block_len as usize * rate_limits_count as usize)?;
873
874    // Skip exchange_filters group
875    let (_exchange_filters_block_len, exchange_filters_count) = cursor.read_group_header()?;
876    for _ in 0..exchange_filters_count {
877        // Each filter is a varString8
878        cursor.read_var_string8()?;
879    }
880
881    // Decode symbols group
882    let (symbols_block_len, symbols_count) = cursor.read_group_header()?;
883
884    if symbols_block_len != SYMBOL_BLOCK_LENGTH as u16 {
885        return Err(SbeDecodeError::InvalidBlockLength {
886            expected: SYMBOL_BLOCK_LENGTH as u16,
887            actual: symbols_block_len,
888        });
889    }
890
891    let mut symbols = Vec::with_capacity(symbols_count as usize);
892
893    for _ in 0..symbols_count {
894        cursor.require(SYMBOL_BLOCK_LENGTH)?;
895
896        // Fixed fields (19 bytes)
897        let status = cursor.read_u8()?;
898        let base_asset_precision = cursor.read_u8()?;
899        let quote_asset_precision = cursor.read_u8()?;
900        let _base_commission_precision = cursor.read_u8()?;
901        let _quote_commission_precision = cursor.read_u8()?;
902        let order_types = cursor.read_u16_le()?;
903        let iceberg_allowed = cursor.read_u8()? == BoolEnum::True as u8;
904        let oco_allowed = cursor.read_u8()? == BoolEnum::True as u8;
905        let oto_allowed = cursor.read_u8()? == BoolEnum::True as u8;
906        let quote_order_qty_market_allowed = cursor.read_u8()? == BoolEnum::True as u8;
907        let allow_trailing_stop = cursor.read_u8()? == BoolEnum::True as u8;
908        let cancel_replace_allowed = cursor.read_u8()? == BoolEnum::True as u8;
909        let amend_allowed = cursor.read_u8()? == BoolEnum::True as u8;
910        let is_spot_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
911        let is_margin_trading_allowed = cursor.read_u8()? == BoolEnum::True as u8;
912        let _default_self_trade_prevention_mode = cursor.read_u8()?;
913        let _allowed_self_trade_prevention_modes = cursor.read_u8()?;
914        let _peg_instructions_allowed = cursor.read_u8()?;
915
916        let (_filters_block_len, filters_count) = cursor.read_group_header()?;
917        let mut filters = BinanceSymbolFiltersSbe::default();
918
919        for _ in 0..filters_count {
920            let filter_bytes = cursor.read_var_bytes8()?;
921
922            // Filters can have header (8 bytes) or be raw body only,
923            // detect format by checking if bytes [2..4] contain a valid template_id
924            let (template_id, offset) = if filter_bytes.len() >= HEADER_LENGTH + 2 {
925                let potential_template = u16::from_le_bytes([filter_bytes[2], filter_bytes[3]]);
926                if potential_template == PRICE_FILTER_TEMPLATE_ID
927                    || potential_template == LOT_SIZE_FILTER_TEMPLATE_ID
928                {
929                    (potential_template, HEADER_LENGTH)
930                } else {
931                    let raw_template = u16::from_le_bytes([filter_bytes[0], filter_bytes[1]]);
932                    (raw_template, 2)
933                }
934            } else if filter_bytes.len() >= 2 {
935                let raw_template = u16::from_le_bytes([filter_bytes[0], filter_bytes[1]]);
936                (raw_template, 2)
937            } else {
938                continue;
939            };
940
941            // Filter body layout: exponent(1) + min(8) + max(8) + size(8) = 25 bytes
942            match template_id {
943                PRICE_FILTER_TEMPLATE_ID if filter_bytes.len() >= offset + 25 => {
944                    let price_exp = filter_bytes[offset] as i8;
945                    let min_price = i64::from_le_bytes(
946                        filter_bytes[offset + 1..offset + 9].try_into().unwrap(),
947                    );
948                    let max_price = i64::from_le_bytes(
949                        filter_bytes[offset + 9..offset + 17].try_into().unwrap(),
950                    );
951                    let tick_size = i64::from_le_bytes(
952                        filter_bytes[offset + 17..offset + 25].try_into().unwrap(),
953                    );
954                    filters.price_filter = Some(BinancePriceFilterSbe {
955                        price_exponent: price_exp,
956                        min_price,
957                        max_price,
958                        tick_size,
959                    });
960                }
961                LOT_SIZE_FILTER_TEMPLATE_ID if filter_bytes.len() >= offset + 25 => {
962                    let qty_exp = filter_bytes[offset] as i8;
963                    let min_qty = i64::from_le_bytes(
964                        filter_bytes[offset + 1..offset + 9].try_into().unwrap(),
965                    );
966                    let max_qty = i64::from_le_bytes(
967                        filter_bytes[offset + 9..offset + 17].try_into().unwrap(),
968                    );
969                    let step_size = i64::from_le_bytes(
970                        filter_bytes[offset + 17..offset + 25].try_into().unwrap(),
971                    );
972                    filters.lot_size_filter = Some(BinanceLotSizeFilterSbe {
973                        qty_exponent: qty_exp,
974                        min_qty,
975                        max_qty,
976                        step_size,
977                    });
978                }
979                _ => {}
980            }
981        }
982
983        // Permission sets nested group
984        let (_perm_sets_block_len, perm_sets_count) = cursor.read_group_header()?;
985        let mut permissions = Vec::with_capacity(perm_sets_count as usize);
986        for _ in 0..perm_sets_count {
987            // Permissions nested group
988            let (_perms_block_len, perms_count) = cursor.read_group_header()?;
989            let mut perm_set = Vec::with_capacity(perms_count as usize);
990            for _ in 0..perms_count {
991                let perm = cursor.read_var_string8()?;
992                perm_set.push(perm);
993            }
994            permissions.push(perm_set);
995        }
996
997        // Variable-length strings
998        let symbol = cursor.read_var_string8()?;
999        let base_asset = cursor.read_var_string8()?;
1000        let quote_asset = cursor.read_var_string8()?;
1001
1002        symbols.push(BinanceSymbolSbe {
1003            symbol,
1004            base_asset,
1005            quote_asset,
1006            base_asset_precision,
1007            quote_asset_precision,
1008            status,
1009            order_types,
1010            iceberg_allowed,
1011            oco_allowed,
1012            oto_allowed,
1013            quote_order_qty_market_allowed,
1014            allow_trailing_stop,
1015            cancel_replace_allowed,
1016            amend_allowed,
1017            is_spot_trading_allowed,
1018            is_margin_trading_allowed,
1019            filters,
1020            permissions,
1021        });
1022    }
1023
1024    // Skip SOR group (we don't need it)
1025
1026    Ok(BinanceExchangeInfoSbe { symbols })
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use rstest::rstest;
1032
1033    use super::*;
1034
1035    /// Schema v1 block length for new order full response (template 302).
1036    const NEW_ORDER_FULL_BLOCK_LENGTH: usize = 153;
1037
1038    /// Schema v1 block length for cancel order response (template 305).
1039    const CANCEL_ORDER_BLOCK_LENGTH: usize = 137;
1040
1041    /// Schema v1 block length for order response / query (template 304).
1042    const ORDER_BLOCK_LENGTH: usize = 153;
1043
1044    fn create_header(block_length: u16, template_id: u16, schema_id: u16, version: u16) -> [u8; 8] {
1045        let mut buf = [0u8; 8];
1046        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
1047        buf[2..4].copy_from_slice(&template_id.to_le_bytes());
1048        buf[4..6].copy_from_slice(&schema_id.to_le_bytes());
1049        buf[6..8].copy_from_slice(&version.to_le_bytes());
1050        buf
1051    }
1052
1053    #[rstest]
1054    fn test_decode_ping_valid() {
1055        // Ping: block_length=0, template_id=101, schema_id=3, version=1
1056        let buf = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1057        assert!(decode_ping(&buf).is_ok());
1058    }
1059
1060    #[rstest]
1061    fn test_decode_ping_buffer_too_short() {
1062        let buf = [0u8; 4];
1063        let err = decode_ping(&buf).unwrap_err();
1064        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1065    }
1066
1067    #[rstest]
1068    fn test_decode_ping_schema_mismatch() {
1069        let buf = create_header(0, PING_TEMPLATE_ID, 99, SBE_SCHEMA_VERSION);
1070        let err = decode_ping(&buf).unwrap_err();
1071        assert!(matches!(err, SbeDecodeError::SchemaMismatch { .. }));
1072    }
1073
1074    #[rstest]
1075    fn test_decode_ping_wrong_template() {
1076        let buf = create_header(0, 999, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1077        let err = decode_ping(&buf).unwrap_err();
1078        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(999)));
1079    }
1080
1081    #[rstest]
1082    fn test_decode_server_time_valid() {
1083        // ServerTime: block_length=8, template_id=102, schema_id=3, version=1
1084        let header = create_header(
1085            8,
1086            SERVER_TIME_TEMPLATE_ID,
1087            SBE_SCHEMA_ID,
1088            SBE_SCHEMA_VERSION,
1089        );
1090        let timestamp: i64 = 1734300000000; // Example timestamp
1091
1092        let mut buf = Vec::with_capacity(16);
1093        buf.extend_from_slice(&header);
1094        buf.extend_from_slice(&timestamp.to_le_bytes());
1095
1096        let result = decode_server_time(&buf).unwrap();
1097        assert_eq!(result, timestamp);
1098    }
1099
1100    #[rstest]
1101    fn test_decode_server_time_buffer_too_short() {
1102        // Header only, missing body
1103        let buf = create_header(
1104            8,
1105            SERVER_TIME_TEMPLATE_ID,
1106            SBE_SCHEMA_ID,
1107            SBE_SCHEMA_VERSION,
1108        );
1109        let err = decode_server_time(&buf).unwrap_err();
1110        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1111    }
1112
1113    #[rstest]
1114    fn test_decode_server_time_wrong_template() {
1115        let header = create_header(8, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1116        let mut buf = Vec::with_capacity(16);
1117        buf.extend_from_slice(&header);
1118        buf.extend_from_slice(&0i64.to_le_bytes());
1119
1120        let err = decode_server_time(&buf).unwrap_err();
1121        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1122    }
1123
1124    #[rstest]
1125    fn test_decode_server_time_version_mismatch() {
1126        let header = create_header(8, SERVER_TIME_TEMPLATE_ID, SBE_SCHEMA_ID, 99);
1127        let mut buf = Vec::with_capacity(16);
1128        buf.extend_from_slice(&header);
1129        buf.extend_from_slice(&0i64.to_le_bytes());
1130
1131        let err = decode_server_time(&buf).unwrap_err();
1132        assert!(matches!(err, SbeDecodeError::VersionMismatch { .. }));
1133    }
1134
1135    fn create_group_header(block_length: u16, count: u32) -> [u8; 6] {
1136        let mut buf = [0u8; 6];
1137        buf[0..2].copy_from_slice(&block_length.to_le_bytes());
1138        buf[2..6].copy_from_slice(&count.to_le_bytes());
1139        buf
1140    }
1141
1142    #[rstest]
1143    fn test_decode_depth_valid() {
1144        // Depth: block_length=10, template_id=200
1145        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1146
1147        let mut buf = Vec::new();
1148        buf.extend_from_slice(&header);
1149
1150        // Block: last_update_id (8) + price_exponent (1) + qty_exponent (1)
1151        let last_update_id: i64 = 123456789;
1152        let price_exponent: i8 = -8;
1153        let qty_exponent: i8 = -8;
1154        buf.extend_from_slice(&last_update_id.to_le_bytes());
1155        buf.push(price_exponent as u8);
1156        buf.push(qty_exponent as u8);
1157
1158        // Bids group: 2 levels
1159        buf.extend_from_slice(&create_group_header(16, 2));
1160        // Bid 1: price=100000000000, qty=50000000
1161        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes());
1162        buf.extend_from_slice(&50_000_000i64.to_le_bytes());
1163        // Bid 2: price=99900000000, qty=30000000
1164        buf.extend_from_slice(&99_900_000_000i64.to_le_bytes());
1165        buf.extend_from_slice(&30_000_000i64.to_le_bytes());
1166
1167        // Asks group: 1 level
1168        buf.extend_from_slice(&create_group_header(16, 1));
1169        // Ask 1: price=100100000000, qty=25000000
1170        buf.extend_from_slice(&100_100_000_000i64.to_le_bytes());
1171        buf.extend_from_slice(&25_000_000i64.to_le_bytes());
1172
1173        let depth = decode_depth(&buf).unwrap();
1174
1175        assert_eq!(depth.last_update_id, 123456789);
1176        assert_eq!(depth.price_exponent, -8);
1177        assert_eq!(depth.qty_exponent, -8);
1178        assert_eq!(depth.bids.len(), 2);
1179        assert_eq!(depth.asks.len(), 1);
1180        assert_eq!(depth.bids[0].price_mantissa, 100_000_000_000);
1181        assert_eq!(depth.bids[0].qty_mantissa, 50_000_000);
1182        assert_eq!(depth.asks[0].price_mantissa, 100_100_000_000);
1183    }
1184
1185    #[rstest]
1186    fn test_decode_depth_empty_book() {
1187        let header = create_header(10, DEPTH_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1188
1189        let mut buf = Vec::new();
1190        buf.extend_from_slice(&header);
1191        buf.extend_from_slice(&0i64.to_le_bytes()); // last_update_id
1192        buf.push(0); // price_exponent
1193        buf.push(0); // qty_exponent
1194
1195        // Empty bids
1196        buf.extend_from_slice(&create_group_header(16, 0));
1197        // Empty asks
1198        buf.extend_from_slice(&create_group_header(16, 0));
1199
1200        let depth = decode_depth(&buf).unwrap();
1201
1202        assert!(depth.bids.is_empty());
1203        assert!(depth.asks.is_empty());
1204    }
1205
1206    #[rstest]
1207    fn test_decode_trades_valid() {
1208        // Trades: block_length=2, template_id=201
1209        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1210
1211        let mut buf = Vec::new();
1212        buf.extend_from_slice(&header);
1213
1214        // Block: price_exponent (1) + qty_exponent (1)
1215        let price_exponent: i8 = -8;
1216        let qty_exponent: i8 = -8;
1217        buf.push(price_exponent as u8);
1218        buf.push(qty_exponent as u8);
1219
1220        // Trades group: 1 trade (42 bytes each)
1221        buf.extend_from_slice(&create_group_header(42, 1));
1222
1223        // Trade: id(8) + price(8) + qty(8) + quoteQty(8) + time(8) + isBuyerMaker(1) + isBestMatch(1)
1224        let trade_id: i64 = 999;
1225        let price: i64 = 100_000_000_000;
1226        let qty: i64 = 10_000_000;
1227        let quote_qty: i64 = 1_000_000_000_000;
1228        let time: i64 = 1734300000000;
1229        let is_buyer_maker: u8 = 1; // true
1230        let is_best_match: u8 = 1; // true
1231
1232        buf.extend_from_slice(&trade_id.to_le_bytes());
1233        buf.extend_from_slice(&price.to_le_bytes());
1234        buf.extend_from_slice(&qty.to_le_bytes());
1235        buf.extend_from_slice(&quote_qty.to_le_bytes());
1236        buf.extend_from_slice(&time.to_le_bytes());
1237        buf.push(is_buyer_maker);
1238        buf.push(is_best_match);
1239
1240        let trades = decode_trades(&buf).unwrap();
1241
1242        assert_eq!(trades.price_exponent, -8);
1243        assert_eq!(trades.qty_exponent, -8);
1244        assert_eq!(trades.trades.len(), 1);
1245        assert_eq!(trades.trades[0].id, 999);
1246        assert_eq!(trades.trades[0].price_mantissa, 100_000_000_000);
1247        assert!(trades.trades[0].is_buyer_maker);
1248        assert!(trades.trades[0].is_best_match);
1249    }
1250
1251    #[rstest]
1252    fn test_decode_trades_empty() {
1253        let header = create_header(2, TRADES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1254
1255        let mut buf = Vec::new();
1256        buf.extend_from_slice(&header);
1257        buf.push(0); // price_exponent
1258        buf.push(0); // qty_exponent
1259
1260        // Empty trades group
1261        buf.extend_from_slice(&create_group_header(42, 0));
1262
1263        let trades = decode_trades(&buf).unwrap();
1264
1265        assert!(trades.trades.is_empty());
1266    }
1267
1268    #[rstest]
1269    fn test_decode_depth_wrong_template() {
1270        let header = create_header(10, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1271
1272        let mut buf = Vec::new();
1273        buf.extend_from_slice(&header);
1274        buf.extend_from_slice(&[0u8; 10]); // dummy block
1275
1276        let err = decode_depth(&buf).unwrap_err();
1277        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1278    }
1279
1280    #[rstest]
1281    fn test_decode_trades_wrong_template() {
1282        let header = create_header(2, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1283
1284        let mut buf = Vec::new();
1285        buf.extend_from_slice(&header);
1286        buf.extend_from_slice(&[0u8; 2]); // dummy block
1287
1288        let err = decode_trades(&buf).unwrap_err();
1289        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1290    }
1291
1292    fn write_var_string(buf: &mut Vec<u8>, s: &str) {
1293        buf.push(s.len() as u8);
1294        buf.extend_from_slice(s.as_bytes());
1295    }
1296
1297    #[rstest]
1298    fn test_decode_order_valid() {
1299        let header = create_header(
1300            ORDER_BLOCK_LENGTH as u16,
1301            ORDER_TEMPLATE_ID,
1302            SBE_SCHEMA_ID,
1303            SBE_SCHEMA_VERSION,
1304        );
1305
1306        let mut buf = Vec::new();
1307        buf.extend_from_slice(&header);
1308
1309        // Fixed block (153 bytes)
1310        buf.push((-8i8) as u8); // price_exponent
1311        buf.push((-8i8) as u8); // qty_exponent
1312        buf.extend_from_slice(&12345i64.to_le_bytes()); // order_id
1313        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1314        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1315        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty_mantissa
1316        buf.extend_from_slice(&5_000_000i64.to_le_bytes()); // executed_qty_mantissa
1317        buf.extend_from_slice(&500_000_000i64.to_le_bytes()); // cummulative_quote_qty_mantissa
1318        buf.push(1); // status (NEW)
1319        buf.push(1); // time_in_force (GTC)
1320        buf.push(1); // order_type (LIMIT)
1321        buf.push(1); // side (BUY)
1322        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1323        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1324        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1325        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1326        buf.push(1); // is_working (true)
1327        buf.extend_from_slice(&1734300000500i64.to_le_bytes()); // working_time
1328        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty_mantissa
1329        buf.push(0); // self_trade_prevention_mode
1330
1331        // Pad to 153 bytes
1332        while buf.len() < 8 + ORDER_BLOCK_LENGTH {
1333            buf.push(0);
1334        }
1335
1336        write_var_string(&mut buf, "BTCUSDT");
1337        write_var_string(&mut buf, "my-order-123");
1338
1339        let order = decode_order(&buf).unwrap();
1340
1341        assert_eq!(order.order_id, 12345);
1342        assert!(order.order_list_id.is_none());
1343        assert_eq!(order.price_exponent, -8);
1344        assert_eq!(order.price_mantissa, 100_000_000_000);
1345        assert!(order.stop_price_mantissa.is_none());
1346        assert!(order.iceberg_qty_mantissa.is_none());
1347        assert!(order.is_working);
1348        assert_eq!(order.working_time, Some(1734300000500));
1349        assert_eq!(order.symbol, "BTCUSDT");
1350        assert_eq!(order.client_order_id, "my-order-123");
1351    }
1352
1353    #[rstest]
1354    fn test_decode_order_future_block_length() {
1355        // Verify the dynamic skip handles a future block_length larger than v1.
1356        const FUTURE_BLOCK_LENGTH: u16 = ORDER_BLOCK_LENGTH as u16 + 4;
1357        let header = create_header(
1358            FUTURE_BLOCK_LENGTH,
1359            ORDER_TEMPLATE_ID,
1360            SBE_SCHEMA_ID,
1361            SBE_SCHEMA_VERSION,
1362        );
1363
1364        let mut buf = Vec::new();
1365        buf.extend_from_slice(&header);
1366
1367        buf.push((-8i8) as u8); // price_exponent
1368        buf.push((-8i8) as u8); // qty_exponent
1369        buf.extend_from_slice(&12345i64.to_le_bytes()); // order_id
1370        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1371        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1372        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1373        buf.extend_from_slice(&5_000_000i64.to_le_bytes()); // executed_qty
1374        buf.extend_from_slice(&500_000_000i64.to_le_bytes()); // cumulative_quote_qty
1375        buf.push(1); // status (NEW)
1376        buf.push(1); // time_in_force (GTC)
1377        buf.push(1); // order_type (LIMIT)
1378        buf.push(1); // side (BUY)
1379        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1380        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1381        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1382        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1383        buf.push(1); // is_working (true)
1384        buf.extend_from_slice(&1734300000500i64.to_le_bytes()); // working_time
1385        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1386        buf.push(0); // self_trade_prevention_mode
1387
1388        while buf.len() < 8 + FUTURE_BLOCK_LENGTH as usize {
1389            buf.push(0); // Pad for hypothetical future fields
1390        }
1391
1392        write_var_string(&mut buf, "ETHUSDT");
1393        write_var_string(&mut buf, "order-future");
1394
1395        let order = decode_order(&buf).unwrap();
1396
1397        assert_eq!(order.order_id, 12345);
1398        assert_eq!(order.symbol, "ETHUSDT");
1399        assert_eq!(order.client_order_id, "order-future");
1400    }
1401
1402    #[rstest]
1403    fn test_decode_orders_multiple() {
1404        // This test verifies cursor advances correctly through multiple orders
1405        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1406
1407        let mut buf = Vec::new();
1408        buf.extend_from_slice(&header);
1409
1410        // Group header: block_length=162, count=2
1411        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 2));
1412
1413        // Order 1
1414        let order1_start = buf.len();
1415        buf.push((-8i8) as u8); // price_exponent
1416        buf.push((-8i8) as u8); // qty_exponent
1417        buf.extend_from_slice(&1001i64.to_le_bytes()); // order_id
1418        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1419        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1420        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1421        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1422        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1423        buf.push(1); // status
1424        buf.push(1); // time_in_force
1425        buf.push(1); // order_type
1426        buf.push(1); // side
1427        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1428        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1429        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1430        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1431        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
1432        buf.push(1); // is_working
1433        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // working_time
1434        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1435
1436        // Pad to 162 bytes from order start
1437        while buf.len() - order1_start < ORDERS_GROUP_BLOCK_LENGTH {
1438            buf.push(0);
1439        }
1440        write_var_string(&mut buf, "BTCUSDT");
1441        write_var_string(&mut buf, "order-1");
1442
1443        // Order 2
1444        let order2_start = buf.len();
1445        buf.push((-8i8) as u8); // price_exponent
1446        buf.push((-8i8) as u8); // qty_exponent
1447        buf.extend_from_slice(&2002i64.to_le_bytes()); // order_id
1448        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1449        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes()); // price_mantissa
1450        buf.extend_from_slice(&20_000_000i64.to_le_bytes()); // orig_qty
1451        buf.extend_from_slice(&0i64.to_le_bytes()); // executed_qty
1452        buf.extend_from_slice(&0i64.to_le_bytes()); // cummulative_quote_qty
1453        buf.push(1); // status
1454        buf.push(1); // time_in_force
1455        buf.push(1); // order_type
1456        buf.push(2); // side (SELL)
1457        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // stop_price (None)
1458        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
1459        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // iceberg_qty (None)
1460        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // time
1461        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // update_time
1462        buf.push(1); // is_working
1463        buf.extend_from_slice(&1734300001000i64.to_le_bytes()); // working_time
1464        buf.extend_from_slice(&0i64.to_le_bytes()); // orig_quote_order_qty
1465
1466        while buf.len() - order2_start < ORDERS_GROUP_BLOCK_LENGTH {
1467            buf.push(0);
1468        }
1469        write_var_string(&mut buf, "ETHUSDT");
1470        write_var_string(&mut buf, "order-2");
1471
1472        let orders = decode_orders(&buf).unwrap();
1473
1474        assert_eq!(orders.len(), 2);
1475        assert_eq!(orders[0].order_id, 1001);
1476        assert_eq!(orders[0].symbol, "BTCUSDT");
1477        assert_eq!(orders[0].client_order_id, "order-1");
1478        assert_eq!(orders[0].price_mantissa, 100_000_000_000);
1479
1480        assert_eq!(orders[1].order_id, 2002);
1481        assert_eq!(orders[1].symbol, "ETHUSDT");
1482        assert_eq!(orders[1].client_order_id, "order-2");
1483        assert_eq!(orders[1].price_mantissa, 200_000_000_000);
1484    }
1485
1486    #[rstest]
1487    fn test_decode_orders_empty() {
1488        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1489
1490        let mut buf = Vec::new();
1491        buf.extend_from_slice(&header);
1492        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 0));
1493
1494        let orders = decode_orders(&buf).unwrap();
1495        assert!(orders.is_empty());
1496    }
1497
1498    #[rstest]
1499    fn test_decode_orders_truncated_var_string() {
1500        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1501
1502        let mut buf = Vec::new();
1503        buf.extend_from_slice(&header);
1504        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 1));
1505
1506        // Pad fixed block to 162 bytes
1507        buf.extend_from_slice(&[0u8; ORDERS_GROUP_BLOCK_LENGTH]);
1508
1509        // Symbol length says 7 bytes but we only provide 3
1510        buf.push(7); // Length prefix claims "BTCUSDT" (7 chars)
1511        buf.extend_from_slice(b"BTC"); // Only 3 bytes - truncated
1512
1513        let err = decode_orders(&buf).unwrap_err();
1514        assert!(matches!(err, SbeDecodeError::BufferTooShort { .. }));
1515    }
1516
1517    #[rstest]
1518    fn test_decode_orders_invalid_utf8() {
1519        let header = create_header(0, ORDERS_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1520
1521        let mut buf = Vec::new();
1522        buf.extend_from_slice(&header);
1523        buf.extend_from_slice(&create_group_header(ORDERS_GROUP_BLOCK_LENGTH as u16, 1));
1524
1525        buf.extend_from_slice(&[0u8; ORDERS_GROUP_BLOCK_LENGTH]);
1526
1527        // Invalid UTF-8 sequence
1528        buf.push(4);
1529        buf.extend_from_slice(&[0xFF, 0xFE, 0x00, 0x01]);
1530
1531        let err = decode_orders(&buf).unwrap_err();
1532        assert!(matches!(err, SbeDecodeError::InvalidUtf8));
1533    }
1534
1535    #[rstest]
1536    fn test_decode_cancel_order_valid() {
1537        let header = create_header(
1538            CANCEL_ORDER_BLOCK_LENGTH as u16,
1539            CANCEL_ORDER_TEMPLATE_ID,
1540            SBE_SCHEMA_ID,
1541            SBE_SCHEMA_VERSION,
1542        );
1543
1544        let mut buf = Vec::new();
1545        buf.extend_from_slice(&header);
1546
1547        buf.push((-8i8) as u8); // price_exponent
1548        buf.push((-8i8) as u8); // qty_exponent
1549        buf.extend_from_slice(&99999i64.to_le_bytes()); // order_id
1550        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1551        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // transact_time
1552        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1553        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1554        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // executed_qty
1555        buf.extend_from_slice(&1_000_000_000i64.to_le_bytes()); // cummulative_quote_qty
1556        buf.push(4); // status (CANCELED)
1557        buf.push(1); // time_in_force
1558        buf.push(1); // order_type
1559        buf.push(1); // side
1560        buf.push(0); // self_trade_prevention_mode
1561
1562        // Pad to block length
1563        while buf.len() < 8 + CANCEL_ORDER_BLOCK_LENGTH {
1564            buf.push(0);
1565        }
1566
1567        write_var_string(&mut buf, "BTCUSDT");
1568        write_var_string(&mut buf, "orig-client-id");
1569        write_var_string(&mut buf, "new-client-id");
1570
1571        let cancel = decode_cancel_order(&buf).unwrap();
1572
1573        assert_eq!(cancel.order_id, 99999);
1574        assert!(cancel.order_list_id.is_none());
1575        assert_eq!(cancel.symbol, "BTCUSDT");
1576        assert_eq!(cancel.orig_client_order_id, "orig-client-id");
1577        assert_eq!(cancel.client_order_id, "new-client-id");
1578    }
1579
1580    #[rstest]
1581    fn test_decode_cancel_order_future_block_length() {
1582        // Verify the dynamic skip handles a future block_length larger than v1.
1583        const FUTURE_BLOCK_LENGTH: u16 = CANCEL_ORDER_BLOCK_LENGTH as u16 + 4;
1584        let header = create_header(
1585            FUTURE_BLOCK_LENGTH,
1586            CANCEL_ORDER_TEMPLATE_ID,
1587            SBE_SCHEMA_ID,
1588            SBE_SCHEMA_VERSION,
1589        );
1590
1591        let mut buf = Vec::new();
1592        buf.extend_from_slice(&header);
1593
1594        buf.push((-8i8) as u8); // price_exponent
1595        buf.push((-8i8) as u8); // qty_exponent
1596        buf.extend_from_slice(&99999i64.to_le_bytes()); // order_id
1597        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1598        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // transact_time
1599        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price_mantissa
1600        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
1601        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // executed_qty
1602        buf.extend_from_slice(&1_000_000_000i64.to_le_bytes()); // cumulative_quote_qty
1603        buf.push(4); // status (CANCELED)
1604        buf.push(1); // time_in_force
1605        buf.push(1); // order_type
1606        buf.push(1); // side
1607        buf.push(0); // self_trade_prevention_mode
1608
1609        while buf.len() < 8 + FUTURE_BLOCK_LENGTH as usize {
1610            buf.push(0); // Pad for hypothetical future fields
1611        }
1612
1613        write_var_string(&mut buf, "BTCUSDT");
1614        write_var_string(&mut buf, "orig-id");
1615        write_var_string(&mut buf, "new-id");
1616
1617        let cancel = decode_cancel_order(&buf).unwrap();
1618
1619        assert_eq!(cancel.order_id, 99999);
1620        assert_eq!(cancel.symbol, "BTCUSDT");
1621        assert_eq!(cancel.orig_client_order_id, "orig-id");
1622        assert_eq!(cancel.client_order_id, "new-id");
1623    }
1624
1625    #[rstest]
1626    fn test_decode_account_with_balances() {
1627        let header = create_header(
1628            ACCOUNT_BLOCK_LENGTH as u16,
1629            ACCOUNT_TEMPLATE_ID,
1630            SBE_SCHEMA_ID,
1631            SBE_SCHEMA_VERSION,
1632        );
1633
1634        let mut buf = Vec::new();
1635        buf.extend_from_slice(&header);
1636
1637        // Fixed block (64 bytes)
1638        buf.push((-8i8) as u8); // commission_exponent
1639        buf.extend_from_slice(&100_000i64.to_le_bytes()); // maker_commission
1640        buf.extend_from_slice(&100_000i64.to_le_bytes()); // taker_commission
1641        buf.extend_from_slice(&0i64.to_le_bytes()); // buyer_commission
1642        buf.extend_from_slice(&0i64.to_le_bytes()); // seller_commission
1643        buf.push(1); // can_trade
1644        buf.push(1); // can_withdraw
1645        buf.push(1); // can_deposit
1646        buf.push(0); // brokered
1647        buf.push(0); // require_self_trade_prevention
1648        buf.push(0); // prevent_sor
1649        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // update_time
1650        buf.push(1); // account_type (SPOT)
1651
1652        // Pad to 64 bytes
1653        while buf.len() < 8 + ACCOUNT_BLOCK_LENGTH {
1654            buf.push(0);
1655        }
1656
1657        // Balances group: 2 balances
1658        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 2));
1659
1660        // Balance 1: BTC
1661        buf.push((-8i8) as u8); // exponent
1662        buf.extend_from_slice(&100_000_000i64.to_le_bytes()); // free (1.0 BTC)
1663        buf.extend_from_slice(&50_000_000i64.to_le_bytes()); // locked (0.5 BTC)
1664        write_var_string(&mut buf, "BTC");
1665
1666        // Balance 2: USDT
1667        buf.push((-8i8) as u8); // exponent
1668        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // free (10000 USDT)
1669        buf.extend_from_slice(&0i64.to_le_bytes()); // locked
1670        write_var_string(&mut buf, "USDT");
1671
1672        let account = decode_account(&buf).unwrap();
1673
1674        assert!(account.can_trade);
1675        assert!(account.can_withdraw);
1676        assert!(account.can_deposit);
1677        assert_eq!(account.balances.len(), 2);
1678        assert_eq!(account.balances[0].asset, "BTC");
1679        assert_eq!(account.balances[0].free_mantissa, 100_000_000);
1680        assert_eq!(account.balances[0].locked_mantissa, 50_000_000);
1681        assert_eq!(account.balances[1].asset, "USDT");
1682        assert_eq!(account.balances[1].free_mantissa, 1_000_000_000_000);
1683    }
1684
1685    #[rstest]
1686    fn test_decode_account_empty_balances() {
1687        let header = create_header(
1688            ACCOUNT_BLOCK_LENGTH as u16,
1689            ACCOUNT_TEMPLATE_ID,
1690            SBE_SCHEMA_ID,
1691            SBE_SCHEMA_VERSION,
1692        );
1693
1694        let mut buf = Vec::new();
1695        buf.extend_from_slice(&header);
1696
1697        // Minimal fixed block
1698        buf.push((-8i8) as u8);
1699        buf.extend_from_slice(&[0u8; 63]); // Rest of fixed block
1700
1701        // Empty balances group
1702        buf.extend_from_slice(&create_group_header(BALANCE_BLOCK_LENGTH, 0));
1703
1704        let account = decode_account(&buf).unwrap();
1705        assert!(account.balances.is_empty());
1706    }
1707
1708    #[rstest]
1709    fn test_decode_account_trades_multiple() {
1710        let header = create_header(
1711            0,
1712            ACCOUNT_TRADES_TEMPLATE_ID,
1713            SBE_SCHEMA_ID,
1714            SBE_SCHEMA_VERSION,
1715        );
1716
1717        let mut buf = Vec::new();
1718        buf.extend_from_slice(&header);
1719
1720        // Group header: 2 trades
1721        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 2));
1722
1723        // Trade 1
1724        buf.push((-8i8) as u8); // price_exponent
1725        buf.push((-8i8) as u8); // qty_exponent
1726        buf.push((-8i8) as u8); // commission_exponent
1727        buf.extend_from_slice(&1001i64.to_le_bytes()); // id
1728        buf.extend_from_slice(&5001i64.to_le_bytes()); // order_id
1729        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id (None)
1730        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price
1731        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // qty
1732        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes()); // quote_qty
1733        buf.extend_from_slice(&100_000i64.to_le_bytes()); // commission
1734        buf.extend_from_slice(&1734300000000i64.to_le_bytes()); // time
1735        buf.push(1); // is_buyer
1736        buf.push(0); // is_maker
1737        buf.push(1); // is_best_match
1738        write_var_string(&mut buf, "BTCUSDT");
1739        write_var_string(&mut buf, "BNB");
1740
1741        // Trade 2
1742        buf.push((-8i8) as u8);
1743        buf.push((-8i8) as u8);
1744        buf.push((-8i8) as u8);
1745        buf.extend_from_slice(&1002i64.to_le_bytes());
1746        buf.extend_from_slice(&5002i64.to_le_bytes());
1747        buf.extend_from_slice(&i64::MIN.to_le_bytes());
1748        buf.extend_from_slice(&200_000_000_000i64.to_le_bytes());
1749        buf.extend_from_slice(&5_000_000i64.to_le_bytes());
1750        buf.extend_from_slice(&1_000_000_000_000i64.to_le_bytes());
1751        buf.extend_from_slice(&50_000i64.to_le_bytes());
1752        buf.extend_from_slice(&1734300001000i64.to_le_bytes());
1753        buf.push(0); // is_buyer (false = seller)
1754        buf.push(1); // is_maker
1755        buf.push(1); // is_best_match
1756        write_var_string(&mut buf, "ETHUSDT");
1757        write_var_string(&mut buf, "USDT");
1758
1759        let trades = decode_account_trades(&buf).unwrap();
1760
1761        assert_eq!(trades.len(), 2);
1762        assert_eq!(trades[0].id, 1001);
1763        assert_eq!(trades[0].order_id, 5001);
1764        assert!(trades[0].order_list_id.is_none());
1765        assert_eq!(trades[0].symbol, "BTCUSDT");
1766        assert_eq!(trades[0].commission_asset, "BNB");
1767        assert!(trades[0].is_buyer);
1768        assert!(!trades[0].is_maker);
1769
1770        assert_eq!(trades[1].id, 1002);
1771        assert_eq!(trades[1].symbol, "ETHUSDT");
1772        assert_eq!(trades[1].commission_asset, "USDT");
1773        assert!(!trades[1].is_buyer);
1774        assert!(trades[1].is_maker);
1775    }
1776
1777    #[rstest]
1778    fn test_decode_account_trades_empty() {
1779        let header = create_header(
1780            0,
1781            ACCOUNT_TRADES_TEMPLATE_ID,
1782            SBE_SCHEMA_ID,
1783            SBE_SCHEMA_VERSION,
1784        );
1785
1786        let mut buf = Vec::new();
1787        buf.extend_from_slice(&header);
1788        buf.extend_from_slice(&create_group_header(ACCOUNT_TRADE_BLOCK_LENGTH, 0));
1789
1790        let trades = decode_account_trades(&buf).unwrap();
1791        assert!(trades.is_empty());
1792    }
1793
1794    #[rstest]
1795    fn test_decode_exchange_info_single_symbol() {
1796        let header = create_header(
1797            0,
1798            EXCHANGE_INFO_TEMPLATE_ID,
1799            SBE_SCHEMA_ID,
1800            SBE_SCHEMA_VERSION,
1801        );
1802
1803        let mut buf = Vec::new();
1804        buf.extend_from_slice(&header);
1805
1806        // Empty rate_limits group
1807        buf.extend_from_slice(&create_group_header(11, 0));
1808
1809        // Empty exchange_filters group
1810        buf.extend_from_slice(&create_group_header(0, 0));
1811
1812        // Symbols group: 1 symbol with block_length=19
1813        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 1));
1814
1815        // Fixed block (19 bytes)
1816        buf.push(0); // status (Trading)
1817        buf.push(8); // base_asset_precision
1818        buf.push(8); // quote_asset_precision
1819        buf.push(8); // base_commission_precision
1820        buf.push(8); // quote_commission_precision
1821        buf.extend_from_slice(&0b0000_0111u16.to_le_bytes()); // order_types (MARKET|LIMIT|STOP_LOSS)
1822        buf.push(1); // iceberg_allowed (True)
1823        buf.push(1); // oco_allowed (True)
1824        buf.push(0); // oto_allowed (False)
1825        buf.push(1); // quote_order_qty_market_allowed (True)
1826        buf.push(1); // allow_trailing_stop (True)
1827        buf.push(1); // cancel_replace_allowed (True)
1828        buf.push(0); // amend_allowed (False)
1829        buf.push(1); // is_spot_trading_allowed (True)
1830        buf.push(0); // is_margin_trading_allowed (False)
1831        buf.push(0); // default_self_trade_prevention_mode
1832        buf.push(0); // allowed_self_trade_prevention_modes
1833        buf.push(0); // peg_instructions_allowed
1834
1835        // Filters nested group: 0 filters (SBE binary filters are skipped)
1836        buf.extend_from_slice(&create_group_header(0, 0));
1837
1838        // Permission sets nested group: 1 set with 1 permission
1839        buf.extend_from_slice(&create_group_header(0, 1));
1840        buf.extend_from_slice(&create_group_header(0, 1));
1841        write_var_string(&mut buf, "SPOT");
1842
1843        // Variable-length strings
1844        write_var_string(&mut buf, "BTCUSDT");
1845        write_var_string(&mut buf, "BTC");
1846        write_var_string(&mut buf, "USDT");
1847
1848        let info = decode_exchange_info(&buf).unwrap();
1849
1850        assert_eq!(info.symbols.len(), 1);
1851        let symbol = &info.symbols[0];
1852        assert_eq!(symbol.symbol, "BTCUSDT");
1853        assert_eq!(symbol.base_asset, "BTC");
1854        assert_eq!(symbol.quote_asset, "USDT");
1855        assert_eq!(symbol.base_asset_precision, 8);
1856        assert_eq!(symbol.quote_asset_precision, 8);
1857        assert_eq!(symbol.status, 0); // Trading
1858        assert_eq!(symbol.order_types, 0b0000_0111);
1859        assert!(symbol.iceberg_allowed);
1860        assert!(symbol.oco_allowed);
1861        assert!(!symbol.oto_allowed);
1862        assert!(symbol.quote_order_qty_market_allowed);
1863        assert!(symbol.allow_trailing_stop);
1864        assert!(symbol.cancel_replace_allowed);
1865        assert!(!symbol.amend_allowed);
1866        assert!(symbol.is_spot_trading_allowed);
1867        assert!(!symbol.is_margin_trading_allowed);
1868        assert!(symbol.filters.price_filter.is_none()); // No filters in test data
1869        assert!(symbol.filters.lot_size_filter.is_none());
1870        assert_eq!(symbol.permissions.len(), 1);
1871        assert_eq!(symbol.permissions[0], vec!["SPOT"]);
1872    }
1873
1874    #[rstest]
1875    fn test_decode_exchange_info_empty() {
1876        let header = create_header(
1877            0,
1878            EXCHANGE_INFO_TEMPLATE_ID,
1879            SBE_SCHEMA_ID,
1880            SBE_SCHEMA_VERSION,
1881        );
1882
1883        let mut buf = Vec::new();
1884        buf.extend_from_slice(&header);
1885
1886        // Empty rate_limits group
1887        buf.extend_from_slice(&create_group_header(11, 0));
1888
1889        // Empty exchange_filters group
1890        buf.extend_from_slice(&create_group_header(0, 0));
1891
1892        // Empty symbols group
1893        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 0));
1894
1895        let info = decode_exchange_info(&buf).unwrap();
1896        assert!(info.symbols.is_empty());
1897    }
1898
1899    #[rstest]
1900    fn test_decode_exchange_info_wrong_template() {
1901        let header = create_header(0, PING_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1902
1903        let mut buf = Vec::new();
1904        buf.extend_from_slice(&header);
1905
1906        let err = decode_exchange_info(&buf).unwrap_err();
1907        assert!(matches!(err, SbeDecodeError::UnknownTemplateId(101)));
1908    }
1909
1910    #[rstest]
1911    fn test_decode_exchange_info_multiple_symbols() {
1912        let header = create_header(
1913            0,
1914            EXCHANGE_INFO_TEMPLATE_ID,
1915            SBE_SCHEMA_ID,
1916            SBE_SCHEMA_VERSION,
1917        );
1918
1919        let mut buf = Vec::new();
1920        buf.extend_from_slice(&header);
1921
1922        // Empty rate_limits group
1923        buf.extend_from_slice(&create_group_header(11, 0));
1924
1925        // Empty exchange_filters group
1926        buf.extend_from_slice(&create_group_header(0, 0));
1927
1928        // Symbols group: 2 symbols
1929        buf.extend_from_slice(&create_group_header(SYMBOL_BLOCK_LENGTH as u16, 2));
1930
1931        // Symbol 1: BTCUSDT
1932        buf.push(0); // status
1933        buf.push(8); // base_asset_precision
1934        buf.push(8); // quote_asset_precision
1935        buf.push(8); // base_commission_precision
1936        buf.push(8); // quote_commission_precision
1937        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
1938        buf.push(1); // iceberg_allowed
1939        buf.push(1); // oco_allowed
1940        buf.push(0); // oto_allowed
1941        buf.push(1); // quote_order_qty_market_allowed
1942        buf.push(1); // allow_trailing_stop
1943        buf.push(1); // cancel_replace_allowed
1944        buf.push(0); // amend_allowed
1945        buf.push(1); // is_spot_trading_allowed
1946        buf.push(0); // is_margin_trading_allowed
1947        buf.push(0); // default_self_trade_prevention_mode
1948        buf.push(0); // allowed_self_trade_prevention_modes
1949        buf.push(0); // peg_instructions_allowed
1950        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
1951        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
1952        write_var_string(&mut buf, "BTCUSDT");
1953        write_var_string(&mut buf, "BTC");
1954        write_var_string(&mut buf, "USDT");
1955
1956        // Symbol 2: ETHUSDT
1957        buf.push(0); // status
1958        buf.push(8); // base_asset_precision
1959        buf.push(8); // quote_asset_precision
1960        buf.push(8); // base_commission_precision
1961        buf.push(8); // quote_commission_precision
1962        buf.extend_from_slice(&0b0000_0011u16.to_le_bytes()); // order_types
1963        buf.push(1); // iceberg_allowed
1964        buf.push(1); // oco_allowed
1965        buf.push(0); // oto_allowed
1966        buf.push(1); // quote_order_qty_market_allowed
1967        buf.push(1); // allow_trailing_stop
1968        buf.push(1); // cancel_replace_allowed
1969        buf.push(0); // amend_allowed
1970        buf.push(1); // is_spot_trading_allowed
1971        buf.push(1); // is_margin_trading_allowed
1972        buf.push(0); // default_self_trade_prevention_mode
1973        buf.push(0); // allowed_self_trade_prevention_modes
1974        buf.push(0); // peg_instructions_allowed
1975        buf.extend_from_slice(&create_group_header(0, 0)); // No filters
1976        buf.extend_from_slice(&create_group_header(0, 0)); // No permission sets
1977        write_var_string(&mut buf, "ETHUSDT");
1978        write_var_string(&mut buf, "ETH");
1979        write_var_string(&mut buf, "USDT");
1980
1981        let info = decode_exchange_info(&buf).unwrap();
1982
1983        assert_eq!(info.symbols.len(), 2);
1984        assert_eq!(info.symbols[0].symbol, "BTCUSDT");
1985        assert_eq!(info.symbols[0].base_asset, "BTC");
1986        assert!(!info.symbols[0].is_margin_trading_allowed);
1987
1988        assert_eq!(info.symbols[1].symbol, "ETHUSDT");
1989        assert_eq!(info.symbols[1].base_asset, "ETH");
1990        assert!(info.symbols[1].is_margin_trading_allowed);
1991    }
1992
1993    #[rstest]
1994    fn test_decode_klines_valid() {
1995        let header = create_header(2, KLINES_TEMPLATE_ID, SBE_SCHEMA_ID, SBE_SCHEMA_VERSION);
1996
1997        let mut buf = Vec::new();
1998        buf.extend_from_slice(&header);
1999        buf.push((-2i8) as u8); // price_exponent
2000        buf.push((-4i8) as u8); // qty_exponent
2001        buf.extend_from_slice(&create_group_header(KLINES_BLOCK_LENGTH, 1));
2002        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes()); // open_time (micros)
2003        buf.extend_from_slice(&12_000i64.to_le_bytes()); // open_price
2004        buf.extend_from_slice(&12_500i64.to_le_bytes()); // high_price
2005        buf.extend_from_slice(&11_900i64.to_le_bytes()); // low_price
2006        buf.extend_from_slice(&12_345i64.to_le_bytes()); // close_price
2007        buf.extend_from_slice(&1_234_500i128.to_le_bytes()); // volume
2008        buf.extend_from_slice(&1_700_000_059_999_000i64.to_le_bytes()); // close_time (micros)
2009        buf.extend_from_slice(&2_345_600i128.to_le_bytes()); // quote_volume
2010        buf.extend_from_slice(&100i64.to_le_bytes()); // num_trades
2011        buf.extend_from_slice(&600_000i128.to_le_bytes()); // taker_buy_base_volume
2012        buf.extend_from_slice(&1_200_000i128.to_le_bytes()); // taker_buy_quote_volume
2013
2014        let klines = decode_klines(&buf).unwrap();
2015
2016        assert_eq!(klines.price_exponent, -2);
2017        assert_eq!(klines.qty_exponent, -4);
2018        assert_eq!(klines.klines.len(), 1);
2019        assert_eq!(klines.klines[0].open_time, 1_700_000_000_000_000);
2020        assert_eq!(klines.klines[0].close_price, 12_345);
2021        assert_eq!(i128::from_le_bytes(klines.klines[0].volume), 1_234_500);
2022        assert_eq!(klines.klines[0].num_trades, 100);
2023    }
2024
2025    #[rstest]
2026    fn test_decode_new_order_full_valid() {
2027        let header = create_header(
2028            NEW_ORDER_FULL_BLOCK_LENGTH as u16,
2029            NEW_ORDER_FULL_TEMPLATE_ID,
2030            SBE_SCHEMA_ID,
2031            SBE_SCHEMA_VERSION,
2032        );
2033
2034        let mut buf = Vec::new();
2035        buf.extend_from_slice(&header);
2036
2037        buf.push((-2i8) as u8); // price_exponent
2038        buf.push((-4i8) as u8); // qty_exponent
2039        buf.extend_from_slice(&12345i64.to_le_bytes()); // order_id
2040        buf.extend_from_slice(&99i64.to_le_bytes()); // order_list_id
2041        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes()); // transact_time
2042        buf.extend_from_slice(&12_345i64.to_le_bytes()); // price_mantissa
2043        buf.extend_from_slice(&25_000i64.to_le_bytes()); // orig_qty
2044        buf.extend_from_slice(&10_000i64.to_le_bytes()); // executed_qty
2045        buf.extend_from_slice(&123_450_000i64.to_le_bytes()); // cumulative_quote_qty
2046        buf.push(2); // status (PARTIALLY_FILLED)
2047        buf.push(1); // time_in_force (GTC)
2048        buf.push(1); // order_type (LIMIT)
2049        buf.push(1); // side (BUY)
2050        buf.extend_from_slice(&12_000i64.to_le_bytes()); // stop_price
2051        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
2052        buf.extend_from_slice(&1_700_000_000_000_500i64.to_le_bytes()); // working_time
2053        buf.extend_from_slice(&[0u8; 23]); // iceberg to used_sor
2054        buf.push(0); // self_trade_prevention_mode
2055        buf.extend_from_slice(&[0u8; 16]); // trade_group_id + prevented_quantity
2056        buf.push((-8i8) as u8); // commission_exponent
2057        buf.extend_from_slice(&[0u8; 18]); // rest of block
2058
2059        buf.extend_from_slice(&create_group_header(FILLS_BLOCK_LENGTH, 1));
2060        buf.push((-8i8) as u8); // commission_exponent
2061        buf.push(0); // match_type
2062        buf.extend_from_slice(&12_345i64.to_le_bytes()); // fill price
2063        buf.extend_from_slice(&10_000i64.to_le_bytes()); // fill qty
2064        buf.extend_from_slice(&10_000i64.to_le_bytes()); // commission
2065        buf.extend_from_slice(&555i64.to_le_bytes()); // trade_id
2066        buf.extend_from_slice(&0i64.to_le_bytes()); // alloc_id
2067        write_var_string(&mut buf, "USDT");
2068
2069        buf.extend_from_slice(&create_group_header(0, 0)); // prevented matches
2070        write_var_string(&mut buf, "ETHUSDT");
2071        write_var_string(&mut buf, "client-123");
2072
2073        let response = decode_new_order_full(&buf).unwrap();
2074
2075        assert_eq!(response.order_id, 12345);
2076        assert_eq!(response.order_list_id, Some(99));
2077        assert_eq!(response.transact_time, 1_700_000_000_000_000);
2078        assert_eq!(response.price_mantissa, 12_345);
2079        assert_eq!(response.orig_qty_mantissa, 25_000);
2080        assert_eq!(response.executed_qty_mantissa, 10_000);
2081        assert_eq!(response.stop_price_mantissa, Some(12_000));
2082        assert_eq!(response.working_time, Some(1_700_000_000_000_500));
2083        assert_eq!(response.symbol, "ETHUSDT");
2084        assert_eq!(response.client_order_id, "client-123");
2085        assert_eq!(response.fills.len(), 1);
2086        assert_eq!(response.fills[0].price_mantissa, 12_345);
2087        assert_eq!(response.fills[0].qty_mantissa, 10_000);
2088        assert_eq!(response.fills[0].trade_id, Some(555));
2089        assert_eq!(response.fills[0].commission_asset, "USDT");
2090    }
2091
2092    #[rstest]
2093    fn test_decode_new_order_full_v3_block_length() {
2094        // Schema v3 adds expiryReason (1 byte) at the end of the fixed block,
2095        // increasing block_length from 153 to 154.
2096        const V3_BLOCK_LENGTH: u16 = 154;
2097        let header = create_header(
2098            V3_BLOCK_LENGTH,
2099            NEW_ORDER_FULL_TEMPLATE_ID,
2100            SBE_SCHEMA_ID,
2101            SBE_SCHEMA_VERSION,
2102        );
2103
2104        let mut buf = Vec::new();
2105        buf.extend_from_slice(&header);
2106
2107        buf.push((-2i8) as u8); // price_exponent
2108        buf.push((-4i8) as u8); // qty_exponent
2109        buf.extend_from_slice(&12345i64.to_le_bytes()); // order_id
2110        buf.extend_from_slice(&99i64.to_le_bytes()); // order_list_id
2111        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes()); // transact_time
2112        buf.extend_from_slice(&12_345i64.to_le_bytes()); // price_mantissa
2113        buf.extend_from_slice(&25_000i64.to_le_bytes()); // orig_qty
2114        buf.extend_from_slice(&10_000i64.to_le_bytes()); // executed_qty
2115        buf.extend_from_slice(&123_450_000i64.to_le_bytes()); // cumulative_quote_qty
2116        buf.push(2); // status (PARTIALLY_FILLED)
2117        buf.push(1); // time_in_force (GTC)
2118        buf.push(1); // order_type (LIMIT)
2119        buf.push(1); // side (BUY)
2120        buf.extend_from_slice(&12_000i64.to_le_bytes()); // stop_price
2121        buf.extend_from_slice(&[0u8; 16]); // trailing_delta + trailing_time
2122        buf.extend_from_slice(&1_700_000_000_000_500i64.to_le_bytes()); // working_time
2123        buf.extend_from_slice(&[0u8; 23]); // iceberg to used_sor
2124        buf.push(0); // self_trade_prevention_mode
2125        buf.extend_from_slice(&[0u8; 16]); // trade_group_id + prevented_quantity
2126        buf.push((-8i8) as u8); // commission_exponent
2127        buf.extend_from_slice(&[0u8; 18]); // peg fields
2128        buf.push(0xFF); // expiryReason (null/not set)
2129
2130        buf.extend_from_slice(&create_group_header(FILLS_BLOCK_LENGTH, 1));
2131        buf.push((-8i8) as u8); // commission_exponent
2132        buf.push(0); // match_type
2133        buf.extend_from_slice(&12_345i64.to_le_bytes()); // fill price
2134        buf.extend_from_slice(&10_000i64.to_le_bytes()); // fill qty
2135        buf.extend_from_slice(&10_000i64.to_le_bytes()); // commission
2136        buf.extend_from_slice(&555i64.to_le_bytes()); // trade_id
2137        buf.extend_from_slice(&0i64.to_le_bytes()); // alloc_id
2138        write_var_string(&mut buf, "USDT");
2139
2140        buf.extend_from_slice(&create_group_header(0, 0)); // prevented matches
2141        write_var_string(&mut buf, "ETHUSDT");
2142        write_var_string(&mut buf, "client-456");
2143
2144        let response = decode_new_order_full(&buf).unwrap();
2145
2146        assert_eq!(response.order_id, 12345);
2147        assert_eq!(response.symbol, "ETHUSDT");
2148        assert_eq!(response.client_order_id, "client-456");
2149        assert_eq!(response.fills.len(), 1);
2150        assert_eq!(response.fills[0].price_mantissa, 12_345);
2151    }
2152
2153    #[rstest]
2154    fn test_decode_cancel_open_orders_valid() {
2155        let header = create_header(
2156            0,
2157            CANCEL_OPEN_ORDERS_TEMPLATE_ID,
2158            SBE_SCHEMA_ID,
2159            SBE_SCHEMA_VERSION,
2160        );
2161        let response_one = create_cancel_order_response_buffer(111, "ETHUSDT", "orig-1", "new-1");
2162        let response_two = create_cancel_order_response_buffer(222, "BTCUSDT", "orig-2", "new-2");
2163
2164        let mut buf = Vec::new();
2165        buf.extend_from_slice(&header);
2166        buf.extend_from_slice(&create_group_header(0, 2));
2167        buf.extend_from_slice(&(response_one.len() as u16).to_le_bytes());
2168        buf.extend_from_slice(&response_one);
2169        buf.extend_from_slice(&(response_two.len() as u16).to_le_bytes());
2170        buf.extend_from_slice(&response_two);
2171
2172        let responses = decode_cancel_open_orders(&buf).unwrap();
2173
2174        assert_eq!(responses.len(), 2);
2175        assert_eq!(responses[0].order_id, 111);
2176        assert_eq!(responses[0].symbol, "ETHUSDT");
2177        assert_eq!(responses[0].orig_client_order_id, "orig-1");
2178        assert_eq!(responses[0].client_order_id, "new-1");
2179        assert_eq!(responses[1].order_id, 222);
2180        assert_eq!(responses[1].symbol, "BTCUSDT");
2181        assert_eq!(responses[1].orig_client_order_id, "orig-2");
2182        assert_eq!(responses[1].client_order_id, "new-2");
2183    }
2184
2185    fn create_cancel_order_response_buffer(
2186        order_id: i64,
2187        symbol: &str,
2188        orig_client_order_id: &str,
2189        client_order_id: &str,
2190    ) -> Vec<u8> {
2191        let header = create_header(
2192            CANCEL_ORDER_BLOCK_LENGTH as u16,
2193            CANCEL_ORDER_TEMPLATE_ID,
2194            SBE_SCHEMA_ID,
2195            SBE_SCHEMA_VERSION,
2196        );
2197
2198        let mut buf = Vec::new();
2199        buf.extend_from_slice(&header);
2200        buf.push((-8i8) as u8); // price_exponent
2201        buf.push((-8i8) as u8); // qty_exponent
2202        buf.extend_from_slice(&order_id.to_le_bytes());
2203        buf.extend_from_slice(&i64::MIN.to_le_bytes()); // order_list_id
2204        buf.extend_from_slice(&1_700_000_000_000_000i64.to_le_bytes()); // transact_time
2205        buf.extend_from_slice(&100_000_000_000i64.to_le_bytes()); // price
2206        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // orig_qty
2207        buf.extend_from_slice(&10_000_000i64.to_le_bytes()); // executed_qty
2208        buf.extend_from_slice(&1_000_000_000i64.to_le_bytes()); // cumulative_quote_qty
2209        buf.push(4); // status (CANCELED)
2210        buf.push(1); // time_in_force
2211        buf.push(1); // order_type
2212        buf.push(1); // side
2213        buf.push(0); // self_trade_prevention_mode
2214
2215        while buf.len() < 8 + CANCEL_ORDER_BLOCK_LENGTH {
2216            buf.push(0);
2217        }
2218
2219        write_var_string(&mut buf, symbol);
2220        write_var_string(&mut buf, orig_client_order_id);
2221        write_var_string(&mut buf, client_order_id);
2222
2223        buf
2224    }
2225}