From b5b583defbae07ffef9a658a831cde505a481bc3 Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:38:50 -0300 Subject: [PATCH 1/6] refactor(api)!: use `bitcoin::Witness` for `Vin::witness` --- src/api.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api.rs b/src/api.rs index dcd8f1c..348699b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -35,9 +35,9 @@ pub struct Vin { pub prevout: Option, /// The [`Script`] that unlocks this input. pub scriptsig: ScriptBuf, - /// The Witness that unlocks this input. - #[serde(deserialize_with = "deserialize_witness", default)] - pub witness: Vec>, + /// The [`Witness`] that unlocks this input. + #[serde(default)] + pub witness: Witness, /// The sequence value for this input. pub sequence: u32, /// Whether this is a coinbase input. @@ -412,7 +412,7 @@ impl EsploraTx { }, script_sig: vin.scriptsig, sequence: bitcoin::Sequence(vin.sequence), - witness: Witness::from_slice(&vin.witness), + witness: vin.witness, }) .collect(), output: self From de4d799856db20a6667732e82e47bf77c789b146 Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:41:42 -0300 Subject: [PATCH 2/6] refactor(api)!: use `bitcoin::Address` for `AddressStats::address` --- src/api.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/api.rs b/src/api.rs index 348699b..64111ce 100644 --- a/src/api.rs +++ b/src/api.rs @@ -232,8 +232,9 @@ pub struct BlockSummary { /// Statistics about an [`Address`]. #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct AddressStats { - /// The [`Address`], as a [`String`]. - pub address: String, + /// The [`Address`]. + #[serde(deserialize_with = "deserialize_address_assume_checked")] + pub address: Address, /// The summary of confirmed [`Transaction`]s for this [`Address`]. pub chain_stats: AddressTxsSummary, /// The summary of unconfirmed mempool [`Transaction`]s for this [`Address`]. @@ -473,20 +474,17 @@ impl From<&EsploraTx> for Transaction { } } -/// Deserializes a witness from a list of hex-encoded strings. +/// Deserializes an [`Address`] from an Esplora address string. /// -/// The Esplora API represents witness data as an array of hex strings, -/// e.g. `["deadbeef", "cafebabe"]`. This deserializer decodes each string -/// into raw bytes. -fn deserialize_witness<'de, D>(d: D) -> Result>, D::Error> +/// Esplora returns address strings without separately providing the expected +/// network, so this deserializer parses the address and assumes the embedded +/// network marker is correct. +fn deserialize_address_assume_checked<'de, D>(d: D) -> Result where D: serde::de::Deserializer<'de>, { - let list = Vec::::deserialize(d)?; - list.into_iter() - .map(|hex_str| Vec::::from_hex(&hex_str)) - .collect::>, _>>() - .map_err(serde::de::Error::custom) + let address = Address::::deserialize(d)?; + Ok(address.assume_checked()) } /// Deserializes an optional [`FeeRate`] from an `f64` BTC/kvB value. From c8b011dbb50cdcb9e08b89cbf80ac153633f5eef Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:46:54 -0300 Subject: [PATCH 3/6] refactor(client)!: use `Duration` for timeout --- src/async.rs | 14 +++++++------- src/blocking.rs | 15 ++++++++------- src/lib.rs | 23 +++++++++++++++++------ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/async.rs b/src/async.rs index ce3a8fc..d8f8ed7 100644 --- a/src/async.rs +++ b/src/async.rs @@ -46,9 +46,9 @@ use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, MerkleBlock, Script, T use bitreq::{Client, Method, Proxy, Request, RequestExt, Response}; use crate::{ - is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats, BlockInfo, BlockStatus, - Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof, OutputStatus, - ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS, + duration_to_timeout_secs, is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats, + BlockInfo, BlockStatus, Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof, + OutputStatus, ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS, }; #[allow(deprecated)] @@ -79,8 +79,8 @@ pub struct AsyncClient { /// /// NOTE: The proxy is ignored when targeting `wasm32`. proxy: Option, - /// Per-request socket timeout, in seconds. - timeout: Option, + /// Per-request socket timeout. + timeout: Option, /// HTTP headers to set on every request made to the Esplora server. headers: HashMap, /// Maximum number of retry attempts for retryable responses. @@ -142,8 +142,8 @@ impl AsyncClient { } #[cfg(not(target_arch = "wasm32"))] - if let Some(timeout) = &self.timeout { - request = request.with_timeout(*timeout); + if let Some(timeout) = self.timeout { + request = request.with_timeout(duration_to_timeout_secs(timeout)); } if !self.headers.is_empty() { diff --git a/src/blocking.rs b/src/blocking.rs index 63f19cb..8249245 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -31,6 +31,7 @@ use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::str::FromStr; use std::thread; +use std::time::Duration; use bitcoin::consensus::encode::serialize_hex; use bitreq::{Method, Proxy, Request, Response}; @@ -42,9 +43,9 @@ use bitcoin::hex::{DisplayHex, FromHex}; use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, MerkleBlock, Script, Transaction, Txid}; use crate::{ - is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats, BlockInfo, BlockStatus, - Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof, OutputStatus, - ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS, + duration_to_timeout_secs, is_retryable, is_success, sat_per_vbyte_to_feerate, AddressStats, + BlockInfo, BlockStatus, Builder, Error, EsploraTx, MempoolRecentTx, MempoolStats, MerkleProof, + OutputStatus, ScriptHashStats, SubmitPackageResult, TxStatus, Utxo, BASE_BACKOFF_MILLIS, }; #[allow(deprecated)] @@ -74,8 +75,8 @@ pub struct BlockingClient { /// /// NOTE: The proxy is ignored when targeting `wasm32`. pub proxy: Option, - /// Per-request socket timeout, in seconds. - pub timeout: Option, + /// Per-request socket timeout. + pub timeout: Option, /// HTTP headers to set on every request made to the Esplora server. pub headers: HashMap, /// Maximum number of retry attempts for retryable responses. @@ -115,8 +116,8 @@ impl BlockingClient { request = request.with_proxy(Proxy::new_http(proxy)?); } - if let Some(timeout) = &self.timeout { - request = request.with_timeout(*timeout); + if let Some(timeout) = self.timeout { + request = request.with_timeout(duration_to_timeout_secs(timeout)); } if !self.headers.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index 5926a35..f1f8c00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,6 @@ use std::collections::HashMap; use std::fmt; use std::num::TryFromIntError; -#[cfg(any(feature = "blocking", feature = "async"))] use std::time::Duration; #[cfg(feature = "async")] @@ -171,6 +170,16 @@ fn is_retryable(response: &Response) -> bool { RETRYABLE_ERROR_CODES.contains(&(response.status_code as u16)) } +/// Convert a [`Duration`] to whole timeout seconds for `bitreq`. +#[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) + } +} + /// Return the [`FeeRate`] for the given confirmation target in blocks. /// /// Selects the highest confirmation target from `estimates` that is at or @@ -201,10 +210,12 @@ pub fn sat_per_vbyte_to_feerate(estimates: HashMap) -> HashMap, - /// Per-request socket timeout, in seconds. - pub timeout: Option, + /// Per-request socket timeout. + pub timeout: Option, /// HTTP headers to set on every request made to the Esplora server. pub headers: HashMap, /// Maximum number of retry attempts for retryable HTTP responses. @@ -264,8 +275,8 @@ impl Builder { self } - /// Set the per-request socket timeout, in seconds. - pub fn timeout(mut self, timeout: u64) -> Self { + /// Set the per-request socket timeout. + pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } From 632a88b9064af57e7f06292b437de854464be74a Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:55:28 -0300 Subject: [PATCH 4/6] refactor(api)!: use `bitcoin::Sequence` for `Vin::sequence` --- src/api.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api.rs b/src/api.rs index 64111ce..4da068a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -9,17 +9,17 @@ //! //! [Esplora API]: -use bitcoin::hash_types; use serde::Deserialize; use std::collections::HashMap; pub use bitcoin::consensus::{deserialize, serialize}; +use bitcoin::hash_types; use bitcoin::hash_types::TxMerkleNode; pub use bitcoin::hex::FromHex; pub use bitcoin::{ absolute, block, transaction, Address, Amount, Block, BlockHash, CompactTarget, FeeRate, - OutPoint, Script, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, Weight, Witness, - Wtxid, + OutPoint, Script, ScriptBuf, ScriptHash, Sequence, Transaction, TxIn, TxOut, Txid, Weight, + Witness, Wtxid, }; /// An input to a [`Transaction`]. @@ -39,7 +39,7 @@ pub struct Vin { #[serde(default)] pub witness: Witness, /// The sequence value for this input. - pub sequence: u32, + pub sequence: Sequence, /// Whether this is a coinbase input. pub is_coinbase: bool, } @@ -412,7 +412,7 @@ impl EsploraTx { vout: vin.vout, }, script_sig: vin.scriptsig, - sequence: bitcoin::Sequence(vin.sequence), + sequence: vin.sequence, witness: vin.witness, }) .collect(), From f4331b98936ab663db61e9208b0feea0ba4bb479 Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:56:56 -0300 Subject: [PATCH 5/6] refactor(api)!: use `bitcoin::transaction::Version` for `EsploraTx::version` --- src/api.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api.rs b/src/api.rs index 4da068a..fc8beb5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -118,8 +118,8 @@ pub struct BlockStatus { pub struct EsploraTx { /// The [`Txid`] of the [`Transaction`]. pub txid: Txid, - /// The version number of the [`Transaction`]. - pub version: i32, + /// The version of the [`Transaction`]. + pub version: transaction::Version, /// The locktime of the [`Transaction`]. /// Sets a time or height after which the [`Transaction`] can be mined. pub locktime: u32, @@ -400,7 +400,7 @@ impl EsploraTx { /// and reconstructs the [`Transaction`] from its inputs and outputs. pub fn to_tx(&self) -> Transaction { Transaction { - version: transaction::Version::non_standard(self.version), + version: self.version, lock_time: bitcoin::absolute::LockTime::from_consensus(self.locktime), input: self .vin From 515fd53ae5fc22a101124f8bbaa8d3a196f0a7a4 Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Fri, 19 Jun 2026 21:58:04 -0300 Subject: [PATCH 6/6] refactor(api)!: use `bitcoin::LockTime` for `EsploraTx::locktime` --- src/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index fc8beb5..dec30f8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -122,7 +122,7 @@ pub struct EsploraTx { pub version: transaction::Version, /// The locktime of the [`Transaction`]. /// Sets a time or height after which the [`Transaction`] can be mined. - pub locktime: u32, + pub locktime: absolute::LockTime, /// The array of inputs in the [`Transaction`]. pub vin: Vec, /// The array of outputs in the [`Transaction`]. @@ -401,7 +401,7 @@ impl EsploraTx { pub fn to_tx(&self) -> Transaction { Transaction { version: self.version, - lock_time: bitcoin::absolute::LockTime::from_consensus(self.locktime), + lock_time: self.locktime, input: self .vin .iter()