Add a ureq version of esplora module
The `Blockchain` implementation for connecting to an Esplora instance is currently based on `reqwest`. Some users may not wish to use reqwest. `ureq` is a simple HTTP client (no async) that is useful when `reqwest` is not suitable. - Move `esplora.rs` -> `esplora/reqwest.rs` - Add an implementation based on the `reqwest` esplora code but using `ureq` - Add feature flags and conditional includes to re-export everything to the `esplora` module so we don't effect the rest of the code base. - Remove the forced dependency on `tokio`. - Make esplora independent of async-interface - Depend on local version of macros crate
This commit is contained in:
		
							parent
							
								
									adceafa40c
								
							
						
					
					
						commit
						f37e735b43
					
				
							
								
								
									
										2
									
								
								.github/workflows/code_coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/code_coverage.yml
									
									
									
									
										vendored
									
									
								
							| @ -24,7 +24,7 @@ jobs: | |||||||
|       - name: Update toolchain |       - name: Update toolchain | ||||||
|         run: rustup update |         run: rustup update | ||||||
|       - name: Test |       - 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 |       - id: coverage | ||||||
|         name: Generate coverage |         name: Generate coverage | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								.github/workflows/cont_integration.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/cont_integration.yml
									
									
									
									
										vendored
									
									
								
							| @ -16,14 +16,16 @@ jobs: | |||||||
|           - default |           - default | ||||||
|           - minimal |           - minimal | ||||||
|           - all-keys |           - all-keys | ||||||
|           - minimal,esplora |           - minimal,esplora,ureq | ||||||
|           - key-value-db |           - key-value-db | ||||||
|           - electrum |           - electrum | ||||||
|           - compact_filters |           - compact_filters | ||||||
|           - esplora,key-value-db,electrum |           - esplora,ureq,key-value-db,electrum | ||||||
|           - compiler |           - compiler | ||||||
|           - rpc |           - rpc | ||||||
|           - verify |           - verify | ||||||
|  |           - async-interface | ||||||
|  |           - async-interface,esplora,reqwest | ||||||
|     steps: |     steps: | ||||||
|       - name: checkout |       - name: checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
| @ -137,7 +139,8 @@ jobs: | |||||||
|       - name: Update toolchain |       - name: Update toolchain | ||||||
|         run: rustup update |         run: rustup update | ||||||
|       - name: Check |       - 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: |   fmt: | ||||||
|     name: Rust fmt |     name: Rust fmt | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/nightly_docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly_docs.yml
									
									
									
									
										vendored
									
									
								
							| @ -24,7 +24,7 @@ jobs: | |||||||
|       - name: Update toolchain |       - name: Update toolchain | ||||||
|         run: rustup update |         run: rustup update | ||||||
|       - name: Build docs |       - 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 |       - name: Upload artifact | ||||||
|         uses: actions/upload-artifact@v2 |         uses: actions/upload-artifact@v2 | ||||||
|         with: |         with: | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
| ## [Unreleased] | ## [Unreleased] | ||||||
| 
 | 
 | ||||||
| - Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`) | - Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`) | ||||||
|  | - Removed hard dependency on `tokio`. | ||||||
| 
 | 
 | ||||||
| ### Wallet | ### Wallet | ||||||
| 
 | 
 | ||||||
| @ -14,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
| 
 | 
 | ||||||
