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