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