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#[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 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 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 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 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 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 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 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 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
168pub struct CancelOrderRequest {
170 http_client: HttpClient,
171 auth: AuthMode,
172 chain_id: u64,
173 order_id: String,
174}
175
176impl CancelOrderRequest {
177 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct OrderScoringResponse {
237 pub order_id: String,
238 pub scoring: bool,
239}
240
241#[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#[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 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}