| ### Blockchain | ### 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] | ## [v0.9.0] - [v0.8.0] | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -12,7 +12,7 @@ readme = "README.md" | |||||||
| license = "MIT OR Apache-2.0" | license = "MIT OR Apache-2.0" | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| bdk-macros = "^0.4" | bdk-macros = { path = "macros"} # TODO: Change this to version number after next release. | ||||||
| log = "^0.4" | log = "^0.4" | ||||||
| miniscript = "5.1" | miniscript = "5.1" | ||||||
| bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] } | bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] } | ||||||
| @ -24,6 +24,7 @@ rand = "^0.7" | |||||||
| sled = { version = "0.34", optional = true } | sled = { version = "0.34", optional = true } | ||||||
| electrum-client = { version = "0.7", optional = true } | electrum-client = { version = "0.7", optional = true } | ||||||
| reqwest = { version = "0.11", optional = true, features = ["json"] } | 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 } | futures = { version = "0.3", optional = true } | ||||||
| async-trait = { version = "0.1", optional = true } | async-trait = { version = "0.1", optional = true } | ||||||
| rocksdb = { version = "0.14", optional = true } | rocksdb = { version = "0.14", optional = true } | ||||||
| @ -37,10 +38,6 @@ bitcoinconsensus = { version = "0.19.0-3", optional = true } | |||||||
| # Needed by bdk_blockchain_tests macro | # Needed by bdk_blockchain_tests macro | ||||||
| bitcoincore-rpc = { version = "0.13", optional = true } | 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] | [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||||
| async-trait = "0.1" | async-trait = "0.1" | ||||||
| js-sys = "0.3" | js-sys = "0.3" | ||||||
| @ -51,21 +48,32 @@ minimal = [] | |||||||
| compiler = ["miniscript/compiler"] | compiler = ["miniscript/compiler"] | ||||||
| verify = ["bitcoinconsensus"] | verify = ["bitcoinconsensus"] | ||||||
| default = ["key-value-db", "electrum"] | default = ["key-value-db", "electrum"] | ||||||
| electrum = ["electrum-client"] |  | ||||||
| esplora = ["reqwest", "futures"] |  | ||||||
| compact_filters = ["rocksdb", "socks", "lazy_static", "cc"] | compact_filters = ["rocksdb", "socks", "lazy_static", "cc"] | ||||||
| key-value-db = ["sled"] | key-value-db = ["sled"] | ||||||
| async-interface = ["async-trait"] |  | ||||||
| all-keys = ["keys-bip39"] | all-keys = ["keys-bip39"] | ||||||
| keys-bip39 = ["tiny-bip39", "zeroize"] | keys-bip39 = ["tiny-bip39", "zeroize"] | ||||||
| rpc = ["bitcoincore-rpc"] | 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 | # Debug/Test features | ||||||
| test-blockchains = ["bitcoincore-rpc", "electrum-client"] | test-blockchains = ["bitcoincore-rpc", "electrum-client"] | ||||||
| test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"] | test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"] | ||||||
| test-rpc = ["rpc", "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"] | test-md-docs = ["electrum"] | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| @ -88,6 +96,6 @@ required-features = ["compiler"] | |||||||
| [workspace] | [workspace] | ||||||
| members = ["macros"] | members = ["macros"] | ||||||
| [package.metadata.docs.rs] | [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` | # defines the configuration attribute `docsrs` | ||||||
| rustdoc-args = ["--cfg", "docsrs"] | rustdoc-args = ["--cfg", "docsrs"] | ||||||
|  | |||||||
| @ -121,26 +121,3 @@ pub fn maybe_await(expr: TokenStream) -> TokenStream { | |||||||
| 
 | 
 | ||||||
|     quoted.into() |     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() |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -37,9 +37,9 @@ | |||||||
| //! )?;
 | //! )?;
 | ||||||
| //! # }
 | //! # }
 | ||||||
| //!
 | //!
 | ||||||
| //! # #[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(
 | //! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
 | ||||||
| //!     "...",
 | //!     "...",
 | ||||||
| //!     None,
 | //!     None,
 | ||||||
| @ -60,6 +60,8 @@ | |||||||
| //! # use bdk::blockchain::*;
 | //! # use bdk::blockchain::*;
 | ||||||
| //! # use bdk::database::MemoryDatabase;
 | //! # use bdk::database::MemoryDatabase;
 | ||||||
| //! # use bdk::Wallet;
 | //! # use bdk::Wallet;
 | ||||||
|  | //! # #[cfg(all(feature = "esplora", feature = "ureq"))]
 | ||||||
|  | //! # {
 | ||||||
| //! let config = serde_json::from_str("...")?;
 | //! let config = serde_json::from_str("...")?;
 | ||||||
| //! let blockchain = AnyBlockchain::from_config(&config)?;
 | //! let blockchain = AnyBlockchain::from_config(&config)?;
 | ||||||
| //! let wallet = Wallet::new(
 | //! let wallet = Wallet::new(
 | ||||||
| @ -69,6 +71,7 @@ | |||||||
| //!     MemoryDatabase::default(),
 | //!     MemoryDatabase::default(),
 | ||||||
| //!     blockchain,
 | //!     blockchain,
 | ||||||
| //! )?;
 | //! )?;
 | ||||||
|  | //! # }
 | ||||||
| //! # Ok::<(), bdk::Error>(())
 | //! # Ok::<(), bdk::Error>(())
 | ||||||
| //! ```
 | //! ```
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										124
									
								
								src/blockchain/esplora/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/blockchain/esplora/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | //! 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); | ||||||
| @ -9,41 +9,30 @@ | |||||||
| // You may not use this file except in accordance with one or both of these
 | // You may not use this file except in accordance with one or both of these
 | ||||||
| // licenses.
 | // licenses.
 | ||||||
| 
 | 
 | ||||||
| //! Esplora
 | //! Esplora by way of `reqwest` HTTP client.
 | ||||||
| //!
 |  | ||||||
| //! 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::collections::{HashMap, HashSet}; | ||||||
| use std::fmt; |  | ||||||
| 
 | 
 | ||||||
| use bitcoin::consensus::{self, deserialize, serialize}; | use bitcoin::consensus::{deserialize, serialize}; | ||||||
| use bitcoin::hashes::hex::{FromHex, ToHex}; | use bitcoin::hashes::hex::{FromHex, ToHex}; | ||||||
| use bitcoin::hashes::{sha256, Hash}; | use bitcoin::hashes::{sha256, Hash}; | ||||||
| use bitcoin::{BlockHash, BlockHeader, Script, Transaction, Txid}; | use bitcoin::{BlockHeader, Script, Transaction, Txid}; | ||||||
| use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt}; | 
 | ||||||
