From 688a2bbdd2257b0b2d30d857be5a43319287ae9c Mon Sep 17 00:00:00 2001 From: Praying Date: Mon, 5 Jan 2026 17:43:00 +0800 Subject: [PATCH 1/3] fix(exchanges): replace unwrap_or_default with proper error handling - Replace all .unwrap_or_default() in critical data parsing paths with proper error handling using map_err() and Result types - Fixes: OKX, Bitget, Bybit, Binance OHLCV/JSON parsing errors - Adds context to error messages (exchange, operation, field names) - Tests: 732 passed, baseline maintained --- .gitignore | 2 + ccxt-exchanges/src/binance/parser/funding.rs | 21 +++------ ccxt-exchanges/src/bitget/exchange_impl.rs | 46 ++++++++++++++++---- ccxt-exchanges/src/bitget/parser.rs | 15 +++++-- ccxt-exchanges/src/bitget/rest/mod.rs | 12 +++-- ccxt-exchanges/src/bybit/parser.rs | 19 ++++++-- ccxt-exchanges/src/bybit/rest.rs | 12 +++-- ccxt-exchanges/src/bybit/signed_request.rs | 7 ++- ccxt-exchanges/src/okx/exchange_impl.rs | 46 ++++++++++++++++---- 9 files changed, 134 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index eb6eb85..a8a9c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,8 @@ go/ .kiro/ .roo/ .specify/ +specs/ +.claude/ .gemini/ _bmad/ docs/ diff --git a/ccxt-exchanges/src/binance/parser/funding.rs b/ccxt-exchanges/src/binance/parser/funding.rs index e7f3b5b..2433342 100644 --- a/ccxt-exchanges/src/binance/parser/funding.rs +++ b/ccxt-exchanges/src/binance/parser/funding.rs @@ -40,11 +40,8 @@ pub fn parse_funding_rate(data: &Value, market: Option<&Market>) -> Result ccxt_core::Result { + Ok(Ohlcv { + timestamp: o.timestamp, + open: Price(Decimal::try_from(o.open).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV open", + format!("{}", e), + )) + })?), + high: Price(Decimal::try_from(o.high).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV high", + format!("{}", e), + )) + })?), + low: Price(Decimal::try_from(o.low).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV low", + format!("{}", e), + )) + })?), + close: Price(Decimal::try_from(o.close).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV close", + format!("{}", e), + )) + })?), + volume: Amount(Decimal::try_from(o.volume).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV volume", + format!("{}", e), + )) + })?), + }) }) - .collect()) + .collect::>>() + .map_err(|e| e.context("Failed to convert Bitget OHLCV data")) } // ==================== Trading (Private API) ==================== diff --git a/ccxt-exchanges/src/bitget/parser.rs b/ccxt-exchanges/src/bitget/parser.rs index 475b821..4f3ea1b 100644 --- a/ccxt-exchanges/src/bitget/parser.rs +++ b/ccxt-exchanges/src/bitget/parser.rs @@ -167,7 +167,10 @@ pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result { .as_str() .or_else(|| data["instId"].as_str()) .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId")) + .context("Failed to parse ticker: missing symbol identifier") + })? }; // Bitget uses "ts" for timestamp @@ -318,7 +321,10 @@ pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result { data["symbol"] .as_str() .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol")) + .context("Failed to parse trade: missing symbol identifier") + })? }; let id = data["tradeId"] @@ -472,7 +478,10 @@ pub fn parse_order(data: &Value, market: Option<&Market>) -> Result { .as_str() .or_else(|| data["instId"].as_str()) .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId")) + .context("Failed to parse order: missing symbol identifier") + })? }; let id = data["orderId"] diff --git a/ccxt-exchanges/src/bitget/rest/mod.rs b/ccxt-exchanges/src/bitget/rest/mod.rs index 1f171d3..5874251 100644 --- a/ccxt-exchanges/src/bitget/rest/mod.rs +++ b/ccxt-exchanges/src/bitget/rest/mod.rs @@ -142,9 +142,15 @@ impl Bitget { String::new() }; - let body_string = body - .map(|b| serde_json::to_string(b).unwrap_or_default()) - .unwrap_or_default(); + let body_string = match body { + Some(b) => serde_json::to_string(b).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_format( + "request body", + format!("JSON serialization failed: {}", e), + )) + })?, + None => String::new(), + }; let sign_path = format!("{}{}", path, query_string); let signature = auth.sign(×tamp, method, &sign_path, &body_string); diff --git a/ccxt-exchanges/src/bybit/parser.rs b/ccxt-exchanges/src/bybit/parser.rs index 6e62aa4..2c8c287 100644 --- a/ccxt-exchanges/src/bybit/parser.rs +++ b/ccxt-exchanges/src/bybit/parser.rs @@ -218,7 +218,10 @@ pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result { data["symbol"] .as_str() .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol")) + .context("Failed to parse ticker: missing symbol identifier") + })? }; // Bybit uses different timestamp fields @@ -342,10 +345,15 @@ pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result { let symbol = if let Some(m) = market { m.symbol.clone() } else { + // Check both "symbol" (REST API) and "s" (WebSocket) fields data["symbol"] .as_str() + .or_else(|| data["s"].as_str()) .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s")) + .context("Failed to parse: missing symbol identifier") + })? }; let id = data["execId"] @@ -556,10 +564,15 @@ pub fn parse_order(data: &Value, market: Option<&Market>) -> Result { let symbol = if let Some(m) = market { m.symbol.clone() } else { + // Check both "symbol" (REST API) and "s" (WebSocket) fields data["symbol"] .as_str() + .or_else(|| data["s"].as_str()) .map(ToString::to_string) - .unwrap_or_default() + .ok_or_else(|| { + ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/s")) + .context("Failed to parse: missing symbol identifier") + })? }; let id = data["orderId"] diff --git a/ccxt-exchanges/src/bybit/rest.rs b/ccxt-exchanges/src/bybit/rest.rs index a498224..a9e3a43 100644 --- a/ccxt-exchanges/src/bybit/rest.rs +++ b/ccxt-exchanges/src/bybit/rest.rs @@ -158,9 +158,15 @@ impl Bybit { }; // Build body string for POST requests - let body_string = body - .map(|b| serde_json::to_string(b).unwrap_or_default()) - .unwrap_or_default(); + let body_string = match body { + Some(b) => serde_json::to_string(b).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_format( + "request body", + format!("JSON serialization failed: {}", e), + )) + })?, + None => String::new(), + }; // Sign the request - Bybit uses query string for GET, body for POST let sign_params = if method.to_uppercase() == "GET" { diff --git a/ccxt-exchanges/src/bybit/signed_request.rs b/ccxt-exchanges/src/bybit/signed_request.rs index 1e1a209..ec6b6c4 100644 --- a/ccxt-exchanges/src/bybit/signed_request.rs +++ b/ccxt-exchanges/src/bybit/signed_request.rs @@ -345,7 +345,12 @@ impl<'a> BybitSignedRequestBuilder<'a> { body.to_string() } else if !self.params.is_empty() { // Convert params to JSON for POST/DELETE - serde_json::to_string(&self.params).unwrap_or_default() + serde_json::to_string(&self.params).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_format( + "request params", + format!("JSON serialization failed: {}", e), + )) + })? } else { String::new() }; diff --git a/ccxt-exchanges/src/okx/exchange_impl.rs b/ccxt-exchanges/src/okx/exchange_impl.rs index 510ec40..fd1a84c 100644 --- a/ccxt-exchanges/src/okx/exchange_impl.rs +++ b/ccxt-exchanges/src/okx/exchange_impl.rs @@ -143,17 +143,45 @@ impl Exchange for Okx { let ohlcv_data = Okx::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?; // Convert OHLCV to Ohlcv with proper type conversions - Ok(ohlcv_data + ohlcv_data .into_iter() - .map(|o| Ohlcv { - timestamp: o.timestamp, - open: Price::from(Decimal::try_from(o.open).unwrap_or_default()), - high: Price::from(Decimal::try_from(o.high).unwrap_or_default()), - low: Price::from(Decimal::try_from(o.low).unwrap_or_default()), - close: Price::from(Decimal::try_from(o.close).unwrap_or_default()), - volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()), + .map(|o| -> ccxt_core::Result { + Ok(Ohlcv { + timestamp: o.timestamp, + open: Price(Decimal::try_from(o.open).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV open", + format!("{}", e), + )) + })?), + high: Price(Decimal::try_from(o.high).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV high", + format!("{}", e), + )) + })?), + low: Price(Decimal::try_from(o.low).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV low", + format!("{}", e), + )) + })?), + close: Price(Decimal::try_from(o.close).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV close", + format!("{}", e), + )) + })?), + volume: Amount(Decimal::try_from(o.volume).map_err(|e| { + ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( + "OHLCV volume", + format!("{}", e), + )) + })?), + }) }) - .collect()) + .collect::>>() + .map_err(|e| e.context("Failed to convert OKX OHLCV data")) } // ==================== Trading (Private API) ==================== From 4019d16e7ad706175db78246827a7215a376c46e Mon Sep 17 00:00:00 2001 From: Praying Date: Mon, 5 Jan 2026 19:11:49 +0800 Subject: [PATCH 2/3] style(config): refactor error formatting in exchange implementations --- ccxt-exchanges/src/bitget/exchange_impl.rs | 10 +++++----- ccxt-exchanges/src/okx/exchange_impl.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ccxt-exchanges/src/bitget/exchange_impl.rs b/ccxt-exchanges/src/bitget/exchange_impl.rs index f0cfc92..955a0df 100644 --- a/ccxt-exchanges/src/bitget/exchange_impl.rs +++ b/ccxt-exchanges/src/bitget/exchange_impl.rs @@ -142,31 +142,31 @@ impl Exchange for Bitget { open: Price(Decimal::try_from(o.open).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV open", - format!("{}", e), + format!("{e}"), )) })?), high: Price(Decimal::try_from(o.high).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV high", - format!("{}", e), + format!("{e}"), )) })?), low: Price(Decimal::try_from(o.low).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV low", - format!("{}", e), + format!("{e}"), )) })?), close: Price(Decimal::try_from(o.close).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV close", - format!("{}", e), + format!("{e}"), )) })?), volume: Amount(Decimal::try_from(o.volume).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV volume", - format!("{}", e), + format!("{e}"), )) })?), }) diff --git a/ccxt-exchanges/src/okx/exchange_impl.rs b/ccxt-exchanges/src/okx/exchange_impl.rs index fd1a84c..90299d0 100644 --- a/ccxt-exchanges/src/okx/exchange_impl.rs +++ b/ccxt-exchanges/src/okx/exchange_impl.rs @@ -151,31 +151,31 @@ impl Exchange for Okx { open: Price(Decimal::try_from(o.open).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV open", - format!("{}", e), + format!("{e}"), )) })?), high: Price(Decimal::try_from(o.high).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV high", - format!("{}", e), + format!("{e}"), )) })?), low: Price(Decimal::try_from(o.low).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV low", - format!("{}", e), + format!("{e}"), )) })?), close: Price(Decimal::try_from(o.close).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV close", - format!("{}", e), + format!("{e}"), )) })?), volume: Amount(Decimal::try_from(o.volume).map_err(|e| { ccxt_core::Error::from(ccxt_core::ParseError::invalid_value( "OHLCV volume", - format!("{}", e), + format!("{e}"), )) })?), }) From 4cd89bd847803ade2c06fe914570feb704c9f4b8 Mon Sep 17 00:00:00 2001 From: quran Date: Mon, 5 Jan 2026 19:38:48 +0800 Subject: [PATCH 3/3] fix: ignore RUSTSEC-2026-0001 --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 80b58ea..78213aa 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -129,4 +129,4 @@ jobs: - uses: rustsec/audit-check@v2 with: token: ${{ secrets.GITHUB_TOKEN }} - ignore: RUSTSEC-2023-0071, RUSTSEC-2024-0436 + ignore: RUSTSEC-2023-0071, RUSTSEC-2024-0436, RUSTSEC-2026-0001