diff --git a/src/api.rs b/src/api.rs index dcd8f1c..dec30f8 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`]. @@ -35,11 +35,11 @@ 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, + pub sequence: Sequence, /// Whether this is a coinbase input. pub is_coinbase: bool, } @@ -118,11 +118,11 @@ 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, + pub locktime: absolute::LockTime, /// The array of inputs in the [`Transaction`]. pub vin: Vec, /// The array of outputs in the [`Transaction`]. @@ -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`]. @@ -399,8 +400,8 @@ 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), - lock_time: bitcoin::absolute::LockTime::from_consensus(self.locktime), + version: self.version, + lock_time: self.locktime, input: self .vin .iter() @@ -411,8 +412,8 @@ impl EsploraTx { vout: vin.vout, }, script_sig: vin.scriptsig, - sequence: bitcoin::Sequence(vin.sequence), - witness: Witness::from_slice(&vin.witness), + sequence: vin.sequence, + witness: vin.witness, }) .collect(), output: self @@ -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. 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 }