nautilus_polymarket/common/
models.rs1use std::fmt::Display;
19
20use nautilus_common::cache::Cache;
21use nautilus_model::{
22 identifiers::InstrumentId,
23 instruments::{Instrument, InstrumentAny},
24};
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use crate::common::{
30 enums::{PolymarketOrderSide, PolymarketOutcome, PolymarketSignerType},
31 parse::{deserialize_decimal_from_str, serialize_decimal_as_str},
32};
33
34#[must_use]
36pub(crate) fn is_owned_by_account(
37 maker_address: &str,
38 owner: &str,
39 user_address: &str,
40 api_key: &str,
41 signer_type: PolymarketSignerType,
42) -> bool {
43 (signer_type == PolymarketSignerType::Owner && maker_address.eq_ignore_ascii_case(user_address))
44 || owner == api_key
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct PolymarketMakerOrder {
60 pub asset_id: Ustr,
61 pub maker_address: String,
62 #[serde(
63 serialize_with = "serialize_decimal_as_str",
64 deserialize_with = "deserialize_decimal_from_str"
65 )]
66 pub matched_amount: Decimal,
67 pub order_id: String,
68 pub outcome: PolymarketOutcome,
69 pub owner: String,
70 #[serde(
71 serialize_with = "serialize_decimal_as_str",
72 deserialize_with = "deserialize_decimal_from_str"
73 )]
74 pub price: Decimal,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub side: Option<PolymarketOrderSide>,
77}
78
79impl PolymarketMakerOrder {
80 #[must_use]
91 pub(crate) fn is_owned_by(
92 &self,
93 user_address: &str,
94 api_key: &str,
95 signer_type: PolymarketSignerType,
96 ) -> bool {
97 is_owned_by_account(
98 &self.maker_address,
99 &self.owner,
100 user_address,
101 api_key,
102 signer_type,
103 )
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct PolymarketLabel {
110 pub description: String,
111 pub outcome: String,
112}
113
114impl Display for PolymarketLabel {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{} [{}]", self.description, self.outcome)
117 }
118}
119
120impl PolymarketLabel {
121 pub fn from_instrument(instrument: &InstrumentAny) -> Self {
123 if let InstrumentAny::BinaryOption(opt) = instrument {
124 Self {
125 description: opt
126 .description
127 .map_or_else(|| instrument.id().to_string(), |d| d.to_string()),
128 outcome: opt
129 .outcome
130 .map_or_else(|| "?".to_string(), |o| o.to_string()),
131 }
132 } else {
133 Self {
134 description: instrument.id().to_string(),
135 outcome: "?".to_string(),
136 }
137 }
138 }
139
140 pub fn from_cache(instrument_id: &InstrumentId, cache: &Cache) -> Option<Self> {
143 cache.instrument(instrument_id).map(Self::from_instrument)
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use rstest::rstest;
150 use rust_decimal_macros::dec;
151
152 use super::*;
153 use crate::{common::enums::PolymarketOutcome, http::models::PolymarketTradeReport};
154
155 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
156 let path = format!("test_data/{filename}");
157 let content = std::fs::read_to_string(path).expect("Failed to read test data");
158 serde_json::from_str(&content).expect("Failed to parse test data")
159 }
160
161 fn sample_maker_order_json() -> &'static str {
162 r#"{
163 "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
164 "fee_rate_bps": "10",
165 "maker_address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
166 "matched_amount": "50.0000",
167 "order_id": "0xorder001",
168 "outcome": "Yes",
169 "owner": "00000000-0000-0000-0000-000000000002",
170 "price": "0.6000"
171 }"#
172 }
173
174 #[rstest]
175 fn test_maker_order_deserialization() {
176 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
177
178 assert_eq!(
179 order.asset_id.as_str(),
180 "71321045679252212594626385532706912750332728571942532289631379312455583992563"
181 );
182 assert_eq!(
183 order.maker_address.as_str(),
184 "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
185 );
186 assert_eq!(order.matched_amount, dec!(50.0000));
187 assert_eq!(order.order_id, "0xorder001");
188 assert_eq!(order.outcome, PolymarketOutcome::yes());
189 assert_eq!(order.price, dec!(0.6000));
190 }
191
192 #[rstest]
193 fn test_maker_order_roundtrip() {
194 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
195 let json = serde_json::to_string(&order).unwrap();
196 let order2: PolymarketMakerOrder = serde_json::from_str(&json).unwrap();
197 assert_eq!(order, order2);
198 }
199
200 #[rstest]
201 fn test_maker_order_ownership_ignores_address_case() {
202 let mut order: PolymarketMakerOrder =
203 serde_json::from_str(sample_maker_order_json()).unwrap();
204 let lowercase_address = order.maker_address.clone();
205 let uppercase_variant_address = lowercase_address
208 .to_ascii_uppercase()
209 .replacen("0X", "0x", 1);
210 assert_ne!(uppercase_variant_address, lowercase_address);
211
212 order.maker_address = uppercase_variant_address.clone();
215 assert!(order.is_owned_by(
216 &lowercase_address,
217 "no-such-key",
218 PolymarketSignerType::Owner
219 ));
220
221 order.maker_address = lowercase_address;
223 assert!(order.is_owned_by(
224 &uppercase_variant_address,
225 "no-such-key",
226 PolymarketSignerType::Owner
227 ));
228 }
229
230 #[rstest]
231 fn test_maker_order_ownership_matches_exact_api_key() {
232 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
233 let owner = order.owner.clone();
234
235 assert!(order.is_owned_by("0xother", &owner, PolymarketSignerType::Owner));
236 }
237
238 #[rstest]
239 fn test_maker_order_ownership_requires_exact_api_key() {
240 let mut order: PolymarketMakerOrder =
241 serde_json::from_str(sample_maker_order_json()).unwrap();
242 order.owner = "abcdefab-0000-0000-0000-000000000002".to_string();
243 let case_variant_key = order.owner.to_ascii_uppercase();
244 assert_ne!(case_variant_key, order.owner);
245
246 assert!(!order.is_owned_by("0xother", &case_variant_key, PolymarketSignerType::Owner));
247 }
248
249 #[rstest]
250 fn test_maker_order_ownership_rejects_foreign_identity() {
251 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
252
253 assert!(!order.is_owned_by("0xother", "no-such-key", PolymarketSignerType::Owner));
254 }
255
256 #[rstest]
257 fn test_maker_order_outcome_no() {
258 let json = r#"{
259 "asset_id": "12345",
260 "fee_rate_bps": "0",
261 "maker_address": "0xaddr",
262 "matched_amount": "10.0",
263 "order_id": "order-1",
264 "outcome": "No",
265 "owner": "owner-1",
266 "price": "0.4"
267 }"#;
268 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
269 assert_eq!(order.outcome, PolymarketOutcome::no());
270 }
271
272 #[rstest]
273 fn test_maker_order_decimal_precision() {
274 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
276 let json = serde_json::to_string(&order).unwrap();
277 assert!(
279 json.contains("\"matched_amount\":\"50.0000\"")
280 || json.contains("\"matched_amount\": \"50.0000\"")
281 );
282 }
283
284 #[rstest]
286 fn test_maker_orders_from_trade_report() {
287 let trade: PolymarketTradeReport = load("http_trade_report.json");
288
289 assert_eq!(trade.maker_orders.len(), 2);
290 let m0 = &trade.maker_orders[0];
291 assert_eq!(m0.matched_amount, dec!(25.0000));
292 assert_eq!(m0.outcome, PolymarketOutcome::yes());
293 assert_eq!(m0.side, Some(PolymarketOrderSide::Sell));
294
295 let m1 = &trade.maker_orders[1];
296 assert_eq!(m1.matched_amount, dec!(5.0000));
297 assert_eq!(m1.side, Some(PolymarketOrderSide::Sell));
298 }
299
300 #[rstest]
301 fn test_maker_order_without_side_is_accepted() {
302 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
305 assert!(order.side.is_none());
306 }
307
308 #[rstest]
309 fn test_maker_order_with_side_is_parsed() {
310 let json = r#"{
311 "asset_id": "12345",
312 "fee_rate_bps": "0",
313 "maker_address": "0xaddr",
314 "matched_amount": "10.0",
315 "order_id": "order-1",
316 "outcome": "Yes",
317 "owner": "owner-1",
318 "price": "0.4",
319 "side": "BUY"
320 }"#;
321 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
322 assert_eq!(order.side, Some(PolymarketOrderSide::Buy));
323 }
324
325 #[rstest]
326 fn test_maker_order_with_empty_fee_rate_bps_is_accepted() {
327 let json = r#"{
331 "asset_id": "12345",
332 "fee_rate_bps": "",
333 "maker_address": "0xaddr",
334 "matched_amount": "10.0",
335 "order_id": "order-1",
336 "outcome": "Yes",
337 "owner": "owner-1",
338 "price": "0.4"
339 }"#;
340 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
341 assert_eq!(order.matched_amount, dec!(10.0));
342 }
343
344 #[rstest]
345 #[case(PolymarketSignerType::Owner, "other", true)]
346 #[case(PolymarketSignerType::Session, "other", false)]
347 #[case(PolymarketSignerType::Session, "session-api", true)]
348 #[case(PolymarketSignerType::Session, "SESSION-API", false)]
349 fn test_shared_wallet_session_ownership(
350 #[case] signer_type: PolymarketSignerType,
351 #[case] owner: &str,
352 #[case] expected: bool,
353 ) {
354 assert_eq!(
355 is_owned_by_account("0xwallet", owner, "0xwallet", "session-api", signer_type),
356 expected
357 );
358 }
359}