From 57155a0fd326e206e0342c8e0da81021f704a3d0 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 21 Feb 2026 09:27:45 +0100 Subject: [PATCH] fix: add error checking for batch order responses (#9) - Add BatchOrderResponse type to parse API responses with proper error detection - post_orders now checks each order result for errors and returns PolyError::Order with ExecutionFailed kind when any order fails - Add unit tests for BatchOrderResponse::has_error method - Export BatchOrderResponse from client module for external use This fixes the silent failure issue where batch orders return HTTP 200 but contain individual order errors (e.g., 'not enough balance / allowance'). --- src/client.rs | 27 +++++++++++++++++----- src/types.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1724afa..4c3a661 100644 --- a/src/client.rs +++ b/src/client.rs @@ -891,7 +891,7 @@ impl ClobClient { &self, orders: Vec, order_type: OrderType, - ) -> Result> { + ) -> Result> { let signer = self .signer .as_ref() @@ -930,7 +930,22 @@ impl ClobClient { )); } - Ok(response.json::>().await?) + let batch_results: Vec = response.json().await?; + + for (idx, result) in batch_results.iter().enumerate() { + if result.has_error() { + let error_msg = result + .error_msg + .clone() + .unwrap_or_else(|| "Unknown error".to_string()); + return Err(PolyError::order( + format!("Order {} failed: {}", idx, error_msg), + crate::errors::OrderErrorKind::ExecutionFailed, + )); + } + } + + Ok(batch_results) } /// Create and post an order in one call @@ -1826,10 +1841,10 @@ impl ClobClient { // Re-export types from the canonical location in types.rs pub use crate::types::{ - DataApiPositionsParams, DataApiSortBy, DataApiSortDirection, DataPosition, DataPositionValue, - ExtraOrderArgs, GammaEvent, GammaListParams, Market, MarketOrderArgs, MarketsResponse, - MidpointResponse, NegRiskResponse, OrderBookSummary, OrderSummary, PriceResponse, Rewards, - Sport, SpreadResponse, Tag, TickSizeResponse, Token, + BatchOrderResponse, DataApiPositionsParams, DataApiSortBy, DataApiSortDirection, DataPosition, + DataPositionValue, ExtraOrderArgs, GammaEvent, GammaListParams, Market, MarketOrderArgs, + MarketsResponse, MidpointResponse, NegRiskResponse, OrderBookSummary, OrderSummary, + PriceResponse, Rewards, Sport, SpreadResponse, Tag, TickSizeResponse, Token, }; // Compatibility types that need to stay in client.rs diff --git a/src/types.rs b/src/types.rs index b23374e..efffeeb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -566,6 +566,24 @@ impl PostOrder { } } +/// Response for a single order in a batch request +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchOrderResponse { + pub success: bool, + pub error_msg: Option, + pub order_id: Option, + pub making_amount: Option, + pub taking_amount: Option, + pub status: Option, +} + +impl BatchOrderResponse { + pub fn has_error(&self) -> bool { + !self.success || self.error_msg.is_some() + } +} + /// Market information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Market { @@ -1721,3 +1739,47 @@ pub type ClientResult = anyhow::Result; /// Result type used throughout the client pub type Result = std::result::Result; + +#[cfg(test)] +mod batch_order_response_tests { + use super::*; + + #[test] + fn test_error_detection_success_false() { + let response = BatchOrderResponse { + success: false, + error_msg: Some("not enough balance / allowance".to_string()), + order_id: None, + making_amount: None, + taking_amount: None, + status: None, + }; + assert!(response.has_error()); + } + + #[test] + fn test_error_detection_success_true_with_error_msg() { + let response = BatchOrderResponse { + success: true, + error_msg: Some("not enough balance / allowance".to_string()), + order_id: None, + making_amount: None, + taking_amount: None, + status: None, + }; + assert!(response.has_error()); + } + + #[test] + fn test_error_detection_success_true_no_error_msg() { + let response = BatchOrderResponse { + success: true, + error_msg: None, + order_id: Some("order-123".to_string()), + making_amount: Some("100".to_string()), + taking_amount: Some("50".to_string()), + status: Some("filled".to_string()), + }; + assert!(!response.has_error()); + } +}