Skip to main content

polyoxide_clob/api/
orders.rs

1use std::collections::HashMap;
2
3use polyoxide_core::{HttpClient, QueryBuilder};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    account::{Credentials, Signer, Wallet},
8    error::ClobError,
9    request::{AuthMode, Request},
10    types::SignedOrder,
11};
12
13/// Orders namespace for order-related operations
14#[derive(Clone)]
15pub struct Orders {
16    pub(crate) http_client: HttpClient,
17    pub(crate) wallet: Wallet,
18    pub(crate) credentials: Credentials,
19    pub(crate) signer: Signer,
20    pub(crate) chain_id: u64,
21}
22
23impl Orders {
24    /// List user's orders
25    pub fn list(&self) -> Request<ListOrdersResponse> {
26        Request::get(
27            self.http_client.clone(),
28            "/data/orders",
29            AuthMode::L2 {
30                address: self.wallet.address(),
31                credentials: self.credentials.clone(),
32                signer: self.signer.clone(),
33            },
34            self.chain_id,
35        )
36    }
37
38    /// Get a specific order by ID
39    pub fn get(&self, order_id: impl Into<String>) -> Request<OpenOrder> {
40        Request::get(
41            self.http_client.clone(),
42            format!("/data/order/{}", urlencoding::encode(&order_id.into())),
43            AuthMode::L2 {
44                address: self.wallet.address(),
45                credentials: self.credentials.clone(),
46                signer: self.signer.clone(),
47            },
48            self.chain_id,
49        )
50    }
51
52    /// Cancel an order
53    pub fn cancel(&self, order_id: impl Into<String>) -> CancelOrderRequest {
54        CancelOrderRequest {
55            http_client: self.http_client.clone(),
56            auth: AuthMode::L2 {
57                address: self.wallet.address(),
58                credentials: self.credentials.clone(),
59                signer: self.signer.clone(),
60            },
61            chain_id: self.chain_id,
62            order_id: order_id.into(),
63        }
64    }
65
66    /// Cancel all open orders
67    pub async fn cancel_all(&self) -> Result<BatchCancelResponse, ClobError> {
68        Request::<BatchCancelResponse>::delete(
69            self.http_client.clone(),
70            "/cancel-all",
71            AuthMode::L2 {
72                address: self.wallet.address(),
73                credentials: self.credentials.clone(),
74                signer: self.signer.clone(),
75            },
76            self.chain_id,
77        )
78        .send()
79        .await
80    }
81
82    /// Cancel all orders for a specific market and asset
83    pub async fn cancel_market(
84        &self,
85        market: impl Into<String>,
86        asset_id: impl Into<String>,
87    ) -> Result<BatchCancelResponse, ClobError> {
88        #[derive(Serialize)]
89        struct Body {
90            market: String,
91            asset_id: String,
92        }
93
94        Request::<BatchCancelResponse>::delete(
95            self.http_client.clone(),
96            "/cancel-market-orders",
97            AuthMode::L2 {
98                address: self.wallet.address(),
99                credentials: self.credentials.clone(),
100                signer: self.signer.clone(),
101            },
102            self.chain_id,
103        )
104        .body(&Body {
105            market: market.into(),
106            asset_id: asset_id.into(),
107        })?
108        .send()
109        .await
110    }
111
112    /// Check if an order is being scored for rewards
113    pub fn is_scoring(&self, order_id: impl Into<String>) -> Request<OrderScoringResponse> {
114        Request::get(
115            self.http_client.clone(),
116            "/order-scoring",
117            AuthMode::L2 {
118                address: self.wallet.address(),
119                credentials: self.credentials.clone(),
120                signer: self.signer.clone(),
121            },
122            self.chain_id,
123        )
124        .query("order_id", order_id.into())
125    }
126
127    /// Check if multiple orders are being scored for rewards
128    pub fn are_scoring(
129        &self,
130        order_ids: impl Into<Vec<String>>,
131    ) -> Request<Vec<OrderScoringResponse>> {
132        Request::get(
133            self.http_client.clone(),
134            "/orders-scoring",
135            AuthMode::L2 {
136                address: self.wallet.address(),
137                credentials: self.credentials.clone(),
138                signer: self.signer.clone(),
139            },
140            self.chain_id,
141        )
142        .query_many("order_ids", order_ids.into())
143    }
144
145    /// Cancel multiple orders by ID (up to 3000)
146    pub async fn cancel_many(
147        &self,
148        order_ids: impl Into<Vec<String>>,
149    ) -> Result<BatchCancelResponse, ClobError> {
150        let ids: Vec<String> = order_ids.into();
151
152        Request::<BatchCancelResponse>::delete(
153            self.http_client.clone(),
154            "/orders",
155            AuthMode::L2 {
156                address: self.wallet.address(),
157                credentials: self.credentials.clone(),
158                signer: self.signer.clone(),
159            },
160            self.chain_id,
161        )
162        .body(&ids)?
163        .send()
164        .await
165    }
166}
167
168/// Request builder for canceling an order
169pub struct CancelOrderRequest {
170    http_client: HttpClient,
171    auth: AuthMode,
172    chain_id: u64,
173    order_id: String,
174}
175
176impl CancelOrderRequest {
177    /// Execute the cancel request
178    pub async fn send(self) -> Result<BatchCancelResponse, ClobError> {
179        #[derive(serde::Serialize)]
180        struct CancelRequest {
181            #[serde(rename = "orderID")]
182            order_id: String,
183        }
184
185        let request = CancelRequest {
186            order_id: self.order_id,
187        };
188
189        Request::delete(self.http_client, "/order", self.auth, self.chain_id)
190            .body(&request)?
191            .send()
192            .await
193    }
194}
195
196/// Open order from API
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all(deserialize = "camelCase"))]
199pub struct OpenOrder {
200    pub id: String,
201    pub market: String,
202    pub asset_id: String,
203    #[serde(flatten)]
204    pub order: SignedOrder,
205    pub status: String,
206    pub owner: Option<String>,
207    pub maker_address: Option<String>,
208    pub original_size: Option<String>,
209    pub size_matched: Option<String>,
210    pub price: Option<String>,
211    #[serde(default)]
212    pub associate_trades: Vec<String>,
213    pub outcome: Option<String>,
214    pub order_type: Option<String>,
215    pub created_at: String,
216    pub updated_at: Option<String>,
217}
218
219/// Response from posting an order
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all(deserialize = "camelCase"))]
222pub struct OrderResponse {
223    pub success: bool,
224    pub error_msg: Option<String>,
225    #[serde(rename(deserialize = "orderID"))]
226    pub order_id: Option<String>,
227    #[serde(default, rename(deserialize = "transactionsHashes"))]
228    pub transaction_hashes: Vec<String>,
229    pub status: Option<String>,
230    pub taking_amount: Option<String>,
231    pub making_amount: Option<String>,
232}
233
234/// Response from order scoring check
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct OrderScoringResponse {
237    pub order_id: String,
238    pub scoring: bool,
239}
240
241/// Response from cancel and batch cancel operations.
242///
243/// The Polymarket API returns this shape for all cancel endpoints:
244/// `DELETE /order`, `DELETE /orders`, `DELETE /cancel-all`, `DELETE /cancel-market-orders`.
245#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(rename_all(deserialize = "camelCase"))]
247pub struct BatchCancelResponse {
248    #[serde(default)]
249    pub canceled: Vec<String>,
250    #[serde(default)]
251    pub not_canceled: HashMap<String, String>,
252}
253
254/// Paginated response from `GET /data/orders`
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ListOrdersResponse {
257    pub data: Vec<OpenOrder>,
258    pub next_cursor: Option<String>,
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use alloy::primitives::Address;
265    use std::str::FromStr;
266
267    #[test]
268    fn open_order_deserializes_with_flattened_signed_order() {
269        let json = r#"{
270            "id": "order-abc",
271            "market": "0xcondition123",
272            "assetId": "0xtoken456",
273            "salt": "999",
274            "maker": "0x0000000000000000000000000000000000000001",
275            "signer": "0x0000000000000000000000000000000000000002",
276            "taker": "0x0000000000000000000000000000000000000000",
277            "tokenId": "0xtoken456",
278            "makerAmount": "1000",
279            "takerAmount": "500",
280            "expiration": "0",
281            "nonce": "0",
282            "feeRateBps": "100",
283            "side": "BUY",
284            "signatureType": 0,
285            "signature": "0xsig",
286            "status": "LIVE",
287            "createdAt": "2024-01-01T00:00:00Z",
288            "updatedAt": null
289        }"#;
290        let order: OpenOrder = serde_json::from_str(json).unwrap();
291        assert_eq!(order.id, "order-abc");
292        assert_eq!(order.market, "0xcondition123");
293        assert_eq!(order.asset_id, "0xtoken456");
294        assert_eq!(order.status, "LIVE");
295        assert_eq!(order.order.signature, "0xsig");
296        assert_eq!(order.order.order.maker_amount, "1000");
297        assert_eq!(
298            order.order.order.maker,
299            Address::from_str("0x0000000000000000000000000000000000000001").unwrap()
300        );
301        assert!(order.updated_at.is_none());
302        // New fields default to None/empty when absent
303        assert!(order.owner.is_none());
304        assert!(order.maker_address.is_none());
305        assert!(order.original_size.is_none());
306        assert!(order.price.is_none());
307        assert!(order.associate_trades.is_empty());
308    }
309
310    #[test]
311    fn open_order_with_full_fields() {
312        let json = r#"{
313            "id": "order-full",
314            "market": "0xcond",
315            "assetId": "0xtoken",
316            "salt": "1",
317            "maker": "0x0000000000000000000000000000000000000001",
318            "signer": "0x0000000000000000000000000000000000000002",
319            "taker": "0x0000000000000000000000000000000000000000",
320            "tokenId": "0xtoken",
321            "makerAmount": "1000",
322            "takerAmount": "500",
323            "expiration": "0",
324            "nonce": "0",
325            "feeRateBps": "100",
326            "side": "BUY",
327            "signatureType": 0,
328            "signature": "0xsig",
329            "status": "LIVE",
330            "owner": "0xowner",
331            "makerAddress": "0xmaker",
332            "originalSize": "200.5",
333            "sizeMatched": "100.0",
334            "price": "0.55",
335            "associateTrades": ["trade-1", "trade-2"],
336            "outcome": "Yes",
337            "orderType": "GTC",
338            "createdAt": "2024-01-01T00:00:00Z",
339            "updatedAt": "2024-01-02T00:00:00Z"
340        }"#;
341        let order: OpenOrder = serde_json::from_str(json).unwrap();
342        assert_eq!(order.owner.as_deref(), Some("0xowner"));
343        assert_eq!(order.maker_address.as_deref(), Some("0xmaker"));
344        assert_eq!(order.original_size.as_deref(), Some("200.5"));
345        assert_eq!(order.size_matched.as_deref(), Some("100.0"));
346        assert_eq!(order.price.as_deref(), Some("0.55"));
347        assert_eq!(order.associate_trades, vec!["trade-1", "trade-2"]);
348        assert_eq!(order.outcome.as_deref(), Some("Yes"));
349        assert_eq!(order.order_type.as_deref(), Some("GTC"));
350        assert_eq!(order.updated_at.as_deref(), Some("2024-01-02T00:00:00Z"));
351    }
352
353    #[test]
354    fn order_response_deserializes() {
355        let json = r#"{
356            "success": true,
357            "errorMsg": null,
358            "orderID": "order-789",
359            "transactionsHashes": ["0xhash1", "0xhash2"],
360            "status": "LIVE",
361            "takingAmount": "500",
362            "makingAmount": "1000"
363        }"#;
364        let resp: OrderResponse = serde_json::from_str(json).unwrap();
365        assert!(resp.success);
366        assert!(resp.error_msg.is_none());
367        assert_eq!(resp.order_id.as_deref(), Some("order-789"));
368        assert_eq!(resp.transaction_hashes.len(), 2);
369        assert_eq!(resp.status.as_deref(), Some("LIVE"));
370        assert_eq!(resp.taking_amount.as_deref(), Some("500"));
371        assert_eq!(resp.making_amount.as_deref(), Some("1000"));
372    }
373
374    #[test]
375    fn order_response_defaults_transaction_hashes() {
376        let json = r#"{"success": false, "errorMsg": "bad order"}"#;
377        let resp: OrderResponse = serde_json::from_str(json).unwrap();
378        assert!(!resp.success);
379        assert_eq!(resp.error_msg.as_deref(), Some("bad order"));
380        assert!(resp.transaction_hashes.is_empty());
381        assert!(resp.order_id.is_none());
382        assert!(resp.status.is_none());
383        assert!(resp.taking_amount.is_none());
384        assert!(resp.making_amount.is_none());
385    }
386
387    #[test]
388    fn batch_cancel_response_deserializes() {
389        let json = r#"{
390            "canceled": ["order-1", "order-2"],
391            "notCanceled": {"order-3": "insufficient balance"}
392        }"#;
393        let resp: BatchCancelResponse = serde_json::from_str(json).unwrap();
394        assert_eq!(resp.canceled, vec!["order-1", "order-2"]);
395        assert_eq!(resp.not_canceled.len(), 1);
396        assert_eq!(
397            resp.not_canceled.get("order-3").unwrap(),
398            "insufficient balance"
399        );
400    }
401
402    #[test]
403    fn batch_cancel_response_defaults_empty() {
404        let json = r#"{}"#;
405        let resp: BatchCancelResponse = serde_json::from_str(json).unwrap();
406        assert!(resp.canceled.is_empty());
407        assert!(resp.not_canceled.is_empty());
408    }
409
410    #[test]
411    fn batch_cancel_response_serializes() {
412        let resp = BatchCancelResponse {
413            canceled: vec!["a".into(), "b".into()],
414            not_canceled: HashMap::from([("c".into(), "error".into())]),
415        };
416        let json = serde_json::to_value(&resp).unwrap();
417        assert_eq!(json["canceled"], serde_json::json!(["a", "b"]));
418        assert_eq!(json["not_canceled"]["c"], "error");
419    }
420
421    #[test]
422    fn list_orders_response_deserializes() {
423        let json = r#"{
424            "data": [{
425                "id": "order-abc",
426                "market": "0xcondition123",
427                "assetId": "0xtoken456",
428                "salt": "1",
429                "maker": "0x0000000000000000000000000000000000000001",
430                "signer": "0x0000000000000000000000000000000000000002",
431                "taker": "0x0000000000000000000000000000000000000000",
432                "tokenId": "0xtoken456",
433                "makerAmount": "1000",
434                "takerAmount": "500",
435                "expiration": "0",
436                "nonce": "0",
437                "feeRateBps": "100",
438                "side": "BUY",
439                "signatureType": 0,
440                "signature": "0xsig",
441                "status": "LIVE",
442                "createdAt": "2024-01-01T00:00:00Z"
443            }],
444            "next_cursor": "MQ=="
445        }"#;
446        let resp: ListOrdersResponse = serde_json::from_str(json).unwrap();
447        assert_eq!(resp.data.len(), 1);
448        assert_eq!(resp.data[0].id, "order-abc");
449        assert_eq!(resp.next_cursor.as_deref(), Some("MQ=="));
450    }
451
452    #[test]
453    fn list_orders_response_empty() {
454        let json = r#"{"data": [], "next_cursor": "LTE="}"#;
455        let resp: ListOrdersResponse = serde_json::from_str(json).unwrap();
456        assert!(resp.data.is_empty());
457        assert_eq!(resp.next_cursor.as_deref(), Some("LTE="));
458    }
459
460    #[test]
461    fn list_orders_response_null_cursor() {
462        let json = r#"{"data": [], "next_cursor": null}"#;
463        let resp: ListOrdersResponse = serde_json::from_str(json).unwrap();
464        assert!(resp.data.is_empty());
465        assert!(resp.next_cursor.is_none());
466    }
467
468    #[test]
469    fn order_scoring_response_deserializes() {
470        let json = r#"{"order_id": "order-1", "scoring": true}"#;
471        let resp: OrderScoringResponse = serde_json::from_str(json).unwrap();
472        assert_eq!(resp.order_id, "order-1");
473        assert!(resp.scoring);
474    }
475
476    #[test]
477    fn order_scoring_response_batch_deserializes() {
478        let json = r#"[
479            {"order_id": "order-1", "scoring": true},
480            {"order_id": "order-2", "scoring": false}
481        ]"#;
482        let resp: Vec<OrderScoringResponse> = serde_json::from_str(json).unwrap();
483        assert_eq!(resp.len(), 2);
484        assert!(resp[0].scoring);
485        assert!(!resp[1].scoring);
486    }
487}