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 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..90299d0 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) ====================