mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-02 15:27:44 +00:00
feat(ws): extend TradeMessage with full trade execution fields
TradeMessage was severely under-specified compared to the Polymarket WS API. Add missing fields needed by the engine: taker_order_id, maker_orders, trader_side, fee_rate_bps, outcome, owner, trade_owner, transaction_hash. Replace untyped status/msg_type strings with proper TradeMessageStatus and TradeMessageType enums.
This commit is contained in:
+9
-1
@@ -623,11 +623,19 @@ impl PolyfillDemo {
|
||||
side: Side::BUY,
|
||||
size: dec!(50.0),
|
||||
price: dec!(0.75),
|
||||
status: Some("MATCHED".to_string()),
|
||||
status: TradeMessageStatus::Matched,
|
||||
msg_type: None,
|
||||
last_update: None,
|
||||
matchtime: None,
|
||||
timestamp: None,
|
||||
outcome: None,
|
||||
owner: None,
|
||||
trade_owner: None,
|
||||
taker_order_id: None,
|
||||
maker_orders: vec![],
|
||||
fee_rate_bps: None,
|
||||
transaction_hash: None,
|
||||
trader_side: None,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
+149
@@ -663,4 +663,153 @@ mod tests {
|
||||
let results: Vec<serde_json::Value> = decoder.parse_json_stream(data).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_full_payload() {
|
||||
use crate::types::{StreamMessage, TradeMessageStatus, TradeMessageType, TraderSide};
|
||||
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "trade-001",
|
||||
"market": "0xabc123",
|
||||
"asset_id": "asset-xyz",
|
||||
"side": "BUY",
|
||||
"size": "100.5",
|
||||
"price": "0.65",
|
||||
"status": "MATCHED",
|
||||
"type": "TRADE",
|
||||
"last_update": "1700000000",
|
||||
"match_time": "1700000001",
|
||||
"timestamp": "1700000002",
|
||||
"outcome": "Yes",
|
||||
"owner": "owner-key-123",
|
||||
"trade_owner": "trader-key-456",
|
||||
"taker_order_id": "taker-order-789",
|
||||
"maker_orders": [
|
||||
{
|
||||
"order_id": "maker-order-1",
|
||||
"owner": "maker-owner-1",
|
||||
"matched_amount": "50.25",
|
||||
"price": "0.65",
|
||||
"asset_id": "asset-xyz",
|
||||
"outcome": "Yes"
|
||||
}
|
||||
],
|
||||
"fee_rate_bps": "2.5",
|
||||
"transaction_hash": "0xdeadbeef",
|
||||
"trader_side": "TAKER"
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade variant");
|
||||
};
|
||||
|
||||
assert_eq!(trade.id, "trade-001");
|
||||
assert_eq!(trade.market, "0xabc123");
|
||||
assert_eq!(trade.asset_id, "asset-xyz");
|
||||
assert_eq!(trade.side, Side::BUY);
|
||||
assert_eq!(trade.size, Decimal::from_str("100.5").unwrap());
|
||||
assert_eq!(trade.price, Decimal::from_str("0.65").unwrap());
|
||||
assert_eq!(trade.status, TradeMessageStatus::Matched);
|
||||
assert_eq!(trade.msg_type, Some(TradeMessageType::Trade));
|
||||
assert_eq!(trade.last_update, Some(1700000000));
|
||||
assert_eq!(trade.matchtime, Some(1700000001));
|
||||
assert_eq!(trade.timestamp, Some(1700000002));
|
||||
assert_eq!(trade.outcome.as_deref(), Some("Yes"));
|
||||
assert_eq!(trade.owner.as_deref(), Some("owner-key-123"));
|
||||
assert_eq!(trade.trade_owner.as_deref(), Some("trader-key-456"));
|
||||
assert_eq!(trade.taker_order_id.as_deref(), Some("taker-order-789"));
|
||||
assert_eq!(trade.maker_orders.len(), 1);
|
||||
assert_eq!(trade.maker_orders[0].order_id, "maker-order-1");
|
||||
assert_eq!(trade.fee_rate_bps, Some(Decimal::from_str("2.5").unwrap()));
|
||||
assert_eq!(trade.transaction_hash.as_deref(), Some("0xdeadbeef"));
|
||||
assert_eq!(trade.trader_side, Some(TraderSide::Taker));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_minimal_payload() {
|
||||
use crate::types::StreamMessage;
|
||||
|
||||
// Only mandatory fields — all new optional fields absent.
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "trade-minimal",
|
||||
"market": "0xdef",
|
||||
"asset_id": "asset-min",
|
||||
"side": "SELL",
|
||||
"size": "10",
|
||||
"price": "0.50"
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade variant");
|
||||
};
|
||||
|
||||
assert_eq!(trade.id, "trade-minimal");
|
||||
assert_eq!(trade.side, Side::SELL);
|
||||
// All optional fields should be None / empty / default.
|
||||
assert!(trade.msg_type.is_none());
|
||||
assert!(trade.outcome.is_none());
|
||||
assert!(trade.owner.is_none());
|
||||
assert!(trade.trade_owner.is_none());
|
||||
assert!(trade.taker_order_id.is_none());
|
||||
assert!(trade.maker_orders.is_empty());
|
||||
assert!(trade.fee_rate_bps.is_none());
|
||||
assert!(trade.transaction_hash.is_none());
|
||||
assert!(trade.trader_side.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_status_lifecycle() {
|
||||
use crate::types::{StreamMessage, TradeMessageStatus};
|
||||
|
||||
for (status_str, expected) in [
|
||||
("MATCHED", TradeMessageStatus::Matched),
|
||||
("matched", TradeMessageStatus::Matched),
|
||||
("Matched", TradeMessageStatus::Matched),
|
||||
("MINED", TradeMessageStatus::Mined),
|
||||
("mined", TradeMessageStatus::Mined),
|
||||
("CONFIRMED", TradeMessageStatus::Confirmed),
|
||||
("confirmed", TradeMessageStatus::Confirmed),
|
||||
] {
|
||||
let json = format!(
|
||||
r#"{{
|
||||
"event_type": "trade",
|
||||
"id": "t1", "market": "m", "asset_id": "a",
|
||||
"side": "BUY", "size": "1", "price": "0.5",
|
||||
"status": "{status_str}"
|
||||
}}"#
|
||||
);
|
||||
let msgs = parse_stream_messages(&json).unwrap();
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade");
|
||||
};
|
||||
assert_eq!(trade.status, expected, "failed for status_str={status_str}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_message_null_maker_orders() {
|
||||
use crate::types::StreamMessage;
|
||||
|
||||
// API sometimes sends `null` instead of `[]`.
|
||||
let json = r#"{
|
||||
"event_type": "trade",
|
||||
"id": "t1", "market": "m", "asset_id": "a",
|
||||
"side": "BUY", "size": "1", "price": "0.5",
|
||||
"maker_orders": null
|
||||
}"#;
|
||||
|
||||
let msgs = parse_stream_messages(json).unwrap();
|
||||
let StreamMessage::Trade(trade) = &msgs[0] else {
|
||||
panic!("expected Trade");
|
||||
};
|
||||
assert!(trade.maker_orders.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,9 @@ pub use crate::types::{
|
||||
TickSizeResponse,
|
||||
Token,
|
||||
TokenPrice,
|
||||
TradeMessage,
|
||||
TradeMessageStatus,
|
||||
TradeMessageType,
|
||||
TradeParams,
|
||||
TradeResponse,
|
||||
TraderSide,
|
||||
|
||||
+64
-3
@@ -206,6 +206,36 @@ impl Default for TraderSide {
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade lifecycle status (Matched → Mined → Confirmed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TradeMessageStatus {
|
||||
#[serde(alias = "matched", alias = "MATCHED")]
|
||||
Matched,
|
||||
#[serde(alias = "mined", alias = "MINED")]
|
||||
Mined,
|
||||
#[serde(alias = "confirmed", alias = "CONFIRMED")]
|
||||
Confirmed,
|
||||
/// Forward-compatible catch-all for unknown statuses.
|
||||
#[serde(untagged)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Default for TradeMessageStatus {
|
||||
fn default() -> Self {
|
||||
Self::Unknown("UNKNOWN".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade message type discriminator.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TradeMessageType {
|
||||
#[serde(alias = "trade", alias = "TRADE")]
|
||||
Trade,
|
||||
/// Forward-compatible catch-all.
|
||||
#[serde(untagged)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Side {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -908,7 +938,7 @@ pub struct EventMessage {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// User trade execution message.
|
||||
/// User trade execution message (authenticated WebSocket channel).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TradeMessage {
|
||||
pub id: String,
|
||||
@@ -917,10 +947,11 @@ pub struct TradeMessage {
|
||||
pub side: Side,
|
||||
pub size: Decimal,
|
||||
pub price: Decimal,
|
||||
/// Trade lifecycle status (Matched → Mined → Confirmed).
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
pub status: TradeMessageStatus,
|
||||
#[serde(rename = "type", default)]
|
||||
pub msg_type: Option<String>,
|
||||
pub msg_type: Option<TradeMessageType>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
@@ -937,6 +968,36 @@ pub struct TradeMessage {
|
||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||
)]
|
||||
pub timestamp: Option<u64>,
|
||||
/// Outcome (e.g. "Yes" / "No").
|
||||
#[serde(default)]
|
||||
pub outcome: Option<String>,
|
||||
/// API key of the event owner.
|
||||
#[serde(default)]
|
||||
pub owner: Option<String>,
|
||||
/// API key of the trade owner.
|
||||
#[serde(default)]
|
||||
pub trade_owner: Option<String>,
|
||||
/// Taker order ID.
|
||||
#[serde(default)]
|
||||
pub taker_order_id: Option<String>,
|
||||
/// Maker order details.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::vec_from_null"
|
||||
)]
|
||||
pub maker_orders: Vec<MakerOrder>,
|
||||
/// Fee rate in basis points.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
|
||||
)]
|
||||
pub fee_rate_bps: Option<Decimal>,
|
||||
/// On-chain transaction hash.
|
||||
#[serde(default)]
|
||||
pub transaction_hash: Option<String>,
|
||||
/// Whether user was maker or taker.
|
||||
#[serde(default)]
|
||||
pub trader_side: Option<TraderSide>,
|
||||
}
|
||||
|
||||
/// User order update message.
|
||||
|
||||
Reference in New Issue
Block a user