diff --git a/client/src/lib.rs b/client/src/lib.rs index ae5ea05..0c3b463 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -176,6 +176,32 @@ impl BpxClient { Ok(res) } + /// Deserializes the response body as JSON, logging the body at error level on failure. + /// + /// Unlike `Response::json()`, this method captures the response body and logs it + /// if deserialization fails, making it easier to debug API changes. + pub async fn json_with_context(res: Response) -> Result { + let body = res.text().await?; + serde_json::from_str(&body).map_err(|error| { + // Truncate very long responses for readability in logs + let body_preview = if body.len() > 2000 { + format!( + "{}...[truncated, {} bytes total]", + &body[..2000], + body.len() + ) + } else { + body + }; + tracing::error!( + %error, + body_preview, + "Failed to deserialize API response" + ); + error.into() + }) + } + /// Sends a GET request to the specified URL and signs it before execution. pub async fn get(&self, url: U) -> Result { let req = self.build_and_maybe_sign_request::<(), _>(url, Method::GET, None)?; diff --git a/client/src/routes/account.rs b/client/src/routes/account.rs index e261434..86e0e5d 100644 --- a/client/src/routes/account.rs +++ b/client/src/routes/account.rs @@ -21,7 +21,7 @@ impl BpxClient { pub async fn get_account(&self) -> Result { let url = self.base_url.join(API_ACCOUNT)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the account's maximum borrow amount for a given symbol. @@ -29,7 +29,7 @@ impl BpxClient { let mut url = self.base_url.join(API_ACCOUNT_MAX_BORROW)?; url.query_pairs_mut().append_pair("symbol", symbol); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the account's maximum order amount for a given symbol. @@ -39,7 +39,7 @@ impl BpxClient { .map_err(|e| Error::UrlParseError(e.to_string().into_boxed_str()))?; url.set_query(Some(&query_string)); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the account's maximum withdrawal amount for a given symbol. @@ -61,7 +61,7 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Updates the account's settings. diff --git a/client/src/routes/borrow_lend.rs b/client/src/routes/borrow_lend.rs index 82b77ea..c7bd9c1 100644 --- a/client/src/routes/borrow_lend.rs +++ b/client/src/routes/borrow_lend.rs @@ -10,6 +10,6 @@ impl BpxClient { pub async fn get_borrow_lend_positions(&self) -> Result> { let url = self.base_url.join(API_BORROW_LEND_POSITIONS)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/capital.rs b/client/src/routes/capital.rs index 88a26df..d7e608e 100644 --- a/client/src/routes/capital.rs +++ b/client/src/routes/capital.rs @@ -24,7 +24,7 @@ impl BpxClient { pub async fn get_balances(&self) -> Result> { let url = self.base_url.join(API_CAPITAL)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves a list of deposits with optional pagination. @@ -44,7 +44,7 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the deposit address for a specified blockchain. @@ -53,7 +53,7 @@ impl BpxClient { url.query_pairs_mut() .append_pair("blockchain", &blockchain.to_string()); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves a list of withdrawals with optional pagination. @@ -73,7 +73,7 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Submits a withdrawal request for the specified payload. @@ -83,13 +83,13 @@ impl BpxClient { ) -> Result { let endpoint = self.base_url.join(API_WITHDRAWALS)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the subaccount's collateral information. pub async fn get_collateral(&self) -> Result { let url = self.base_url.join(API_COLLATERAL)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/futures.rs b/client/src/routes/futures.rs index b3a9abe..55e1ee1 100644 --- a/client/src/routes/futures.rs +++ b/client/src/routes/futures.rs @@ -10,6 +10,6 @@ impl BpxClient { pub async fn get_open_future_positions(&self) -> Result> { let url = self.base_url.join(API_FUTURES_POSITION)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/history.rs b/client/src/routes/history.rs index 353522f..cf9b16b 100644 --- a/client/src/routes/history.rs +++ b/client/src/routes/history.rs @@ -14,6 +14,6 @@ impl BpxClient { let mut url = self.base_url.join(API_FILLS_HISTORY)?; url.set_query(Some(&query_string)); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/markets.rs b/client/src/routes/markets.rs index bb7368e..69e97e5 100644 --- a/client/src/routes/markets.rs +++ b/client/src/routes/markets.rs @@ -19,21 +19,21 @@ impl BpxClient { pub async fn get_assets(&self) -> Result> { let url = self.base_url.join(API_ASSETS)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves a list of available markets. pub async fn get_markets(&self) -> Result> { let url = self.base_url.join(API_MARKETS)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves mark price, index price and the funding rate for the current interval for all symbols, or the symbol specified. pub async fn get_all_mark_prices(&self) -> Result> { let url = self.base_url.join(API_MARK_PRICES)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the ticker information for a given symbol. @@ -41,14 +41,14 @@ impl BpxClient { let mut url = self.base_url.join(API_TICKER)?; url.query_pairs_mut().append_pair("symbol", symbol); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches the ticker information for all symbols. pub async fn get_tickers(&self) -> Result> { let url = self.base_url.join(API_TICKERS)?; let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves the order book depth for a given symbol. @@ -63,7 +63,7 @@ impl BpxClient { url.query_pairs_mut().append_pair("limit", limit.as_ref()); } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Funding interval rate history for futures. @@ -71,7 +71,7 @@ impl BpxClient { let mut url = self.base_url.join(API_FUNDING)?; url.query_pairs_mut().append_pair("symbol", symbol); let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches historical K-line (candlestick) data for a given symbol and interval. @@ -93,6 +93,6 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/order.rs b/client/src/routes/order.rs index 693a630..6a1b84e 100644 --- a/client/src/routes/order.rs +++ b/client/src/routes/order.rs @@ -36,14 +36,14 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Executes a new order with the given payload. pub async fn execute_order(&self, payload: ExecuteOrderPayload) -> Result { let endpoint = self.base_url.join(API_ORDER)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Cancels a specific order by symbol and either order ID or client ID. @@ -61,7 +61,7 @@ impl BpxClient { }; let res = self.delete(url, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Retrieves all open orders, optionally filtered by symbol. @@ -71,13 +71,13 @@ impl BpxClient { url.query_pairs_mut().append_pair("symbol", s); } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Cancels all open orders matching the specified payload. pub async fn cancel_open_orders(&self, payload: CancelOpenOrdersPayload) -> Result> { let url = self.base_url.join(API_ORDERS)?; let res = self.delete(url, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/rfq.rs b/client/src/routes/rfq.rs index 23f130d..701910c 100644 --- a/client/src/routes/rfq.rs +++ b/client/src/routes/rfq.rs @@ -29,7 +29,7 @@ impl BpxClient { pub async fn submit_rfq(&self, payload: RequestForQuotePayload) -> Result { let endpoint = self.base_url.join(API_RFQ)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } pub async fn cancel_rfq( @@ -38,7 +38,7 @@ impl BpxClient { ) -> Result { let endpoint = self.base_url.join(API_RFQ_CANCEL)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } pub async fn refresh_rfq( @@ -47,19 +47,19 @@ impl BpxClient { ) -> Result { let endpoint = self.base_url.join(API_RFQ_REFRESH)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } pub async fn accept_quote(&self, payload: QuoteAcceptPayload) -> Result { let endpoint = self.base_url.join(API_RFQ_ACCEPT)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } pub async fn submit_quote(&self, payload: QuotePayload) -> Result { let endpoint = self.base_url.join(API_RFQ_QUOTE)?; let res = self.post(endpoint, payload).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } #[cfg(feature = "ws")] diff --git a/client/src/routes/trades.rs b/client/src/routes/trades.rs index 1bf4766..211991e 100644 --- a/client/src/routes/trades.rs +++ b/client/src/routes/trades.rs @@ -18,7 +18,7 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } /// Fetches historical trades for a given symbol, with optional limit and offset. @@ -40,6 +40,6 @@ impl BpxClient { } } let res = self.get(url).await?; - res.json().await.map_err(Into::into) + Self::json_with_context(res).await } } diff --git a/client/src/routes/user.rs b/client/src/routes/user.rs index 73eab3c..58e3776 100644 --- a/client/src/routes/user.rs +++ b/client/src/routes/user.rs @@ -16,8 +16,6 @@ impl BpxClient { ) -> Result { let endpoint = self.base_url.join(API_USER_2FA)?; let res = self.post(endpoint, payload).await?; - - let data: RequestTwoFactorResponse = res.json().await?; - Ok(data) + Self::json_with_context(res).await } } diff --git a/types/src/lib.rs b/types/src/lib.rs index 60388a7..6f7df7e 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -59,6 +59,7 @@ pub enum Blockchain { Optimism, Aptos, Sei, + Stable, Tron, #[strum(serialize = "0G")] #[serde(rename = "0G")]