mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-20 07:58:08 +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,
|
side: Side::BUY,
|
||||||
size: dec!(50.0),
|
size: dec!(50.0),
|
||||||
price: dec!(0.75),
|
price: dec!(0.75),
|
||||||
status: Some("MATCHED".to_string()),
|
status: TradeMessageStatus::Matched,
|
||||||
msg_type: None,
|
msg_type: None,
|
||||||
last_update: None,
|
last_update: None,
|
||||||
matchtime: None,
|
matchtime: None,
|
||||||
timestamp: 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();
|
let results: Vec<serde_json::Value> = decoder.parse_json_stream(data).unwrap();
|
||||||
assert_eq!(results.len(), 2);
|
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,
|
TickSizeResponse,
|
||||||
Token,
|
Token,
|
||||||
TokenPrice,
|
TokenPrice,
|
||||||
|
TradeMessage,
|
||||||
|
TradeMessageStatus,
|
||||||
|
TradeMessageType,
|
||||||
TradeParams,
|
TradeParams,
|
||||||
TradeResponse,
|
TradeResponse,
|
||||||
TraderSide,
|
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 {
|
impl Side {
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@@ -908,7 +938,7 @@ pub struct EventMessage {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User trade execution message.
|
/// User trade execution message (authenticated WebSocket channel).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TradeMessage {
|
pub struct TradeMessage {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -917,10 +947,11 @@ pub struct TradeMessage {
|
|||||||
pub side: Side,
|
pub side: Side,
|
||||||
pub size: Decimal,
|
pub size: Decimal,
|
||||||
pub price: Decimal,
|
pub price: Decimal,
|
||||||
|
/// Trade lifecycle status (Matched → Mined → Confirmed).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub status: Option<String>,
|
pub status: TradeMessageStatus,
|
||||||
#[serde(rename = "type", default)]
|
#[serde(rename = "type", default)]
|
||||||
pub msg_type: Option<String>,
|
pub msg_type: Option<TradeMessageType>,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
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"
|
deserialize_with = "crate::decode::deserializers::optional_number_from_string"
|
||||||
)]
|
)]
|
||||||
pub timestamp: Option<u64>,
|
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.
|
/// User order update message.
|
||||||
|
|||||||
Reference in New Issue
Block a user