Skip to main content

polyfill_rs/
decode.rs

1//! Data decoding utilities for the Polymarket client.
2//!
3//! This module contains the ergonomic decoding layer used for broad API compatibility:
4//! tolerant string-or-number deserializers, raw REST response adapters, and generic
5//! WebSocket message parsing. Some helpers intentionally parse through
6//! [`serde_json::Value`] to support Polymarket fields that vary between strings,
7//! numbers, nulls, and mixed event batches.
8//!
9//! The zero-allocation WS `book` processing path is separate. Use
10//! [`crate::ws_hot_path::WsBookUpdateProcessor`] when applying high-throughput book
11//! messages directly to [`crate::book::OrderBookManager`].
12
13use crate::errors::{PolyfillError, Result};
14use crate::types::*;
15use alloy_primitives::{Address, U256};
16use chrono::{DateTime, Utc};
17use rust_decimal::Decimal;
18use serde::{Deserialize, Deserializer};
19use serde_json::Value;
20use std::str::FromStr;
21
22/// Flexible string-or-number deserializers for inconsistent API fields.
23pub mod deserializers {
24    use super::*;
25    use std::fmt::Display;
26
27    /// Deserialize a number from a string or JSON number.
28    ///
29    /// This compatibility helper accepts multiple API shapes by first decoding into
30    /// `serde_json::Value`. It is not intended for allocation-sensitive hot paths.
31    pub fn number_from_string<'de, T, D>(deserializer: D) -> std::result::Result<T, D::Error>
32    where
33        D: Deserializer<'de>,
34        T: FromStr + serde::Deserialize<'de> + Clone,
35        <T as FromStr>::Err: Display,
36    {
37        let value = serde_json::Value::deserialize(deserializer)?;
38        match value {
39            serde_json::Value::Number(n) => {
40                if let Some(v) = n.as_u64() {
41                    T::deserialize(serde_json::Value::Number(serde_json::Number::from(v)))
42                        .map_err(|_| serde::de::Error::custom("Failed to deserialize number"))
43                } else if let Some(v) = n.as_f64() {
44                    T::deserialize(serde_json::Value::Number(
45                        serde_json::Number::from_f64(v).unwrap(),
46                    ))
47                    .map_err(|_| serde::de::Error::custom("Failed to deserialize number"))
48                } else {
49                    Err(serde::de::Error::custom("Invalid number format"))
50                }
51            },
52            serde_json::Value::String(s) => s.parse::<T>().map_err(serde::de::Error::custom),
53            _ => Err(serde::de::Error::custom("Expected number or string")),
54        }
55    }
56
57    /// Deserialize an optional number from a string, JSON number, or null.
58    ///
59    /// This compatibility helper accepts multiple API shapes by first decoding into
60    /// `serde_json::Value`. It is not intended for allocation-sensitive hot paths.
61    pub fn optional_number_from_string<'de, T, D>(
62        deserializer: D,
63    ) -> std::result::Result<Option<T>, D::Error>
64    where
65        D: Deserializer<'de>,
66        T: FromStr + serde::Deserialize<'de> + Clone,
67        <T as FromStr>::Err: Display,
68    {
69        let value = serde_json::Value::deserialize(deserializer)?;
70        match value {
71            serde_json::Value::Null => Ok(None),
72            serde_json::Value::Number(n) => {
73                if let Some(v) = n.as_u64() {
74                    T::deserialize(serde_json::Value::Number(serde_json::Number::from(v)))
75                        .map(Some)
76                        .map_err(|_| serde::de::Error::custom("Failed to deserialize number"))
77                } else if let Some(v) = n.as_f64() {
78                    T::deserialize(serde_json::Value::Number(
79                        serde_json::Number::from_f64(v).unwrap(),
80                    ))
81                    .map(Some)
82                    .map_err(|_| serde::de::Error::custom("Failed to deserialize number"))
83                } else {
84                    Err(serde::de::Error::custom("Invalid number format"))
85                }
86            },
87            serde_json::Value::String(s) => {
88                if s.is_empty() {
89                    Ok(None)
90                } else {
91                    s.parse::<T>().map(Some).map_err(serde::de::Error::custom)
92                }
93            },
94            _ => Err(serde::de::Error::custom("Expected number, string, or null")),
95        }
96    }
97
98    /// Deserialize DateTime from Unix timestamp
99    pub fn datetime_from_timestamp<'de, D>(
100        deserializer: D,
101    ) -> std::result::Result<DateTime<Utc>, D::Error>
102    where
103        D: Deserializer<'de>,
104    {
105        let timestamp = number_from_string::<u64, D>(deserializer)?;
106        DateTime::from_timestamp(timestamp as i64, 0)
107            .ok_or_else(|| serde::de::Error::custom("Invalid timestamp"))
108    }
109
110    /// Deserialize optional DateTime from Unix timestamp
111    pub fn optional_datetime_from_timestamp<'de, D>(
112        deserializer: D,
113    ) -> std::result::Result<Option<DateTime<Utc>>, D::Error>
114    where
115        D: Deserializer<'de>,
116    {
117        match optional_number_from_string::<u64, D>(deserializer)? {
118            Some(timestamp) => DateTime::from_timestamp(timestamp as i64, 0)
119                .map(Some)
120                .ok_or_else(|| serde::de::Error::custom("Invalid timestamp")),
121            None => Ok(None),
122        }
123    }
124
125    /// Deserialize a vec that may be `null` (treat `null` as empty vec).
126    pub fn vec_from_null<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
127    where
128        D: Deserializer<'de>,
129        T: serde::Deserialize<'de>,
130    {
131        Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
132    }
133
134    /// Deserialize an optional Decimal from string/number/null.
135    ///
136    /// This compatibility helper accepts multiple API shapes by first decoding into
137    /// `serde_json::Value`. It is not intended for allocation-sensitive hot paths.
138    ///
139    /// - `null` => `None`
140    /// - `""` => `None`
141    /// - invalid values => error
142    pub fn optional_decimal_from_string<'de, D>(
143        deserializer: D,
144    ) -> std::result::Result<Option<Decimal>, D::Error>
145    where
146        D: Deserializer<'de>,
147    {
148        let value = serde_json::Value::deserialize(deserializer)?;
149        match value {
150            serde_json::Value::Null => Ok(None),
151            serde_json::Value::String(s) => {
152                let s = s.trim();
153                if s.is_empty() {
154                    Ok(None)
155                } else {
156                    s.parse::<Decimal>()
157                        .map(Some)
158                        .map_err(serde::de::Error::custom)
159                }
160            },
161            serde_json::Value::Number(n) => Decimal::from_str(&n.to_string())
162                .map(Some)
163                .map_err(serde::de::Error::custom),
164            other => Err(serde::de::Error::custom(format!(
165                "Expected decimal as string/number/null, got {other}"
166            ))),
167        }
168    }
169
170    /// Like `optional_decimal_from_string`, but returns `None` on parse errors.
171    ///
172    /// This compatibility helper accepts multiple API shapes by first decoding into
173    /// `serde_json::Value`. It is not intended for allocation-sensitive hot paths.
174    pub fn optional_decimal_from_string_default_on_error<'de, D>(
175        deserializer: D,
176    ) -> std::result::Result<Option<Decimal>, D::Error>
177    where
178        D: Deserializer<'de>,
179    {
180        let value = serde_json::Value::deserialize(deserializer)?;
181        match value {
182            serde_json::Value::Null => Ok(None),
183            serde_json::Value::String(s) => {
184                let s = s.trim();
185                if s.is_empty() {
186                    Ok(None)
187                } else {
188                    Ok(s.parse::<Decimal>().ok())
189                }
190            },
191            serde_json::Value::Number(n) => Ok(Decimal::from_str(&n.to_string()).ok()),
192            _ => Ok(None),
193        }
194    }
195
196    /// Deserialize a Decimal from string/number.
197    ///
198    /// This compatibility helper accepts multiple API shapes through
199    /// `optional_decimal_from_string`. It is not intended for allocation-sensitive
200    /// hot paths.
201    ///
202    /// - `""` => error
203    /// - invalid values => error
204    pub fn decimal_from_string<'de, D>(deserializer: D) -> std::result::Result<Decimal, D::Error>
205    where
206        D: Deserializer<'de>,
207    {
208        optional_decimal_from_string(deserializer)?.ok_or_else(|| {
209            serde::de::Error::custom("Expected decimal as string/number, got null/empty string")
210        })
211    }
212
213    /// Deserialize a Decimal from string/number/null, defaulting missing-ish values to zero.
214    ///
215    /// This compatibility helper accepts multiple API shapes through
216    /// `optional_decimal_from_string`. It is not intended for allocation-sensitive
217    /// hot paths.
218    pub fn decimal_from_string_or_zero<'de, D>(
219        deserializer: D,
220    ) -> std::result::Result<Decimal, D::Error>
221    where
222        D: Deserializer<'de>,
223    {
224        Ok(optional_decimal_from_string(deserializer)?.unwrap_or(Decimal::ZERO))
225    }
226}
227
228/// Raw API response types for efficient parsing
229#[derive(Debug, Deserialize)]
230pub struct RawOrderBookResponse {
231    pub market: String,
232    pub asset_id: String,
233    pub hash: String,
234    #[serde(deserialize_with = "deserializers::number_from_string")]
235    pub timestamp: u64,
236    pub bids: Vec<RawBookLevel>,
237    pub asks: Vec<RawBookLevel>,
238}
239
240#[derive(Debug, Deserialize)]
241pub struct RawBookLevel {
242    #[serde(with = "rust_decimal::serde::str")]
243    pub price: Decimal,
244    #[serde(with = "rust_decimal::serde::str")]
245    pub size: Decimal,
246}
247
248#[derive(Debug, Deserialize)]
249pub struct RawOrderResponse {
250    pub id: String,
251    pub status: String,
252    pub market: String,
253    pub asset_id: String,
254    pub maker_address: String,
255    pub owner: String,
256    pub outcome: String,
257    #[serde(rename = "type")]
258    pub order_type: OrderType,
259    pub side: Side,
260    #[serde(with = "rust_decimal::serde::str")]
261    pub original_size: Decimal,
262    #[serde(with = "rust_decimal::serde::str")]
263    pub price: Decimal,
264    #[serde(with = "rust_decimal::serde::str")]
265    pub size_matched: Decimal,
266    #[serde(deserialize_with = "deserializers::number_from_string")]
267    pub expiration: u64,
268    #[serde(deserialize_with = "deserializers::number_from_string")]
269    pub created_at: u64,
270}
271
272#[derive(Debug, Deserialize)]
273pub struct RawTradeResponse {
274    pub id: String,
275    pub market: String,
276    pub asset_id: String,
277    pub side: Side,
278    #[serde(with = "rust_decimal::serde::str")]
279    pub price: Decimal,
280    #[serde(with = "rust_decimal::serde::str")]
281    pub size: Decimal,
282    pub maker_address: String,
283    pub taker_address: String,
284    #[serde(deserialize_with = "deserializers::number_from_string")]
285    pub timestamp: u64,
286}
287
288#[derive(Debug, Deserialize)]
289pub struct RawMarketResponse {
290    pub condition_id: String,
291    pub tokens: [RawToken; 2],
292    pub active: bool,
293    pub closed: bool,
294    pub question: String,
295    pub description: String,
296    pub category: Option<String>,
297    pub end_date_iso: Option<String>,
298    #[serde(with = "rust_decimal::serde::str")]
299    pub minimum_order_size: Decimal,
300    #[serde(with = "rust_decimal::serde::str")]
301    pub minimum_tick_size: Decimal,
302}
303
304#[derive(Debug, Deserialize)]
305pub struct RawToken {
306    pub token_id: String,
307    pub outcome: String,
308}
309
310/// Decoder implementations for converting raw responses to client types
311pub trait Decoder<T> {
312    fn decode(&self) -> Result<T>;
313}
314
315impl Decoder<OrderBook> for RawOrderBookResponse {
316    fn decode(&self) -> Result<OrderBook> {
317        let timestamp = chrono::DateTime::from_timestamp(self.timestamp as i64, 0)
318            .ok_or_else(|| PolyfillError::parse("Invalid timestamp".to_string(), None))?;
319
320        let bids = self
321            .bids
322            .iter()
323            .map(|level| BookLevel {
324                price: level.price,
325                size: level.size,
326            })
327            .collect();
328
329        let asks = self
330            .asks
331            .iter()
332            .map(|level| BookLevel {
333                price: level.price,
334                size: level.size,
335            })
336            .collect();
337
338        Ok(OrderBook {
339            token_id: self.asset_id.clone(),
340            timestamp,
341            bids,
342            asks,
343            sequence: 0, // TODO: Get from response if available
344            last_delta_sequence: 0,
345            last_snapshot_timestamp_ms: 0,
346        })
347    }
348}
349
350impl Decoder<Order> for RawOrderResponse {
351    fn decode(&self) -> Result<Order> {
352        let status = match self.status.as_str() {
353            "LIVE" => OrderStatus::Live,
354            "CANCELLED" => OrderStatus::Cancelled,
355            "FILLED" => OrderStatus::Filled,
356            "PARTIAL" => OrderStatus::Partial,
357            "EXPIRED" => OrderStatus::Expired,
358            _ => {
359                return Err(PolyfillError::parse(
360                    format!("Unknown order status: {}", self.status),
361                    None,
362                ))
363            },
364        };
365
366        let created_at =
367            chrono::DateTime::from_timestamp(self.created_at as i64, 0).ok_or_else(|| {
368                PolyfillError::parse("Invalid created_at timestamp".to_string(), None)
369            })?;
370
371        let expiration = if self.expiration > 0 {
372            Some(
373                chrono::DateTime::from_timestamp(self.expiration as i64, 0).ok_or_else(|| {
374                    PolyfillError::parse("Invalid expiration timestamp".to_string(), None)
375                })?,
376            )
377        } else {
378            None
379        };
380
381        Ok(Order {
382            id: self.id.clone(),
383            token_id: self.asset_id.clone(),
384            side: self.side,
385            price: self.price,
386            original_size: self.original_size,
387            filled_size: self.size_matched,
388            remaining_size: self.original_size - self.size_matched,
389            status,
390            order_type: self.order_type,
391            created_at,
392            updated_at: created_at, // Use same as created for now
393            expiration,
394            client_id: None,
395        })
396    }
397}
398
399impl Decoder<FillEvent> for RawTradeResponse {
400    fn decode(&self) -> Result<FillEvent> {
401        let timestamp = chrono::DateTime::from_timestamp(self.timestamp as i64, 0)
402            .ok_or_else(|| PolyfillError::parse("Invalid trade timestamp".to_string(), None))?;
403
404        let maker_address = Address::from_str(&self.maker_address)
405            .map_err(|e| PolyfillError::parse(format!("Invalid maker address: {}", e), None))?;
406
407        let taker_address = Address::from_str(&self.taker_address)
408            .map_err(|e| PolyfillError::parse(format!("Invalid taker address: {}", e), None))?;
409
410        Ok(FillEvent {
411            id: self.id.clone(),
412            order_id: "".to_string(), // TODO: Get from response if available
413            token_id: self.asset_id.clone(),
414            side: self.side,
415            price: self.price,
416            size: self.size,
417            timestamp,
418            maker_address,
419            taker_address,
420            fee: Decimal::ZERO, // TODO: Calculate or get from response
421        })
422    }
423}
424
425impl Decoder<Market> for RawMarketResponse {
426    fn decode(&self) -> Result<Market> {
427        let tokens = [
428            Token {
429                token_id: self.tokens[0].token_id.clone(),
430                outcome: self.tokens[0].outcome.clone(),
431                price: Decimal::ZERO,
432                winner: false,
433            },
434            Token {
435                token_id: self.tokens[1].token_id.clone(),
436                outcome: self.tokens[1].outcome.clone(),
437                price: Decimal::ZERO,
438                winner: false,
439            },
440        ];
441
442        Ok(Market {
443            condition_id: self.condition_id.clone(),
444            tokens,
445            rewards: crate::types::Rewards {
446                rates: None,
447                min_size: Decimal::ZERO,
448                max_spread: Decimal::ONE,
449                event_start_date: None,
450                event_end_date: None,
451                in_game_multiplier: None,
452                reward_epoch: None,
453            },
454            min_incentive_size: None,
455            max_incentive_spread: None,
456            active: self.active,
457            closed: self.closed,
458            question_id: self.condition_id.clone(), // Use condition_id as fallback
459            minimum_order_size: self.minimum_order_size,
460            minimum_tick_size: self.minimum_tick_size,
461            description: self.description.clone(),
462            category: self.category.clone(),
463            end_date_iso: self.end_date_iso.clone(),
464            game_start_time: None,
465            question: self.question.clone(),
466            market_slug: format!("market-{}", self.condition_id), // Generate a slug
467            seconds_delay: Decimal::ZERO,
468            icon: String::new(),
469            fpmm: String::new(),
470            // Additional fields
471            enable_order_book: false,
472            archived: false,
473            accepting_orders: false,
474            accepting_order_timestamp: None,
475            maker_base_fee: Decimal::ZERO,
476            taker_base_fee: Decimal::ZERO,
477            notifications_enabled: false,
478            neg_risk: false,
479            neg_risk_market_id: String::new(),
480            neg_risk_request_id: String::new(),
481            image: String::new(),
482            is_50_50_outcome: false,
483        })
484    }
485}
486
487/// Ergonomic WebSocket message parsing (official `event_type` shape).
488///
489/// Polymarket WebSocket servers may send either a single JSON object or a batch array.
490/// This parser is tolerant:
491/// - Unknown/unsupported `event_type`s are ignored.
492/// - Invalid entries inside a batch are skipped (do not fail the whole batch).
493///
494/// This is the compatibility parser for general stream consumers. It parses into
495/// `serde_json::Value` first so it can inspect event types and skip unknown messages.
496/// For allocation-sensitive WS `book` updates, use
497/// [`crate::ws_hot_path::WsBookUpdateProcessor`] instead.
498pub fn parse_stream_messages(raw: &str) -> Result<Vec<StreamMessage>> {
499    parse_stream_messages_bytes(raw.as_bytes())
500}
501
502/// See `parse_stream_messages`.
503pub fn parse_stream_messages_bytes(bytes: &[u8]) -> Result<Vec<StreamMessage>> {
504    let value: Value = serde_json::from_slice(bytes)?;
505
506    match value {
507        Value::Object(map) => {
508            let event_type = map.get("event_type").and_then(Value::as_str);
509            match event_type {
510                None => Ok(vec![]),
511                Some(_) => {
512                    let msg: StreamMessage = serde_json::from_value(Value::Object(map))?;
513                    match msg {
514                        StreamMessage::Unknown => Ok(vec![]),
515                        other => Ok(vec![other]),
516                    }
517                },
518            }
519        },
520        Value::Array(arr) => Ok(arr
521            .into_iter()
522            .filter_map(|elem| {
523                let Value::Object(map) = elem else {
524                    return None;
525                };
526
527                let event_type = map.get("event_type").and_then(Value::as_str)?;
528                // Skip unknown event types early (forward compatibility).
529                match event_type {
530                    "book" | "price_change" | "tick_size_change" | "last_trade_price"
531                    | "best_bid_ask" | "new_market" | "market_resolved" | "trade" | "order" => {},
532                    _ => return None,
533                }
534
535                match serde_json::from_value::<StreamMessage>(Value::Object(map)) {
536                    Ok(StreamMessage::Unknown) => None,
537                    Ok(msg) => Some(msg),
538                    Err(_) => None,
539                }
540            })
541            .collect()),
542        _ => Ok(vec![]),
543    }
544}
545
546/// Batch parsing utilities for high-throughput scenarios
547pub struct BatchDecoder {
548    buffer: Vec<u8>,
549}
550
551impl BatchDecoder {
552    pub fn new() -> Self {
553        Self {
554            buffer: Vec::with_capacity(8192),
555        }
556    }
557
558    /// Parse multiple JSON objects from a byte stream
559    pub fn parse_json_stream<T>(&mut self, data: &[u8]) -> Result<Vec<T>>
560    where
561        T: for<'de> serde::Deserialize<'de>,
562    {
563        self.buffer.extend_from_slice(data);
564        let mut results = Vec::new();
565        let mut start = 0;
566
567        while let Some(end) = self.find_json_boundary(start) {
568            let json_slice = &self.buffer[start..end];
569            if let Ok(obj) = serde_json::from_slice::<T>(json_slice) {
570                results.push(obj);
571            }
572            start = end;
573        }
574
575        // Keep remaining incomplete data
576        if start > 0 {
577            self.buffer.drain(0..start);
578        }
579
580        Ok(results)
581    }
582
583    /// Find the end of a JSON object in the buffer
584    fn find_json_boundary(&self, start: usize) -> Option<usize> {
585        let mut depth = 0;
586        let mut in_string = false;
587        let mut escaped = false;
588
589        for (i, &byte) in self.buffer[start..].iter().enumerate() {
590            if escaped {
591                escaped = false;
592                continue;
593            }
594
595            match byte {
596                b'\\' if in_string => escaped = true,
597                b'"' => in_string = !in_string,
598                b'{' if !in_string => depth += 1,
599                b'}' if !in_string => {
600                    depth -= 1;
601                    if depth == 0 {
602                        return Some(start + i + 1);
603                    }
604                },
605                _ => {},
606            }
607        }
608
609        None
610    }
611}
612
613impl Default for BatchDecoder {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619/// Optimized parsers for common data types
620pub mod fast_parse {
621    use super::*;
622
623    /// Fast decimal parsing for prices
624    #[inline]
625    pub fn parse_decimal(s: &str) -> Result<Decimal> {
626        Decimal::from_str(s)
627            .map_err(|e| PolyfillError::parse(format!("Invalid decimal: {}", e), None))
628    }
629
630    /// Fast address parsing
631    #[inline]
632    pub fn parse_address(s: &str) -> Result<Address> {
633        Address::from_str(s)
634            .map_err(|e| PolyfillError::parse(format!("Invalid address: {}", e), None))
635    }
636
637    /// Fast JSON parsing using SIMD instructions when possible
638    /// Falls back to serde_json if simd-json fails
639    /// Note: This requires owned types (no borrowing from input)
640    #[inline]
641    pub fn parse_json_fast<T>(bytes: &mut [u8]) -> Result<T>
642    where
643        T: for<'de> serde::Deserialize<'de>,
644    {
645        // Try SIMD parsing first (2-3x faster)
646        match simd_json::serde::from_slice(bytes) {
647            Ok(val) => Ok(val),
648            Err(_) => {
649                // Fallback to standard serde_json for safety
650                serde_json::from_slice(bytes)
651                    .map_err(|e| PolyfillError::parse(format!("JSON parse error: {}", e), None))
652            },
653        }
654    }
655
656    /// Fast JSON parsing for immutable data
657    #[inline]
658    pub fn parse_json_fast_owned<T>(bytes: &[u8]) -> Result<T>
659    where
660        T: for<'de> serde::Deserialize<'de>,
661    {
662        // Make a mutable copy for SIMD parsing
663        let mut data = bytes.to_vec();
664        parse_json_fast(&mut data)
665    }
666
667    /// Fast U256 parsing
668    #[inline]
669    pub fn parse_u256(s: &str) -> Result<U256> {
670        U256::from_str_radix(s, 10)
671            .map_err(|e| PolyfillError::parse(format!("Invalid U256: {}", e), None))
672    }
673
674    /// Parse Side enum
675    #[inline]
676    pub fn parse_side(s: &str) -> Result<Side> {
677        match s.to_uppercase().as_str() {
678            "BUY" => Ok(Side::BUY),
679            "SELL" => Ok(Side::SELL),
680            _ => Err(PolyfillError::parse(format!("Invalid side: {}", s), None)),
681        }
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688
689    #[test]
690    fn test_parse_decimal() {
691        let result = fast_parse::parse_decimal("123.456").unwrap();
692        assert_eq!(result, Decimal::from_str("123.456").unwrap());
693    }
694
695    #[test]
696    fn test_parse_side() {
697        assert_eq!(fast_parse::parse_side("BUY").unwrap(), Side::BUY);
698        assert_eq!(fast_parse::parse_side("sell").unwrap(), Side::SELL);
699        assert!(fast_parse::parse_side("invalid").is_err());
700    }
701
702    #[test]
703    fn test_batch_decoder() {
704        let mut decoder = BatchDecoder::new();
705        let data = r#"{"test":1}{"test":2}"#.as_bytes();
706
707        let results: Vec<serde_json::Value> = decoder.parse_json_stream(data).unwrap();
708        assert_eq!(results.len(), 2);
709    }
710
711    #[test]
712    fn stream_book_message_requires_bids_and_asks() {
713        let missing_asks = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"bids":[]}"#;
714        assert!(parse_stream_messages_bytes(missing_asks).is_err());
715
716        let missing_bids = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"asks":[]}"#;
717        assert!(parse_stream_messages_bytes(missing_bids).is_err());
718
719        let empty_sides = br#"{"event_type":"book","asset_id":"test_asset_id","market":"0xabc","timestamp":1000,"bids":[],"asks":[]}"#;
720        let messages = parse_stream_messages_bytes(empty_sides).unwrap();
721        assert_eq!(messages.len(), 1);
722    }
723}