| #[allow(unused_imports)] | #[allow(unused_imports)] | ||||||
| use log::{debug, error, info, trace}; | use log::{debug, error, info, trace}; | ||||||
| use reqwest::{Client, StatusCode}; |  | ||||||
| use serde::Deserialize; |  | ||||||
| 
 | 
 | ||||||
|  | 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::database::BatchDatabase; | ||||||
| use crate::error::Error; | use crate::error::Error; | ||||||
| use crate::wallet::utils::ChunksIterator; | use crate::wallet::utils::ChunksIterator; | ||||||
| use crate::FeeRate; | use crate::FeeRate; | ||||||
| 
 | 
 | ||||||
| use super::*; |  | ||||||
| 
 |  | ||||||
| use self::utils::{ElectrumLikeSync, ElsGetHistoryRes}; |  | ||||||
| 
 |  | ||||||
| const DEFAULT_CONCURRENT_REQUESTS: u8 = 4; | const DEFAULT_CONCURRENT_REQUESTS: u8 = 4; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| @ -75,17 +64,23 @@ impl std::convert::From<UrlClient> for EsploraBlockchain { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl EsploraBlockchain { | impl EsploraBlockchain { | ||||||
|     /// Create a new instance of the client from a base URL
 |     /// Create a new instance of the client from a base URL and `stop_gap`.
 | ||||||
|     pub fn new(base_url: &str, concurrency: Option<u8>, stop_gap: usize) -> Self { |     pub fn new(base_url: &str, stop_gap: usize) -> Self { | ||||||
|         EsploraBlockchain { |         EsploraBlockchain { | ||||||
|             url_client: UrlClient { |             url_client: UrlClient { | ||||||
|                 url: base_url.to_string(), |                 url: base_url.to_string(), | ||||||
|                 client: Client::new(), |                 client: Client::new(), | ||||||
|                 concurrency: concurrency.unwrap_or(DEFAULT_CONCURRENT_REQUESTS), |                 concurrency: DEFAULT_CONCURRENT_REQUESTS, | ||||||
|             }, |             }, | ||||||
|             stop_gap, |             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] | #[maybe_async] | ||||||
| @ -111,19 +106,19 @@ impl Blockchain for EsploraBlockchain { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> { |     fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> { | ||||||
|         Ok(await_or_block!(self.url_client._get_tx(txid))?) |         Ok(self.url_client._get_tx(txid).await?) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { |     fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { | ||||||
|         Ok(await_or_block!(self.url_client._broadcast(tx))?) |         Ok(self.url_client._broadcast(tx).await?) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_height(&self) -> Result<u32, Error> { |     fn get_height(&self) -> Result<u32, Error> { | ||||||
|         Ok(await_or_block!(self.url_client._get_height())?) |         Ok(self.url_client._get_height().await?) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> { |     fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> { | ||||||
|         let estimates = await_or_block!(self.url_client._get_fee_estimates())?; |         let estimates = self.url_client._get_fee_estimates().await?; | ||||||
| 
 | 
 | ||||||
|         let fee_val = estimates |         let fee_val = estimates | ||||||
|             .into_iter() |             .into_iter() | ||||||
| @ -298,47 +293,38 @@ impl ElectrumLikeSync for UrlClient { | |||||||
|         &self, |         &self, | ||||||
|         scripts: I, |         scripts: I, | ||||||
|     ) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> { |     ) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> { | ||||||
|         let future = async { |  | ||||||
|         let mut results = vec![]; |         let mut results = vec![]; | ||||||
|         for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) { |         for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) { | ||||||
|             let mut futs = FuturesOrdered::new(); |             let mut futs = FuturesOrdered::new(); | ||||||
|             for script in chunk { |             for script in chunk { | ||||||
|                     futs.push(self._script_get_history(&script)); |                 futs.push(self._script_get_history(script)); | ||||||
|             } |             } | ||||||
|             let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?; |             let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?; | ||||||
|             results.extend(partial_results); |             results.extend(partial_results); | ||||||
|         } |         } | ||||||
|         Ok(stream::iter(results).collect().await) |         Ok(stream::iter(results).collect().await) | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         await_or_block!(future) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>( |     fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>( | ||||||
|         &self, |         &self, | ||||||
|         txids: I, |         txids: I, | ||||||
|     ) -> Result<Vec<Transaction>, Error> { |     ) -> Result<Vec<Transaction>, Error> { | ||||||
|         let future = async { |  | ||||||
|         let mut results = vec![]; |         let mut results = vec![]; | ||||||
|         for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) { |         for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) { | ||||||
|             let mut futs = FuturesOrdered::new(); |             let mut futs = FuturesOrdered::new(); | ||||||
|             for txid in chunk { |             for txid in chunk { | ||||||
|                     futs.push(self._get_tx_no_opt(&txid)); |                 futs.push(self._get_tx_no_opt(txid)); | ||||||
|             } |             } | ||||||
|             let partial_results: Vec<Transaction> = futs.try_collect().await?; |             let partial_results: Vec<Transaction> = futs.try_collect().await?; | ||||||
|             results.extend(partial_results); |             results.extend(partial_results); | ||||||
|         } |         } | ||||||
|         Ok(stream::iter(results).collect().await) |         Ok(stream::iter(results).collect().await) | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         await_or_block!(future) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn els_batch_block_header<I: IntoIterator<Item = u32>>( |     fn els_batch_block_header<I: IntoIterator<Item = u32>>( | ||||||
|         &self, |         &self, | ||||||
|         heights: I, |         heights: I, | ||||||
|     ) -> Result<Vec<BlockHeader>, Error> { |     ) -> Result<Vec<BlockHeader>, Error> { | ||||||
|         let future = async { |  | ||||||
|         let mut results = vec![]; |         let mut results = vec![]; | ||||||
|         for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) { |         for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) { | ||||||
|             let mut futs = FuturesOrdered::new(); |             let mut futs = FuturesOrdered::new(); | ||||||
| @ -349,23 +335,9 @@ impl ElectrumLikeSync for UrlClient { | |||||||
|             results.extend(partial_results); |             results.extend(partial_results); | ||||||
|         } |         } | ||||||
|         Ok(stream::iter(results).collect().await) |         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`]
 | /// Configuration for an [`EsploraBlockchain`]
 | ||||||
| #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] | #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] | ||||||
| pub struct EsploraBlockchainConfig { | pub struct EsploraBlockchainConfig { | ||||||
| @ -375,7 +347,7 @@ pub struct EsploraBlockchainConfig { | |||||||
|     pub base_url: String, |     pub base_url: String, | ||||||
|     /// Number of parallel requests sent to the esplora service (default: 4)
 |     /// Number of parallel requests sent to the esplora service (default: 4)
 | ||||||
|     pub concurrency: Option<u8>, |     pub concurrency: Option<u8>, | ||||||
|     /// Stop searching addresses for transactions after finding an unused gap of this length
 |     /// Stop searching addresses for transactions after finding an unused gap of this length.
 | ||||||
|     pub stop_gap: usize, |     pub stop_gap: usize, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -383,47 +355,14 @@ impl ConfigurableBlockchain for EsploraBlockchain { | |||||||
|     type Config = EsploraBlockchainConfig; |     type Config = EsploraBlockchainConfig; | ||||||
| 
 | 
 | ||||||
|     fn from_config(config: &Self::Config) -> Result<Self, Error> { |     fn from_config(config: &Self::Config) -> Result<Self, Error> { | ||||||
|         Ok(EsploraBlockchain::new( |         let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap); | ||||||
|             config.base_url.as_str(), |         if let Some(concurrency) = config.concurrency { | ||||||
|             config.concurrency, |             blockchain.url_client.concurrency = concurrency; | ||||||
|             config.stop_gap, |         }; | ||||||
|         )) |         Ok(blockchain) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// 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(test)] | ||||||
| #[cfg(feature = "test-esplora")] | #[cfg(feature = "test-esplora")] | ||||||
| crate::bdk_blockchain_tests! { | crate::bdk_blockchain_tests! { | ||||||
							
								
								
									
										391
									
								
								src/blockchain/esplora/ureq.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								src/blockchain/esplora/ureq.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,391 @@ | |||||||
|  | // 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)) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/error.rs
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/error.rs
									
									
									
									
									
								
							| @ -130,7 +130,7 @@ pub enum Error { | |||||||
|     Electrum(electrum_client::Error), |     Electrum(electrum_client::Error), | ||||||
|     #[cfg(feature = "esplora")] |     #[cfg(feature = "esplora")] | ||||||
|     /// Esplora client error
 |     /// Esplora client error
 | ||||||
|     Esplora(crate::blockchain::esplora::EsploraError), |     Esplora(Box<crate::blockchain::esplora::EsploraError>), | ||||||
|     #[cfg(feature = "compact_filters")] |     #[cfg(feature = "compact_filters")] | ||||||
|     /// Compact filters client error)
 |     /// Compact filters client error)
 | ||||||
|     CompactFilters(crate::blockchain::compact_filters::CompactFiltersError), |     CompactFilters(crate::blockchain::compact_filters::CompactFiltersError), | ||||||
| @ -190,8 +190,6 @@ impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse); | |||||||
| 
 | 
 | ||||||
| #[cfg(feature = "electrum")] | #[cfg(feature = "electrum")] | ||||||
| impl_error!(electrum_client::Error, Electrum); | impl_error!(electrum_client::Error, Electrum); | ||||||
| #[cfg(feature = "esplora")] |  | ||||||
| impl_error!(crate::blockchain::esplora::EsploraError, Esplora); |  | ||||||
| #[cfg(feature = "key-value-db")] | #[cfg(feature = "key-value-db")] | ||||||
| impl_error!(sled::Error, Sled); | impl_error!(sled::Error, Sled); | ||||||
| #[cfg(feature = "rpc")] | #[cfg(feature = "rpc")] | ||||||
| @ -216,3 +214,10 @@ impl From<crate::wallet::verify::VerifyError> for Error { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[cfg(feature = "esplora")] | ||||||
|  | impl From<crate::blockchain::esplora::EsploraError> for Error { | ||||||
|  |     fn from(other: crate::blockchain::esplora::EsploraError) -> Self { | ||||||
|  |         Error::Esplora(Box::new(other)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/lib.rs
									
									
									
									
									
								
							| @ -205,11 +205,27 @@ extern crate serde; | |||||||
| #[macro_use] | #[macro_use] | ||||||
| extern crate serde_json; | 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"))] | #[cfg(all(feature = "async-interface", feature = "electrum"))] | ||||||
| compile_error!( | compile_error!( | ||||||
|     "Features async-interface and electrum are mutually exclusive and cannot be enabled together" |     "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")] | #[cfg(feature = "keys-bip39")] | ||||||
| extern crate bip39; | extern crate bip39; | ||||||
| 
 | 
 | ||||||
| @ -228,9 +244,6 @@ pub extern crate bitcoincore_rpc; | |||||||
| #[cfg(feature = "electrum")] | #[cfg(feature = "electrum")] | ||||||
| pub extern crate electrum_client; | pub extern crate electrum_client; | ||||||
| 
 | 
 | ||||||
| #[cfg(feature = "esplora")] |  | ||||||
| pub extern crate reqwest; |  | ||||||
| 
 |  | ||||||
| #[cfg(feature = "key-value-db")] | #[cfg(feature = "key-value-db")] | ||||||
| pub extern crate sled; | pub extern crate sled; | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user