- name: Update toolchain
run: rustup update
- name: Test
- run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
+ run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
- id: coverage
name: Generate coverage
- default
- minimal
- all-keys
- - minimal,esplora
+ - minimal,esplora,ureq
- key-value-db
- electrum
- compact_filters
- - esplora,key-value-db,electrum
+ - esplora,ureq,key-value-db,electrum
- compiler
- rpc
- verify
+ - async-interface
+ - async-interface,esplora,reqwest
steps:
- name: checkout
uses: actions/checkout@v2
- name: Update toolchain
run: rustup update
- name: Check
- run: cargo check --target wasm32-unknown-unknown --features esplora --no-default-features
+ run: cargo check --target wasm32-unknown-unknown --features esplora,reqwest --no-default-features
+
fmt:
name: Rust fmt
- name: Update toolchain
run: rustup update
- name: Build docs
- run: cargo rustdoc --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
+ run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
## [Unreleased]
- Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`)
+- Removed hard dependency on `tokio`.
### Wallet
### Blockchain
-- Removed `stop_gap` from `Blockchain` trait and added it to only `ElectrumBlockchain` and `EsploraBlockchain` structs
+- Removed `stop_gap` from `Blockchain` trait and added it to only `ElectrumBlockchain` and `EsploraBlockchain` structs.
+- Added a `ureq` backend for use when not using feature `async-interface` or target WASM. `ureq` is a blocking HTTP client.
## [v0.9.0] - [v0.8.0]
license = "MIT OR Apache-2.0"
[dependencies]
-bdk-macros = "^0.4"
+bdk-macros = { path = "macros"} # TODO: Change this to version number after next release.
log = "^0.4"
miniscript = "5.1"
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.7", optional = true }
reqwest = { version = "0.11", optional = true, features = ["json"] }
+ureq = { version = "2.1", default-features = false, features = ["json"], optional = true }
futures = { version = "0.3", optional = true }
async-trait = { version = "0.1", optional = true }
rocksdb = { version = "0.14", optional = true }
# Needed by bdk_blockchain_tests macro
bitcoincore-rpc = { version = "0.13", optional = true }
-# Platform-specific dependencies
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tokio = { version = "1", features = ["rt"] }
-
[target.'cfg(target_arch = "wasm32")'.dependencies]
async-trait = "0.1"
js-sys = "0.3"
compiler = ["miniscript/compiler"]
verify = ["bitcoinconsensus"]
default = ["key-value-db", "electrum"]
-electrum = ["electrum-client"]
-esplora = ["reqwest", "futures"]
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
key-value-db = ["sled"]
-async-interface = ["async-trait"]
all-keys = ["keys-bip39"]
keys-bip39 = ["tiny-bip39", "zeroize"]
rpc = ["bitcoincore-rpc"]
+# We currently provide mulitple implementations of `Blockchain`, all are
+# blocking except for the `EsploraBlockchain` which can be either async or
+# blocking, depending on the HTTP client in use.
+#
+# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
+# access to the asynchronous method implementations. Then, if Esplora is wanted,
+# enable `esplora` AND `reqwest`.
+# - Users wanting blocking HTTP calls can use any of the other blockchain
+# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
+# use Esplora should enable `esplora` AND `ureq`.
+async-interface = ["async-trait"]
+electrum = ["electrum-client"]
+esplora = ["futures"] # Requires one of: `ureq` or `reqwest`.
+
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
-test-esplora = ["esplora", "electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
+test-esplora = ["esplora", "ureq", "electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
test-md-docs = ["electrum"]
[dev-dependencies]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]
-features = ["compiler", "electrum", "esplora", "compact_filters", "rpc", "key-value-db", "all-keys", "verify"]
+features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "all-keys", "verify"]
# defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]
quoted.into()
}
-
-/// Awaits if target_arch is "wasm32", uses `tokio::Runtime::block_on()` otherwise
-///
-/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build on non-wasm32 platforms.
-#[proc_macro]
-pub fn await_or_block(expr: TokenStream) -> TokenStream {
- let expr: proc_macro2::TokenStream = expr.into();
- let quoted = quote! {
- {
- #[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
- {
- tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(#expr)
- }
-
- #[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
- {
- #expr.await
- }
- }
- };
-
- quoted.into()
-}
//! )?;
//! # }
//!
-//! # #[cfg(feature = "esplora")]
+//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
//! # {
-//! let esplora_blockchain = EsploraBlockchain::new("...", None, 20);
+//! let esplora_blockchain = EsploraBlockchain::new("...", 20);
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
//! "...",
//! None,
//! # use bdk::blockchain::*;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
+//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
+//! # {
//! let config = serde_json::from_str("...")?;
//! let blockchain = AnyBlockchain::from_config(&config)?;
//! let wallet = Wallet::new(
//! MemoryDatabase::default(),
//! blockchain,
//! )?;
+//! # }
//! # Ok::<(), bdk::Error>(())
//! ```
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Esplora
-//!
-//! This module defines a [`Blockchain`] struct that can query an Esplora backend
-//! populate the wallet's [database](crate::database::Database) by
-//!
-//! ## Example
-//!
-//! ```no_run
-//! # use bdk::blockchain::esplora::EsploraBlockchain;
-//! let blockchain = EsploraBlockchain::new("https://blockstream.info/testnet/api", None, 20);
-//! # Ok::<(), bdk::Error>(())
-//! ```
-
-use std::collections::{HashMap, HashSet};
-use std::fmt;
-
-use bitcoin::consensus::{self, deserialize, serialize};
-use bitcoin::hashes::hex::{FromHex, ToHex};
-use bitcoin::hashes::{sha256, Hash};
-use bitcoin::{BlockHash, BlockHeader, Script, Transaction, Txid};
-use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
-#[allow(unused_imports)]
-use log::{debug, error, info, trace};
-use reqwest::{Client, StatusCode};
-use serde::Deserialize;
-
-use crate::database::BatchDatabase;
-use crate::error::Error;
-use crate::wallet::utils::ChunksIterator;
-use crate::FeeRate;
-
-use super::*;
-
-use self::utils::{ElectrumLikeSync, ElsGetHistoryRes};
-
-const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
-
-#[derive(Debug)]
-struct UrlClient {
- url: String,
- // We use the async client instead of the blocking one because it automatically uses `fetch`
- // when the target platform is wasm32.
- client: Client,
- concurrency: u8,
-}
-
-/// Structure that implements the logic to sync with Esplora
-///
-/// ## Example
-/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
-#[derive(Debug)]
-pub struct EsploraBlockchain {
- url_client: UrlClient,
- stop_gap: usize,
-}
-
-impl std::convert::From<UrlClient> for EsploraBlockchain {
- fn from(url_client: UrlClient) -> Self {
- EsploraBlockchain {
- url_client,
- stop_gap: 20,
- }
- }
-}
-
-impl EsploraBlockchain {
- /// Create a new instance of the client from a base URL
- pub fn new(base_url: &str, concurrency: Option<u8>, stop_gap: usize) -> Self {
- EsploraBlockchain {
- url_client: UrlClient {
- url: base_url.to_string(),
- client: Client::new(),
- concurrency: concurrency.unwrap_or(DEFAULT_CONCURRENT_REQUESTS),
- },
- stop_gap,
- }
- }
-}
-
-#[maybe_async]
-impl Blockchain for EsploraBlockchain {
- fn get_capabilities(&self) -> HashSet<Capability> {
- vec![
- Capability::FullHistory,
- Capability::GetAnyTx,
- Capability::AccurateFees,
- ]
- .into_iter()
- .collect()
- }
-
- fn setup<D: BatchDatabase, P: Progress>(
- &self,
- database: &mut D,
- progress_update: P,
- ) -> Result<(), Error> {
- maybe_await!(self
- .url_client
- .electrum_like_setup(self.stop_gap, database, progress_update))
- }
-
- fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
- Ok(await_or_block!(self.url_client._get_tx(txid))?)
- }
-
- fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
- Ok(await_or_block!(self.url_client._broadcast(tx))?)
- }
-
- fn get_height(&self) -> Result<u32, Error> {
- Ok(await_or_block!(self.url_client._get_height())?)
- }
-
- fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
- let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
-
- let fee_val = estimates
- .into_iter()
- .map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::<usize>()?, v)))
- .collect::<Result<Vec<_>, _>>()
- .map_err(|e| Error::Generic(e.to_string()))?
- .into_iter()
- .take_while(|(k, _)| k <= &target)
- .map(|(_, v)| v)
- .last()
- .unwrap_or(1.0);
-
- Ok(FeeRate::from_sat_per_vb(fee_val as f32))
- }
-}
-
-impl UrlClient {
- fn script_to_scripthash(script: &Script) -> String {
- sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
- }
-
- async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
- let resp = self
- .client
- .get(&format!("{}/tx/{}/raw", self.url, txid))
- .send()
- .await?;
-
- if let StatusCode::NOT_FOUND = resp.status() {
- return Ok(None);
- }
-
- Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
- }
-
- async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
- match self._get_tx(txid).await {
- Ok(Some(tx)) => Ok(tx),
- Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
- Err(e) => Err(e),
- }
- }
-
- async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
- let resp = self
- .client
- .get(&format!("{}/block-height/{}", self.url, block_height))
- .send()
- .await?;
-
- if let StatusCode::NOT_FOUND = resp.status() {
- return Err(EsploraError::HeaderHeightNotFound(block_height));
- }
- let bytes = resp.bytes().await?;
- let hash = std::str::from_utf8(&bytes)
- .map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
-
- let resp = self
- .client
- .get(&format!("{}/block/{}/header", self.url, hash))
- .send()
- .await?;
-
- let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
-
- Ok(header)
- }
-
- async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
- self.client
- .post(&format!("{}/tx", self.url))
- .body(serialize(transaction).to_hex())
- .send()
- .await?
- .error_for_status()?;
-
- Ok(())
- }
-
- async fn _get_height(&self) -> Result<u32, EsploraError> {
- let req = self
- .client
- .get(&format!("{}/blocks/tip/height", self.url))
- .send()
- .await?;
-
- Ok(req.error_for_status()?.text().await?.parse()?)
- }
-
- async fn _script_get_history(
- &self,
- script: &Script,
- ) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
- let mut result = Vec::new();
- let scripthash = Self::script_to_scripthash(script);
-
- // Add the unconfirmed transactions first
- result.extend(
- self.client
- .get(&format!(
- "{}/scripthash/{}/txs/mempool",
- self.url, scripthash
- ))
- .send()
- .await?
- .error_for_status()?
- .json::<Vec<EsploraGetHistory>>()
- .await?
- .into_iter()
- .map(|x| ElsGetHistoryRes {
- tx_hash: x.txid,
- height: x.status.block_height.unwrap_or(0) as i32,
- }),
- );
-
- debug!(
- "Found {} mempool txs for {} - {:?}",
- result.len(),
- scripthash,
- script
- );
-
- // Then go through all the pages of confirmed transactions
- let mut last_txid = String::new();
- loop {
- let response = self
- .client
- .get(&format!(
- "{}/scripthash/{}/txs/chain/{}",
- self.url, scripthash, last_txid
- ))
- .send()
- .await?
- .error_for_status()?
- .json::<Vec<EsploraGetHistory>>()
- .await?;
- let len = response.len();
- if let Some(elem) = response.last() {
- last_txid = elem.txid.to_hex();
- }
-
- debug!("... adding {} confirmed transactions", len);
-
- result.extend(response.into_iter().map(|x| ElsGetHistoryRes {
- tx_hash: x.txid,
- height: x.status.block_height.unwrap_or(0) as i32,
- }));
-
- if len < 25 {
- break;
- }
- }
-
- Ok(result)
- }
-
- async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
- Ok(self
- .client
- .get(&format!("{}/fee-estimates", self.url,))
- .send()
- .await?
- .error_for_status()?
- .json::<HashMap<String, f64>>()
- .await?)
- }
-}
-
-#[maybe_async]
-impl ElectrumLikeSync for UrlClient {
- fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
- &self,
- scripts: I,
- ) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
- let future = async {
- let mut results = vec![];
- for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
- let mut futs = FuturesOrdered::new();
- for script in chunk {
- futs.push(self._script_get_history(&script));
- }
- let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?;
- results.extend(partial_results);
- }
- Ok(stream::iter(results).collect().await)
- };
-
- await_or_block!(future)
- }
-
- fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
- &self,
- txids: I,
- ) -> Result<Vec<Transaction>, Error> {
- let future = async {
- let mut results = vec![];
- for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) {
- let mut futs = FuturesOrdered::new();
- for txid in chunk {
- futs.push(self._get_tx_no_opt(&txid));
- }
- let partial_results: Vec<Transaction> = futs.try_collect().await?;
- results.extend(partial_results);
- }
- Ok(stream::iter(results).collect().await)
- };
-
- await_or_block!(future)
- }
-
- fn els_batch_block_header<I: IntoIterator<Item = u32>>(
- &self,
- heights: I,
- ) -> Result<Vec<BlockHeader>, Error> {
- let future = async {
- let mut results = vec![];
- for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) {
- let mut futs = FuturesOrdered::new();
- for height in chunk {
- futs.push(self._get_header(height));
- }
- let partial_results: Vec<BlockHeader> = futs.try_collect().await?;
- results.extend(partial_results);
- }
- Ok(stream::iter(results).collect().await)
- };
-
- await_or_block!(future)
- }
-}
-
-#[derive(Deserialize)]
-struct EsploraGetHistoryStatus {
- block_height: Option<usize>,
-}
-
-#[derive(Deserialize)]
-struct EsploraGetHistory {
- txid: Txid,
- status: EsploraGetHistoryStatus,
-}
-
-/// Configuration for an [`EsploraBlockchain`]
-#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
-pub struct EsploraBlockchainConfig {
- /// Base URL of the esplora service
- ///
- /// eg. `https://blockstream.info/api/`
- pub base_url: String,
- /// Number of parallel requests sent to the esplora service (default: 4)
- pub concurrency: Option<u8>,
- /// Stop searching addresses for transactions after finding an unused gap of this length
- pub stop_gap: usize,
-}
-
-impl ConfigurableBlockchain for EsploraBlockchain {
- type Config = EsploraBlockchainConfig;
-
- fn from_config(config: &Self::Config) -> Result<Self, Error> {
- Ok(EsploraBlockchain::new(
- config.base_url.as_str(),
- config.concurrency,
- config.stop_gap,
- ))
- }
-}
-
-/// Errors that can happen during a sync with [`EsploraBlockchain`]
-#[derive(Debug)]
-pub enum EsploraError {
- /// Error with the HTTP call
- Reqwest(reqwest::Error),
- /// Invalid number returned
- Parsing(std::num::ParseIntError),
- /// Invalid Bitcoin data returned
- BitcoinEncoding(bitcoin::consensus::encode::Error),
- /// Invalid Hex data returned
- Hex(bitcoin::hashes::hex::Error),
-
- /// Transaction not found
- TransactionNotFound(Txid),
- /// Header height not found
- HeaderHeightNotFound(u32),
- /// Header hash not found
- HeaderHashNotFound(BlockHash),
-}
-
-impl fmt::Display for EsploraError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{:?}", self)
- }
-}
-
-impl std::error::Error for EsploraError {}
-
-impl_error!(reqwest::Error, Reqwest, EsploraError);
-impl_error!(std::num::ParseIntError, Parsing, EsploraError);
-impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
-impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
-
-#[cfg(test)]
-#[cfg(feature = "test-esplora")]
-crate::bdk_blockchain_tests! {
- fn test_instance(test_client: &TestClient) -> EsploraBlockchain {
- EsploraBlockchain::new(&format!("http://{}",test_client.electrsd.esplora_url.as_ref().unwrap()), None, 20)
- }
-}
--- /dev/null
+//! Esplora
+//!
+//! This module defines a [`EsploraBlockchain`] struct that can query an Esplora
+//! backend populate the wallet's [database](crate::database::Database) by:
+//!
+//! ## Example
+//!
+//! ```no_run
+//! # use bdk::blockchain::esplora::EsploraBlockchain;
+//! let blockchain = EsploraBlockchain::new("https://blockstream.info/testnet/api", 20);
+//! # Ok::<(), bdk::Error>(())
+//! ```
+//!
+//! Esplora blockchain can use either `ureq` or `reqwest` for the HTTP client
+//! depending on your needs (blocking or async respectively).
+//!
+//! Please note, to configure the Esplora HTTP client correctly use one of:
+//! Blocking: --features='esplora,ureq'
+//! Async: --features='async-interface,esplora,reqwest' --no-default-features
+use std::fmt;
+use std::io;
+
+use serde::Deserialize;
+
+use bitcoin::consensus;
+use bitcoin::{BlockHash, Txid};
+
+#[cfg(all(
+ feature = "esplora",
+ feature = "reqwest",
+ any(feature = "async-interface", target_arch = "wasm32"),
+))]
+mod reqwest;
+
+#[cfg(all(
+ feature = "esplora",
+ feature = "reqwest",
+ any(feature = "async-interface", target_arch = "wasm32"),
+))]
+pub use self::reqwest::*;
+
+#[cfg(all(
+ feature = "esplora",
+ not(any(
+ feature = "async-interface",
+ feature = "reqwest",
+ target_arch = "wasm32"
+ )),
+))]
+mod ureq;
+
+#[cfg(all(
+ feature = "esplora",
+ not(any(
+ feature = "async-interface",
+ feature = "reqwest",
+ target_arch = "wasm32"
+ )),
+))]
+pub use self::ureq::*;
+
+/// Data type used when fetching transaction history from Esplora.
+#[derive(Deserialize)]
+pub struct EsploraGetHistory {
+ txid: Txid,
+ status: EsploraGetHistoryStatus,
+}
+
+#[derive(Deserialize)]
+struct EsploraGetHistoryStatus {
+ block_height: Option<usize>,
+}
+
+/// Errors that can happen during a sync with [`EsploraBlockchain`]
+#[derive(Debug)]
+pub enum EsploraError {
+ /// Error during ureq HTTP request
+ #[cfg(feature = "ureq")]
+ Ureq(::ureq::Error),
+ /// Transport error during the ureq HTTP call
+ #[cfg(feature = "ureq")]
+ UreqTransport(::ureq::Transport),
+ /// Error during reqwest HTTP request
+ #[cfg(feature = "reqwest")]
+ Reqwest(::reqwest::Error),
+ /// HTTP response error
+ HttpResponse(u16),
+ /// IO error during ureq response read
+ Io(io::Error),
+ /// No header found in ureq response
+ NoHeader,
+ /// Invalid number returned
+ Parsing(std::num::ParseIntError),
+ /// Invalid Bitcoin data returned
+ BitcoinEncoding(bitcoin::consensus::encode::Error),
+ /// Invalid Hex data returned
+ Hex(bitcoin::hashes::hex::Error),
+
+ /// Transaction not found
+ TransactionNotFound(Txid),
+ /// Header height not found
+ HeaderHeightNotFound(u32),
+ /// Header hash not found
+ HeaderHashNotFound(BlockHash),
+}
+
+impl fmt::Display for EsploraError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for EsploraError {}
+
+#[cfg(feature = "ureq")]
+impl_error!(::ureq::Error, Ureq, EsploraError);
+#[cfg(feature = "ureq")]
+impl_error!(::ureq::Transport, UreqTransport, EsploraError);
+#[cfg(feature = "reqwest")]
+impl_error!(::reqwest::Error, Reqwest, EsploraError);
+impl_error!(io::Error, Io, EsploraError);
+impl_error!(std::num::ParseIntError, Parsing, EsploraError);
+impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
+impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// 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, HashSet};
+
+use bitcoin::consensus::{deserialize, serialize};
+use bitcoin::hashes::hex::{FromHex, ToHex};
+use bitcoin::hashes::{sha256, Hash};
+use bitcoin::{BlockHeader, Script, Transaction, Txid};
+
+#[allow(unused_imports)]
+use log::{debug, error, info, trace};
+
+use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
+
+use ::reqwest::{Client, StatusCode};
+
+use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
+use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
+use crate::blockchain::*;
+use crate::database::BatchDatabase;
+use crate::error::Error;
+use crate::wallet::utils::ChunksIterator;
+use crate::FeeRate;
+
+const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
+
+#[derive(Debug)]
+struct UrlClient {
+ url: String,
+ // We use the async client instead of the blocking one because it automatically uses `fetch`
+ // when the target platform is wasm32.
+ client: Client,
+ concurrency: u8,
+}
+
+/// Structure that implements the logic to sync with Esplora
+///
+/// ## Example
+/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
+#[derive(Debug)]
+pub struct EsploraBlockchain {
+ url_client: UrlClient,
+ stop_gap: usize,
+}
+
+impl std::convert::From<UrlClient> for EsploraBlockchain {
+ fn from(url_client: UrlClient) -> Self {
+ EsploraBlockchain {
+ url_client,
+ stop_gap: 20,
+ }
+ }
+}
+
+impl EsploraBlockchain {
+ /// Create a new instance of the client from a base URL and `stop_gap`.
+ pub fn new(base_url: &str, stop_gap: usize) -> Self {
+ EsploraBlockchain {
+ url_client: UrlClient {
+ url: base_url.to_string(),
+ client: Client::new(),
+ concurrency: DEFAULT_CONCURRENT_REQUESTS,
+ },
+ stop_gap,
+ }
+ }
+
+ /// Set the concurrency to use when doing batch queries against the Esplora instance.
+ pub fn with_concurrency(mut self, concurrency: u8) -> Self {
+ self.url_client.concurrency = concurrency;
+ self
+ }
+}
+
+#[maybe_async]
+impl Blockchain for EsploraBlockchain {
+ fn get_capabilities(&self) -> HashSet<Capability> {
+ vec![
+ Capability::FullHistory,
+ Capability::GetAnyTx,
+ Capability::AccurateFees,
+ ]
+ .into_iter()
+ .collect()
+ }
+
+ fn setup<D: BatchDatabase, P: Progress>(
+ &self,
+ database: &mut D,
+ progress_update: P,
+ ) -> Result<(), Error> {
+ maybe_await!(self
+ .url_client
+ .electrum_like_setup(self.stop_gap, database, progress_update))
+ }
+
+ fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
+ Ok(self.url_client._get_tx(txid).await?)
+ }
+
+ fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
+ Ok(self.url_client._broadcast(tx).await?)
+ }
+
+ fn get_height(&self) -> Result<u32, Error> {
+ Ok(self.url_client._get_height().await?)
+ }
+
+ fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
+ let estimates = self.url_client._get_fee_estimates().await?;
+
+ let fee_val = estimates
+ .into_iter()
+ .map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::<usize>()?, v)))
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|e| Error::Generic(e.to_string()))?
+ .into_iter()
+ .take_while(|(k, _)| k <= &target)
+ .map(|(_, v)| v)
+ .last()
+ .unwrap_or(1.0);
+
+ Ok(FeeRate::from_sat_per_vb(fee_val as f32))
+ }
+}
+
+impl UrlClient {
+ fn script_to_scripthash(script: &Script) -> String {
+ sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
+ }
+
+ async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
+ let resp = self
+ .client
+ .get(&format!("{}/tx/{}/raw", self.url, txid))
+ .send()
+ .await?;
+
+ if let StatusCode::NOT_FOUND = resp.status() {
+ return Ok(None);
+ }
+
+ Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
+ }
+
+ async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
+ match self._get_tx(txid).await {
+ Ok(Some(tx)) => Ok(tx),
+ Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
+ Err(e) => Err(e),
+ }
+ }
+
+ async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
+ let resp = self
+ .client
+ .get(&format!("{}/block-height/{}", self.url, block_height))
+ .send()
+ .await?;
+
+ if let StatusCode::NOT_FOUND = resp.status() {
+ return Err(EsploraError::HeaderHeightNotFound(block_height));
+ }
+ let bytes = resp.bytes().await?;
+ let hash = std::str::from_utf8(&bytes)
+ .map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
+
+ let resp = self
+ .client
+ .get(&format!("{}/block/{}/header", self.url, hash))
+ .send()
+ .await?;
+
+ let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
+
+ Ok(header)
+ }
+
+ async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
+ self.client
+ .post(&format!("{}/tx", self.url))
+ .body(serialize(transaction).to_hex())
+ .send()
+ .await?
+ .error_for_status()?;
+
+ Ok(())
+ }
+
+ async fn _get_height(&self) -> Result<u32, EsploraError> {
+ let req = self
+ .client
+ .get(&format!("{}/blocks/tip/height", self.url))
+ .send()
+ .await?;
+
+ Ok(req.error_for_status()?.text().await?.parse()?)
+ }
+
+ async fn _script_get_history(
+ &self,
+ script: &Script,
+ ) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
+ let mut result = Vec::new();
+ let scripthash = Self::script_to_scripthash(script);
+
+ // Add the unconfirmed transactions first
+ result.extend(
+ self.client
+ .get(&format!(
+ "{}/scripthash/{}/txs/mempool",
+ self.url, scripthash
+ ))
+ .send()
+ .await?
+ .error_for_status()?
+ .json::<Vec<EsploraGetHistory>>()
+ .await?
+ .into_iter()
+ .map(|x| ElsGetHistoryRes {
+ tx_hash: x.txid,
+ height: x.status.block_height.unwrap_or(0) as i32,
+ }),
+ );
+
+ debug!(
+ "Found {} mempool txs for {} - {:?}",
+ result.len(),
+ scripthash,
+ script
+ );
+
+ // Then go through all the pages of confirmed transactions
+ let mut last_txid = String::new();
+ loop {
+ let response = self
+ .client
+ .get(&format!(
+ "{}/scripthash/{}/txs/chain/{}",
+ self.url, scripthash, last_txid
+ ))
+ .send()
+ .await?
+ .error_for_status()?
+ .json::<Vec<EsploraGetHistory>>()
+ .await?;
+ let len = response.len();
+ if let Some(elem) = response.last() {
+ last_txid = elem.txid.to_hex();
+ }
+
+ debug!("... adding {} confirmed transactions", len);
+
+ result.extend(response.into_iter().map(|x| ElsGetHistoryRes {
+ tx_hash: x.txid,
+ height: x.status.block_height.unwrap_or(0) as i32,
+ }));
+
+ if len < 25 {
+ break;
+ }
+ }
+
+ Ok(result)
+ }
+
+ async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
+ Ok(self
+ .client
+ .get(&format!("{}/fee-estimates", self.url,))
+ .send()
+ .await?
+ .error_for_status()?
+ .json::<HashMap<String, f64>>()
+ .await?)
+ }
+}
+
+#[maybe_async]
+impl ElectrumLikeSync for UrlClient {
+ fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
+ &self,
+ scripts: I,
+ ) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
+ let mut results = vec![];
+ for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
+ let mut futs = FuturesOrdered::new();
+ for script in chunk {
+ futs.push(self._script_get_history(script));
+ }
+ let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?;
+ results.extend(partial_results);
+ }
+ Ok(stream::iter(results).collect().await)
+ }
+
+ fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
+ &self,
+ txids: I,
+ ) -> Result<Vec<Transaction>, Error> {
+ let mut results = vec![];
+ for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) {
+ let mut futs = FuturesOrdered::new();
+ for txid in chunk {
+ futs.push(self._get_tx_no_opt(txid));
+ }
+ let partial_results: Vec<Transaction> = futs.try_collect().await?;
+ results.extend(partial_results);
+ }
+ Ok(stream::iter(results).collect().await)
+ }
+
+ fn els_batch_block_header<I: IntoIterator<Item = u32>>(
+ &self,
+ heights: I,
+ ) -> Result<Vec<BlockHeader>, Error> {
+ let mut results = vec![];
+ for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) {
+ let mut futs = FuturesOrdered::new();
+ for height in chunk {
+ futs.push(self._get_header(height));
+ }
+ let partial_results: Vec<BlockHeader> = futs.try_collect().await?;
+ results.extend(partial_results);
+ }
+ Ok(stream::iter(results).collect().await)
+ }
+}
+
+/// Configuration for an [`EsploraBlockchain`]
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
+pub struct EsploraBlockchainConfig {
+ /// Base URL of the esplora service
+ ///
+ /// eg. `https://blockstream.info/api/`
+ pub base_url: String,
+ /// Number of parallel requests sent to the esplora service (default: 4)
+ pub concurrency: Option<u8>,
+ /// Stop searching addresses for transactions after finding an unused gap of this length.
+ pub stop_gap: usize,
+}
+
+impl ConfigurableBlockchain for EsploraBlockchain {
+ type Config = EsploraBlockchainConfig;
+
+ fn from_config(config: &Self::Config) -> Result<Self, Error> {
+ let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
+ if let Some(concurrency) = config.concurrency {
+ blockchain.url_client.concurrency = concurrency;
+ };
+ Ok(blockchain)
+ }
+}
+
+#[cfg(test)]
+#[cfg(feature = "test-esplora")]
+crate::bdk_blockchain_tests! {
+ fn test_instance(test_client: &TestClient) -> EsploraBlockchain {
+ EsploraBlockchain::new(&format!("http://{}",test_client.electrsd.esplora_url.as_ref().unwrap()), None, 20)
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Esplora by way of `ureq` HTTP client.
+
+use std::collections::{HashMap, HashSet};
+use std::io;
+use std::io::Read;
+use std::time::Duration;
+
+#[allow(unused_imports)]
+use log::{debug, error, info, trace};
+
+use ureq::{Agent, Response};
+
+use bitcoin::consensus::{deserialize, serialize};
+use bitcoin::hashes::hex::{FromHex, ToHex};
+use bitcoin::hashes::{sha256, Hash};
+use bitcoin::{BlockHeader, Script, Transaction, Txid};
+
+use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
+use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
+use crate::blockchain::*;
+use crate::database::BatchDatabase;
+use crate::error::Error;
+use crate::FeeRate;
+
+#[derive(Debug)]
+struct UrlClient {
+ url: String,
+ agent: Agent,
+}
+
+/// Structure that implements the logic to sync with Esplora
+///
+/// ## Example
+/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
+#[derive(Debug)]
+pub struct EsploraBlockchain {
+ url_client: UrlClient,
+ stop_gap: usize,
+}
+
+impl std::convert::From<UrlClient> for EsploraBlockchain {
+ fn from(url_client: UrlClient) -> Self {
+ EsploraBlockchain {
+ url_client,
+ stop_gap: 20,
+ }
+ }
+}
+
+impl EsploraBlockchain {
+ /// Create a new instance of the client from a base URL and `stop_gap`.
+ pub fn new(base_url: &str, stop_gap: usize) -> Self {
+ EsploraBlockchain {
+ url_client: UrlClient {
+ url: base_url.to_string(),
+ agent: Agent::new(),
+ },
+ stop_gap,
+ }
+ }
+
+ /// Set the inner `ureq` agent.
+ pub fn with_agent(mut self, agent: Agent) -> Self {
+ self.url_client.agent = agent;
+ self
+ }
+}
+
+impl Blockchain for EsploraBlockchain {
+ fn get_capabilities(&self) -> HashSet<Capability> {
+ vec![
+ Capability::FullHistory,
+ Capability::GetAnyTx,
+ Capability::AccurateFees,
+ ]
+ .into_iter()
+ .collect()
+ }
+
+ fn setup<D: BatchDatabase, P: Progress>(
+ &self,
+ database: &mut D,
+ progress_update: P,
+ ) -> Result<(), Error> {
+ self.url_client
+ .electrum_like_setup(self.stop_gap, database, progress_update)
+ }
+
+ fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
+ Ok(self.url_client._get_tx(txid)?)
+ }
+
+ fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
+ let _txid = self.url_client._broadcast(tx)?;
+ Ok(())
+ }
+
+ fn get_height(&self) -> Result<u32, Error> {
+ Ok(self.url_client._get_height()?)
+ }
+
+ fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
+ let estimates = self.url_client._get_fee_estimates()?;
+
+ let fee_val = estimates
+ .into_iter()
+ .map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::<usize>()?, v)))
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|e| Error::Generic(e.to_string()))?
+ .into_iter()
+ .take_while(|(k, _)| k <= &target)
+ .map(|(_, v)| v)
+ .last()
+ .unwrap_or(1.0);
+
+ Ok(FeeRate::from_sat_per_vb(fee_val as f32))
+ }
+}
+
+impl UrlClient {
+ fn script_to_scripthash(script: &Script) -> String {
+ sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
+ }
+
+ fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
+ let resp = self
+ .agent
+ .get(&format!("{}/tx/{}/raw", self.url, txid))
+ .call();
+
+ match resp {
+ Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
+ Err(ureq::Error::Status(code, _)) => {
+ if is_status_not_found(code) {
+ return Ok(None);
+ }
+ Err(EsploraError::HttpResponse(code))
+ }
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }
+ }
+
+ fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
+ match self._get_tx(txid) {
+ Ok(Some(tx)) => Ok(tx),
+ Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
+ Err(e) => Err(e),
+ }
+ }
+
+ fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
+ let resp = self
+ .agent
+ .get(&format!("{}/block-height/{}", self.url, block_height))
+ .call();
+
+ let bytes = match resp {
+ Ok(resp) => Ok(into_bytes(resp)?),
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }?;
+
+ let hash = std::str::from_utf8(&bytes)
+ .map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
+
+ let resp = self
+ .agent
+ .get(&format!("{}/block/{}/header", self.url, hash))
+ .call();
+
+ match resp {
+ Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }
+ }
+
+ fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
+ let resp = self
+ .agent
+ .post(&format!("{}/tx", self.url))
+ .send_string(&serialize(transaction).to_hex());
+
+ match resp {
+ Ok(_) => Ok(()), // We do not return the txid?
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }
+ }
+
+ fn _get_height(&self) -> Result<u32, EsploraError> {
+ let resp = self
+ .agent
+ .get(&format!("{}/blocks/tip/height", self.url))
+ .call();
+
+ match resp {
+ Ok(resp) => Ok(resp.into_string()?.parse()?),
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }
+ }
+
+ fn _script_get_history(&self, script: &Script) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
+ let mut result = Vec::new();
+ let scripthash = Self::script_to_scripthash(script);
+
+ // Add the unconfirmed transactions first
+
+ let resp = self
+ .agent
+ .get(&format!(
+ "{}/scripthash/{}/txs/mempool",
+ self.url, scripthash
+ ))
+ .call();
+
+ let v = match resp {
+ Ok(resp) => {
+ let v: Vec<EsploraGetHistory> = resp.into_json()?;
+ Ok(v)
+ }
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }?;
+
+ result.extend(v.into_iter().map(|x| ElsGetHistoryRes {
+ tx_hash: x.txid,
+ height: x.status.block_height.unwrap_or(0) as i32,
+ }));
+
+ debug!(
+ "Found {} mempool txs for {} - {:?}",
+ result.len(),
+ scripthash,
+ script
+ );
+
+ // Then go through all the pages of confirmed transactions
+ let mut last_txid = String::new();
+ loop {
+ let resp = self
+ .agent
+ .get(&format!(
+ "{}/scripthash/{}/txs/chain/{}",
+ self.url, scripthash, last_txid
+ ))
+ .call();
+
+ let v = match resp {
+ Ok(resp) => {
+ let v: Vec<EsploraGetHistory> = resp.into_json()?;
+ Ok(v)
+ }
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }?;
+
+ let len = v.len();
+ if let Some(elem) = v.last() {
+ last_txid = elem.txid.to_hex();
+ }
+
+ debug!("... adding {} confirmed transactions", len);
+
+ result.extend(v.into_iter().map(|x| ElsGetHistoryRes {
+ tx_hash: x.txid,
+ height: x.status.block_height.unwrap_or(0) as i32,
+ }));
+
+ if len < 25 {
+ break;
+ }
+ }
+
+ Ok(result)
+ }
+
+ fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
+ let resp = self
+ .agent
+ .get(&format!("{}/fee-estimates", self.url,))
+ .call();
+
+ let map = match resp {
+ Ok(resp) => {
+ let map: HashMap<String, f64> = resp.into_json()?;
+ Ok(map)
+ }
+ Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
+ Err(e) => Err(EsploraError::Ureq(e)),
+ }?;
+
+ Ok(map)
+ }
+}
+
+fn is_status_not_found(status: u16) -> bool {
+ status == 404
+}
+
+fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
+ const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
+
+ let mut buf: Vec<u8> = vec![];
+ resp.into_reader()
+ .take((BYTES_LIMIT + 1) as u64)
+ .read_to_end(&mut buf)?;
+ if buf.len() > BYTES_LIMIT {
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "response too big for into_bytes",
+ ));
+ }
+
+ Ok(buf)
+}
+
+impl ElectrumLikeSync for UrlClient {
+ fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
+ &self,
+ scripts: I,
+ ) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
+ let mut results = vec![];
+ for script in scripts.into_iter() {
+ let v = self._script_get_history(script)?;
+ results.push(v);
+ }
+ Ok(results)
+ }
+
+ fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
+ &self,
+ txids: I,
+ ) -> Result<Vec<Transaction>, Error> {
+ let mut results = vec![];
+ for txid in txids.into_iter() {
+ let tx = self._get_tx_no_opt(txid)?;
+ results.push(tx);
+ }
+ Ok(results)
+ }
+
+ fn els_batch_block_header<I: IntoIterator<Item = u32>>(
+ &self,
+ heights: I,
+ ) -> Result<Vec<BlockHeader>, Error> {
+ let mut results = vec![];
+ for height in heights.into_iter() {
+ let header = self._get_header(height)?;
+ results.push(header);
+ }
+ Ok(results)
+ }
+}
+
+/// Configuration for an [`EsploraBlockchain`]
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
+pub struct EsploraBlockchainConfig {
+ /// Base URL of the esplora service eg. `https://blockstream.info/api/`
+ pub base_url: String,
+ /// Socket read timeout.
+ pub timeout_read: u64,
+ /// Socket write timeout.
+ pub timeout_write: u64,
+ /// Stop searching addresses for transactions after finding an unused gap of this length.
+ pub stop_gap: usize,
+}
+
+impl ConfigurableBlockchain for EsploraBlockchain {
+ type Config = EsploraBlockchainConfig;
+
+ fn from_config(config: &Self::Config) -> Result<Self, Error> {
+ let agent: Agent = ureq::AgentBuilder::new()
+ .timeout_read(Duration::from_secs(config.timeout_read))
+ .timeout_write(Duration::from_secs(config.timeout_write))
+ .build();
+ Ok(EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap).with_agent(agent))
+ }
+}
Electrum(electrum_client::Error),
#[cfg(feature = "esplora")]
/// Esplora client error
- Esplora(crate::blockchain::esplora::EsploraError),
+ Esplora(Box<crate::blockchain::esplora::EsploraError>),
#[cfg(feature = "compact_filters")]
/// Compact filters client error)
CompactFilters(crate::blockchain::compact_filters::CompactFiltersError),
#[cfg(feature = "electrum")]
impl_error!(electrum_client::Error, Electrum);
-#[cfg(feature = "esplora")]
-impl_error!(crate::blockchain::esplora::EsploraError, Esplora);
#[cfg(feature = "key-value-db")]
impl_error!(sled::Error, Sled);
#[cfg(feature = "rpc")]
}
}
}
+
+#[cfg(feature = "esplora")]
+impl From<crate::blockchain::esplora::EsploraError> for Error {
+ fn from(other: crate::blockchain::esplora::EsploraError) -> Self {
+ Error::Esplora(Box::new(other))
+ }
+}
#[macro_use]
extern crate serde_json;
+#[cfg(all(feature = "reqwest", feature = "ureq"))]
+compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
+
#[cfg(all(feature = "async-interface", feature = "electrum"))]
compile_error!(
"Features async-interface and electrum are mutually exclusive and cannot be enabled together"
);
+#[cfg(all(feature = "async-interface", feature = "ureq"))]
+compile_error!(
+ "Features async-interface and ureq are mutually exclusive and cannot be enabled together"
+);
+
+#[cfg(all(feature = "async-interface", feature = "compact_filters"))]
+compile_error!(
+ "Features async-interface and compact_filters are mutually exclusive and cannot be enabled together"
+);
+
+#[cfg(all(feature = "esplora", not(feature = "ureq"), not(feature = "reqwest")))]
+compile_error!("Feature missing: esplora requires either ureq or reqwest to be enabled");
+
#[cfg(feature = "keys-bip39")]
extern crate bip39;
#[cfg(feature = "electrum")]
pub extern crate electrum_client;
-#[cfg(feature = "esplora")]
-pub extern crate reqwest;
-
#[cfg(feature = "key-value-db")]
pub extern crate sled;