From 84c3d269eabf607e7b0281e6899a9485fbb349e0 Mon Sep 17 00:00:00 2001 From: Andrey Kuznetsov Date: Thu, 12 Feb 2026 23:49:50 +0000 Subject: [PATCH] feat(client): use typed HTTP response models instead of serde_json::Value --- src/client.rs | 131 +++++++++++++++------------- src/lib.rs | 7 ++ src/orders.rs | 107 ++++++++++++++++++++--- src/types.rs | 166 ++++++++++++++++++++++++++++++++++-- tests/integration_tests.rs | 6 +- tests/order_posting_test.rs | 4 +- 6 files changed, 339 insertions(+), 82 deletions(-) diff --git a/src/client.rs b/src/client.rs index 516e5f1..c382072 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,7 +14,6 @@ use alloy_signer_local::PrivateKeySigner; use reqwest::header::HeaderName; use reqwest::Client; use reqwest::{Method, RequestBuilder}; -use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; use serde_json::Value; use std::str::FromStr; @@ -623,18 +622,8 @@ impl ClobClient { )); } - let tick_size_response: Value = response.json().await?; - let tick_size = tick_size_response["minimum_tick_size"] - .as_str() - .and_then(|s| Decimal::from_str(s).ok()) - .or_else(|| { - tick_size_response["minimum_tick_size"] - .as_f64() - .map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO)) - }) - .ok_or_else(|| PolyfillError::parse("Invalid tick size format", None))?; - - Ok(tick_size) + let tick_size_response: TickSizeResponse = response.json().await?; + Ok(tick_size_response.minimum_tick_size) } /// Get maker fee rate (in bps) for a token @@ -658,7 +647,7 @@ impl ClobClient { .json() .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; - Ok(fee_rate.fee_rate_bps) + Ok(fee_rate.base_fee) } /// Create a new API key @@ -820,12 +809,8 @@ impl ClobClient { )); } - let neg_risk_response: Value = response.json().await?; - let neg_risk = neg_risk_response["neg_risk"] - .as_bool() - .ok_or_else(|| PolyfillError::parse("Invalid neg risk format", None))?; - - Ok(neg_risk) + let neg_risk_response: NegRiskResponse = response.json().await?; + Ok(neg_risk_response.neg_risk) } /// Resolve tick size for an order @@ -953,7 +938,7 @@ impl ClobClient { .collect(), }; - order_builder.calculate_market_price(&levels, amount) + order_builder.calculate_market_price(side, &levels, amount) } /// Create a market order @@ -974,7 +959,7 @@ impl ClobClient { let extras = extras.unwrap_or_default(); let price = self - .calculate_market_price(&order_args.token_id, Side::BUY, order_args.amount) + .calculate_market_price(&order_args.token_id, order_args.side, order_args.amount) .await?; if !self.is_price_in_range( @@ -1000,7 +985,7 @@ impl ClobClient { &self, order: SignedOrderRequest, order_type: OrderType, - ) -> Result { + ) -> Result { let signer = self .signer .as_ref() @@ -1029,7 +1014,7 @@ impl ClobClient { return Err(PolyfillError::api(status, message)); } - Ok(response.json::().await?) + Ok(response.json::().await?) } /// Post multiple orders to the exchange in a single request @@ -1037,7 +1022,7 @@ impl ClobClient { &self, orders: Vec, order_type: OrderType, - ) -> Result { + ) -> Result> { if orders.is_empty() { return Err(PolyfillError::validation("orders cannot be empty")); } @@ -1076,7 +1061,9 @@ impl ClobClient { return Err(PolyfillError::api(status, message)); } - Ok(response.json::().await?) + Ok(response + .json::>() + .await?) } /// Create and post an order in one call with an explicit order type @@ -1084,13 +1071,16 @@ impl ClobClient { &self, order_args: &OrderArgs, order_type: OrderType, - ) -> Result { + ) -> Result { let order = self.create_order(order_args, None, None, None).await?; self.post_order(order, order_type).await } /// Create and post an order in one call (defaults to GTC) - pub async fn create_and_post_order(&self, order_args: &OrderArgs) -> Result { + pub async fn create_and_post_order( + &self, + order_args: &OrderArgs, + ) -> Result { self.create_and_post_order_with_type(order_args, OrderType::GTC) .await } @@ -1100,7 +1090,7 @@ impl ClobClient { &self, order_args: &[OrderArgs], order_type: OrderType, - ) -> Result { + ) -> Result> { if order_args.is_empty() { return Err(PolyfillError::validation("order_args cannot be empty")); } @@ -1114,13 +1104,16 @@ impl ClobClient { } /// Create and post multiple orders in one call (defaults to GTC) - pub async fn create_and_post_orders(&self, order_args: &[OrderArgs]) -> Result { + pub async fn create_and_post_orders( + &self, + order_args: &[OrderArgs], + ) -> Result> { self.create_and_post_orders_with_type(order_args, OrderType::GTC) .await } /// Cancel an order - pub async fn cancel(&self, order_id: &str) -> Result { + pub async fn cancel(&self, order_id: &str) -> Result { let signer = self .signer .as_ref() @@ -1143,11 +1136,16 @@ impl ClobClient { )); } - Ok(response.json::().await?) + Ok(response + .json::() + .await?) } /// Cancel multiple orders - pub async fn cancel_orders(&self, order_ids: &[String]) -> Result { + pub async fn cancel_orders( + &self, + order_ids: &[String], + ) -> Result { let signer = self .signer .as_ref() @@ -1168,11 +1166,13 @@ impl ClobClient { )); } - Ok(response.json::().await?) + Ok(response + .json::() + .await?) } /// Cancel all orders - pub async fn cancel_all(&self) -> Result { + pub async fn cancel_all(&self) -> Result { let signer = self .signer .as_ref() @@ -1182,7 +1182,7 @@ impl ClobClient { .as_ref() .ok_or_else(|| PolyfillError::auth("API credentials not set"))?; - let headers = create_l2_headers::(signer, api_creds, "DELETE", "/cancel-all", None)?; + let headers = create_l2_headers::<()>(signer, api_creds, "DELETE", "/cancel-all", None)?; let req = self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter()); @@ -1194,7 +1194,9 @@ impl ClobClient { )); } - Ok(response.json::().await?) + Ok(response + .json::() + .await?) } /// Get open orders with optional filtering @@ -1292,7 +1294,7 @@ impl ClobClient { &self, trade_params: Option<&crate::types::TradeParams>, next_cursor: Option<&str>, - ) -> Result> { + ) -> Result> { let signer = self .signer .as_ref() @@ -1348,7 +1350,14 @@ impl ClobClient { next_cursor = new_cursor; let results = resp["data"].clone(); - output.push(results); + let trades = serde_json::from_value::>(results) + .map_err(|e| { + PolyfillError::parse( + format!("Failed to parse data from trades response: {}", e), + None, + ) + })?; + output.extend(trades); } Ok(output) @@ -1646,7 +1655,7 @@ impl ClobClient { &self, market: Option<&str>, asset_id: Option<&str>, - ) -> Result { + ) -> Result { let signer = self .signer .as_ref() @@ -1680,7 +1689,7 @@ impl ClobClient { .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; response - .json::() + .json::() .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None)) } @@ -1802,12 +1811,12 @@ impl ClobClient { .await .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?; - let result: Value = response + let result: crate::types::OrderScoringResponse = response .json() .await .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?; - Ok(result["scoring"].as_bool().unwrap_or(false)) + Ok(result.scoring) } /// Check if multiple orders are scoring @@ -2357,9 +2366,11 @@ impl ClobClient { // Re-export types from the canonical location in types.rs pub use crate::types::{ - ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse, - OrderBookSummary, OrderSummary, PriceResponse, PricesHistoryInterval, PricesHistoryResponse, - Rewards, SpreadResponse, TickSizeResponse, Token, + CancelOrdersResponse, ExtraOrderArgs, MakerOrder, Market, MarketOrderArgs, MarketsResponse, + MidpointResponse, NegRiskResponse, OrderBookSummary, OrderScoringResponse, OrderSummary, + PostOrderResponse, PriceHistoryPoint, PriceResponse, PricesHistoryInterval, + PricesHistoryResponse, Rewards, SpreadResponse, TickSizeResponse, Token, TradeResponse, + TraderSide, }; // Compatibility types that need to stay in client.rs @@ -2715,7 +2726,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_get_prices_history_interval_success() { let mut server = Server::new_async().await; - let mock_response = r#"{"history":[{"t":1}]}"#; + let mock_response = r#"{"history":[{"t":1,"p":"0.52"}]}"#; let mock = server .mock("GET", "/prices-history") @@ -2738,6 +2749,8 @@ mod tests { mock.assert_async().await; assert_eq!(response.history.len(), 1); + assert_eq!(response.history[0].t, 1); + assert_eq!(response.history[0].p, Decimal::from_str("0.52").unwrap()); } #[tokio::test(flavor = "multi_thread")] @@ -2980,7 +2993,7 @@ mod tests { .mock("POST", "/orders") .with_status(200) .with_header("content-type", "application/json") - .with_body(r#"{"success":true,"orderIDs":["a","b"]}"#) + .with_body(r#"[{"success":true,"orderID":"a"},{"success":true,"orderID":"b"}]"#) .create_async() .await; @@ -2996,24 +3009,28 @@ mod tests { mock.assert_async().await; assert!(result.is_ok()); - assert_eq!(result.unwrap()["success"], true); + let orders = result.unwrap(); + assert_eq!(orders.len(), 2); + assert!(orders.iter().all(|o| o.success)); } #[tokio::test(flavor = "multi_thread")] async fn test_post_orders_batch_empty_validation() { let client = create_test_client_with_l2_auth("https://test.example.com"); - let result = client.post_orders(Vec::new(), crate::types::OrderType::GTC).await; + let result = client + .post_orders(Vec::new(), crate::types::OrderType::GTC) + .await; assert!(matches!(result, Err(PolyfillError::Validation { .. }))); } #[tokio::test(flavor = "multi_thread")] async fn test_post_orders_batch_too_many_validation() { let client = create_test_client_with_l2_auth("https://test.example.com"); - let orders = (0..16) - .map(|_| sample_signed_order()) - .collect::>(); + let orders = (0..16).map(|_| sample_signed_order()).collect::>(); - let result = client.post_orders(orders, crate::types::OrderType::GTC).await; + let result = client + .post_orders(orders, crate::types::OrderType::GTC) + .await; assert!(matches!(result, Err(PolyfillError::Validation { .. }))); } @@ -3036,7 +3053,7 @@ mod tests { mock.assert_async().await; assert!(result.is_ok()); - assert_eq!(result.unwrap()["success"], true); + assert!(result.unwrap().success); } #[tokio::test(flavor = "multi_thread")] @@ -3058,7 +3075,7 @@ mod tests { mock.assert_async().await; assert!(result.is_ok()); - assert_eq!(result.unwrap()[0]["success"], true); + assert!(result.unwrap()[0].success); } #[tokio::test(flavor = "multi_thread")] @@ -3222,7 +3239,7 @@ mod tests { .match_query(Matcher::UrlEncoded("token_id".into(), "123".into())) .with_status(200) .with_header("content-type", "application/json") - .with_body(r#"{"fee_rate_bps":1000}"#) + .with_body(r#"{"base_fee":1000}"#) .create_async() .await; diff --git a/src/lib.rs b/src/lib.rs index d7c5b08..692be08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,10 +97,12 @@ pub use crate::types::{ BatchPriceRequest, BatchPriceResponse, BookParams, + CancelOrdersResponse, ClientConfig, ClientResult, FeeRateResponse, FillEvent, + MakerOrder, Market, MarketSnapshot, MarketsResponse, @@ -114,9 +116,12 @@ pub use crate::types::{ OrderBookSummary, OrderDelta, OrderRequest, + OrderScoringResponse, OrderStatus, OrderSummary, OrderType, + PostOrderResponse, + PriceHistoryPoint, PriceResponse, PricesHistoryInterval, PricesHistoryResponse, @@ -143,6 +148,8 @@ pub use crate::types::{ Token, TokenPrice, TradeParams, + TradeResponse, + TraderSide, WssAuth, WssChannelType, WssSubscription, diff --git a/src/orders.rs b/src/orders.rs index 15cde0c..91e522c 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -193,32 +193,48 @@ impl OrderBuilder { /// Get order amounts for a market order fn get_market_order_amounts( &self, + side: Side, amount: Decimal, price: Decimal, round_config: &RoundConfig, ) -> (u32, u32) { - let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero); let raw_price = price.round_dp_with_strategy(round_config.price, MidpointTowardZero); - - let raw_taker_amt = raw_maker_amt / raw_price; - let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config); - - ( - decimal_to_token_u32(raw_maker_amt), - decimal_to_token_u32(raw_taker_amt), - ) + match side { + Side::BUY => { + let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero); + let raw_taker_amt = raw_maker_amt / raw_price; + let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config); + ( + decimal_to_token_u32(raw_maker_amt), + decimal_to_token_u32(raw_taker_amt), + ) + }, + Side::SELL => { + let raw_maker_amt = amount.round_dp_with_strategy(round_config.size, ToZero); + let raw_taker_amt = raw_maker_amt * raw_price; + let raw_taker_amt = self.fix_amount_rounding(raw_taker_amt, round_config); + ( + decimal_to_token_u32(raw_maker_amt), + decimal_to_token_u32(raw_taker_amt), + ) + }, + } } /// Calculate market price from order book levels pub fn calculate_market_price( &self, + side: Side, positions: &[crate::types::BookLevel], amount_to_match: Decimal, ) -> Result { let mut sum = Decimal::ZERO; for level in positions { - sum += level.size * level.price; + sum += match side { + Side::BUY => level.size * level.price, + Side::SELL => level.size, + }; if sum >= amount_to_match { return Ok(level.price); } @@ -246,8 +262,12 @@ impl OrderBuilder { .tick_size .ok_or_else(|| PolyfillError::validation("Cannot create order without tick size"))?; - let (maker_amount, taker_amount) = - self.get_market_order_amounts(order_args.amount, price, &ROUNDING_CONFIG[&tick_size]); + let (maker_amount, taker_amount) = self.get_market_order_amounts( + order_args.side, + order_args.amount, + price, + &ROUNDING_CONFIG[&tick_size], + ); let neg_risk = options .neg_risk @@ -262,7 +282,7 @@ impl OrderBuilder { self.build_signed_order( order_args.token_id.clone(), - Side::BUY, + order_args.side, chain_id, exchange_address, maker_amount, @@ -437,4 +457,65 @@ mod tests { assert!(seed < u64::MAX); } } + + #[test] + fn test_calculate_market_price_respects_side_amount_semantics() { + let signer: PrivateKeySigner = + "0x1234567890123456789012345678901234567890123456789012345678901234" + .parse() + .unwrap(); + let builder = OrderBuilder::new(signer, None, None); + + let levels = vec![ + crate::types::BookLevel { + price: Decimal::from_str("0.50").unwrap(), + size: Decimal::from_str("10").unwrap(), + }, + crate::types::BookLevel { + price: Decimal::from_str("0.55").unwrap(), + size: Decimal::from_str("10").unwrap(), + }, + ]; + + // BUY amounts are quote-denominated: need 6 USDC -> first level (10 * 0.50 = 5) is not enough. + let buy_price = builder + .calculate_market_price(Side::BUY, &levels, Decimal::from_str("6").unwrap()) + .unwrap(); + assert_eq!(buy_price, Decimal::from_str("0.55").unwrap()); + + // SELL amounts are base-denominated: need 6 tokens -> first level (size 10) is enough. + let sell_price = builder + .calculate_market_price(Side::SELL, &levels, Decimal::from_str("6").unwrap()) + .unwrap(); + assert_eq!(sell_price, Decimal::from_str("0.50").unwrap()); + } + + #[test] + fn test_create_market_order_uses_input_side() { + let signer: PrivateKeySigner = + "0x1234567890123456789012345678901234567890123456789012345678901234" + .parse() + .unwrap(); + let builder = OrderBuilder::new(signer, None, None); + + let order = builder + .create_market_order( + 137, + &MarketOrderArgs { + token_id: "123".to_string(), + side: Side::SELL, + amount: Decimal::from_str("5").unwrap(), + }, + Decimal::from_str("0.40").unwrap(), + &ExtraOrderArgs::default(), + &OrderOptions { + tick_size: Some(Decimal::from_str("0.01").unwrap()), + neg_risk: Some(false), + fee_rate_bps: None, + }, + ) + .unwrap(); + + assert_eq!(order.side, "SELL"); + } } diff --git a/src/types.rs b/src/types.rs index 97e59e6..c9551f6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -191,6 +191,21 @@ pub enum Side { SELL = 1, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum TraderSide { + Taker, + Maker, + #[serde(untagged)] + Unknown(String), +} + +impl Default for TraderSide { + fn default() -> Self { + Self::Unknown("UNKNOWN".to_string()) + } +} + impl Side { pub fn as_str(&self) -> &'static str { match self { @@ -526,6 +541,8 @@ impl Default for ExtraOrderArgs { #[derive(Debug, Clone)] pub struct MarketOrderArgs { pub token_id: String, + pub side: Side, + /// Quote amount for buys, base token amount for sells. pub amount: Decimal, } @@ -1108,6 +1125,131 @@ pub struct OpenOrder { pub created_at: u64, } +/// Response from posting a single order. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostOrderResponse { + #[serde(default)] + pub error_msg: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error" + )] + pub making_amount: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error" + )] + pub taking_amount: Option, + #[serde(rename = "orderID")] + pub order_id: String, + #[serde(default)] + pub status: Option, + pub success: bool, + #[serde(default, alias = "transactionsHashes")] + pub transaction_hashes: Vec, + #[serde(default)] + pub trade_ids: Vec, +} + +/// Response from cancel endpoints. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CancelOrdersResponse { + #[serde( + default, + deserialize_with = "crate::decode::deserializers::vec_from_null" + )] + pub canceled: Vec, + #[serde(default, alias = "not_canceled")] + pub not_canceled: std::collections::HashMap, +} + +/// Trade item returned by `/data/trades`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct TradeResponse { + pub id: String, + #[serde(default)] + pub taker_order_id: Option, + pub market: String, + pub asset_id: String, + pub side: Side, + #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")] + pub size: Decimal, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string" + )] + pub fee_rate_bps: Option, + #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")] + pub price: Decimal, + #[serde(default)] + pub status: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_number_from_string" + )] + pub match_time: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_number_from_string" + )] + pub last_update: Option, + #[serde(default)] + pub outcome: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_number_from_string" + )] + pub bucket_index: Option, + #[serde(default)] + pub owner: Option, + #[serde(default)] + pub maker_address: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::vec_from_null" + )] + pub maker_orders: Vec, + #[serde(default)] + pub transaction_hash: Option, + #[serde(default)] + pub trader_side: TraderSide, + #[serde(default, alias = "err_msg")] + pub error_msg: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MakerOrder { + pub order_id: String, + #[serde(default)] + pub owner: Option, + #[serde(default)] + pub maker_address: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string" + )] + pub matched_amount: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string" + )] + pub price: Option, + #[serde( + default, + deserialize_with = "crate::decode::deserializers::optional_decimal_from_string" + )] + pub fee_rate_bps: Option, + #[serde(default)] + pub asset_id: Option, + #[serde(default)] + pub outcome: Option, + #[serde(default)] + pub side: Option, +} + /// Balance allowance information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BalanceAllowance { @@ -1256,24 +1398,29 @@ impl PricesHistoryInterval { } } -/// Raw response from `/prices-history`. +/// A single price-history datapoint from `/prices-history`. /// -/// We intentionally keep `history` entries as `serde_json::Value` because the upstream API has -/// no stable public schema here and currently may return empty history for many markets. +/// Mirrors the official Polymarket SDK shape (`t`, `p`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceHistoryPoint { + pub t: i64, + #[serde(deserialize_with = "crate::decode::deserializers::decimal_from_string")] + pub p: Decimal, +} + +/// Response from `/prices-history`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PricesHistoryResponse { - pub history: Vec, + pub history: Vec, } #[derive(Debug, Deserialize)] pub struct SpreadResponse { - #[serde(with = "rust_decimal::serde::str")] pub spread: Decimal, } #[derive(Debug, Deserialize)] pub struct TickSizeResponse { - #[serde(with = "rust_decimal::serde::str")] pub minimum_tick_size: Decimal, } @@ -1282,6 +1429,11 @@ pub struct NegRiskResponse { pub neg_risk: bool, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderScoringResponse { + pub scoring: bool, +} + #[derive(Debug, Serialize, Deserialize)] pub struct BookParams { pub token_id: String, @@ -1376,7 +1528,7 @@ pub struct Rewards { /// Fee rate in basis points for a given token. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeeRateResponse { - pub fee_rate_bps: u32, + pub base_fee: u32, } /// Create RFQ request (Requester). diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index b15ebff..d7ecb7c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -112,9 +112,9 @@ async fn test_real_api_authenticated_order_flow() { println!("PASS: Order posted successfully!"); // Step 5: Cancel the order - if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) { - println!("Step 5: Canceling order {}...", order_id); - let cancel_result = client.cancel(order_id).await; + if !response.order_id.is_empty() { + println!("Step 5: Canceling order {}...", response.order_id); + let cancel_result = client.cancel(&response.order_id).await; assert!( cancel_result.is_ok(), "Failed to cancel order: {:?}", diff --git a/tests/order_posting_test.rs b/tests/order_posting_test.rs index f41deeb..03ee008 100644 --- a/tests/order_posting_test.rs +++ b/tests/order_posting_test.rs @@ -41,9 +41,9 @@ async fn test_post_order_authentication() { println!(" Response: {:?}", response); // Try to cancel it if we got an order ID - if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) { + if !response.order_id.is_empty() { println!("\nStep 3: Canceling order..."); - match client.cancel(order_id).await { + match client.cancel(&response.order_id).await { Ok(_) => println!("Order canceled successfully"), Err(e) => println!("Cancel failed (order might have expired): {:?}", e), }