diff --git a/Cargo.lock b/Cargo.lock index 3a79b0d8..47363319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,7 +428,7 @@ dependencies = [ [[package]] name = "bothan-api" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-band", @@ -456,7 +456,7 @@ dependencies = [ [[package]] name = "bothan-api-cli" -version = "0.0.1" +version = "0.1.0" dependencies = [ "anyhow", "bothan-api", @@ -493,7 +493,7 @@ dependencies = [ [[package]] name = "bothan-band" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "bothan-binance" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "bothan-bitfinex" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -553,7 +553,7 @@ dependencies = [ [[package]] name = "bothan-bybit" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -571,7 +571,7 @@ dependencies = [ [[package]] name = "bothan-client" -version = "0.0.1" +version = "0.1.0" dependencies = [ "pbjson", "prost", @@ -585,7 +585,7 @@ dependencies = [ [[package]] name = "bothan-coinbase" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -605,7 +605,7 @@ dependencies = [ [[package]] name = "bothan-coingecko" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -624,7 +624,7 @@ dependencies = [ [[package]] name = "bothan-coinmarketcap" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -645,7 +645,7 @@ dependencies = [ [[package]] name = "bothan-core" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "axum 0.8.4", @@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "bothan-htx" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -706,7 +706,7 @@ dependencies = [ [[package]] name = "bothan-kraken" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "bothan-lib" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bincode", @@ -747,7 +747,7 @@ dependencies = [ [[package]] name = "bothan-okx" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-trait", "bothan-lib", diff --git a/Cargo.toml b/Cargo.toml index 76f49707..2e9336be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,20 +17,20 @@ resolver = "2" [workspace.dependencies] bothan-api = { path = "bothan-api/server" } -bothan-core = { path = "bothan-core", version = "0.0.1" } -bothan-client = { path = "bothan-api/client/rust-client", version = "0.0.1" } -bothan-lib = { path = "bothan-lib", version = "0.0.1" } +bothan-core = { path = "bothan-core", version = "0.1.0" } +bothan-client = { path = "bothan-api/client/rust-client", version = "0.1.0" } +bothan-lib = { path = "bothan-lib", version = "0.1.0" } -bothan-binance = { path = "bothan-binance", version = "0.0.1" } -bothan-bitfinex = { path = "bothan-bitfinex", version = "0.0.1" } -bothan-bybit = { path = "bothan-bybit", version = "0.0.1" } -bothan-coinbase = { path = "bothan-coinbase", version = "0.0.1" } -bothan-coingecko = { path = "bothan-coingecko", version = "0.0.1" } -bothan-coinmarketcap = { path = "bothan-coinmarketcap", version = "0.0.1" } -bothan-htx = { path = "bothan-htx", version = "0.0.1" } -bothan-kraken = { path = "bothan-kraken", version = "0.0.1" } -bothan-okx = { path = "bothan-okx", version = "0.0.1" } -bothan-band = { path = "bothan-band", version = "0.0.1" } +bothan-binance = { path = "bothan-binance", version = "0.1.0" } +bothan-bitfinex = { path = "bothan-bitfinex", version = "0.1.0" } +bothan-bybit = { path = "bothan-bybit", version = "0.1.0" } +bothan-coinbase = { path = "bothan-coinbase", version = "0.1.0" } +bothan-coingecko = { path = "bothan-coingecko", version = "0.1.0" } +bothan-coinmarketcap = { path = "bothan-coinmarketcap", version = "0.1.0" } +bothan-htx = { path = "bothan-htx", version = "0.1.0" } +bothan-kraken = { path = "bothan-kraken", version = "0.1.0" } +bothan-okx = { path = "bothan-okx", version = "0.1.0" } +bothan-band = { path = "bothan-band", version = "0.1.0" } anyhow = "1.0.86" async-trait = "0.1.77" diff --git a/bothan-api-proxy/go.mod b/bothan-api-proxy/go.mod index 16f06d16..807783b4 100644 --- a/bothan-api-proxy/go.mod +++ b/bothan-api-proxy/go.mod @@ -3,7 +3,7 @@ module go-proxy go 1.24.2 require ( - github.com/bandprotocol/bothan/bothan-api/client/go-client v0.0.1 + github.com/bandprotocol/bothan/bothan-api/client/go-client v0.1.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 google.golang.org/grpc v1.67.1 ) diff --git a/bothan-api/client/rust-client/Cargo.toml b/bothan-api/client/rust-client/Cargo.toml index cc68da3f..5c629a70 100644 --- a/bothan-api/client/rust-client/Cargo.toml +++ b/bothan-api/client/rust-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-client" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Bothan API" authors.workspace = true edition.workspace = true diff --git a/bothan-api/server-cli/Cargo.toml b/bothan-api/server-cli/Cargo.toml index 24da2467..e4041850 100644 --- a/bothan-api/server-cli/Cargo.toml +++ b/bothan-api/server-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-api-cli" -version = "0.0.1" +version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/bothan-api/server/Cargo.toml b/bothan-api/server/Cargo.toml index b25e4b39..f820df49 100644 --- a/bothan-api/server/Cargo.toml +++ b/bothan-api/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-api" -version = "0.0.1" +version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/bothan-api/server/src/api/utils.rs b/bothan-api/server/src/api/utils.rs index 1c4a0151..f80aca2b 100644 --- a/bothan-api/server/src/api/utils.rs +++ b/bothan-api/server/src/api/utils.rs @@ -9,7 +9,7 @@ use bothan_core::manager::crypto_asset_info::types::PriceState; use rust_decimal::prelude::Zero; -use tracing::warn; +use tracing::error; use crate::api::server::PRECISION; use crate::proto::bothan::v1::{Price, Status}; @@ -32,7 +32,7 @@ pub fn parse_price_state(id: String, price_state: PriceState) -> Price { match u64::try_from(mantissa) { Ok(p) => Price::new(id, p, Status::Available), Err(_) => { - warn!("failed to convert {mantissa} to u64 for id {id}"); + error!("failed to convert {mantissa} to u64 for id {id}"); Price::new(id, 0u64, Status::Unavailable) } } diff --git a/bothan-band/Cargo.toml b/bothan-band/Cargo.toml index 206a3ae2..e2b90c5c 100644 --- a/bothan-band/Cargo.toml +++ b/bothan-band/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-band" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Band source with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-band/src/api/error.rs b/bothan-band/src/api/error.rs index e3db4564..b76503be 100644 --- a/bothan-band/src/api/error.rs +++ b/bothan-band/src/api/error.rs @@ -3,6 +3,7 @@ //! This module provides custom error types used throughout the Band REST API integration, //! particularly for REST API client configuration and concurrent background data fetching. +use reqwest::StatusCode; use thiserror::Error; /// Errors from initializing the Band REST API builder. @@ -16,7 +17,7 @@ pub enum BuildError { InvalidURL(#[from] url::ParseError), /// Represents general failures during HTTP client construction (e.g., TLS configuration issues). - #[error("reqwest error: {0}")] + #[error("failed to build with error: {0}")] FailedToBuild(#[from] reqwest::Error), } @@ -26,21 +27,40 @@ pub enum BuildError { #[derive(Debug, Error)] pub enum ProviderError { /// Indicates HTTP request failure due to network issues or HTTP errors. - #[error("failed to fetch prices: {0}")] - RequestError(#[from] reqwest::Error), + #[error("failed to fetch prices (signals={signals}): {error}")] + SendingRequestError { + #[source] + error: reqwest::Error, + signals: String, + }, - /// Indicates a failure to parse the API response. - #[error("parse error: {0}")] - ParseError(#[from] ParseError), + /// Indicates the API returned a non-success HTTP status code. + #[error("returned HTTP {status} for signals={signals}: {body}")] + HttpStatusError { + status: StatusCode, + body: String, + signals: String, + }, + + /// Indicates the response body could not be deserialized. + #[error("failed to parse response for signals={signals}: {source}")] + ParseResponseError { + #[source] + source: reqwest::Error, + signals: String, + }, } /// Errors that can occur while parsing Band API responses. #[derive(Debug, Error)] pub enum ParseError { - /// Indicates that the price value is not a valid number (NaN). - #[error("price is NaN")] - InvalidPrice, - /// Indicates that the timestamp value is missing or invalid. - #[error("invalid timestamp")] - InvalidTimestamp, + /// Indicates that the price field was missing. + #[error("missing price from signal {0}")] + MissingPrice(String), + /// Indicates that the price value is present but not a valid number (NaN/inf). + #[error("invalid price value {price} from signal {signal}")] + InvalidPrice { price: f64, signal: String }, + /// Indicates that the timestamp field was missing. + #[error("missing timestamp from signal {0}")] + MissingTimestamp(String), } diff --git a/bothan-band/src/api/rest.rs b/bothan-band/src/api/rest.rs index aaa9ac6d..0067acbe 100644 --- a/bothan-band/src/api/rest.rs +++ b/bothan-band/src/api/rest.rs @@ -15,7 +15,7 @@ use bothan_lib::worker::rest::AssetInfoProvider; use itertools::Itertools; use reqwest::{Client, Url}; use rust_decimal::Decimal; -use tracing::warn; +use tracing::{error, warn}; use crate::api::error::{ParseError, ProviderError}; use crate::api::types::Price; @@ -67,20 +67,45 @@ impl RestApi { /// /// # Errors /// - /// Returns a [`reqwest::Error`] if: + /// Returns a [`ProviderError`] if: /// - The request fails due to network issues /// - The response status is not 2xx /// - JSON deserialization into `Vec` fails - pub async fn get_latest_prices(&self, ids: &[String]) -> Result, reqwest::Error> { + pub async fn get_latest_prices(&self, ids: &[String]) -> Result, ProviderError> { let url = format!("{}prices", self.url); - let ids_string = ids.iter().map(|id| id.to_string()).join(","); - let params = vec![("signals", ids_string)]; - - let request_builder = self.client.get(&url).query(¶ms); - let response = request_builder.send().await?.error_for_status()?; - let prices = response.json::>().await?; + let ids_string = ids.iter().map(String::as_str).join(","); + let params = vec![("signals", &ids_string)]; + + let resp = self + .client + .get(&url) + .query(¶ms) + .send() + .await + .map_err(|error| ProviderError::SendingRequestError { + error, + signals: ids_string.clone(), + })?; + + let status = resp.status(); + if !status.is_success() { + let body = resp + .text() + .await + .unwrap_or_else(|err| format!("failed to read response body: {err}")); + return Err(ProviderError::HttpStatusError { + status, + body, + signals: ids_string.clone(), + }); + } - Ok(prices) + resp.json::>() + .await + .map_err(|source| ProviderError::ParseResponseError { + source, + signals: ids_string, + }) } } @@ -112,11 +137,16 @@ impl AssetInfoProvider for RestApi { let mut asset_info = Vec::with_capacity(prices.len()); for band_price in prices { - let signal = band_price.signal.clone(); match parse_price(band_price) { Ok(info) => asset_info.push(info), - Err(e) => { - warn!("failed to parse price id '{signal}': {e}"); + Err(ParseError::InvalidPrice { price, signal }) => { + error!("failed to parse price '{price}' for signal '{signal}'"); + } + Err(ParseError::MissingPrice(signal)) => { + warn!("missing price for '{signal}'"); + } + Err(ParseError::MissingTimestamp(signal)) => { + warn!("missing timestamp for '{signal}'"); } } } @@ -127,10 +157,18 @@ impl AssetInfoProvider for RestApi { /// Parses a `Price` into an [`AssetInfo`] struct. fn parse_price(band_price: Price) -> Result { - let price = band_price.price.ok_or(ParseError::InvalidPrice)?; - let price = Decimal::from_f64_retain(price).ok_or(ParseError::InvalidPrice)?; - let ts = band_price.timestamp.ok_or(ParseError::InvalidTimestamp)?; - Ok(AssetInfo::new(band_price.signal, price, ts)) + let signal = band_price.signal; + let price = band_price + .price + .ok_or(ParseError::MissingPrice(signal.clone()))?; + let price = Decimal::from_f64_retain(price).ok_or(ParseError::InvalidPrice { + price, + signal: signal.clone(), + })?; + let ts = band_price + .timestamp + .ok_or(ParseError::MissingTimestamp(signal.clone()))?; + Ok(AssetInfo::new(signal, price, ts)) } #[cfg(test)] diff --git a/bothan-binance/Cargo.toml b/bothan-binance/Cargo.toml index d4cfed22..6e79322d 100644 --- a/bothan-binance/Cargo.toml +++ b/bothan-binance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-binance" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Binance exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-binance/src/api/error.rs b/bothan-binance/src/api/error.rs index b6311129..c001c12b 100644 --- a/bothan-binance/src/api/error.rs +++ b/bothan-binance/src/api/error.rs @@ -9,12 +9,16 @@ #[derive(Debug, thiserror::Error)] pub enum Error { /// Indicates a failure to parse a websocket message. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Indicates that the websocket message type is not supported. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors encountered while listening for Binance API events. @@ -28,6 +32,11 @@ pub enum ListeningError { Error(#[from] Error), /// Indicates an error while parsing a message from the WebSocket stream. - #[error(transparent)] - InvalidPrice(#[from] rust_decimal::Error), + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { + #[source] + source: rust_decimal::Error, + symbol: String, + price: String, + }, } diff --git a/bothan-binance/src/api/websocket.rs b/bothan-binance/src/api/websocket.rs index d8ae88ca..2a161ddf 100644 --- a/bothan-binance/src/api/websocket.rs +++ b/bothan-binance/src/api/websocket.rs @@ -163,13 +163,12 @@ impl WebSocketConnection { /// Supported message types include text messages (parsed as `Event`), ping messages, and close messages. pub async fn next(&mut self) -> Option> { match self.ws_stream.next().await { - Some(Ok(Message::Text(msg))) => match serde_json::from_str::(&msg) { - Ok(msg) => Some(Ok(msg)), - Err(e) => Some(Err(Error::ParseError(e))), - }, + Some(Ok(Message::Text(msg))) => Some(parse_msg(msg)), Some(Ok(Message::Ping(_))) => Some(Ok(Event::Ping)), Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -184,6 +183,10 @@ impl WebSocketConnection { } } +fn parse_msg(msg: String) -> Result { + serde_json::from_str::(&msg).map_err(|source| Error::ParseError { source, msg }) +} + #[async_trait::async_trait] impl AssetInfoProvider for WebSocketConnection { type SubscriptionError = tungstenite::Error; @@ -233,10 +236,16 @@ impl AssetInfoProvider for WebSocketConnection { } } -fn parse_mini_ticker(mini_ticker: MiniTickerInfo) -> Result { +fn parse_mini_ticker(mini_ticker: MiniTickerInfo) -> Result { + let symbol = mini_ticker.symbol.to_ascii_lowercase(); + let price = mini_ticker.close_price; let asset_info = AssetInfo::new( - mini_ticker.symbol.to_ascii_lowercase(), - Decimal::from_str(&mini_ticker.close_price)?, + symbol.clone(), + Decimal::from_str(&price).map_err(|source| ListeningError::InvalidPrice { + source, + symbol, + price, + })?, mini_ticker.event_time / 1000, // convert from millisecond to second ); Ok(Data::AssetInfo(vec![asset_info])) diff --git a/bothan-bitfinex/Cargo.toml b/bothan-bitfinex/Cargo.toml index 3337f7e6..41dd2bcf 100644 --- a/bothan-bitfinex/Cargo.toml +++ b/bothan-bitfinex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-bitfinex" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Bitfinex exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-bitfinex/src/api/error.rs b/bothan-bitfinex/src/api/error.rs index f5b68c6f..770071ba 100644 --- a/bothan-bitfinex/src/api/error.rs +++ b/bothan-bitfinex/src/api/error.rs @@ -2,7 +2,7 @@ //! //! This module provides custom error types used throughout the Bitfinex API integration, //! particularly for handling REST API requests and price validation errors. - +use reqwest::StatusCode; use thiserror::Error; /// Errors related to Bitfinex API client configuration and building. @@ -27,10 +27,26 @@ pub enum BuildError { #[derive(Debug, Error)] pub enum ProviderError { /// Indicates a failure to fetch ticker data from the Bitfinex API. - #[error("failed to fetch tickers: {0}")] - RequestError(#[from] reqwest::Error), + #[error("failed to fetch tickers (symbols={symbols}): {error}")] + SendingRequestError { + #[source] + error: reqwest::Error, + symbols: String, + }, + + /// Indicates a non-success HTTP status code. + #[error("returned HTTP {status} for symbols={symbols}: {body}")] + HttpStatusError { + status: StatusCode, + body: String, + symbols: String, + }, - /// Indicates that the ticker data contains invalid values (e.g., NaN). - #[error("value contains nan")] - InvalidValue, + /// Indicates the response body could not be parsed into the expected shape. + #[error("failed to parse response for symbols={symbols}: {source}")] + ParseResponseError { + #[source] + source: reqwest::Error, + symbols: String, + }, } diff --git a/bothan-bitfinex/src/api/rest.rs b/bothan-bitfinex/src/api/rest.rs index f92a38d2..2cf87c99 100644 --- a/bothan-bitfinex/src/api/rest.rs +++ b/bothan-bitfinex/src/api/rest.rs @@ -16,7 +16,7 @@ use bothan_lib::types::AssetInfo; use bothan_lib::worker::rest::AssetInfoProvider; use reqwest::{Client, Url}; use rust_decimal::Decimal; -use tracing::warn; +use tracing::{error, warn}; use crate::api::error::ProviderError; use crate::api::msg::ticker::Ticker; @@ -110,25 +110,49 @@ impl RestApi { /// /// # Errors /// - /// Returns a `reqwest::Error` if: - /// - The HTTP request fails due to network issues - /// - The API returns an error response - /// - The response cannot be parsed as JSON + /// Returns a [`ProviderError`] if: + /// - The HTTP request fails due to network issues (`SendingRequestError`) + /// - The API returns an error response (`HttpStatusError`) + /// - The response cannot be parsed as JSON (`ParseResponseError`) pub async fn get_tickers>( &self, tickers: &[T], - ) -> Result, reqwest::Error> { + ) -> Result, ProviderError> { let url = format!("{}/tickers", self.url); let symbols = tickers .iter() .map(|t| t.as_ref()) .collect::>() .join(","); - let params = vec![("symbols", symbols)]; + let params = vec![("symbols", &symbols)]; - let resp = self.client.get(&url).query(¶ms).send().await?; - resp.error_for_status_ref()?; - resp.json().await + let resp = self + .client + .get(&url) + .query(¶ms) + .send() + .await + .map_err(|e| ProviderError::SendingRequestError { + error: e, + symbols: symbols.clone(), + })?; + + let status = resp.status(); + if !status.is_success() { + let body = resp + .text() + .await + .unwrap_or_else(|err| format!("failed to read response body: {err}")); + return Err(ProviderError::HttpStatusError { + status, + body, + symbols: symbols.clone(), + }); + } + + resp.json() + .await + .map_err(|source| ProviderError::ParseResponseError { source, symbols }) } } @@ -172,7 +196,11 @@ impl AssetInfoProvider for RestApi { asset_infos.push(AssetInfo::new(id.clone(), price, timestamp)); } None => { - warn!("failed to parse price for symbol '{}'", t.symbol()); + error!( + "failed to parse price {} for symbol '{}'", + t.price(), + t.symbol() + ); } } } else { diff --git a/bothan-bybit/Cargo.toml b/bothan-bybit/Cargo.toml index 16cc6333..768be99d 100644 --- a/bothan-bybit/Cargo.toml +++ b/bothan-bybit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-bybit" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Bybit exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-bybit/src/api/error.rs b/bothan-bybit/src/api/error.rs index 5bfc2b84..f45856e3 100644 --- a/bothan-bybit/src/api/error.rs +++ b/bothan-bybit/src/api/error.rs @@ -7,12 +7,16 @@ #[derive(Debug, thiserror::Error)] pub enum Error { /// Failed to parse a message from the WebSocket. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Received an unsupported message type from the WebSocket. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors that can occur while listening for Bybit WebSocket events. @@ -26,6 +30,11 @@ pub enum ListeningError { Error(#[from] Error), /// An invalid price was encountered while parsing a message. - #[error(transparent)] - InvalidPrice(#[from] rust_decimal::Error), + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { + #[source] + source: rust_decimal::Error, + symbol: String, + price: String, + }, } diff --git a/bothan-bybit/src/api/websocket.rs b/bothan-bybit/src/api/websocket.rs index 71a8a0e0..2a9646cd 100644 --- a/bothan-bybit/src/api/websocket.rs +++ b/bothan-bybit/src/api/websocket.rs @@ -142,7 +142,9 @@ impl WebSocketConnection { Some(Ok(Message::Text(msg))) => Some(parse_msg(msg)), Some(Ok(Message::Ping(_))) => Some(Ok(Response::Ping)), Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -158,7 +160,7 @@ impl WebSocketConnection { } fn parse_msg(msg: String) -> Result { - Ok(serde_json::from_str::(&msg)?) + serde_json::from_str::(&msg).map_err(|source| Error::ParseError { source, msg }) } #[async_trait::async_trait] @@ -188,11 +190,17 @@ impl AssetInfoProvider for WebSocketConnection { } } -fn parse_public_ticker(ticker: PublicTickerResponse) -> Result { +fn parse_public_ticker(ticker: PublicTickerResponse) -> Result { + let symbol = ticker.data.symbol; + let price = ticker.data.last_price; let asset_info = AssetInfo::new( - ticker.data.symbol, - Decimal::from_str_exact(&ticker.data.last_price)?, - ticker.ts / 1000, // convert from millisecond to second + symbol.clone(), + Decimal::from_str_exact(&price).map_err(|source| ListeningError::InvalidPrice { + source, + symbol, + price, + })?, + ticker.ts / 1000, ); Ok(Data::AssetInfo(vec![asset_info])) } diff --git a/bothan-coinbase/Cargo.toml b/bothan-coinbase/Cargo.toml index 8ee74858..9a188983 100644 --- a/bothan-coinbase/Cargo.toml +++ b/bothan-coinbase/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-coinbase" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Coinbase exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-coinbase/src/api/error.rs b/bothan-coinbase/src/api/error.rs index d309e458..2823f8b3 100644 --- a/bothan-coinbase/src/api/error.rs +++ b/bothan-coinbase/src/api/error.rs @@ -7,12 +7,16 @@ #[derive(Debug, thiserror::Error)] pub enum Error { /// Indicates a failure to parse a WebSocket message. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Indicates that the WebSocket message type is not supported. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors encountered while listening for Coinbase API events. @@ -25,9 +29,14 @@ pub enum ListeningError { #[error(transparent)] Error(#[from] Error), - /// Indicates an error while parsing a message from the WebSocket stream. - #[error(transparent)] - InvalidPrice(#[from] rust_decimal::Error), + /// Indicates an error while parsing price data from the WebSocket stream. + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { + #[source] + source: rust_decimal::Error, + symbol: String, + price: String, + }, /// Indicates an error while parsing a timestamp from the WebSocket message. #[error(transparent)] diff --git a/bothan-coinbase/src/api/websocket.rs b/bothan-coinbase/src/api/websocket.rs index 95cdf549..e527a4a0 100644 --- a/bothan-coinbase/src/api/websocket.rs +++ b/bothan-coinbase/src/api/websocket.rs @@ -20,7 +20,7 @@ use serde_json::json; use tokio::net::TcpStream; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async, tungstenite}; -use tracing::warn; +use tracing::error; use crate::api::Ticker; use crate::api::error::{Error, ListeningError}; @@ -169,7 +169,9 @@ impl WebSocketConnection { Some(Ok(Message::Text(msg))) => Some(parse_msg(msg)), Some(Ok(Message::Ping(_))) => Some(Ok(Response::Ping)), Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -184,7 +186,7 @@ impl WebSocketConnection { } fn parse_msg(msg: String) -> Result { - Ok(serde_json::from_str::(&msg)?) + serde_json::from_str::(&msg).map_err(|source| Error::ParseError { source, msg }) } #[async_trait::async_trait] @@ -225,7 +227,7 @@ impl AssetInfoProvider for WebSocketConnection { Response::Ticker(t) => parse_ticker(t)?, Response::Ping => Data::Ping, Response::Error(e) => { - warn!("received error in response: {:?}", e); + error!("received error in response: {:?}", e); Data::Unused } _ => Data::Unused, @@ -242,9 +244,15 @@ impl AssetInfoProvider for WebSocketConnection { } fn parse_ticker(ticker: Box) -> Result { + let symbol = ticker.product_id; + let price = ticker.price; let asset_info = AssetInfo::new( - ticker.product_id, - Decimal::from_str_exact(&ticker.price)?, + symbol.clone(), + Decimal::from_str_exact(&price).map_err(|source| ListeningError::InvalidPrice { + source, + symbol, + price, + })?, chrono::DateTime::parse_from_rfc3339(&ticker.time)?.timestamp(), ); Ok(Data::AssetInfo(vec![asset_info])) diff --git a/bothan-coingecko/Cargo.toml b/bothan-coingecko/Cargo.toml index 5bd77682..b033007d 100644 --- a/bothan-coingecko/Cargo.toml +++ b/bothan-coingecko/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-coingecko" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the CoinGecko exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-coingecko/src/api/error.rs b/bothan-coingecko/src/api/error.rs index da725d1b..e7029c3e 100644 --- a/bothan-coingecko/src/api/error.rs +++ b/bothan-coingecko/src/api/error.rs @@ -3,6 +3,7 @@ //! This module provides custom error types used throughout the CoinGecko REST API integration, //! particularly for REST API client configuration and concurrent background data fetching. +use reqwest::StatusCode; use thiserror::Error; /// Errors from initializing the CoinGecko REST API builder. @@ -30,10 +31,30 @@ pub enum BuildError { #[derive(Debug, Error)] pub enum ProviderError { /// Indicates HTTP request failure due to network issues or HTTP errors. - #[error("failed to fetch tickers: {0}")] - RequestError(#[from] reqwest::Error), + #[error("failed to fetch {resource}: {error}")] + SendingRequestError { + #[source] + error: reqwest::Error, + resource: String, + }, + + /// Indicates a non-success HTTP status code. + #[error("returned HTTP {status} for {resource}: {body}")] + HttpStatusError { + status: StatusCode, + body: String, + resource: String, + }, + + /// Indicates the response body could not be parsed into the expected shape. + #[error("failed to parse {resource}: {source}")] + ParseResponseError { + #[source] + source: reqwest::Error, + resource: String, + }, /// Indicates that the response data contains invalid numeric values (e.g., `NaN`). - #[error("value contains nan")] - InvalidValue, + #[error("invalid price value {price} for id {id}")] + InvalidValue { price: f64, id: String }, } diff --git a/bothan-coingecko/src/api/rest.rs b/bothan-coingecko/src/api/rest.rs index 7b4c0c9f..c7a6d4b6 100644 --- a/bothan-coingecko/src/api/rest.rs +++ b/bothan-coingecko/src/api/rest.rs @@ -17,7 +17,7 @@ use bothan_lib::worker::rest::AssetInfoProvider; use reqwest::{Client, RequestBuilder, Url}; use rust_decimal::Decimal; use serde::de::DeserializeOwned; -use tracing::warn; +use tracing::{error, warn}; use crate::api::error::ProviderError; use crate::api::types::{Coin, Price}; @@ -63,11 +63,11 @@ impl RestApi { } /// Retrieves a list of coins from the CoinGecko REST API. - pub async fn get_coins_list(&self) -> Result, reqwest::Error> { + pub async fn get_coins_list(&self) -> Result, ProviderError> { let url = format!("{}coins/list", self.url); let builder = self.client.get(url); - request::>(builder).await + request::>(builder, "coins list".to_string()).await } /// Retrieves market data for the specified coins from the CoinGecko REST API. @@ -86,14 +86,14 @@ impl RestApi { /// /// # Errors /// - /// Returns a [`reqwest::Error`] if: - /// - The request fails due to network issues - /// - The response status is not 2xx - /// - JSON deserialization into [`HashMap`] fails + /// Returns a [`ProviderError`] if: + /// - The request fails due to network issues (`SendingRequestError`) + /// - The response status is not 2xx (`HttpStatusError`) + /// - JSON deserialization into [`HashMap`] fails (`ParseResponseError`) pub async fn get_simple_price_usd>( &self, ids: &[T], - ) -> Result, reqwest::Error> { + ) -> Result, ProviderError> { let url = format!("{}simple/price", self.url); let joined_ids = ids .iter() @@ -110,7 +110,11 @@ impl RestApi { let builder_with_query = self.client.get(&url).query(¶ms); - request::>(builder_with_query).await + request::>( + builder_with_query, + format!("simple price (ids={joined_ids})"), + ) + .await } } @@ -121,16 +125,40 @@ impl RestApi { /// /// # Errors /// -/// Returns a [`reqwest::Error`] if: +/// Returns a [`ProviderError`] if: /// - The request fails to send (e.g., network issues) /// - The response returns a non-success status code (e.g., 400, 500) /// - JSON deserialization into type `T` fails async fn request( request_builder: RequestBuilder, -) -> Result { - let response = request_builder.send().await?.error_for_status()?; + resource: String, +) -> Result { + let response = + request_builder + .send() + .await + .map_err(|error| ProviderError::SendingRequestError { + error, + resource: resource.clone(), + })?; + + let status = response.status(); + if !status.is_success() { + let body = response + .text() + .await + .unwrap_or_else(|err| format!("failed to read response body: {err}")); + return Err(ProviderError::HttpStatusError { + status, + body, + resource, + }); + } - response.json::().await + response + .json::() + .await + .map_err(|source| ProviderError::ParseResponseError { source, resource }) } #[async_trait::async_trait] @@ -169,7 +197,7 @@ impl AssetInfoProvider for RestApi { asset_infos.push(AssetInfo::new(id.clone(), price, p.last_updated_at)); } None => { - warn!("failed to parse price for id '{id}': invalid or NaN value."); + error!("failed to parse price '{usd}' for id '{id}'"); } }, None => { diff --git a/bothan-coinmarketcap/Cargo.toml b/bothan-coinmarketcap/Cargo.toml index 6d08c70b..61888470 100644 --- a/bothan-coinmarketcap/Cargo.toml +++ b/bothan-coinmarketcap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-coinmarketcap" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the CoinMarketCap exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-coinmarketcap/src/api/error.rs b/bothan-coinmarketcap/src/api/error.rs index c74b6b9a..6e2600fb 100644 --- a/bothan-coinmarketcap/src/api/error.rs +++ b/bothan-coinmarketcap/src/api/error.rs @@ -3,6 +3,7 @@ //! This module provides custom error types used throughout the CoinMarketCap REST API integration, //! particularly for REST API client configuration and concurrent background data fetching. +use reqwest::StatusCode; use thiserror::Error; /// Errors from initializing the CoinMarketCap REST API builder. @@ -48,16 +49,32 @@ pub enum Error { #[derive(Debug, Error)] pub enum ProviderError { /// Indicates that an ID in the request is not a valid integer. - #[error("ids contains non integer value")] - InvalidId, + #[error("ids contains non integer value: {0}")] + InvalidId(String), /// Indicates HTTP request failure due to network issues or HTTP errors. - #[error("failed to fetch tickers: {0}")] - RequestError(#[from] reqwest::Error), + #[error("failed to fetch quotes (ids={ids}): {error}")] + SendingRequestError { + #[source] + error: reqwest::Error, + ids: String, + }, - /// Indicates a failure to parse the API response. - #[error("parse error: {0}")] - ParseError(#[from] ParseError), + /// Indicates a non-success HTTP status code. + #[error("returned HTTP {status} for ids={ids}: {body}")] + HttpStatusError { + status: StatusCode, + body: String, + ids: String, + }, + + /// Indicates the response body could not be parsed into the expected shape. + #[error("failed to parse response for ids={ids}: {source}")] + ParseResponseError { + #[source] + source: reqwest::Error, + ids: String, + }, } /// Errors that can occur while parsing CoinMarketCap API responses. diff --git a/bothan-coinmarketcap/src/api/rest.rs b/bothan-coinmarketcap/src/api/rest.rs index 3dc32f3f..15a8d24d 100644 --- a/bothan-coinmarketcap/src/api/rest.rs +++ b/bothan-coinmarketcap/src/api/rest.rs @@ -17,11 +17,10 @@ use bothan_lib::worker::rest::AssetInfoProvider; use itertools::Itertools; use reqwest::{Client, Url}; use rust_decimal::Decimal; -use tracing::warn; +use tracing::{error, warn}; -use crate::api::error::ParseError; +use crate::api::error::{ParseError, ProviderError}; use crate::api::types::{Quote, Response as CmcResponse}; -use crate::worker::error::ProviderError; /// Client for interacting with the CoinMarketCap REST API. /// @@ -76,23 +75,48 @@ impl RestApi { /// /// # Errors /// - /// Returns a [`reqwest::Error`] if: - /// - The request fails due to network issues - /// - The response status is not 2xx - /// - JSON deserialization into `HashMap` fails + /// Returns a [`ProviderError`] if: + /// - The request fails due to network issues (`SendingRequestError`) + /// - The response status is not 2xx (`HttpStatusError`) + /// - JSON deserialization into `HashMap` fails (`ParseResponseError`) pub async fn get_latest_quotes( &self, ids: &[u64], - ) -> Result>, reqwest::Error> { + ) -> Result>, ProviderError> { let url = format!("{}v2/cryptocurrency/quotes/latest", self.url); let ids_string = ids.iter().map(|id| id.to_string()).join(","); - let params = vec![("id", ids_string)]; + let params = vec![("id", &ids_string)]; let request_builder = self.client.get(&url).query(¶ms); - let response = request_builder.send().await?.error_for_status()?; + let response = + request_builder + .send() + .await + .map_err(|error| ProviderError::SendingRequestError { + error, + ids: ids_string.clone(), + })?; + + let status = response.status(); + if !status.is_success() { + let body = response + .text() + .await + .unwrap_or_else(|err| format!("failed to read response body: {err}")); + return Err(ProviderError::HttpStatusError { + status, + body, + ids: ids_string, + }); + } + let cmc_response = response .json::>>() - .await?; + .await + .map_err(|source| ProviderError::ParseResponseError { + source, + ids: ids_string.clone(), + })?; let mut quote_map = cmc_response.data; let quotes = ids @@ -126,14 +150,14 @@ impl AssetInfoProvider for RestApi { /// [`RestApi::get_latest_quotes`]: crate::api::RestApi::get_latest_quotes /// [`AssetInfo`]: bothan_lib::types::AssetInfo /// [`Decimal`]: rust_decimal::Decimal - /// [`ProviderError`]: crate::worker::error::ProviderError + /// [`ProviderError`]: crate::api::error::ProviderError async fn get_asset_info(&self, ids: &[String]) -> Result, Self::Error> { let mut int_ids = Vec::with_capacity(ids.len()); for id in ids { match id.parse::() { Ok(val) => int_ids.push(val), Err(_) => { - warn!("invalid CoinMarketCap id '{id}': cannot parse to u64",); + error!("invalid CoinMarketCap id '{id}': cannot parse to u64",); } } } @@ -146,7 +170,7 @@ impl AssetInfoProvider for RestApi { Some(q) => match parse_quote(q) { Ok(info) => asset_info.push(info), Err(e) => { - warn!("failed to parse quote for id '{}': {e}", int_ids[idx]); + error!("failed to parse quote for id '{}': {e}", int_ids[idx]); } }, None => { diff --git a/bothan-core/Cargo.toml b/bothan-core/Cargo.toml index ad921ea9..d7e6d514 100644 --- a/bothan-core/Cargo.toml +++ b/bothan-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-core" -version = "0.0.1" +version = "0.1.0" description = "Core library for Bothan" authors.workspace = true edition.workspace = true diff --git a/bothan-core/src/manager/crypto_asset_info/price/tasks.rs b/bothan-core/src/manager/crypto_asset_info/price/tasks.rs index ff0ced53..e89ec102 100644 --- a/bothan-core/src/manager/crypto_asset_info/price/tasks.rs +++ b/bothan-core/src/manager/crypto_asset_info/price/tasks.rs @@ -16,7 +16,7 @@ use bothan_lib::store::Store; use bothan_lib::types::AssetInfo; use num_traits::Zero; use rust_decimal::Decimal; -use tracing::{debug, info, warn}; +use tracing::{debug, error, info, warn}; use crate::manager::crypto_asset_info::price::cache::PriceCache; use crate::manager::crypto_asset_info::price::error::{Error, MissingPrerequisiteError}; @@ -71,15 +71,15 @@ pub async fn get_signal_price_states( continue; } Err(Error::InvalidSignal) => { - warn!("signal with id {} is not supported", id); + debug!("signal with id {} is not supported", id); cache.set_unsupported(id); } Err(Error::FailedToProcessSignal(e)) => { - warn!("error while processing signal id {}: {}", id, e); + error!("error while processing signal id {}: {}", id, e); cache.set_unavailable(id); } Err(Error::FailedToPostProcessSignal(e)) => { - warn!("error while post processing signal id {}: {}", id, e); + error!("error while post processing signal id {}: {}", id, e); cache.set_unavailable(id); } } @@ -241,7 +241,7 @@ async fn process_source_query( Ok(None) } Err(_) => { - warn!("error while querying source {source_id} for {query_id}"); + error!("error while querying source {source_id} for {query_id}"); metrics.update_store_operation( source_id.clone(), start_time.elapsed().as_micros(), diff --git a/bothan-htx/Cargo.toml b/bothan-htx/Cargo.toml index 111d24f8..516ad857 100644 --- a/bothan-htx/Cargo.toml +++ b/bothan-htx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-htx" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the HTX exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-htx/src/api/error.rs b/bothan-htx/src/api/error.rs index ce4c1dda..50fa7039 100644 --- a/bothan-htx/src/api/error.rs +++ b/bothan-htx/src/api/error.rs @@ -18,12 +18,16 @@ pub enum Error { Io(#[from] io::Error), /// Indicates a failure to parse a WebSocket message. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Indicates that the WebSocket message type is not supported. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors encountered while listening for HTX API events. @@ -37,12 +41,12 @@ pub enum ListeningError { Error(#[from] Error), /// Indicates that the received channel ID is invalid or malformed. - #[error("received invalid channel id")] - InvalidChannelId, + #[error("received invalid channel id: {0}")] + InvalidChannelId(String), - /// Indicates that the received price data contains NaN values. - #[error("received NaN")] - InvalidPrice, + /// Indicates that the received price data contains invalid values. + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { symbol: String, price: f64 }, /// Indicates a failure to send a pong response to a ping message. #[error("failed to pong")] diff --git a/bothan-htx/src/api/websocket.rs b/bothan-htx/src/api/websocket.rs index 83946499..09be7416 100644 --- a/bothan-htx/src/api/websocket.rs +++ b/bothan-htx/src/api/websocket.rs @@ -23,7 +23,7 @@ use serde_json::json; use tokio::net::TcpStream; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async, tungstenite}; -use tracing::warn; +use tracing::error; use crate::api::error::{Error, ListeningError}; use crate::api::types::Response; @@ -260,7 +260,9 @@ impl WebSocketConnection { Some(Ok(Message::Binary(msg))) => Some(decode_response(&msg)), Some(Ok(Message::Ping(_))) => None, Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -300,7 +302,10 @@ fn decode_response(msg: &[u8]) -> Result { let mut decoder = GzDecoder::new(msg); let mut decompressed_msg = String::new(); decoder.read_to_string(&mut decompressed_msg)?; - Ok(serde_json::from_str::(&decompressed_msg)?) + serde_json::from_str::(&decompressed_msg).map_err(|source| Error::ParseError { + source, + msg: decompressed_msg, + }) } #[async_trait::async_trait] @@ -352,7 +357,7 @@ impl AssetInfoProvider for WebSocketConnection { Ok(Response::DataUpdate(d)) => parse_data(d), Ok(Response::Ping(p)) => reply_pong(self, p.ping).await, Ok(Response::Error(e)) => { - warn!("received error in response: {:?}", e); + error!("received error in response: {:?}", e); Ok(Data::Unused) } Err(e) => Err(ListeningError::Error(e)), @@ -389,16 +394,19 @@ impl AssetInfoProvider for WebSocketConnection { /// - The channel ID cannot be extracted from the channel name /// - The price data contains invalid values (NaN) fn parse_data(data: super::types::Data) -> Result { - let id = data - .ch + let ch = data.ch; + let id = ch + .clone() .split('.') .nth(1) - .ok_or(ListeningError::InvalidChannelId)? + .ok_or(ListeningError::InvalidChannelId(ch))? .to_string(); + let price = data.tick.last_price; let asset_info = AssetInfo::new( - id, - Decimal::from_f64_retain(data.tick.last_price).ok_or(ListeningError::InvalidPrice)?, - data.timestamp / 1000, // convert from millisecond to second + id.clone(), + Decimal::from_f64_retain(data.tick.last_price) + .ok_or(ListeningError::InvalidPrice { symbol: id, price })?, + data.timestamp / 1000, ); Ok(Data::AssetInfo(vec![asset_info])) } diff --git a/bothan-kraken/Cargo.toml b/bothan-kraken/Cargo.toml index 5f8e677f..222b4855 100644 --- a/bothan-kraken/Cargo.toml +++ b/bothan-kraken/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-kraken" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the Kraken exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-kraken/src/api/error.rs b/bothan-kraken/src/api/error.rs index e2a8f5fb..0ae6bd83 100644 --- a/bothan-kraken/src/api/error.rs +++ b/bothan-kraken/src/api/error.rs @@ -22,12 +22,16 @@ pub enum Error { IO(#[from] io::Error), /// Indicates a failure to parse a WebSocket message. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Indicates that the WebSocket message type is not supported. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors that can occur during the listening and processing phase. @@ -41,11 +45,7 @@ pub enum ListeningError { #[error(transparent)] Error(#[from] Error), - /// Indicates that the received channel ID is invalid or malformed. - #[error("received invalid channel id")] - InvalidChannelId, - - /// Indicates that the received price data contains NaN values. - #[error("received NaN")] - InvalidPrice, + /// Indicates that the received price data contains invalid values. + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { symbol: String, price: f64 }, } diff --git a/bothan-kraken/src/api/websocket.rs b/bothan-kraken/src/api/websocket.rs index 977c1ecf..3ad51351 100644 --- a/bothan-kraken/src/api/websocket.rs +++ b/bothan-kraken/src/api/websocket.rs @@ -315,7 +315,9 @@ impl WebSocketConnection { Some(Ok(Message::Text(msg))) => Some(parse_msg(msg)), Some(Ok(Message::Ping(_))) => Some(Ok(Response::Ping)), Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -398,7 +400,7 @@ fn build_ticker_request( /// Returns a `Result` containing a parsed `Response` on success, /// or an `Error` if parsing fails. fn parse_msg(msg: String) -> Result { - Ok(serde_json::from_str::(&msg)?) + serde_json::from_str::(&msg).map_err(|source| Error::ParseError { source, msg }) } #[async_trait::async_trait] @@ -506,9 +508,11 @@ fn parse_tickers(tickers: Vec, timestamp: i64) -> Result Result { + let symbol = ticker.symbol; + let price = ticker.last; Ok(AssetInfo::new( - ticker.symbol, - Decimal::from_f64_retain(ticker.last).ok_or(ListeningError::InvalidPrice)?, + symbol.clone(), + Decimal::from_f64_retain(price).ok_or(ListeningError::InvalidPrice { symbol, price })?, timestamp, )) } diff --git a/bothan-lib/Cargo.toml b/bothan-lib/Cargo.toml index 77a86712..093b9676 100644 --- a/bothan-lib/Cargo.toml +++ b/bothan-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-lib" -version = "0.0.1" +version = "0.1.0" description = "Library contain base functionality and types for Bothan" authors.workspace = true edition.workspace = true diff --git a/bothan-okx/Cargo.toml b/bothan-okx/Cargo.toml index 272e7149..6d78662c 100644 --- a/bothan-okx/Cargo.toml +++ b/bothan-okx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bothan-okx" -version = "0.0.1" +version = "0.1.0" description = "Rust client for the OKX exchange with Bothan integration" edition.workspace = true license.workspace = true diff --git a/bothan-okx/src/api/error.rs b/bothan-okx/src/api/error.rs index afcb0d63..b0c85953 100644 --- a/bothan-okx/src/api/error.rs +++ b/bothan-okx/src/api/error.rs @@ -28,15 +28,19 @@ pub enum Error { /// /// This variant wraps serde JSON errors that can occur when parsing /// WebSocket messages from the OKX API. - #[error("failed to parse message")] - ParseError(#[from] serde_json::Error), + #[error("failed to parse message: {msg}")] + ParseError { + #[source] + source: serde_json::Error, + msg: String, + }, /// Received an unsupported WebSocket message type. /// /// This variant indicates that the WebSocket connection received a message /// type that is not supported by the OKX integration. - #[error("unsupported message")] - UnsupportedWebsocketMessageType, + #[error("unsupported message: {0}")] + UnsupportedWebsocketMessageType(String), } /// Errors that can occur during the listening and processing phase. @@ -57,8 +61,13 @@ pub enum ListeningError { /// /// This variant indicates that the price data received from the OKX API /// could not be converted to a valid decimal value. - #[error(transparent)] - InvalidPrice(#[from] rust_decimal::Error), + #[error("invalid price value {price} for symbol {symbol}")] + InvalidPrice { + #[source] + source: rust_decimal::Error, + symbol: String, + price: String, + }, /// Invalid timestamp data encountered during processing. /// diff --git a/bothan-okx/src/api/websocket.rs b/bothan-okx/src/api/websocket.rs index ae11b4f7..ea4e7778 100644 --- a/bothan-okx/src/api/websocket.rs +++ b/bothan-okx/src/api/websocket.rs @@ -284,7 +284,9 @@ impl WebSocketConnection { Some(Ok(Message::Text(msg))) => Some(parse_msg(msg)), Some(Ok(Message::Ping(_))) => Some(Ok(Response::Ping)), Some(Ok(Message::Close(_))) => None, - Some(Ok(_)) => Some(Err(Error::UnsupportedWebsocketMessageType)), + Some(Ok(m)) => Some(Err(Error::UnsupportedWebsocketMessageType(format!( + "{m:?}" + )))), Some(Err(_)) => None, // Consider the connection closed if error detected None => None, } @@ -365,7 +367,7 @@ fn build_ticker_request(inst_ids: &[T]) -> Vec { /// Returns a `Result` containing a parsed `Response` on success, /// or an `Error` if parsing fails. fn parse_msg(msg: String) -> Result { - Ok(serde_json::from_str::(&msg)?) + serde_json::from_str::(&msg).map_err(|source| Error::ParseError { source, msg }) } #[async_trait::async_trait] @@ -472,9 +474,15 @@ fn parse_tickers(tickers: Vec) -> Result { /// - The price data contains invalid values /// - The timestamp cannot be parsed fn parse_ticker(ticker: Ticker) -> Result { + let symbol = ticker.inst_id; + let price = ticker.last; Ok(AssetInfo::new( - ticker.inst_id, - Decimal::from_str_exact(&ticker.last)?, + symbol.clone(), + Decimal::from_str_exact(&price).map_err(|source| ListeningError::InvalidPrice { + source, + symbol, + price, + })?, str::parse::(&ticker.ts)? / 1000, )) }