Skip to main content

polyoxide_clob/
types.rs

1use std::fmt;
2
3use alloy::primitives::Address;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// Error when parsing a tick size from an invalid value
8#[derive(Error, Debug, Clone, PartialEq)]
9#[error("invalid tick size: {0}. Valid values are 0.1, 0.01, 0.001, or 0.0001")]
10pub struct ParseTickSizeError(String);
11
12/// Side of an order (buy or sell).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
14#[serde(rename_all = "UPPERCASE")]
15pub enum OrderSide {
16    Buy,
17    Sell,
18}
19
20impl OrderSide {
21    /// Returns the uppercase string representation ("BUY" / "SELL") for API queries.
22    pub fn as_str(&self) -> &'static str {
23        match self {
24            Self::Buy => "BUY",
25            Self::Sell => "SELL",
26        }
27    }
28}
29
30impl Serialize for OrderSide {
31    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
32    where
33        S: serde::Serializer,
34    {
35        match self {
36            Self::Buy => serializer.serialize_str("BUY"),
37            Self::Sell => serializer.serialize_str("SELL"),
38        }
39    }
40}
41
42impl fmt::Display for OrderSide {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Buy => write!(f, "0"),
46            Self::Sell => write!(f, "1"),
47        }
48    }
49}
50
51/// Order type/kind
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "UPPERCASE")]
54pub enum OrderKind {
55    /// Good-till-Cancelled
56    Gtc,
57    /// Fill-or-Kill
58    Fok,
59    /// Good-till-Date
60    Gtd,
61    /// Fill-and-Kill
62    Fak,
63}
64
65impl fmt::Display for OrderKind {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::Gtc => write!(f, "GTC"),
69            Self::Fok => write!(f, "FOK"),
70            Self::Gtd => write!(f, "GTD"),
71            Self::Fak => write!(f, "FAK"),
72        }
73    }
74}
75
76/// Signature type for order signing (EOA, Proxy, or Gnosis Safe).
77#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
78pub enum SignatureType {
79    #[default]
80    Eoa = 0,
81    PolyProxy = 1,
82    PolyGnosisSafe = 2,
83}
84
85impl SignatureType {
86    /// Returns true if the signature type indicates a proxy wallet
87    pub fn is_proxy(&self) -> bool {
88        matches!(self, Self::PolyProxy | Self::PolyGnosisSafe)
89    }
90}
91
92impl Serialize for SignatureType {
93    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94    where
95        S: serde::Serializer,
96    {
97        serializer.serialize_u8(*self as u8)
98    }
99}
100
101impl<'de> Deserialize<'de> for SignatureType {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: serde::Deserializer<'de>,
105    {
106        let v = u8::deserialize(deserializer)?;
107        match v {
108            0 => Ok(Self::Eoa),
109            1 => Ok(Self::PolyProxy),
110            2 => Ok(Self::PolyGnosisSafe),
111            _ => Err(serde::de::Error::custom(format!(
112                "invalid signature type: {}",
113                v
114            ))),
115        }
116    }
117}
118
119impl fmt::Display for SignatureType {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            Self::Eoa => write!(f, "eoa"),
123            Self::PolyProxy => write!(f, "poly-proxy"),
124            Self::PolyGnosisSafe => write!(f, "poly-gnosis-safe"),
125        }
126    }
127}
128
129/// Tick size (minimum price increment)
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub enum TickSize {
132    /// 0.1
133    Tenth,
134    /// 0.01
135    Hundredth,
136    /// 0.001
137    Thousandth,
138    /// 0.0001
139    TenThousandth,
140}
141
142impl TickSize {
143    /// Returns the tick size as an `f64` value.
144    pub fn as_f64(&self) -> f64 {
145        match self {
146            Self::Tenth => 0.1,
147            Self::Hundredth => 0.01,
148            Self::Thousandth => 0.001,
149            Self::TenThousandth => 0.0001,
150        }
151    }
152
153    /// Returns the number of decimal places for this tick size.
154    pub fn decimals(&self) -> u32 {
155        match self {
156            Self::Tenth => 1,
157            Self::Hundredth => 2,
158            Self::Thousandth => 3,
159            Self::TenThousandth => 4,
160        }
161    }
162}
163
164/// Options for creating an order
165#[derive(Debug, Clone, Copy, Default)]
166pub struct PartialCreateOrderOptions {
167    pub tick_size: Option<TickSize>,
168    pub neg_risk: Option<bool>,
169}
170
171impl TryFrom<&str> for TickSize {
172    type Error = ParseTickSizeError;
173
174    fn try_from(s: &str) -> Result<Self, Self::Error> {
175        match s {
176            "0.1" => Ok(Self::Tenth),
177            "0.01" => Ok(Self::Hundredth),
178            "0.001" => Ok(Self::Thousandth),
179            "0.0001" => Ok(Self::TenThousandth),
180            _ => Err(ParseTickSizeError(s.to_string())),
181        }
182    }
183}
184
185impl TryFrom<f64> for TickSize {
186    type Error = ParseTickSizeError;
187
188    fn try_from(n: f64) -> Result<Self, Self::Error> {
189        const EPSILON: f64 = 1e-10;
190        if (n - 0.1).abs() < EPSILON {
191            Ok(Self::Tenth)
192        } else if (n - 0.01).abs() < EPSILON {
193            Ok(Self::Hundredth)
194        } else if (n - 0.001).abs() < EPSILON {
195            Ok(Self::Thousandth)
196        } else if (n - 0.0001).abs() < EPSILON {
197            Ok(Self::TenThousandth)
198        } else {
199            Err(ParseTickSizeError(n.to_string()))
200        }
201    }
202}
203
204impl std::str::FromStr for TickSize {
205    type Err = ParseTickSizeError;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        Self::try_from(s)
209    }
210}
211
212fn serialize_salt<S>(salt: &str, serializer: S) -> Result<S::Ok, S::Error>
213where
214    S: serde::Serializer,
215{
216    // Validate that the salt is a valid u128, but serialize as a string.
217    // The Polymarket API expects string-encoded salts, and u128 values
218    // exceed JSON's safe number range (serde_json rejects u128 > u64::MAX).
219    salt.parse::<u128>()
220        .map_err(|_| serde::ser::Error::custom("invalid salt"))?;
221    serializer.serialize_str(salt)
222}
223
224/// Unsigned order
225#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct Order {
228    #[serde(serialize_with = "serialize_salt")]
229    pub salt: String,
230    pub maker: Address,
231    pub signer: Address,
232    pub taker: Address,
233    pub token_id: String,
234    pub maker_amount: String,
235    pub taker_amount: String,
236    pub expiration: String,
237    pub nonce: String,
238    pub fee_rate_bps: String,
239    pub side: OrderSide,
240    pub signature_type: SignatureType,
241    #[serde(skip)]
242    pub neg_risk: bool,
243}
244
245/// Arguments for creating a market order
246#[derive(Debug, Clone)]
247pub struct MarketOrderArgs {
248    pub token_id: String,
249    /// For BUY: Amount in USDC to spend
250    /// For SELL: Amount of token to sell
251    pub amount: f64,
252    pub side: OrderSide,
253    /// Worst acceptable price to fill at.
254    /// If None, it will be calculated from the orderbook.
255    pub price: Option<f64>,
256    pub fee_rate_bps: Option<u16>,
257    pub nonce: Option<u64>,
258    pub funder: Option<Address>,
259    pub signature_type: Option<SignatureType>,
260    pub order_type: Option<OrderKind>,
261}
262
263/// Signed order
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct SignedOrder {
267    #[serde(flatten)]
268    pub order: Order,
269    pub signature: String,
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use alloy::primitives::Address;
276    use std::str::FromStr;
277
278    #[test]
279    fn test_order_serialization() {
280        let order = Order {
281            salt: "123".to_string(),
282            maker: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(),
283            signer: Address::from_str("0x0000000000000000000000000000000000000002").unwrap(),
284            taker: Address::ZERO,
285            token_id: "456".to_string(),
286            maker_amount: "1000".to_string(),
287            taker_amount: "2000".to_string(),
288            expiration: "0".to_string(),
289            nonce: "789".to_string(),
290            fee_rate_bps: "0".to_string(),
291            side: OrderSide::Buy,
292            signature_type: SignatureType::Eoa,
293            neg_risk: false,
294        };
295
296        let signed_order = SignedOrder {
297            order,
298            signature: "0xabc".to_string(),
299        };
300
301        let json = serde_json::to_value(&signed_order).unwrap();
302
303        // Check camelCase
304        assert!(json.get("makerAmount").is_some());
305        assert!(json.get("takerAmount").is_some());
306        assert!(json.get("tokenId").is_some());
307        assert!(json.get("feeRateBps").is_some());
308        assert!(json.get("signatureType").is_some());
309
310        // Check flattened fields
311        assert!(json.get("signature").is_some());
312        assert!(json.get("salt").is_some());
313
314        // Check values
315        assert_eq!(json["makerAmount"], "1000");
316        assert_eq!(json["side"], "BUY");
317        assert_eq!(json["signatureType"], 0);
318        assert_eq!(json["nonce"], "789");
319    }
320
321    #[test]
322    fn order_side_serde_roundtrip() {
323        let buy: OrderSide = serde_json::from_str("\"BUY\"").unwrap();
324        let sell: OrderSide = serde_json::from_str("\"SELL\"").unwrap();
325        assert_eq!(buy, OrderSide::Buy);
326        assert_eq!(sell, OrderSide::Sell);
327
328        assert_eq!(serde_json::to_string(&OrderSide::Buy).unwrap(), "\"BUY\"");
329        assert_eq!(serde_json::to_string(&OrderSide::Sell).unwrap(), "\"SELL\"");
330    }
331
332    #[test]
333    fn order_side_display_is_numeric() {
334        // Display uses 0/1 for EIP-712 encoding
335        assert_eq!(OrderSide::Buy.to_string(), "0");
336        assert_eq!(OrderSide::Sell.to_string(), "1");
337    }
338
339    #[test]
340    fn order_side_rejects_lowercase() {
341        let result = serde_json::from_str::<OrderSide>("\"buy\"");
342        assert!(result.is_err(), "Should reject lowercase order side");
343    }
344
345    #[test]
346    fn order_kind_serde_roundtrip() {
347        for (variant, expected) in [
348            (OrderKind::Gtc, "GTC"),
349            (OrderKind::Fok, "FOK"),
350            (OrderKind::Gtd, "GTD"),
351            (OrderKind::Fak, "FAK"),
352        ] {
353            let serialized = serde_json::to_string(&variant).unwrap();
354            assert_eq!(serialized, format!("\"{}\"", expected));
355
356            let deserialized: OrderKind = serde_json::from_str(&serialized).unwrap();
357            assert_eq!(deserialized, variant);
358        }
359    }
360
361    #[test]
362    fn order_kind_display() {
363        assert_eq!(OrderKind::Gtc.to_string(), "GTC");
364        assert_eq!(OrderKind::Fok.to_string(), "FOK");
365        assert_eq!(OrderKind::Gtd.to_string(), "GTD");
366        assert_eq!(OrderKind::Fak.to_string(), "FAK");
367    }
368
369    #[test]
370    fn signature_type_serde_as_u8() {
371        assert_eq!(serde_json::to_string(&SignatureType::Eoa).unwrap(), "0");
372        assert_eq!(
373            serde_json::to_string(&SignatureType::PolyProxy).unwrap(),
374            "1"
375        );
376        assert_eq!(
377            serde_json::to_string(&SignatureType::PolyGnosisSafe).unwrap(),
378            "2"
379        );
380
381        let eoa: SignatureType = serde_json::from_str("0").unwrap();
382        assert_eq!(eoa, SignatureType::Eoa);
383        let proxy: SignatureType = serde_json::from_str("1").unwrap();
384        assert_eq!(proxy, SignatureType::PolyProxy);
385        let gnosis: SignatureType = serde_json::from_str("2").unwrap();
386        assert_eq!(gnosis, SignatureType::PolyGnosisSafe);
387    }
388
389    #[test]
390    fn signature_type_rejects_invalid_u8() {
391        let result = serde_json::from_str::<SignatureType>("3");
392        assert!(result.is_err(), "Should reject invalid signature type 3");
393
394        let result = serde_json::from_str::<SignatureType>("255");
395        assert!(result.is_err(), "Should reject invalid signature type 255");
396    }
397
398    #[test]
399    fn signature_type_display() {
400        assert_eq!(SignatureType::Eoa.to_string(), "eoa");
401        assert_eq!(SignatureType::PolyProxy.to_string(), "poly-proxy");
402        assert_eq!(
403            SignatureType::PolyGnosisSafe.to_string(),
404            "poly-gnosis-safe"
405        );
406    }
407
408    #[test]
409    fn signature_type_default_is_eoa() {
410        assert_eq!(SignatureType::default(), SignatureType::Eoa);
411    }
412
413    #[test]
414    fn signature_type_is_proxy() {
415        assert!(!SignatureType::Eoa.is_proxy());
416        assert!(SignatureType::PolyProxy.is_proxy());
417        assert!(SignatureType::PolyGnosisSafe.is_proxy());
418    }
419
420    #[test]
421    fn tick_size_from_str() {
422        assert_eq!(TickSize::try_from("0.1").unwrap(), TickSize::Tenth);
423        assert_eq!(TickSize::try_from("0.01").unwrap(), TickSize::Hundredth);
424        assert_eq!(TickSize::try_from("0.001").unwrap(), TickSize::Thousandth);
425        assert_eq!(
426            TickSize::try_from("0.0001").unwrap(),
427            TickSize::TenThousandth
428        );
429    }
430
431    #[test]
432    fn tick_size_from_str_rejects_invalid() {
433        assert!(TickSize::try_from("0.5").is_err());
434        assert!(TickSize::try_from("1.0").is_err());
435        assert!(TickSize::try_from("abc").is_err());
436        assert!(TickSize::try_from("0.00001").is_err());
437    }
438
439    #[test]
440    fn tick_size_from_f64() {
441        assert_eq!(TickSize::try_from(0.1).unwrap(), TickSize::Tenth);
442        assert_eq!(TickSize::try_from(0.01).unwrap(), TickSize::Hundredth);
443        assert_eq!(TickSize::try_from(0.001).unwrap(), TickSize::Thousandth);
444        assert_eq!(TickSize::try_from(0.0001).unwrap(), TickSize::TenThousandth);
445    }
446
447    #[test]
448    fn tick_size_from_f64_rejects_invalid() {
449        assert!(TickSize::try_from(0.5).is_err());
450        assert!(TickSize::try_from(0.0).is_err());
451        assert!(TickSize::try_from(1.0).is_err());
452    }
453
454    #[test]
455    fn tick_size_as_f64() {
456        assert!((TickSize::Tenth.as_f64() - 0.1).abs() < f64::EPSILON);
457        assert!((TickSize::Hundredth.as_f64() - 0.01).abs() < f64::EPSILON);
458        assert!((TickSize::Thousandth.as_f64() - 0.001).abs() < f64::EPSILON);
459        assert!((TickSize::TenThousandth.as_f64() - 0.0001).abs() < f64::EPSILON);
460    }
461
462    #[test]
463    fn tick_size_decimals() {
464        assert_eq!(TickSize::Tenth.decimals(), 1);
465        assert_eq!(TickSize::Hundredth.decimals(), 2);
466        assert_eq!(TickSize::Thousandth.decimals(), 3);
467        assert_eq!(TickSize::TenThousandth.decimals(), 4);
468    }
469
470    #[test]
471    fn tick_size_from_str_trait() {
472        let ts: TickSize = "0.01".parse().unwrap();
473        assert_eq!(ts, TickSize::Hundredth);
474    }
475
476    #[test]
477    fn parse_tick_size_error_display() {
478        let err = TickSize::try_from("bad").unwrap_err();
479        let msg = err.to_string();
480        assert!(
481            msg.contains("bad"),
482            "Error should contain invalid value: {}",
483            msg
484        );
485        assert!(
486            msg.contains("0.1"),
487            "Error should list valid values: {}",
488            msg
489        );
490    }
491
492    #[test]
493    fn order_neg_risk_skipped_in_serialization() {
494        let order = Order {
495            salt: "1".to_string(),
496            maker: Address::ZERO,
497            signer: Address::ZERO,
498            taker: Address::ZERO,
499            token_id: "1".to_string(),
500            maker_amount: "1".to_string(),
501            taker_amount: "1".to_string(),
502            expiration: "0".to_string(),
503            nonce: "0".to_string(),
504            fee_rate_bps: "0".to_string(),
505            side: OrderSide::Buy,
506            signature_type: SignatureType::Eoa,
507            neg_risk: true,
508        };
509        let json = serde_json::to_value(&order).unwrap();
510        assert!(
511            json.get("neg_risk").is_none() && json.get("negRisk").is_none(),
512            "neg_risk should be skipped in serialization: {}",
513            json
514        );
515    }
516
517    #[test]
518    fn salt_serialized_as_string() {
519        let order = Order {
520            salt: "340282366920938463463374607431768211455".to_string(), // u128::MAX
521            maker: Address::ZERO,
522            signer: Address::ZERO,
523            taker: Address::ZERO,
524            token_id: "1".to_string(),
525            maker_amount: "1".to_string(),
526            taker_amount: "1".to_string(),
527            expiration: "0".to_string(),
528            nonce: "0".to_string(),
529            fee_rate_bps: "0".to_string(),
530            side: OrderSide::Buy,
531            signature_type: SignatureType::Eoa,
532            neg_risk: false,
533        };
534        let json = serde_json::to_value(&order).unwrap();
535        assert!(
536            json["salt"].is_string(),
537            "Salt should be a string: {:?}",
538            json["salt"]
539        );
540        assert_eq!(
541            json["salt"].as_str().unwrap(),
542            "340282366920938463463374607431768211455"
543        );
544    }
545}