use nautilus_core::serialization::deserialize_empty_string_as_none;
use serde::{
Deserialize, Serialize,
de::{
DeserializeSeed, MapAccess, Visitor,
value::{BorrowedStrDeserializer, MapAccessDeserializer},
},
};
use ustr::Ustr;
use crate::common::{
enums::{
PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
},
models::PolymarketMakerOrder,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketUserOrder {
pub asset_id: Ustr,
pub associate_trades: Option<Vec<String>>,
pub created_at: String,
pub expiration: Option<String>,
pub id: String,
pub maker_address: Ustr,
pub market: Ustr,
pub order_owner: Ustr,
pub order_type: PolymarketOrderType,
pub original_size: String,
pub outcome: PolymarketOutcome,
pub owner: Ustr,
pub price: String,
pub side: PolymarketOrderSide,
pub size_matched: String,
pub status: PolymarketOrderStatus,
pub timestamp: String,
#[serde(rename = "type")]
pub event_type: PolymarketEventType,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketUserTrade {
pub asset_id: Ustr,
pub bucket_index: u64,
pub fee_rate_bps: String,
pub id: String,
pub last_update: String,
pub maker_address: Ustr,
pub maker_orders: Vec<PolymarketMakerOrder>,
pub market: Ustr,
pub match_time: String,
pub outcome: PolymarketOutcome,
pub owner: Ustr,
pub price: String,
pub side: PolymarketOrderSide,
pub size: String,
pub status: PolymarketTradeStatus,
pub taker_order_id: String,
pub timestamp: String,
pub trade_owner: Ustr,
#[serde(
default,
deserialize_with = "deserialize_empty_string_as_none",
skip_serializing_if = "Option::is_none"
)]
pub transaction_hash: Option<String>,
pub trader_side: PolymarketLiquiditySide,
#[serde(rename = "type")]
pub event_type: PolymarketEventType,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketBookLevel {
pub price: String,
pub size: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketBookSnapshot {
pub market: Ustr,
pub asset_id: Ustr,
pub bids: Vec<PolymarketBookLevel>,
pub asks: Vec<PolymarketBookLevel>,
pub timestamp: String,
#[serde(default)]
pub hash: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketQuote {
pub asset_id: Ustr,
pub price: String,
pub side: PolymarketOrderSide,
pub size: String,
pub hash: String,
#[serde(default)]
pub best_bid: Option<String>,
#[serde(default)]
pub best_ask: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketQuotes {
pub market: Ustr,
pub price_changes: Vec<PolymarketQuote>,
pub timestamp: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketTrade {
pub market: Ustr,
pub asset_id: Ustr,
pub fee_rate_bps: String,
pub price: String,
pub side: PolymarketOrderSide,
pub size: String,
pub timestamp: String,
#[serde(default)]
pub transaction_hash: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketTickSizeChange {
pub market: Ustr,
pub asset_id: Ustr,
pub new_tick_size: String,
pub old_tick_size: String,
pub timestamp: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketNewMarketEvent {
pub id: String,
pub ticker: String,
pub slug: String,
pub title: String,
pub description: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketNewMarket {
pub id: String,
pub question: String,
pub market: Ustr,
pub slug: String,
pub description: String,
pub assets_ids: Vec<String>,
pub outcomes: Vec<String>,
pub timestamp: String,
pub tags: Vec<String>,
pub condition_id: String,
pub active: bool,
pub clob_token_ids: Vec<String>,
#[serde(default)]
pub order_price_min_tick_size: Option<String>,
#[serde(default)]
pub group_item_title: Option<String>,
#[serde(default)]
pub event_message: Option<PolymarketNewMarketEvent>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketMarketResolved {
pub id: String,
pub market: Ustr,
pub assets_ids: Vec<String>,
pub winning_asset_id: String,
pub winning_outcome: String,
pub timestamp: String,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolymarketBestBidAsk {
pub market: Ustr,
pub asset_id: Ustr,
pub best_bid: String,
pub best_ask: String,
pub spread: String,
pub timestamp: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event_type")]
pub enum MarketWsMessage {
#[serde(rename = "book")]
Book(PolymarketBookSnapshot),
#[serde(rename = "price_change")]
PriceChange(PolymarketQuotes),
#[serde(rename = "last_trade_price")]
LastTradePrice(PolymarketTrade),
#[serde(rename = "tick_size_change")]
TickSizeChange(PolymarketTickSizeChange),
#[serde(rename = "new_market")]
NewMarket(Box<PolymarketNewMarket>),
#[serde(rename = "market_resolved")]
MarketResolved(PolymarketMarketResolved),
#[serde(rename = "best_bid_ask")]
BestBidAsk(PolymarketBestBidAsk),
}
struct PayloadMapAccess<A> {
inner: A,
}
impl<A> PayloadMapAccess<A> {
const fn new(inner: A) -> Self {
Self { inner }
}
}
impl<'de, A> MapAccess<'de> for PayloadMapAccess<A>
where
A: MapAccess<'de>,
{
type Error = A::Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
let Some(key) = self.inner.next_key::<&'de str>()? else {
return Ok(None);
};
if key == "event_type" {
return Err(serde::de::Error::duplicate_field("event_type"));
}
seed.deserialize(BorrowedStrDeserializer::new(key))
.map(Some)
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
self.inner.next_value_seed(seed)
}
fn size_hint(&self) -> Option<usize> {
self.inner.size_hint()
}
}
struct MarketWsMessageVisitor;
impl<'de> Visitor<'de> for MarketWsMessageVisitor {
type Value = MarketWsMessage;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a Polymarket market-channel message with event_type first")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let Some(key) = map.next_key::<&str>()? else {
return Err(serde::de::Error::custom("expected event_type field"));
};
if key != "event_type" {
return Err(serde::de::Error::custom(
"event_type was not the first field",
));
}
let event_type = map.next_value::<&str>()?;
let remaining = MapAccessDeserializer::new(PayloadMapAccess::new(map));
match event_type {
"book" => PolymarketBookSnapshot::deserialize(remaining).map(Self::Value::Book),
"price_change" => {
PolymarketQuotes::deserialize(remaining).map(Self::Value::PriceChange)
}
"last_trade_price" => {
PolymarketTrade::deserialize(remaining).map(Self::Value::LastTradePrice)
}
"tick_size_change" => {
PolymarketTickSizeChange::deserialize(remaining).map(Self::Value::TickSizeChange)
}
"new_market" => PolymarketNewMarket::deserialize(remaining)
.map(Box::new)
.map(Self::Value::NewMarket),
"market_resolved" => {
PolymarketMarketResolved::deserialize(remaining).map(Self::Value::MarketResolved)
}
"best_bid_ask" => {
PolymarketBestBidAsk::deserialize(remaining).map(Self::Value::BestBidAsk)
}
other => Err(serde::de::Error::unknown_variant(
other,
&[
"book",
"price_change",
"last_trade_price",
"tick_size_change",
"new_market",
"market_resolved",
"best_bid_ask",
],
)),
}
}
}
impl MarketWsMessage {
pub fn parse(text: &str) -> serde_json::Result<Self> {
let mut deserializer = serde_json::Deserializer::from_str(text);
serde::Deserializer::deserialize_map(&mut deserializer, MarketWsMessageVisitor)
.and_then(|message| {
deserializer.end()?;
Ok(message)
})
.or_else(|_| Self::parse_reordered(text))
.or_else(|_| serde_json::from_str(text))
}
fn parse_reordered(text: &str) -> serde_json::Result<Self> {
let tag = serde_json::from_str::<MarketWsTag>(text)?;
match tag.event_type {
MarketWsEventTag::Book => serde_json::from_str(text).map(Self::Book),
MarketWsEventTag::PriceChange => serde_json::from_str(text).map(Self::PriceChange),
MarketWsEventTag::LastTradePrice => {
serde_json::from_str(text).map(Self::LastTradePrice)
}
MarketWsEventTag::TickSizeChange => {
serde_json::from_str(text).map(Self::TickSizeChange)
}
MarketWsEventTag::NewMarket => serde_json::from_str(text)
.map(Box::new)
.map(Self::NewMarket),
MarketWsEventTag::MarketResolved => {
serde_json::from_str(text).map(Self::MarketResolved)
}
MarketWsEventTag::BestBidAsk => serde_json::from_str(text).map(Self::BestBidAsk),
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum MarketWsEventTag {
Book,
PriceChange,
LastTradePrice,
TickSizeChange,
NewMarket,
MarketResolved,
BestBidAsk,
}
#[derive(Deserialize)]
struct MarketWsTag {
event_type: MarketWsEventTag,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event_type")]
pub enum UserWsMessage {
#[serde(rename = "order")]
Order(PolymarketUserOrder),
#[serde(rename = "trade")]
Trade(PolymarketUserTrade),
}
struct UserWsMessageVisitor;
impl<'de> Visitor<'de> for UserWsMessageVisitor {
type Value = UserWsMessage;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a Polymarket user-channel message with event_type first")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let Some(key) = map.next_key::<&str>()? else {
return Err(serde::de::Error::custom("expected event_type field"));
};
if key != "event_type" {
return Err(serde::de::Error::custom(
"event_type was not the first field",
));
}
let event_type = map.next_value::<&str>()?;
let remaining = MapAccessDeserializer::new(PayloadMapAccess::new(map));
match event_type {
"order" => PolymarketUserOrder::deserialize(remaining).map(Self::Value::Order),
"trade" => PolymarketUserTrade::deserialize(remaining).map(Self::Value::Trade),
other => Err(serde::de::Error::unknown_variant(
other,
&["order", "trade"],
)),
}
}
}
impl UserWsMessage {
pub fn parse(text: &str) -> serde_json::Result<Self> {
let mut deserializer = serde_json::Deserializer::from_str(text);
serde::Deserializer::deserialize_map(&mut deserializer, UserWsMessageVisitor)
.and_then(|message| {
deserializer.end()?;
Ok(message)
})
.or_else(|_| Self::parse_reordered(text))
.or_else(|_| serde_json::from_str(text))
}
pub fn parse_batch(text: &str) -> serde_json::Result<Vec<Self>> {
serde_json::from_str(text)
}
fn parse_reordered(text: &str) -> serde_json::Result<Self> {
let tag = serde_json::from_str::<UserWsTag>(text)?;
match tag.event_type {
UserWsEventTag::Order => serde_json::from_str(text).map(Self::Order),
UserWsEventTag::Trade => serde_json::from_str(text).map(Self::Trade),
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum UserWsEventTag {
Order,
Trade,
}
#[derive(Deserialize)]
struct UserWsTag {
event_type: UserWsEventTag,
}
#[derive(Debug)]
pub enum PolymarketWsMessage {
Market(MarketWsMessage),
User(UserWsMessage),
Reconnected,
}
#[derive(Debug, Serialize)]
pub struct PolymarketWsAuth {
#[serde(rename = "apiKey")]
pub api_key: String,
pub secret: String,
pub passphrase: String,
}
#[derive(Debug, Serialize)]
pub struct MarketInitialSubscribeRequest {
pub assets_ids: Vec<String>,
#[serde(rename = "type")]
pub msg_type: &'static str,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub custom_feature_enabled: bool,
}
#[derive(Debug, Serialize)]
pub struct MarketSubscribeRequest {
pub assets_ids: Vec<String>,
pub operation: &'static str,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub custom_feature_enabled: bool,
}
#[derive(Debug, Serialize)]
pub struct MarketUnsubscribeRequest {
pub assets_ids: Vec<String>,
pub operation: &'static str,
}
#[derive(Debug, Serialize)]
pub struct UserSubscribeRequest {
pub auth: PolymarketWsAuth,
pub markets: Vec<String>,
pub assets_ids: Vec<String>,
#[serde(rename = "type")]
pub msg_type: &'static str,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::common::enums::{
PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
};
fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
let path = format!("test_data/{filename}");
let content = std::fs::read_to_string(path).expect("Failed to read test data");
serde_json::from_str(&content).expect("Failed to parse test data")
}
fn load_text(filename: &str) -> String {
let path = format!("test_data/{filename}");
std::fs::read_to_string(path).expect("Failed to read test data")
}
#[rstest]
fn test_book_snapshot() {
let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
assert_eq!(
snap.asset_id.as_str(),
"71321045679252212594626385532706912750332728571942532289631379312455583992563"
);
assert_eq!(snap.bids.len(), 3);
assert_eq!(snap.asks.len(), 3);
assert_eq!(snap.bids[0].price, "0.48");
assert_eq!(snap.bids[0].size, "500.0");
assert_eq!(snap.asks[0].price, "0.53");
assert_eq!(snap.timestamp, "1703875200000");
assert_eq!(
snap.hash.as_deref(),
Some("655a38d4977427b086c25b985993691b753ed166")
);
}
#[rstest]
fn test_book_snapshot_roundtrip() {
let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
let json = serde_json::to_string(&snap).unwrap();
let snap2: PolymarketBookSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(snap, snap2);
}
#[rstest]
fn test_quotes() {
let quotes: PolymarketQuotes = load("ws_quotes.json");
assert_eq!(quotes.price_changes.len(), 2);
assert_eq!(quotes.price_changes[0].side, PolymarketOrderSide::Buy);
assert_eq!(quotes.price_changes[0].price, "0.51");
assert_eq!(quotes.price_changes[0].best_bid.as_deref(), Some("0.51"));
assert_eq!(quotes.price_changes[0].best_ask.as_deref(), Some("0.52"));
assert_eq!(quotes.price_changes[1].side, PolymarketOrderSide::Sell);
assert_eq!(quotes.timestamp, "1703875201000");
}
#[rstest]
fn test_last_trade() {
let trade: PolymarketTrade = load("ws_last_trade.json");
assert_eq!(trade.price, "0.51");
assert_eq!(trade.size, "25.0");
assert_eq!(trade.side, PolymarketOrderSide::Buy);
assert_eq!(trade.fee_rate_bps, "0");
assert_eq!(trade.timestamp, "1703875202000");
assert_eq!(
trade.transaction_hash.as_deref(),
Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
);
}
#[rstest]
fn test_optional_market_hash_fields_default() {
let snap: PolymarketBookSnapshot = load("ws_book_snapshot_missing_hash.json");
let trade: PolymarketTrade = load("ws_last_trade_missing_transaction_hash.json");
assert!(snap.hash.is_none());
assert!(trade.transaction_hash.is_none());
}
#[rstest]
fn test_tick_size_change() {
let msg: PolymarketTickSizeChange = load("ws_tick_size_change.json");
assert_eq!(msg.new_tick_size, "0.01");
assert_eq!(msg.old_tick_size, "0.1");
assert_eq!(msg.timestamp, "1703875210000");
}
#[rstest]
fn test_user_order_placement() {
let order: PolymarketUserOrder = load("ws_user_order_placement.json");
assert_eq!(order.event_type, PolymarketEventType::Placement);
assert_eq!(order.status, PolymarketOrderStatus::Live);
assert_eq!(order.side, PolymarketOrderSide::Buy);
assert_eq!(order.order_type, PolymarketOrderType::GTC);
assert_eq!(order.outcome, PolymarketOutcome::yes());
assert_eq!(order.original_size, "100.0");
assert_eq!(order.size_matched, "0.0");
assert!(order.associate_trades.is_none());
assert!(order.expiration.is_none());
}
#[rstest]
fn test_user_order_update() {
let order: PolymarketUserOrder = load("ws_user_order_update.json");
assert_eq!(order.event_type, PolymarketEventType::Update);
assert_eq!(order.size_matched, "25.0");
assert_eq!(
order.associate_trades.as_deref(),
Some(&["trade-0xabcdef1234".to_string()][..])
);
}
#[rstest]
fn test_user_order_cancellation() {
let order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
assert_eq!(order.event_type, PolymarketEventType::Cancellation);
assert_eq!(order.status, PolymarketOrderStatus::Canceled);
assert_eq!(order.size_matched, "0.0");
}
#[rstest]
fn test_user_order_fok_killed() {
let msg: UserWsMessage = load("ws_user_order_fok_killed.json");
let UserWsMessage::Order(order) = msg else {
panic!("Expected UserWsMessage::Order");
};
assert_eq!(order.event_type, PolymarketEventType::Cancellation);
assert_eq!(order.status, PolymarketOrderStatus::Canceled);
assert_eq!(order.order_type, PolymarketOrderType::FOK);
assert_eq!(order.size_matched, "");
assert_eq!(order.created_at, "");
assert_eq!(order.outcome.as_str(), "");
}
#[rstest]
fn test_user_trade() {
let trade: PolymarketUserTrade = load("ws_user_trade.json");
assert_eq!(trade.event_type, PolymarketEventType::Trade);
assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
assert_eq!(trade.side, PolymarketOrderSide::Buy);
assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
assert_eq!(trade.price, "0.5");
assert_eq!(trade.size, "25.0");
assert_eq!(trade.fee_rate_bps, "0");
assert_eq!(trade.bucket_index, 1);
assert_eq!(trade.maker_orders.len(), 1);
assert_eq!(
trade.transaction_hash.as_deref(),
Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
);
assert_eq!(
trade.taker_order_id,
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
);
}
#[rstest]
fn test_user_trade_missing_transaction_hash() {
let mut value: serde_json::Value = load("ws_user_trade.json");
value
.as_object_mut()
.expect("trade fixture should be an object")
.remove("transaction_hash");
let trade: PolymarketUserTrade =
serde_json::from_value(value).expect("trade fixture should deserialize");
assert!(trade.transaction_hash.is_none());
}
#[rstest]
fn test_market_ws_message_book() {
let msg: MarketWsMessage = load("ws_market_book_msg.json");
assert!(matches!(msg, MarketWsMessage::Book(_)));
if let MarketWsMessage::Book(snap) = msg {
assert_eq!(snap.bids.len(), 2);
assert_eq!(snap.asks.len(), 2);
assert_eq!(snap.timestamp, "1703875200000");
}
}
#[rstest]
#[case("ws_market_book_msg.json")]
#[case("ws_market_price_change_msg.json")]
#[case("ws_market_last_trade_msg.json")]
#[case("ws_market_tick_size_msg.json")]
#[case("ws_market_new_market_msg.json")]
#[case("ws_market_resolved_msg.json")]
#[case("ws_market_best_bid_ask_msg.json")]
fn test_market_ws_message_parse(#[case] filename: &str) {
let text = load_text(filename);
let expected: MarketWsMessage =
serde_json::from_str(&text).expect("market fixture should deserialize");
let actual = MarketWsMessage::parse(&text).expect("market fixture should parse");
assert_eq!(actual, expected);
}
#[rstest]
fn test_market_ws_message_parse_with_reordered_event_type() {
let expected: MarketWsMessage = load("ws_market_book_msg.json");
let mut value: serde_json::Value = load("ws_market_book_msg.json");
let object = value
.as_object_mut()
.expect("market fixture should be an object");
let event_type = object
.remove("event_type")
.expect("market fixture should contain event_type");
object.insert("event_type".to_string(), event_type);
let text = serde_json::to_string(&value).expect("market fixture should serialize");
assert!(!text.starts_with(r#"{"event_type":"#));
assert_eq!(
MarketWsMessage::parse(&text).expect("reordered market fixture should parse"),
expected
);
}
#[rstest]
fn test_market_ws_message_parse_rejects_duplicate_event_type() {
let text = load_text("ws_market_book_msg.json").replacen(
r#""event_type": "book","#,
r#""event_type": "book", "event_type": "book","#,
1,
);
let expected = serde_json::from_str::<MarketWsMessage>(&text)
.expect_err("derived parser should reject a duplicate event_type");
let actual = MarketWsMessage::parse(&text)
.expect_err("optimized parser should reject a duplicate event_type");
assert_eq!(actual.to_string(), expected.to_string());
}
#[rstest]
fn test_market_ws_message_price_change() {
let msg: MarketWsMessage = load("ws_market_price_change_msg.json");
assert!(matches!(msg, MarketWsMessage::PriceChange(_)));
if let MarketWsMessage::PriceChange(quotes) = msg {
assert_eq!(quotes.price_changes.len(), 1);
}
}
#[rstest]
fn test_market_ws_message_last_trade_price() {
let msg: MarketWsMessage = load("ws_market_last_trade_msg.json");
assert!(matches!(msg, MarketWsMessage::LastTradePrice(_)));
if let MarketWsMessage::LastTradePrice(trade) = msg {
assert_eq!(trade.price, "0.51");
}
}
#[rstest]
fn test_market_ws_message_tick_size_change() {
let msg: MarketWsMessage = load("ws_market_tick_size_msg.json");
assert!(matches!(msg, MarketWsMessage::TickSizeChange(_)));
if let MarketWsMessage::TickSizeChange(change) = msg {
assert_eq!(change.new_tick_size, "0.01");
assert_eq!(change.old_tick_size, "0.1");
}
}
#[rstest]
fn test_user_ws_message_order() {
let msg: UserWsMessage = load("ws_user_order_msg.json");
assert!(matches!(msg, UserWsMessage::Order(_)));
if let UserWsMessage::Order(order) = msg {
assert_eq!(order.event_type, PolymarketEventType::Placement);
assert_eq!(order.side, PolymarketOrderSide::Buy);
}
}
#[rstest]
fn test_user_ws_message_trade() {
let msg: UserWsMessage = load("ws_user_trade_msg.json");
assert!(matches!(msg, UserWsMessage::Trade(_)));
if let UserWsMessage::Trade(trade) = msg {
assert_eq!(trade.event_type, PolymarketEventType::Trade);
assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
assert!(trade.transaction_hash.is_none());
}
}
#[rstest]
#[case("ws_user_order_msg.json")]
#[case("ws_user_order_fok_killed.json")]
#[case("ws_user_trade_msg.json")]
fn test_user_ws_message_parse(#[case] filename: &str) {
let text = load_text(filename);
let expected: UserWsMessage =
serde_json::from_str(&text).expect("user fixture should deserialize");
let actual = UserWsMessage::parse(&text).expect("user fixture should parse");
assert_eq!(actual, expected);
}
#[rstest]
fn test_user_ws_message_parse_with_reordered_event_type() {
let expected: UserWsMessage = load("ws_user_trade_msg.json");
let mut value: serde_json::Value = load("ws_user_trade_msg.json");
let object = value
.as_object_mut()
.expect("user fixture should be an object");
let event_type = object
.remove("event_type")
.expect("user fixture should contain event_type");
object.insert("event_type".to_string(), event_type);
let text = serde_json::to_string(&value).expect("user fixture should serialize");
assert!(!text.starts_with(r#"{"event_type":"#));
assert_eq!(
UserWsMessage::parse(&text).expect("reordered user fixture should parse"),
expected
);
}
#[rstest]
fn test_user_ws_message_parse_rejects_duplicate_event_type() {
let text = load_text("ws_user_order_msg.json").replacen(
r#""event_type": "order","#,
r#""event_type": "order", "event_type": "order","#,
1,
);
let expected = serde_json::from_str::<UserWsMessage>(&text)
.expect_err("derived parser should reject a duplicate event_type");
let actual = UserWsMessage::parse(&text)
.expect_err("optimized parser should reject a duplicate event_type");
assert_eq!(actual.to_string(), expected.to_string());
}
#[rstest]
fn test_user_ws_message_parse_batch() {
let text = load_text("ws_user_batch_msg.json");
let expected: Vec<UserWsMessage> =
serde_json::from_str(&text).expect("user batch fixture should deserialize");
let actual = UserWsMessage::parse_batch(&text).expect("user batch fixture should parse");
assert_eq!(actual, expected);
}
#[rstest]
fn test_user_ws_message_parse_batch_with_reordered_event_type() {
let expected: Vec<UserWsMessage> = load("ws_user_batch_msg.json");
let mut value: serde_json::Value = load("ws_user_batch_msg.json");
let first = value
.as_array_mut()
.expect("user batch fixture should be an array")[0]
.as_object_mut()
.expect("user batch element should be an object");
let event_type = first
.remove("event_type")
.expect("user batch element should contain event_type");
first.insert("event_type".to_string(), event_type);
let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
assert_eq!(
UserWsMessage::parse_batch(&text).expect("reordered user batch should parse"),
expected
);
}
#[rstest]
fn test_user_ws_message_parse_batch_rejects_invalid_element() {
let mut value: serde_json::Value = load("ws_user_batch_msg.json");
value
.as_array_mut()
.expect("user batch fixture should be an array")[1]
.as_object_mut()
.expect("user batch element should be an object")
.remove("event_type");
let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
assert!(UserWsMessage::parse_batch(&text).is_err());
}
#[rstest]
fn test_market_ws_message_new_market() {
let msg: MarketWsMessage = load("ws_market_new_market_msg.json");
assert!(matches!(msg, MarketWsMessage::NewMarket(_)));
if let MarketWsMessage::NewMarket(nm) = msg {
assert_eq!(nm.id, "1031769");
assert_eq!(nm.slug, "nvda-above-240-on-january-30-2026");
assert_eq!(
nm.market.as_str(),
"0x311d0c4b6671ab54af4970c06fcf58662516f5168997bdda209ec3db5aa6b0c1"
);
assert_eq!(
nm.condition_id,
"0x311d0c4b6671ab54af4970c06fcf58662516f5168997bdda209ec3db5aa6b0c1"
);
assert!(nm.active);
assert_eq!(nm.outcomes.len(), 2);
assert_eq!(nm.clob_token_ids.len(), 2);
assert_eq!(nm.order_price_min_tick_size.as_deref(), Some("0.01"));
let event = nm
.event_message
.as_ref()
.expect("event_message should be parsed");
assert_eq!(event.id, "125819");
assert_eq!(event.ticker, "nvda-above-in-january-2026");
assert_eq!(event.slug, "nvda-above-in-january-2026");
assert_eq!(
event.title,
"Will NVIDIA (NVDA) close above ___ end of January?"
);
}
}
#[rstest]
fn test_market_ws_message_resolved() {
let msg: MarketWsMessage = load("ws_market_resolved_msg.json");
assert!(matches!(msg, MarketWsMessage::MarketResolved(_)));
if let MarketWsMessage::MarketResolved(mr) = msg {
assert_eq!(mr.id, "1031769");
assert_eq!(mr.winning_outcome, "Yes");
assert_eq!(mr.assets_ids.len(), 2);
assert_eq!(
mr.winning_asset_id,
"76043073756653678226373981964075571318267289248134717369284518995922789326425"
);
}
}
#[rstest]
fn test_market_ws_message_best_bid_ask() {
let msg: MarketWsMessage = load("ws_market_best_bid_ask_msg.json");
assert!(matches!(msg, MarketWsMessage::BestBidAsk(_)));
if let MarketWsMessage::BestBidAsk(bba) = msg {
assert_eq!(bba.best_bid, "0.73");
assert_eq!(bba.best_ask, "0.77");
assert_eq!(bba.spread, "0.04");
}
}
}