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};
11
12#[derive(Clone)]
14pub struct Orders {
15 pub(crate) http_client: HttpClient,
16 pub(crate) wallet: Wallet,
17 pub(crate) credentials: Credentials,
18 pub(crate) signer: Signer,
19 pub(crate) chain_id: u64,
20}
21
22impl Orders {
23 pub fn list(&self) -> ListOrders {
28 ListOrders {
29 request: Request::get(
30 self.http_client.clone(),
31 "/data/orders",
32 AuthMode::L2 {
33 address: self.wallet.address(),
34 credentials: self.credentials.clone(),
35 signer: self.signer.clone(),
36 },
37 self.chain_id,
38 ),
39 }
40 }
41
42 pub fn get(&self, order_id: impl Into<String>) -> Request<OpenOrder> {
44 Request::get(
45 self.http_client.clone(),
46 format!("/data/order/{}", urlencoding::encode(&order_id.into())),
47 AuthMode::L2 {
48 address: self.wallet.address(),
49 credentials: self.credentials.clone(),
50 signer: self.signer.clone(),
51 },
52 self.chain_id,
53 )
54 }
55
56 pub fn cancel(&self, order_id: impl Into<String>) -> CancelOrderRequest {
58 CancelOrderRequest {
59 http_client: self.http_client.clone(),
60 auth: AuthMode::L2 {
61 address: self.wallet.address(),
62 credentials: self.credentials.clone(),
63 signer: self.signer.clone(),
64 },
65 chain_id: self.chain_id,
66 order_id: order_id.into(),
67 }
68 }
69
70 pub async fn cancel_all(&self) -> Result<BatchCancelResponse, ClobError> {
72 Request::<BatchCancelResponse>::delete(
73 self.http_client.clone(),
74 "/cancel-all",
75 AuthMode::L2 {
76 address: self.wallet.address(),
77 credentials: self.credentials.clone(),
78 signer: self.signer.clone(),
79 },
80 self.chain_id,
81 )
82 .send()
83 .await
84 }
85
86 pub async fn cancel_market(
88 &self,
89 market: impl Into<String>,
90 asset_id: impl Into<String>,
91 ) -> Result<BatchCancelResponse, ClobError> {
92 #[derive(Serialize)]
93 struct Body {
94 market: String,
95 asset_id: String,
96 }
97
98 Request::<BatchCancelResponse>::delete(
99 self.http_client.clone(),
100 "/cancel-market-orders",
101 AuthMode::L2 {
102 address: self.wallet.address(),
103 credentials: self.credentials.clone(),
104 signer: self.signer.clone(),
105 },
106 self.chain_id,
107 )
108 .body(&Body {
109 market: market.into(),
110 asset_id: asset_id.into(),
111 })?
112 .send()
113 .await
114 }
115
116 pub fn is_scoring(&self, order_id: impl Into<String>) -> Request<OrderScoringResponse> {
118 Request::get(
119 self.http_client.clone(),
120 "/order-scoring",
121 AuthMode::L2 {
122 address: self.wallet.address(),
123 credentials: self.credentials.clone(),
124 signer: self.signer.clone(),
125 },
126 self.chain_id,
127 )
128 .query("order_id", order_id.into())
129 }
130
131 pub fn are_scoring(
133 &self,
134 order_ids: impl Into<Vec<String>>,
135 ) -> Request<Vec<OrderScoringResponse>> {
136 Request::get(
137 self.http_client.clone(),
138 "/orders-scoring",
139 AuthMode::L2 {
140 address: self.wallet.address(),
141 credentials: self.credentials.clone(),
142 signer: self.signer.clone(),
143 },
144 self.chain_id,
145 )
146 .query_many("order_ids", order_ids.into())
147 }
148
149 pub async fn cancel_many(
151 &self,
152 order_ids: impl Into<Vec<String>>,
153 ) -> Result<BatchCancelResponse, ClobError> {
154 let ids: Vec<String> = order_ids.into();
155
156 Request::<BatchCancelResponse>::delete(
157 self.http_client.clone(),
158 "/orders",
159 AuthMode::L2 {
160 address: self.wallet.address(),
161 credentials: self.credentials.clone(),
162 signer: self.signer.clone(),
163 },
164 self.chain_id,
165 )
166 .body(&ids)?
167 .send()
168 .await
169 }
170}
171
172pub struct CancelOrderRequest {
174 http_client: HttpClient,
175 auth: AuthMode,
176 chain_id: u64,
177 order_id: String,
178}
179
180impl CancelOrderRequest {
181 pub async fn send(self) -> Result<BatchCancelResponse, ClobError> {
183 #[derive(serde::Serialize)]
184 struct CancelRequest {
185 #[serde(rename = "orderID")]
186 order_id: String,
187 }
188
189 let request = CancelRequest {
190 order_id: self.order_id,
191 };
192
193 Request::delete(self.http_client, "/order", self.auth, self.chain_id)
194 .body(&request)?
195 .send()
196 .await
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct OpenOrder {
221 pub id: String,
223 pub status: String,
225 pub owner: String,
227 pub maker_address: String,
229 pub market: String,
231 pub asset_id: String,
233 pub side: String,
235 pub original_size: String,
237 pub size_matched: String,
239 pub price: String,
241 pub outcome: String,
243 pub expiration: String,
245 pub order_type: String,
247 #[serde(default)]
249 pub associate_trades: Vec<String>,
250 pub created_at: i64,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all(deserialize = "camelCase"))]
258pub struct OrderResponse {
259 pub success: bool,
260 pub error_msg: Option<String>,
261 #[serde(rename(deserialize = "orderID"))]
262 pub order_id: Option<String>,
263 #[serde(default, rename(deserialize = "transactionsHashes"))]
264 pub transaction_hashes: Vec<String>,
265 pub status: Option<String>,
266 pub taking_amount: Option<String>,
267 pub making_amount: Option<String>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct OrderScoringResponse {
273 pub order_id: String,
274 pub scoring: bool,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
282#[serde(rename_all(deserialize = "camelCase"))]
283pub struct BatchCancelResponse {
284 #[serde(default)]
285 pub canceled: Vec<String>,
286 #[serde(default)]
287 pub not_canceled: HashMap<String, String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ListOrdersResponse {
293 pub data: Vec<OpenOrder>,
294 pub next_cursor: Option<String>,
295}
296
297pub struct ListOrders {
299 request: Request<ListOrdersResponse>,
300}
301
302impl ListOrders {
303 pub fn id(mut self, order_id: impl Into<String>) -> Self {
305 self.request = self.request.query("id", order_id.into());
306 self
307 }
308
309 pub fn market(mut self, condition_id: impl Into<String>) -> Self {
311 self.request = self.request.query("market", condition_id.into());
312 self
313 }
314
315 pub fn asset_id(mut self, token_id: impl Into<String>) -> Self {
317 self.request = self.request.query("asset_id", token_id.into());
318 self
319 }
320
321 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
323 self.request = self.request.query("next_cursor", cursor.into());
324 self
325 }
326
327 pub async fn send(self) -> Result<ListOrdersResponse, ClobError> {
329 self.request.send().await
330 }
331
332 pub async fn send_raw(self) -> Result<reqwest::Response, ClobError> {
349 self.request.send_raw().await
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 const CAPTURED_OPEN_ORDERS: &str = r#"{
367 "data": [{
368 "id": "0xc566ca3bb8d61f08d649214f7a5daf5041f5577c1e60c9131ae418aa62eff165",
369 "status": "LIVE",
370 "owner": "aa17dfae-754d-2498-f336-8bd1db84f525",
371 "maker_address": "0xb98ad946c7f753596F26396Bf3F34A2EeBc39E86",
372 "market": "0x7018d32e315a69c0537fc42f8e574ee4a24b3babaae302cda3d9f5f8e5b0bd6e",
373 "asset_id": "84371186359433032934344934234365568331165136826770712345678901234567",
374 "side": "BUY",
375 "original_size": "5",
376 "size_matched": "0",
377 "price": "0.01",
378 "outcome": "Yes",
379 "expiration": "0",
380 "order_type": "GTC",
381 "associate_trades": [],
382 "created_at": 1784930007
383 }],
384 "next_cursor": "LTE=",
385 "limit": 100,
386 "count": 1
387 }"#;
388
389 #[test]
390 fn open_order_deserializes_captured_response() {
391 let resp: ListOrdersResponse = serde_json::from_str(CAPTURED_OPEN_ORDERS)
395 .expect("captured live body must deserialize");
396 assert_eq!(resp.data.len(), 1);
397
398 let o = &resp.data[0];
399 assert_eq!(o.status, "LIVE");
400 assert_eq!(o.side, "BUY");
401 assert_eq!(o.original_size, "5");
402 assert_eq!(o.size_matched, "0");
403 assert_eq!(o.price, "0.01");
404 assert_eq!(o.outcome, "Yes");
405 assert_eq!(o.expiration, "0");
406 assert_eq!(o.order_type, "GTC");
407 assert!(o.associate_trades.is_empty());
408 assert_eq!(o.created_at, 1_784_930_007);
410 assert!(o.maker_address.starts_with("0x"));
412 assert!(!o.owner.starts_with("0x"));
414 assert_eq!(resp.next_cursor.as_deref(), Some("LTE="));
415 }
416
417 #[test]
418 fn open_order_rejects_the_camel_case_shape_it_used_to_expect() {
419 let json = r#"{"id":"x","status":"LIVE","owner":"o","makerAddress":"0x1",
423 "market":"0x2","assetId":"0x3","side":"BUY","originalSize":"5",
424 "sizeMatched":"0","price":"0.01","outcome":"Yes","expiration":"0",
425 "orderType":"GTC","associateTrades":[],"createdAt":1}"#;
426 assert!(
427 serde_json::from_str::<OpenOrder>(json).is_err(),
428 "camelCase must not deserialize; the venue sends snake_case"
429 );
430 }
431
432 #[test]
433 fn open_order_created_at_must_be_an_integer() {
434 let json = CAPTURED_OPEN_ORDERS.replace("1784930007", "\"2024-01-01T00:00:00Z\"");
437 assert!(
438 serde_json::from_str::<ListOrdersResponse>(&json).is_err(),
439 "a string created_at must be rejected"
440 );
441 }
442
443 #[test]
444 fn order_response_deserializes() {
445 let json = r#"{
446 "success": true,
447 "errorMsg": null,
448 "orderID": "order-789",
449 "transactionsHashes": ["0xhash1", "0xhash2"],
450 "status": "LIVE",
451 "takingAmount": "500",
452 "makingAmount": "1000"
453 }"#;
454 let resp: OrderResponse = serde_json::from_str(json).unwrap();
455 assert!(resp.success);
456 assert!(resp.error_msg.is_none());
457 assert_eq!(resp.order_id.as_deref(), Some("order-789"));
458 assert_eq!(resp.transaction_hashes.len(), 2);
459 assert_eq!(resp.status.as_deref(), Some("LIVE"));
460 assert_eq!(resp.taking_amount.as_deref(), Some("500"));
461 assert_eq!(resp.making_amount.as_deref(), Some("1000"));
462 }
463
464 #[test]
465 fn order_response_defaults_transaction_hashes() {
466 let json = r#"{"success": false, "errorMsg": "bad order"}"#;
467 let resp: OrderResponse = serde_json::from_str(json).unwrap();
468 assert!(!resp.success);
469 assert_eq!(resp.error_msg.as_deref(), Some("bad order"));
470 assert!(resp.transaction_hashes.is_empty());
471 assert!(resp.order_id.is_none());
472 assert!(resp.status.is_none());
473 assert!(resp.taking_amount.is_none());
474 assert!(resp.making_amount.is_none());
475 }
476
477 #[test]
478 fn batch_cancel_response_deserializes() {
479 let json = r#"{
480 "canceled": ["order-1", "order-2"],
481 "notCanceled": {"order-3": "insufficient balance"}
482 }"#;
483 let resp: BatchCancelResponse = serde_json::from_str(json).unwrap();
484 assert_eq!(resp.canceled, vec!["order-1", "order-2"]);
485 assert_eq!(resp.not_canceled.len(), 1);
486 assert_eq!(
487 resp.not_canceled.get("order-3").unwrap(),
488 "insufficient balance"
489 );
490 }
491
492 #[test]
493 fn batch_cancel_response_defaults_empty() {
494 let json = r#"{}"#;
495 let resp: BatchCancelResponse = serde_json::from_str(json).unwrap();
496 assert!(resp.canceled.is_empty());
497 assert!(resp.not_canceled.is_empty());
498 }
499
500 #[test]
501 fn batch_cancel_response_serializes() {
502 let resp = BatchCancelResponse {
503 canceled: vec!["a".into(), "b".into()],
504 not_canceled: HashMap::from([("c".into(), "error".into())]),
505 };
506 let json = serde_json::to_value(&resp).unwrap();
507 assert_eq!(json["canceled"], serde_json::json!(["a", "b"]));
508 assert_eq!(json["not_canceled"]["c"], "error");
509 }
510
511 #[test]
512 fn list_orders_response_empty() {
513 let json = r#"{"data": [], "next_cursor": "LTE="}"#;
514 let resp: ListOrdersResponse = serde_json::from_str(json).unwrap();
515 assert!(resp.data.is_empty());
516 assert_eq!(resp.next_cursor.as_deref(), Some("LTE="));
517 }
518
519 #[test]
520 fn list_orders_response_null_cursor() {
521 let json = r#"{"data": [], "next_cursor": null}"#;
522 let resp: ListOrdersResponse = serde_json::from_str(json).unwrap();
523 assert!(resp.data.is_empty());
524 assert!(resp.next_cursor.is_none());
525 }
526
527 #[test]
528 fn order_scoring_response_deserializes() {
529 let json = r#"{"order_id": "order-1", "scoring": true}"#;
530 let resp: OrderScoringResponse = serde_json::from_str(json).unwrap();
531 assert_eq!(resp.order_id, "order-1");
532 assert!(resp.scoring);
533 }
534
535 #[test]
536 fn order_scoring_response_batch_deserializes() {
537 let json = r#"[
538 {"order_id": "order-1", "scoring": true},
539 {"order_id": "order-2", "scoring": false}
540 ]"#;
541 let resp: Vec<OrderScoringResponse> = serde_json::from_str(json).unwrap();
542 assert_eq!(resp.len(), 2);
543 assert!(resp[0].scoring);
544 assert!(!resp[1].scoring);
545 }
546}