diff --git a/.clippy.toml b/.clippy.toml index 69478ce..e0ec8dd 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1 +1 @@ -msrv="1.63.0" +msrv="1.71.0" diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index d10df2d..ede35b6 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -17,7 +17,7 @@ jobs: matrix: rust: - version: stable # STABLE - - version: 1.63.0 # MSRV + - version: 1.71.0 # MSRV features: - default - blocking @@ -52,16 +52,13 @@ jobs: - name: Update toolchain run: rustup update - name: Pin dependencies for MSRV - if: matrix.rust.version == '1.63.0' + if: matrix.rust.version == '1.71.0' run: | - cargo update -p reqwest --precise "0.12.4" cargo update -p minreq --precise "2.13.2" cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5" cargo update -p time --precise "0.3.20" cargo update -p home --precise "0.5.5" - cargo update -p url --precise "2.5.0" - cargo update -p tokio --precise "1.38.1" - cargo update -p security-framework-sys --precise "2.11.1" + cargo update -p security-framework-sys --precise "2.14.0" cargo update -p native-tls --precise "0.2.13" cargo update -p ring --precise "0.17.12" cargo update -p flate2 --precise "1.0.35" diff --git a/Cargo.toml b/Cargo.toml index f1e85b9..1b6a47e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ documentation = "https://docs.rs/esplora-client/" description = "Bitcoin Esplora API client library. Supports plaintext, TLS and Onion servers. Blocking or async" keywords = ["bitcoin", "esplora"] readme = "README.md" -rust-version = "1.63.0" +rust-version = "1.71.0" [lib] name = "esplora_client" @@ -22,7 +22,7 @@ bitcoin = { version = "0.32", features = ["serde", "std"], default-features = fa hex = { version = "0.2", package = "hex-conservative" } log = "^0.4" minreq = { version = "2.11.0", features = ["json-using-serde"], optional = true } -reqwest = { version = "0.12", features = ["json"], default-features = false, optional = true } +async_minreq = { git = "https://github.com/BEULAHEVANJALIN/async-minreq", default-features = false, features = ["json-using-serde"], optional = true } # default async runtime tokio = { version = "1", features = ["time"], optional = true } @@ -42,8 +42,8 @@ blocking-https-native = ["blocking", "minreq/https-native"] blocking-https-bundled = ["blocking", "minreq/https-bundled"] tokio = ["dep:tokio"] -async = ["reqwest", "reqwest/socks", "tokio?/time"] -async-https = ["async", "reqwest/default-tls"] -async-https-native = ["async", "reqwest/native-tls"] -async-https-rustls = ["async", "reqwest/rustls-tls"] -async-https-rustls-manual-roots = ["async", "reqwest/rustls-tls-manual-roots"] +async = ["async_minreq", "async_minreq/proxy", "tokio?/time"] +async-https = ["async", "async_minreq/https"] +async-https-native = ["async", "async_minreq/https-native"] +async-https-rustls = ["async", "async_minreq/https-rustls"] +async-https-rustls-manual-roots = ["async"] diff --git a/src/async.rs b/src/async.rs index a5175b9..662ed04 100644 --- a/src/async.rs +++ b/src/async.rs @@ -9,11 +9,7 @@ // You may not use this file except in accordance with one or both of these // licenses. -//! Esplora by way of `reqwest` HTTP client. - -use std::collections::HashMap; -use std::marker::PhantomData; -use std::str::FromStr; +//! Esplora by way of `asyn_minreq` HTTP client. use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable}; use bitcoin::hashes::{sha256, Hash}; @@ -22,75 +18,50 @@ use bitcoin::Address; use bitcoin::{ block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid, }; - -#[allow(unused_imports)] -use log::{debug, error, info, trace}; - -use reqwest::{header, Client, Response}; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::str::FromStr; use crate::api::AddressStats; use crate::{ BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus, - BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, + BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, VALID_HTTP_CODE, }; +use async_minreq::{Method, Request, Response}; +#[allow(unused_imports)] +use log::{debug, error, info, trace}; #[derive(Debug, Clone)] pub struct AsyncClient { /// The URL of the Esplora Server. url: String, - /// The inner [`reqwest::Client`] to make HTTP requests. - client: Client, - /// Number of times to retry a request + /// Number of times to retry a request. max_retries: usize, - - /// Marker for the type of sleeper used + /// Default headers (applied to every request). + headers: HashMap, + /// Marker for the sleeper. marker: PhantomData, } impl AsyncClient { /// Build an async client from a builder pub fn from_builder(builder: Builder) -> Result { - let mut client_builder = Client::builder(); - - #[cfg(not(target_arch = "wasm32"))] - if let Some(proxy) = &builder.proxy { - client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?); - } - - #[cfg(not(target_arch = "wasm32"))] - if let Some(timeout) = builder.timeout { - client_builder = client_builder.timeout(core::time::Duration::from_secs(timeout)); - } - - if !builder.headers.is_empty() { - let mut headers = header::HeaderMap::new(); - for (k, v) in builder.headers { - let header_name = header::HeaderName::from_lowercase(k.to_lowercase().as_bytes()) - .map_err(|_| Error::InvalidHttpHeaderName(k))?; - let header_value = header::HeaderValue::from_str(&v) - .map_err(|_| Error::InvalidHttpHeaderValue(v))?; - headers.insert(header_name, header_value); - } - client_builder = client_builder.default_headers(headers); - } - Ok(AsyncClient { url: builder.base_url, - client: client_builder.build()?, max_retries: builder.max_retries, + headers: builder.headers, marker: PhantomData, }) } - pub fn from_client(url: String, client: Client) -> Self { + pub fn from_client(url: String, headers: HashMap) -> Self { AsyncClient { url, - client, + headers, max_retries: crate::DEFAULT_MAX_RETRIES, marker: PhantomData, } } - /// Make an HTTP GET request to given URL, deserializing to any `T` that /// implement [`bitcoin::consensus::Decodable`]. /// @@ -106,14 +77,17 @@ impl AsyncClient { let url = format!("{}{}", self.url, path); let response = self.get_with_retry(&url).await?; - if !response.status().is_success() { + if response.status_code > VALID_HTTP_CODE { return Err(Error::HttpResponse { - status: response.status().as_u16(), - message: response.text().await?, + status: response.status_code as u16, + message: match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }, }); } - Ok(deserialize::(&response.bytes().await?)?) + Ok(deserialize::(response.as_bytes())?) } /// Make an HTTP GET request to given URL, deserializing to `Option`. @@ -146,14 +120,16 @@ impl AsyncClient { let url = format!("{}{}", self.url, path); let response = self.get_with_retry(&url).await?; - if !response.status().is_success() { + if response.status_code > VALID_HTTP_CODE { return Err(Error::HttpResponse { - status: response.status().as_u16(), - message: response.text().await?, + status: response.status_code as u16, + message: match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }, }); } - - response.json::().await.map_err(Error::Reqwest) + response.json().map_err(Error::AsyncMinreq) } /// Make an HTTP GET request to given URL, deserializing to `Option`. @@ -188,14 +164,19 @@ impl AsyncClient { let url = format!("{}{}", self.url, path); let response = self.get_with_retry(&url).await?; - if !response.status().is_success() { + if response.status_code > VALID_HTTP_CODE { return Err(Error::HttpResponse { - status: response.status().as_u16(), - message: response.text().await?, + status: response.status_code as u16, + message: match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }, }); } - - let hex_str = response.text().await?; + let hex_str = match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }; Ok(deserialize(&Vec::from_hex(&hex_str)?)?) } @@ -225,14 +206,19 @@ impl AsyncClient { let url = format!("{}{}", self.url, path); let response = self.get_with_retry(&url).await?; - if !response.status().is_success() { + if response.status_code > VALID_HTTP_CODE { return Err(Error::HttpResponse { - status: response.status().as_u16(), - message: response.text().await?, + status: response.status_code as u16, + message: match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }, }); } - - Ok(response.text().await?) + Ok(match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }) } /// Make an HTTP GET request to given URL, deserializing to `Option`. @@ -263,15 +249,21 @@ impl AsyncClient { let url = format!("{}{}", self.url, path); let body = serialize::(&body).to_lower_hex_string(); - let response = self.client.post(url).body(body).send().await?; + let mut request = Request::new(Method::Post, &url).with_body(body); + for (key, value) in &self.headers { + request = request.with_header(key, value); + } - if !response.status().is_success() { + let response = request.send().await.map_err(Error::AsyncMinreq)?; + if response.status_code > VALID_HTTP_CODE { return Err(Error::HttpResponse { - status: response.status().as_u16(), - message: response.text().await?, + status: response.status_code as u16, + message: match response.as_str() { + Ok(resp) => resp.to_string(), + Err(_) => return Err(Error::InvalidResponse), + }, }); } - Ok(()) } @@ -454,11 +446,6 @@ impl AsyncClient { &self.url } - /// Get the underlying [`Client`]. - pub fn client(&self) -> &Client { - &self.client - } - /// Sends a GET request to the given `url`, retrying failed attempts /// for retryable error codes until max retries hit. async fn get_with_retry(&self, url: &str) -> Result { @@ -466,8 +453,13 @@ impl AsyncClient { let mut attempts = 0; loop { - match self.client.get(url).send().await? { - resp if attempts < self.max_retries && is_status_retryable(resp.status()) => { + let mut request = Request::new(Method::Get, url); + for (key, value) in &self.headers { + request = request.with_header(key, value); + } + + match request.send().await? { + resp if attempts < self.max_retries && is_status_retryable(resp.status_code) => { S::sleep(delay).await; attempts += 1; delay *= 2; @@ -478,8 +470,8 @@ impl AsyncClient { } } -fn is_status_retryable(status: reqwest::StatusCode) -> bool { - RETRYABLE_ERROR_CODES.contains(&status.as_u16()) +fn is_status_retryable(status: i32) -> bool { + RETRYABLE_ERROR_CODES.contains(&(status as u16)) } pub trait Sleeper: 'static { diff --git a/src/lib.rs b/src/lib.rs index ce00211..66aa522 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! async Esplora client to query Esplora's backend. //! //! The library provides the possibility to build a blocking -//! client using [`minreq`] and an async client using [`reqwest`]. +//! client using [`minreq`] and an async client using [`async_minreq`]. //! The library supports communicating to Esplora via a proxy //! and also using TLS (SSL) for secure communication. //! @@ -53,14 +53,14 @@ //! capabilities using the platform's native TLS backend (likely OpenSSL). //! * `blocking-https-bundled` enables [`minreq`], the blocking client with proxy and TLS (SSL) //! capabilities using a bundled OpenSSL library backend. -//! * `async` enables [`reqwest`], the async client with proxy capabilities. -//! * `async-https` enables [`reqwest`], the async client with support for proxying and TLS (SSL) -//! using the default [`reqwest`] TLS backend. -//! * `async-https-native` enables [`reqwest`], the async client with support for proxying and TLS -//! (SSL) using the platform's native TLS backend (likely OpenSSL). -//! * `async-https-rustls` enables [`reqwest`], the async client with support for proxying and TLS -//! (SSL) using the `rustls` TLS backend. -//! * `async-https-rustls-manual-roots` enables [`reqwest`], the async client with support for +//! * `async` enables [`async_minreq`], the async client with proxy capabilities. +//! * `async-https` enables [`async_minreq`], the async client with support for proxying and TLS +//! (SSL) using the default [`async_minreq`] TLS backend. +//! * `async-https-native` enables [`async_minreq`], the async client with support for proxying and +//! TLS (SSL) using the platform's native TLS backend (likely OpenSSL). +//! * `async-https-rustls` enables [`async_minreq`], the async client with support for proxying and +//! TLS (SSL) using the `rustls` TLS backend. +//! * `async-https-rustls-manual-roots` enables [`async_minreq`], the async client with support for //! proxying and TLS (SSL) using the `rustls` TLS backend without using its the default root //! certificates. //! @@ -102,6 +102,9 @@ const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256); /// Default max retries. const DEFAULT_MAX_RETRIES: usize = 6; +/// Valid HTTP code +const VALID_HTTP_CODE: i32 = 299; + /// Get a fee value in sats/vbytes from the estimates /// that matches the confirmation target set as parameter. /// @@ -203,9 +206,9 @@ pub enum Error { /// Error during `minreq` HTTP request #[cfg(feature = "blocking")] Minreq(::minreq::Error), - /// Error during reqwest HTTP request + /// Error during async_minreq HTTP request #[cfg(feature = "async")] - Reqwest(::reqwest::Error), + AsyncMinreq(async_minreq::Error), /// HTTP response error HttpResponse { status: u16, message: String }, /// Invalid number returned @@ -250,12 +253,11 @@ macro_rules! impl_error { } }; } - impl std::error::Error for Error {} #[cfg(feature = "blocking")] impl_error!(::minreq::Error, Minreq, Error); #[cfg(feature = "async")] -impl_error!(::reqwest::Error, Reqwest, Error); +impl_error!(::async_minreq::Error, AsyncMinreq, Error); impl_error!(std::num::ParseIntError, Parsing, Error); impl_error!(bitcoin::consensus::encode::Error, BitcoinEncoding, Error); impl_error!(bitcoin::hex::HexToArrayError, HexToArray, Error);