use serde_json::{Map, Value};
use crate::futures::models::account::{BatchOrderResult, CancelAllResponse};
use crate::futures::models::trading::PlaceOrderParams;
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;
const BATCH_ORDER_MAX: usize = 5;
fn order_to_json_obj(params: PlaceOrderParams) -> Map<String, Value> {
let mut obj = Map::new();
obj.insert("symbol".into(), Value::String(params.symbol));
obj.insert("side".into(), Value::String(params.side));
obj.insert("type".into(), Value::String(params.order_type));
obj.insert("quantity".into(), Value::String(params.quantity));
if let Some(p) = params.price {
obj.insert("price".into(), Value::String(p));
}
if let Some(tif) = params.time_in_force {
obj.insert("timeInForce".into(), Value::String(tif));
}
if let Some(b) = params.reduce_only {
obj.insert("reduceOnly".into(), Value::String(if b { "true" } else { "false" }.into()));
}
if let Some(id) = params.new_client_order_id {
obj.insert("newClientOrderId".into(), Value::String(id));
}
if let Some(sp) = params.stop_price {
obj.insert("stopPrice".into(), Value::String(sp));
}
if let Some(ap) = params.activation_price {
obj.insert("activationPrice".into(), Value::String(ap));
}
if let Some(cr) = params.callback_rate {
obj.insert("callbackRate".into(), Value::String(cr));
}
if let Some(wt) = params.working_type {
obj.insert("workingType".into(), Value::String(wt));
}
if let Some(b) = params.price_protect {
obj.insert("priceProtect".into(), Value::String(if b { "true" } else { "false" }.into()));
}
if let Some(ort) = params.new_order_resp_type {
obj.insert("newOrderRespType".into(), Value::String(ort));
}
obj
}
impl RestClient {
pub async fn place_batch_orders(
&self,
orders: Vec<PlaceOrderParams>,
) -> Result<ApiResponse<Vec<BatchOrderResult>>, AsterDexError> {
if orders.len() > BATCH_ORDER_MAX {
return Err(AsterDexError::InvalidParams {
message: format!("Too many orders: max {BATCH_ORDER_MAX}, got {}", orders.len()),
});
}
let arr: Vec<Value> = orders.into_iter().map(|p| Value::Object(order_to_json_obj(p))).collect();
let orders_json = serde_json::to_string(&arr).map_err(|e| AsterDexError::SerdeError {
message: e.to_string(),
})?;
tracing::debug!(orders = %orders_json, "place_batch_orders request");
let resp = self.signed_post::<serde_json::Value>("/fapi/v3/batchOrders", &[("batchOrders", &orders_json)]).await?;
let has_failed = resp.data.as_array()
.map(|arr| arr.iter().any(|item| item.get("orderId").is_none()))
.unwrap_or(false);
if has_failed {
tracing::error!(raw = %resp.data, "place_batch_orders raw response (partial failure)");
}
let results: Vec<BatchOrderResult> = serde_json::from_value(resp.data).map_err(|e| AsterDexError::SerdeError {
message: format!("failed to deserialize batch results: {e}"),
})?;
Ok(ApiResponse {
data: results,
used_weight: resp.used_weight,
order_count: resp.order_count,
})
}
pub async fn cancel_batch_orders(
&self,
symbol: &str,
order_ids: Vec<i64>,
) -> Result<ApiResponse<Vec<BatchOrderResult>>, AsterDexError> {
let ids_json = serde_json::to_string(&order_ids).map_err(|e| AsterDexError::SerdeError {
message: e.to_string(),
})?;
self.signed_delete("/fapi/v3/batchOrders", &[
("symbol", symbol),
("orderIdList", &ids_json),
]).await
}
pub async fn cancel_all_open_orders(
&self,
symbol: &str,
) -> Result<ApiResponse<CancelAllResponse>, AsterDexError> {
self.signed_delete("/fapi/v3/allOpenOrders", &[("symbol", symbol)]).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Credentials;
#[tokio::test]
async fn place_batch_orders_too_many_returns_invalid_params() {
let client = RestClient::new_public("http://localhost:9999").unwrap();
let orders: Vec<PlaceOrderParams> = (0..6).map(|_| PlaceOrderParams::default()).collect();
let result = client.place_batch_orders(orders).await;
assert!(
matches!(result, Err(AsterDexError::InvalidParams { .. })),
"expected InvalidParams, got: {:?}",
result
);
}
#[tokio::test]
async fn cancel_all_open_orders_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("DELETE", "/fapi/v3/allOpenOrders")
.match_query(mockito::Matcher::Any)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"code":200,"msg":"The operation of cancel orders is done."}"#)
.create_async()
.await;
let creds = Credentials::new(
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
"0000000000000000000000000000000000000000000000000000000000000001",
714,
)
.unwrap();
let client = RestClient::new(&server.url(), creds).unwrap();
let resp = client.cancel_all_open_orders("BTCUSDT").await.unwrap();
assert_eq!(resp.data.code, 200);
}
}