#![allow(clippy::result_large_err)]
#![warn(missing_docs)]
#![allow(deprecated)]
use std::collections::HashMap;
use std::fmt;
use std::num::TryFromIntError;
use std::time::Duration;
#[cfg(feature = "async")]
pub use r#async::Sleeper;
pub mod api;
#[cfg(feature = "async")]
pub mod r#async;
#[cfg(feature = "blocking")]
pub mod blocking;
pub use api::*;
#[cfg(any(feature = "blocking", feature = "async"))]
use bitreq::Response;
#[cfg(feature = "blocking")]
pub use blocking::BlockingClient;
#[cfg(feature = "async")]
pub use r#async::AsyncClient;
pub const RETRYABLE_ERROR_CODES: [u16; 3] = [
429, 500, 503, ];
#[cfg(any(feature = "blocking", feature = "async"))]
const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256);
const DEFAULT_MAX_RETRIES: usize = 6;
#[cfg(feature = "async")]
const DEFAULT_MAX_CONNECTIONS: usize = 10;
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_informational(response: &Response) -> bool {
(100..200).contains(&response.status_code)
}
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_success(response: &Response) -> bool {
(200..300).contains(&response.status_code)
}
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_redirection(response: &Response) -> bool {
(300..400).contains(&response.status_code)
}
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_client_error(response: &Response) -> bool {
(400..500).contains(&response.status_code)
}
#[allow(unused)]
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_server_error(response: &Response) -> bool {
(500..600).contains(&response.status_code)
}
#[cfg(any(feature = "blocking", feature = "async"))]
fn is_retryable(response: &Response) -> bool {
RETRYABLE_ERROR_CODES.contains(&(response.status_code as u16))
}
#[cfg(any(feature = "blocking", feature = "async"))]
fn duration_to_timeout_secs(duration: Duration) -> u64 {
if duration.subsec_nanos() == 0 {
duration.as_secs()
} else {
duration.as_secs().saturating_add(1)
}
}
pub fn convert_fee_rate(target_blocks: usize, estimates: HashMap<u16, FeeRate>) -> Option<FeeRate> {
estimates
.into_iter()
.filter(|(k, _)| *k as usize <= target_blocks)
.max_by_key(|(k, _)| *k)
.map(|(_, feerate)| feerate)
}
pub fn sat_per_vbyte_to_feerate(estimates: HashMap<u16, f64>) -> HashMap<u16, FeeRate> {
estimates
.into_iter()
.map(|(k, v)| (k, FeeRate::from_sat_per_kwu((v * 250.0).round() as u64)))
.collect()
}
#[derive(Debug, Clone)]
pub struct Builder {
pub base_url: String,
pub proxy: Option<String>,
pub timeout: Option<Duration>,
pub headers: HashMap<String, String>,
pub max_retries: usize,
#[cfg(feature = "async")]
pub max_connections: usize,
}
impl Builder {
pub fn new(base_url: &str) -> Self {
Builder {
base_url: base_url.to_string(),
proxy: None,
timeout: None,
headers: HashMap::new(),
max_retries: DEFAULT_MAX_RETRIES,
#[cfg(feature = "async")]
max_connections: DEFAULT_MAX_CONNECTIONS,
}
}
pub fn proxy(mut self, proxy: &str) -> Self {
self.proxy = Some(proxy.to_string());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers.insert(key.to_string(), value.to_string());
self
}
pub fn max_retries(mut self, count: usize) -> Self {
self.max_retries = count;
self
}
#[cfg(feature = "async")]
pub fn max_connections(mut self, count: usize) -> Self {
self.max_connections = count;
self
}
#[cfg(feature = "blocking")]
pub fn build_blocking(self) -> BlockingClient {
BlockingClient::from_builder(self)
}
#[cfg(all(feature = "async", feature = "tokio"))]
pub fn build_async(self) -> Result<AsyncClient, Error> {
AsyncClient::from_builder(self)
}
#[cfg(feature = "async")]
pub fn build_async_with_sleeper<S: Sleeper>(self) -> Result<AsyncClient<S>, Error> {
AsyncClient::from_builder(self)
}
}
#[derive(Debug)]
pub enum Error {
#[cfg(any(feature = "blocking", feature = "async"))]
BitReq(bitreq::Error),
SerdeJson(serde_json::Error),
HttpResponse {
status: u16,
message: String,
},
Parsing(std::num::ParseIntError),
StatusCode(TryFromIntError),
BitcoinEncoding(bitcoin::consensus::encode::Error),
HexToArray(bitcoin::hex::HexToArrayError),
HexToBytes(bitcoin::hex::HexToBytesError),
TransactionNotFound(Txid),
HeaderHeightNotFound(u32),
HeaderHashNotFound(BlockHash),
InvalidHttpHeaderName(String),
InvalidHttpHeaderValue(String),
InvalidResponse,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(any(feature = "blocking", feature = "async"))]
Error::BitReq(e) => write!(f, "Bitreq HTTP error: {e}"),
Error::SerdeJson(e) => write!(f, "JSON (de)serialization error: {e}"),
Error::HttpResponse { status, message } => {
write!(f, "HTTP error {status}: {message}")
}
Error::Parsing(e) => write!(f, "Failed to parse invalid number: {e}"),
Error::StatusCode(e) => write!(f, "Invalid status code: {e}"),
Error::BitcoinEncoding(e) => write!(f, "Invalid Bitcoin data: {e}"),
Error::HexToArray(e) => write!(f, "Invalid hex to array conversion: {e}"),
Error::HexToBytes(e) => write!(f, "Invalid hex to bytes conversion: {e}"),
Error::TransactionNotFound(txid) => {
write!(f, "Transaction not found: {txid}")
}
Error::HeaderHeightNotFound(height) => {
write!(f, "Block header at height {height} not found")
}
Error::HeaderHashNotFound(hash) => {
write!(f, "Block header with hash {hash} not found")
}
Error::InvalidHttpHeaderName(name) => {
write!(f, "Invalid HTTP header name: {name}")
}
Error::InvalidHttpHeaderValue(value) => {
write!(f, "Invalid HTTP header value: {value}")
}
Error::InvalidResponse => write!(f, "The server sent an invalid response"),
}
}
}
impl std::error::Error for Error {}
macro_rules! impl_error {
( $from:ty, $to:ident ) => {
impl_error!($from, $to, Error);
};
( $from:ty, $to:ident, $impl_for:ty ) => {
impl std::convert::From<$from> for $impl_for {
fn from(err: $from) -> Self {
<$impl_for>::$to(err)
}
}
};
}
#[cfg(any(feature = "blocking", feature = "async"))]
impl_error!(::bitreq::Error, BitReq, Error);
impl_error!(serde_json::Error, SerdeJson, Error);
impl_error!(std::num::ParseIntError, Parsing, Error);
impl_error!(bitcoin::consensus::encode::Error, BitcoinEncoding, Error);
impl_error!(bitcoin::hex::HexToArrayError, HexToArray, Error);
impl_error!(bitcoin::hex::HexToBytesError, HexToBytes, Error);