asterdex_sdk/futures/endpoints/
batch.rs1use serde_json::{Map, Value};
4
5use crate::futures::models::account::{BatchOrderResult, CancelAllResponse};
6use crate::futures::models::trading::PlaceOrderParams;
7use crate::rest::client::RestClient;
8use crate::rest::error::AsterDexError;
9use crate::rest::response::ApiResponse;
10
11const BATCH_ORDER_MAX: usize = 5;
12
13fn order_to_json_obj(params: PlaceOrderParams) -> Map<String, Value> {
17 let mut obj = Map::new();
18 obj.insert("symbol".into(), Value::String(params.symbol));
19 obj.insert("side".into(), Value::String(params.side));
20 obj.insert("type".into(), Value::String(params.order_type));
21 obj.insert("quantity".into(), Value::String(params.quantity));
22 if let Some(p) = params.price {
23 obj.insert("price".into(), Value::String(p));
24 }
25 if let Some(tif) = params.time_in_force {
26 obj.insert("timeInForce".into(), Value::String(tif));
27 }
28 if let Some(b) = params.reduce_only {
29 obj.insert("reduceOnly".into(), Value::String(if b { "true" } else { "false" }.into()));
30 }
31 if let Some(id) = params.new_client_order_id {
32 obj.insert("newClientOrderId".into(), Value::String(id));
33 }
34 if let Some(sp) = params.stop_price {
35 obj.insert("stopPrice".into(), Value::String(sp));
36 }
37 if let Some(ap) = params.activation_price {
38 obj.insert("activationPrice".into(), Value::String(ap));
39 }
40 if let Some(cr) = params.callback_rate {
41 obj.insert("callbackRate".into(), Value::String(cr));
42 }
43 if let Some(wt) = params.working_type {
44 obj.insert("workingType".into(), Value::String(wt));
45 }
46 if let Some(b) = params.price_protect {
47 obj.insert("priceProtect".into(), Value::String(if b { "true" } else { "false" }.into()));
48 }
49 if let Some(ort) = params.new_order_resp_type {
50 obj.insert("newOrderRespType".into(), Value::String(ort));
51 }
52 obj
53}
54
55impl RestClient {
56 pub async fn place_batch_orders(
59 &self,
60 orders: Vec<PlaceOrderParams>,
61 ) -> Result<ApiResponse<Vec<BatchOrderResult>>, AsterDexError> {
62 if orders.len() > BATCH_ORDER_MAX {
63 return Err(AsterDexError::InvalidParams {
64 message: format!("Too many orders: max {BATCH_ORDER_MAX}, got {}", orders.len()),
65 });
66 }
67 let arr: Vec<Value> = orders.into_iter().map(|p| Value::Object(order_to_json_obj(p))).collect();
70 let orders_json = serde_json::to_string(&arr).map_err(|e| AsterDexError::SerdeError {
71 message: e.to_string(),
72 })?;
73 tracing::debug!(orders = %orders_json, "place_batch_orders request");
74 let resp = self.signed_post::<serde_json::Value>("/fapi/v3/batchOrders", &[("batchOrders", &orders_json)]).await?;
75 let has_failed = resp.data.as_array()
76 .map(|arr| arr.iter().any(|item| item.get("orderId").is_none()))
77 .unwrap_or(false);
78 if has_failed {
79 tracing::error!(raw = %resp.data, "place_batch_orders raw response (partial failure)");
80 }
81 let results: Vec<BatchOrderResult> = serde_json::from_value(resp.data).map_err(|e| AsterDexError::SerdeError {
82 message: format!("failed to deserialize batch results: {e}"),
83 })?;
84 Ok(ApiResponse {
85 data: results,
86 used_weight: resp.used_weight,
87 order_count: resp.order_count,
88 })
89 }
90
91 pub async fn cancel_batch_orders(
93 &self,
94 symbol: &str,
95 order_ids: Vec<i64>,
96 ) -> Result<ApiResponse<Vec<BatchOrderResult>>, AsterDexError> {
97 let ids_json = serde_json::to_string(&order_ids).map_err(|e| AsterDexError::SerdeError {
98 message: e.to_string(),
99 })?;
100 self.signed_delete("/fapi/v3/batchOrders", &[
101 ("symbol", symbol),
102 ("orderIdList", &ids_json),
103 ]).await
104 }
105
106 pub async fn cancel_all_open_orders(
108 &self,
109 symbol: &str,
110 ) -> Result<ApiResponse<CancelAllResponse>, AsterDexError> {
111 self.signed_delete("/fapi/v3/allOpenOrders", &[("symbol", symbol)]).await
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::auth::Credentials;
119
120 #[tokio::test]
124 async fn place_batch_orders_too_many_returns_invalid_params() {
125 let client = RestClient::new_public("http://localhost:9999").unwrap();
126 let orders: Vec<PlaceOrderParams> = (0..6).map(|_| PlaceOrderParams::default()).collect();
127 let result = client.place_batch_orders(orders).await;
128 assert!(
129 matches!(result, Err(AsterDexError::InvalidParams { .. })),
130 "expected InvalidParams, got: {:?}",
131 result
132 );
133 }
134
135 #[tokio::test]
137 async fn cancel_all_open_orders_mock() {
138 let mut server = mockito::Server::new_async().await;
139 let _mock = server
140 .mock("DELETE", "/fapi/v3/allOpenOrders")
141 .match_query(mockito::Matcher::Any)
142 .with_status(200)
143 .with_header("content-type", "application/json")
144 .with_body(r#"{"code":200,"msg":"The operation of cancel orders is done."}"#)
145 .create_async()
146 .await;
147
148 let creds = Credentials::new(
149 "0x0000000000000000000000000000000000000001",
150 "0x0000000000000000000000000000000000000002",
151 "0000000000000000000000000000000000000000000000000000000000000001",
152 714,
153 )
154 .unwrap();
155 let client = RestClient::new(&server.url(), creds).unwrap();
156 let resp = client.cancel_all_open_orders("BTCUSDT").await.unwrap();
157 assert_eq!(resp.data.code, 200);
158 }
159}