diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 76aef1f6f..cbf65d779 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1763,9 +1763,16 @@ impl BreezServices { &self, channel: &crate::models::Channel, ) -> Result<(Option, Option)> { - let maybe_outspend = self + let maybe_outspend_res = self .lookup_chain_service_closing_outspend(channel.clone()) - .await?; + .await; + let maybe_outspend: Option = match maybe_outspend_res { + Ok(s) => s, + Err(e) => { + error!("Failed to lookup channel closing data: {:?}", e); + None + } + }; let maybe_closed_at = maybe_outspend .clone() diff --git a/libs/sdk-core/src/chain.rs b/libs/sdk-core/src/chain.rs index c7cbbce70..4d7ee7053 100644 --- a/libs/sdk-core/src/chain.rs +++ b/libs/sdk-core/src/chain.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::bitcoin::hashes::hex::FromHex; use crate::bitcoin::{OutPoint, Txid}; use crate::error::{SdkError, SdkResult}; -use crate::input_parser::{get_parse_and_log_response, get_reqwest_client, post_and_log_response}; +use crate::input_parser::{get_parse_and_log_response, post_and_log_response}; pub const DEFAULT_MEMPOOL_SPACE_URL: &str = "https://mempool.space/api"; @@ -337,27 +337,20 @@ impl MempoolSpace { #[tonic::async_trait] impl ChainService for MempoolSpace { async fn recommended_fees(&self) -> SdkResult { - get_parse_and_log_response(&format!("{}/v1/fees/recommended", self.base_url)).await + get_parse_and_log_response(&format!("{}/v1/fees/recommended", self.base_url), true).await } async fn address_transactions(&self, address: String) -> SdkResult> { - get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url)).await + get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url), true).await } async fn current_tip(&self) -> SdkResult { - get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url)).await + get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url), true).await } async fn transaction_outspends(&self, txid: String) -> SdkResult> { let url = format!("{}/tx/{txid}/outspends", self.base_url); - Ok(get_reqwest_client()? - .get(url) - .send() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })? - .json() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?) + get_parse_and_log_response(&url, true).await } async fn broadcast_transaction(&self, tx: Vec) -> SdkResult { @@ -373,8 +366,9 @@ impl ChainService for MempoolSpace { } #[cfg(test)] mod tests { - use crate::chain::{ - MempoolSpace, OnchainTx, RedundantChainService, RedundantChainServiceTrait, + use crate::{ + chain::{MempoolSpace, OnchainTx, RedundantChainService, RedundantChainServiceTrait}, + error::SdkError, }; use anyhow::Result; use tokio::test; @@ -437,6 +431,28 @@ mod tests { assert_eq!(expected_serialized, serialized_res); + let outspends = ms + .transaction_outspends( + "5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985".to_string(), + ) + .await?; + assert_eq!(outspends.len(), 2); + + let outspends = ms + .transaction_outspends( + "07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901".to_string(), + ) + .await; + match outspends { + Ok(_) => panic!("Expected an error"), + Err(e) => match e { + SdkError::ServiceConnectivity { err } => { + assert_eq!(err, "GET request https://mempool.space/api/tx/07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901/outspends failed with status: 404 Not Found") + } + _ => panic!("Expected a service connectivity error"), + }, + }; + Ok(()) } diff --git a/libs/sdk-core/src/input_parser.rs b/libs/sdk-core/src/input_parser.rs index 8036ea1c5..000963306 100644 --- a/libs/sdk-core/src/input_parser.rs +++ b/libs/sdk-core/src/input_parser.rs @@ -3,6 +3,7 @@ use std::time::Duration; use anyhow::{anyhow, Result}; use bip21::Uri; +use reqwest::StatusCode; use serde::Deserialize; use serde::Serialize; @@ -249,30 +250,49 @@ pub(crate) async fn post_and_log_response(url: &str, body: Option) -> Sd /// Makes a GET request to the specified `url` and logs on DEBUG: /// - the URL /// - the raw response body -pub(crate) async fn get_and_log_response(url: &str) -> SdkResult { +/// - the response HTTP status code +pub(crate) async fn get_and_log_response(url: &str) -> SdkResult<(String, StatusCode)> { debug!("Making GET request to: {url}"); - let raw_body = get_reqwest_client()? + let response = get_reqwest_client()? .get(url) .send() .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })? + .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?; + let status = response.status(); + let raw_body = response .text() .await .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?; - debug!("Received raw response body: {raw_body}"); + debug!("Received response, status: {status}, raw response body: {raw_body}"); - Ok(raw_body) + Ok((raw_body, status)) } -/// Wrapper around [get_and_log_response] that, in addition, parses the payload into an expected type -pub(crate) async fn get_parse_and_log_response(url: &str) -> SdkResult +/// Wrapper around [get_and_log_response] that, in addition, parses the payload into an expected type. +/// +/// ### Arguments +/// +/// - `url`: the URL on which GET will be called +/// - `enforce_status_check`: if true, the HTTP status code is checked in addition to trying to +/// parse the payload. In this case, an HTTP error code will automatically cause this function to +/// return `Err`, regardless of the payload. If false, the result type will be determined only +/// by the result of parsing the payload into the desired target type. +pub(crate) async fn get_parse_and_log_response( + url: &str, + enforce_status_check: bool, +) -> SdkResult where for<'a> T: serde::de::Deserialize<'a>, { - let raw_body = get_and_log_response(url).await?; + let (raw_body, status) = get_and_log_response(url).await?; + if enforce_status_check && !status.is_success() { + let err = format!("GET request {url} failed with status: {status}"); + error!("{err}"); + return Err(SdkError::ServiceConnectivity { err }); + } - Ok(serde_json::from_str(&raw_body)?) + serde_json::from_str::(&raw_body).map_err(Into::into) } /// Prepends the given prefix to the input, if the input doesn't already start with it @@ -416,7 +436,7 @@ async fn resolve_lnurl( } lnurl_endpoint = maybe_replace_host_with_mockito_test_host(lnurl_endpoint)?; - let lnurl_data: LnUrlRequestData = get_parse_and_log_response(&lnurl_endpoint) + let lnurl_data: LnUrlRequestData = get_parse_and_log_response(&lnurl_endpoint, false) .await .map_err(|_| anyhow!("Failed to parse response"))?; let temp = lnurl_data.into(); @@ -1055,15 +1075,20 @@ pub(crate) mod tests { } "#.replace('\n', ""); - let response_body = match return_lnurl_error { - None => expected_lnurl_withdraw_data, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + let (response_body, status) = match &return_lnurl_error { + None => (expected_lnurl_withdraw_data, 200), + Some(err_reason) => ( + ["{\"status\": \"ERROR\", \"reason\": \"", err_reason, "\"}"].join(""), + 400, + ), }; let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - server.mock("GET", path).with_body(response_body).create() + server + .mock("GET", path) + .with_body(response_body) + .with_status(status) + .create() } #[tokio::test] diff --git a/libs/sdk-core/src/lnurl/auth.rs b/libs/sdk-core/src/lnurl/auth.rs index 0f64037fa..32e352a86 100644 --- a/libs/sdk-core/src/lnurl/auth.rs +++ b/libs/sdk-core/src/lnurl/auth.rs @@ -39,7 +39,7 @@ pub(crate) async fn perform_lnurl_auth( .query_pairs_mut() .append_pair("key", &linking_keys.public_key().to_hex()); - get_parse_and_log_response(callback_url.as_ref()) + get_parse_and_log_response(callback_url.as_ref(), false) .await .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string())) } diff --git a/libs/sdk-core/src/lnurl/pay.rs b/libs/sdk-core/src/lnurl/pay.rs index ef28659b5..e43a316f3 100644 --- a/libs/sdk-core/src/lnurl/pay.rs +++ b/libs/sdk-core/src/lnurl/pay.rs @@ -29,7 +29,7 @@ pub(crate) async fn validate_lnurl_pay( )?; let callback_url = build_pay_callback_url(user_amount_msat, comment, req_data)?; - let callback_resp_text = get_and_log_response(&callback_url) + let (callback_resp_text, _) = get_and_log_response(&callback_url) .await .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string()))?; diff --git a/libs/sdk-core/src/lnurl/withdraw.rs b/libs/sdk-core/src/lnurl/withdraw.rs index 7de5d51d8..3353f707f 100644 --- a/libs/sdk-core/src/lnurl/withdraw.rs +++ b/libs/sdk-core/src/lnurl/withdraw.rs @@ -38,7 +38,7 @@ pub(crate) async fn validate_lnurl_withdraw( // Send invoice to the LNURL-w endpoint via the callback let callback_url = build_withdraw_callback_url(&req_data, &invoice)?; - let callback_res: LnUrlCallbackStatus = get_parse_and_log_response(&callback_url) + let callback_res: LnUrlCallbackStatus = get_parse_and_log_response(&callback_url, false) .await .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string()))?; let withdraw_status = match callback_res { diff --git a/libs/sdk-core/src/swap_out/boltzswap.rs b/libs/sdk-core/src/swap_out/boltzswap.rs index 8685b0ec1..3afcd7854 100644 --- a/libs/sdk-core/src/swap_out/boltzswap.rs +++ b/libs/sdk-core/src/swap_out/boltzswap.rs @@ -340,7 +340,7 @@ impl ReverseSwapServiceAPI for BoltzApi { } pub async fn reverse_swap_pair_info() -> ReverseSwapResult { - let pairs: Pairs = get_parse_and_log_response(GET_PAIRS_ENDPOINT).await?; + let pairs: Pairs = get_parse_and_log_response(GET_PAIRS_ENDPOINT, true).await?; match pairs.pairs.get("BTC/BTC") { None => Err(ReverseSwapError::generic("BTC pair not found")), Some(btc_pair) => {