Compare commits
50 Commits
compact_fi
...
release/0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e508748a3 | ||
|
|
84b8579df5 | ||
|
|
7cb0116c44 | ||
|
|
6e12468b12 | ||
|
|
5694b98304 | ||
|
|
aa786fbb21 | ||
|
|
8c570ae7eb | ||
|
|
56a7bc9874 | ||
|
|
cf1815a1c0 | ||
|
|
721748e98f | ||
|
|
976e641ba6 | ||
|
|
7117557dea | ||
|
|
fa013aeb83 | ||
|
|
38d1d0b0e2 | ||
|
|
582d2f3814 | ||
|
|
5e0011e1a8 | ||
|
|
39d2bd0d21 | ||
|
|
0e10952b80 | ||
|
|
19d74955e2 | ||
|
|
73a7faf144 | ||
|
|
ea56a87b4b | ||
|
|
67f5f45e07 | ||
|
|
b8680b299d | ||
|
|
a5d3a4d31a | ||
|
|
d03d3c0dbd | ||
|
|
9aba3196ff | ||
|
|
c8593ecf70 | ||
|
|
cbec0b0bcf | ||
|
|
e80be49d1e | ||
|
|
fe30716fa2 | ||
|
|
e52550cfec | ||
|
|
f57c0ca98e | ||
|
|
c54e1e9652 | ||
|
|
5cdc5fb58a | ||
|
|
27cd9bbcd6 | ||
|
|
f37e735b43 | ||
|
|
adceafa40c | ||
|
|
2b0c4f0817 | ||
|
|
e9428433a0 | ||
|
|
63592f169f | ||
|
|
27600f4a11 | ||
|
|
77eae76459 | ||
|
|
ad69702aa3 | ||
|
|
fd254536d3 | ||
|
|
c4d5dd14fa | ||
|
|
13bed2667a | ||
|
|
2db24fb8c5 | ||
|
|
d2d37fc06d | ||
|
|
2986fce7c6 | ||
|
|
1dc648508c |
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- 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
|
||||
|
||||
21
.github/workflows/cont_integration.yml
vendored
21
.github/workflows/cont_integration.yml
vendored
@@ -16,14 +16,16 @@ jobs:
|
||||
- default
|
||||
- minimal
|
||||
- all-keys
|
||||
- minimal,esplora
|
||||
- minimal,use-esplora-ureq
|
||||
- key-value-db
|
||||
- electrum
|
||||
- compact_filters
|
||||
- esplora,key-value-db,electrum
|
||||
- esplora,ureq,key-value-db,electrum
|
||||
- compiler
|
||||
- rpc
|
||||
- verify
|
||||
- async-interface
|
||||
- use-esplora-reqwest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -76,15 +78,20 @@ jobs:
|
||||
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
|
||||
|
||||
test-blockchains:
|
||||
name: Test ${{ matrix.blockchain.name }}
|
||||
name: Blockchain ${{ matrix.blockchain.features }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
blockchain:
|
||||
- name: electrum
|
||||
features: test-electrum
|
||||
- name: rpc
|
||||
features: test-rpc
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-reqwest
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-ureq
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -93,8 +100,6 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/bitcoin
|
||||
~/.cargo/electrs
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
@@ -104,8 +109,8 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Test
|
||||
run: cargo test --features test-${{ matrix.blockchain.name }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||
|
||||
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||
|
||||
check-wasm:
|
||||
name: Check WASM
|
||||
runs-on: ubuntu-16.04
|
||||
@@ -137,7 +142,7 @@ jobs:
|
||||
- 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 use-esplora-reqwest --no-default-features
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,13 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.11.0] - [v0.10.0]
|
||||
|
||||
- Added `flush` method to the `Database` trait to explicitly flush to disk latest changes on the db.
|
||||
|
||||
## [v0.10.0] - [v0.9.0]
|
||||
|
||||
- Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`)
|
||||
- Removed hard dependency on `tokio`.
|
||||
|
||||
### Wallet
|
||||
|
||||
- Removed and replaced `set_single_recipient` with more general `drain_to` and replaced `maintain_single_recipient` with `allow_shrinking`.
|
||||
|
||||
### 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]
|
||||
|
||||
@@ -354,7 +364,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Use `MemoryDatabase` in the compiler example
|
||||
- Make the REPL return JSON
|
||||
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.9.0...HEAD
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...HEAD
|
||||
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
||||
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
|
||||
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
||||
@@ -365,3 +375,5 @@ final transaction is created by calling `finish` on the builder.
|
||||
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
|
||||
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0
|
||||
[v0.9.0]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...v0.9.0
|
||||
[v0.10.0]: https://github.com/bitcoindevkit/bdk/compare/v0.9.0...v0.10.0
|
||||
[v0.11.0]: https://github.com/bitcoindevkit/bdk/compare/v0.10.0...v0.11.0
|
||||
@@ -57,6 +57,21 @@ comment suggesting that you're working on it. If someone is already assigned,
|
||||
don't hesitate to ask if the assigned party or previous commenters are still
|
||||
working on it if it has been awhile.
|
||||
|
||||
Deprecation policy
|
||||
------------------
|
||||
|
||||
Where possible, breaking existing APIs should be avoided. Instead, add new APIs and
|
||||
use [`#[deprecated]`](https://github.com/rust-lang/rfcs/blob/master/text/1270-deprecation.md)
|
||||
to discourage use of the old one.
|
||||
|
||||
Deprecated APIs are typically maintained for one release cycle. In other words, an
|
||||
API that has been deprecated with the 0.10 release can be expected to be removed in the
|
||||
0.11 release. This allows for smoother upgrades without incurring too much technical
|
||||
debt inside this library.
|
||||
|
||||
If you deprecated an API as part of a contribution, we encourage you to "own" that API
|
||||
and send a follow-up to remove it as part of the next release cycle.
|
||||
|
||||
Peer review
|
||||
-----------
|
||||
|
||||
|
||||
49
Cargo.toml
49
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.9.1-dev"
|
||||
version = "0.11.1-dev"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -12,21 +12,22 @@ readme = "README.md"
|
||||
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"] }
|
||||
miniscript = "^6.0"
|
||||
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.7"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.7", optional = true }
|
||||
electrum-client = { version = "0.8", 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 }
|
||||
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
||||
cc = { version = ">=1.0.64", optional = true }
|
||||
socks = { version = "0.3", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
@@ -35,7 +36,7 @@ zeroize = { version = "<1.4.0", optional = true }
|
||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||
|
||||
# Needed by bdk_blockchain_tests macro
|
||||
bitcoincore-rpc = { version = "0.13", optional = true }
|
||||
core-rpc = { version = "0.14", optional = true }
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
@@ -51,28 +52,46 @@ minimal = []
|
||||
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"]
|
||||
rpc = ["core-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` (`--features=use-esplora-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` (`--features=use-esplora-ureq`).
|
||||
#
|
||||
# WARNING: Please take care with the features below, various combinations will
|
||||
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
||||
async-interface = ["async-trait"]
|
||||
electrum = ["electrum-client"]
|
||||
# MUST ALSO USE `--no-default-features`.
|
||||
use-esplora-reqwest = ["esplora", "reqwest", "futures"]
|
||||
use-esplora-ureq = ["esplora", "ureq"]
|
||||
# Typical configurations will not need to use `esplora` feature directly.
|
||||
esplora = []
|
||||
|
||||
|
||||
# Debug/Test features
|
||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||
test-blockchains = ["core-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 = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
|
||||
test-md-docs = ["electrum"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
clap = "2.33"
|
||||
electrsd = { version= "0.6", features = ["trigger", "bitcoind_0_21_1"] }
|
||||
electrsd = { version= "0.10", features = ["trigger", "bitcoind_0_21_1"] }
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
@@ -88,6 +107,6 @@ required-features = ["compiler"]
|
||||
[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"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk-macros"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
|
||||
@@ -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(
|
||||
//! "...",
|
||||
//! None,
|
||||
@@ -60,6 +60,8 @@
|
||||
//! # 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(
|
||||
@@ -69,6 +71,7 @@
|
||||
//! MemoryDatabase::default(),
|
||||
//! blockchain,
|
||||
//! )?;
|
||||
//! # }
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
|
||||
@@ -94,6 +97,8 @@ macro_rules! impl_inner_method {
|
||||
AnyBlockchain::Esplora(inner) => inner.$name( $($args, )* ),
|
||||
#[cfg(feature = "compact_filters")]
|
||||
AnyBlockchain::CompactFilters(inner) => inner.$name( $($args, )* ),
|
||||
#[cfg(feature = "rpc")]
|
||||
AnyBlockchain::Rpc(inner) => inner.$name( $($args, )* ),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +121,10 @@ pub enum AnyBlockchain {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||
/// Compact filters client
|
||||
CompactFilters(compact_filters::CompactFiltersBlockchain),
|
||||
#[cfg(feature = "rpc")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||
/// RPC client
|
||||
Rpc(rpc::RpcBlockchain),
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
@@ -157,6 +166,7 @@ impl Blockchain for AnyBlockchain {
|
||||
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
||||
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
||||
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||
impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||
|
||||
/// Type that can contain any of the blockchain configurations defined by the library
|
||||
///
|
||||
@@ -206,6 +216,10 @@ pub enum AnyBlockchainConfig {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||
/// Compact filters client
|
||||
CompactFilters(compact_filters::CompactFiltersBlockchainConfig),
|
||||
#[cfg(feature = "rpc")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||
/// RPC client configuration
|
||||
Rpc(rpc::RpcConfig),
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for AnyBlockchain {
|
||||
@@ -225,6 +239,10 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
||||
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
|
||||
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
||||
),
|
||||
#[cfg(feature = "rpc")]
|
||||
AnyBlockchainConfig::Rpc(inner) => {
|
||||
AnyBlockchain::Rpc(rpc::RpcBlockchain::from_config(inner)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -232,3 +250,4 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
||||
impl_from!(electrum::ElectrumBlockchainConfig, AnyBlockchainConfig, Electrum, #[cfg(feature = "electrum")]);
|
||||
impl_from!(esplora::EsploraBlockchainConfig, AnyBlockchainConfig, Esplora, #[cfg(feature = "esplora")]);
|
||||
impl_from!(compact_filters::CompactFiltersBlockchainConfig, AnyBlockchainConfig, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||
impl_from!(rpc::RpcConfig, AnyBlockchainConfig, Rpc, #[cfg(feature = "rpc")]);
|
||||
|
||||
129
src/blockchain/esplora/mod.rs
Normal file
129
src/blockchain/esplora/mod.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! 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::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::{BlockHash, Txid};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
mod reqwest;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use self::reqwest::*;
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
mod ureq;
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
pub use self::ureq::*;
|
||||
|
||||
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
#[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()), 20)
|
||||
}
|
||||
}
|
||||
@@ -9,41 +9,30 @@
|
||||
// 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>(())
|
||||
//! ```
|
||||
//! Esplora by way of `reqwest` HTTP client.
|
||||
|
||||
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::{sha256, Hash};
|
||||
use bitcoin::{BlockHash, BlockHeader, Script, Transaction, Txid};
|
||||
use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
|
||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
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::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)]
|
||||
@@ -75,17 +64,23 @@ impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||
}
|
||||
|
||||
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 {
|
||||
/// 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: concurrency.unwrap_or(DEFAULT_CONCURRENT_REQUESTS),
|
||||
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]
|
||||
@@ -124,19 +119,7 @@ impl Blockchain for EsploraBlockchain {
|
||||
|
||||
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))
|
||||
super::into_fee_rate(target, estimates)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,74 +281,51 @@ impl ElectrumLikeSync for UrlClient {
|
||||
&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);
|
||||
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));
|
||||
}
|
||||
Ok(stream::iter(results).collect().await)
|
||||
};
|
||||
|
||||
await_or_block!(future)
|
||||
let partial_results: Vec<Vec<ElsGetHistoryRes>> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
}
|
||||
Ok(stream::iter(results).collect().await)
|
||||
};
|
||||
|
||||
await_or_block!(future)
|
||||
let partial_results: Vec<Transaction> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
}
|
||||
Ok(stream::iter(results).collect().await)
|
||||
};
|
||||
|
||||
await_or_block!(future)
|
||||
let partial_results: Vec<BlockHeader> = await_or_block!(futs.try_collect())?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(await_or_block!(stream::iter(results).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -375,7 +335,7 @@ pub struct EsploraBlockchainConfig {
|
||||
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
|
||||
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||
pub stop_gap: usize,
|
||||
}
|
||||
|
||||
@@ -383,51 +343,10 @@ 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
379
src/blockchain/esplora/ureq.rs
Normal file
379
src/blockchain/esplora/ureq.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
// 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()?;
|
||||
super::into_fee_rate(target, estimates)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,19 @@ use crate::FeeRate;
|
||||
#[cfg(any(feature = "electrum", feature = "esplora"))]
|
||||
pub(crate) mod utils;
|
||||
|
||||
#[cfg(any(feature = "electrum", feature = "esplora", feature = "compact_filters"))]
|
||||
#[cfg(any(
|
||||
feature = "electrum",
|
||||
feature = "esplora",
|
||||
feature = "compact_filters",
|
||||
feature = "rpc"
|
||||
))]
|
||||
pub mod any;
|
||||
#[cfg(any(feature = "electrum", feature = "esplora", feature = "compact_filters"))]
|
||||
#[cfg(any(
|
||||
feature = "electrum",
|
||||
feature = "esplora",
|
||||
feature = "compact_filters",
|
||||
feature = "rpc"
|
||||
))]
|
||||
pub use any::{AnyBlockchain, AnyBlockchainConfig};
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain};
|
||||
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain, rpc::Auth};
|
||||
//! let config = RpcConfig {
|
||||
//! url: "127.0.0.1:18332".to_string(),
|
||||
//! auth: bitcoincore_rpc::Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
|
||||
//! auth: Auth::Cookie {
|
||||
//! file: "/home/user/.bitcoin/.cookie".into(),
|
||||
//! },
|
||||
//! network: bdk::bitcoin::Network::Testnet,
|
||||
//! wallet_name: "wallet_name".to_string(),
|
||||
//! skip_blocks: None,
|
||||
@@ -36,15 +38,17 @@ use crate::database::{BatchDatabase, DatabaseUtils};
|
||||
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
|
||||
use crate::wallet::utils::SecpCtx;
|
||||
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use bitcoincore_rpc::json::{
|
||||
use core_rpc::json::{
|
||||
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
|
||||
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
||||
};
|
||||
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||
use core_rpc::jsonrpc::serde_json::Value;
|
||||
use core_rpc::Auth as RpcAuth;
|
||||
use core_rpc::{Client, RpcApi};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
|
||||
@@ -64,7 +68,7 @@ pub struct RpcBlockchain {
|
||||
}
|
||||
|
||||
/// RpcBlockchain configuration options
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct RpcConfig {
|
||||
/// The bitcoin node url
|
||||
pub url: String,
|
||||
@@ -78,6 +82,39 @@ pub struct RpcConfig {
|
||||
pub skip_blocks: Option<u32>,
|
||||
}
|
||||
|
||||
/// This struct is equivalent to [core_rpc::Auth] but it implements [serde::Serialize]
|
||||
/// To be removed once upstream equivalent is implementing Serialize (json serialization format
|
||||
/// should be the same) https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/181
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(untagged)]
|
||||
pub enum Auth {
|
||||
/// None authentication
|
||||
None,
|
||||
/// Authentication with username and password, usually [Auth::Cookie] should be preferred
|
||||
UserPass {
|
||||
/// Username
|
||||
username: String,
|
||||
/// Password
|
||||
password: String,
|
||||
},
|
||||
/// Authentication with a cookie file
|
||||
Cookie {
|
||||
/// Cookie file
|
||||
file: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Auth> for RpcAuth {
|
||||
fn from(auth: Auth) -> Self {
|
||||
match auth {
|
||||
Auth::None => RpcAuth::None,
|
||||
Auth::UserPass { username, password } => RpcAuth::UserPass(username, password),
|
||||
Auth::Cookie { file } => RpcAuth::CookieFile(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcBlockchain {
|
||||
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
||||
let info = self.client.get_address_info(&self._storage_address)?;
|
||||
@@ -320,7 +357,7 @@ impl ConfigurableBlockchain for RpcBlockchain {
|
||||
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
||||
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
||||
|
||||
let client = Client::new(wallet_url, config.auth.clone())?;
|
||||
let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?;
|
||||
let loaded_wallets = client.list_wallets()?;
|
||||
if loaded_wallets.contains(&wallet_name) {
|
||||
debug!("wallet already loaded {:?}", wallet_name);
|
||||
@@ -427,7 +464,7 @@ crate::bdk_blockchain_tests! {
|
||||
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
||||
let config = RpcConfig {
|
||||
url: test_client.bitcoind.rpc_url(),
|
||||
auth: Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone()),
|
||||
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
|
||||
network: Network::Regtest,
|
||||
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
|
||||
skip_blocks: None,
|
||||
|
||||
@@ -369,7 +369,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||
}
|
||||
|
||||
/// returns utxo dependency as the inputs needed for the utxo to exist
|
||||
/// `tx_raw_in_db` must contains utxo's generating txs or errors witt [crate::Error::TransactionNotFound]
|
||||
/// `tx_raw_in_db` must contains utxo's generating txs or errors with [crate::Error::TransactionNotFound]
|
||||
fn utxos_deps<D: BatchDatabase>(
|
||||
db: &mut D,
|
||||
tx_raw_in_db: &HashMap<Txid, Transaction>,
|
||||
|
||||
@@ -233,6 +233,10 @@ impl Database for AnyDatabase {
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyDatabase, self, flush)
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchOperations for AnyBatch {
|
||||
|
||||
@@ -367,6 +367,10 @@ impl Database for Tree {
|
||||
Ok(val)
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(Tree::flush(self).map(|_| ())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for Tree {
|
||||
|
||||
@@ -419,6 +419,10 @@ impl Database for MemoryDatabase {
|
||||
|
||||
Ok(*value)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for MemoryDatabase {
|
||||
|
||||
@@ -134,6 +134,9 @@ pub trait Database: BatchOperations {
|
||||
///
|
||||
/// It should insert and return `0` if not present in the database
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
||||
|
||||
/// Force changes to be written to disk
|
||||
fn flush(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Trait for a database that supports batch operations
|
||||
|
||||
15
src/error.rs
15
src/error.rs
@@ -130,7 +130,7 @@ pub enum Error {
|
||||
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),
|
||||
@@ -139,7 +139,7 @@ pub enum Error {
|
||||
Sled(sled::Error),
|
||||
#[cfg(feature = "rpc")]
|
||||
/// Rpc client error
|
||||
Rpc(bitcoincore_rpc::Error),
|
||||
Rpc(core_rpc::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
@@ -190,12 +190,10 @@ impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
|
||||
|
||||
#[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")]
|
||||
impl_error!(bitcoincore_rpc::Error, Rpc);
|
||||
impl_error!(core_rpc::Error, Rpc);
|
||||
|
||||
#[cfg(feature = "compact_filters")]
|
||||
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
20
src/lib.rs
20
src/lib.rs
@@ -40,7 +40,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.9.0"
|
||||
//! bdk = "0.11.0"
|
||||
//! ```
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
@@ -205,11 +205,24 @@ extern crate serde;
|
||||
#[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(feature = "keys-bip39")]
|
||||
extern crate bip39;
|
||||
|
||||
@@ -223,14 +236,11 @@ extern crate bdk_macros;
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
pub extern crate bitcoincore_rpc;
|
||||
pub extern crate core_rpc;
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
pub extern crate electrum_client;
|
||||
|
||||
#[cfg(feature = "esplora")]
|
||||
pub extern crate reqwest;
|
||||
|
||||
#[cfg(feature = "key-value-db")]
|
||||
pub extern crate sled;
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ impl PsbtUtils for Psbt {
|
||||
mod test {
|
||||
use crate::bitcoin::TxIn;
|
||||
use crate::psbt::Psbt;
|
||||
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||
use crate::wallet::AddressIndex;
|
||||
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
|
||||
use crate::SignOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::sha256d;
|
||||
use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||
use core::str::FromStr;
|
||||
pub use core_rpc::core_rpc_json::AddressType;
|
||||
pub use core_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||
use electrsd::bitcoind::BitcoinD;
|
||||
use electrsd::{bitcoind, ElectrsD};
|
||||
use electrsd::{bitcoind, Conf, ElectrsD};
|
||||
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
use log::{debug, error, info, log_enabled, trace, Level};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
@@ -24,11 +24,20 @@ pub struct TestClient {
|
||||
impl TestClient {
|
||||
pub fn new(bitcoind_exe: String, electrs_exe: String) -> Self {
|
||||
debug!("launching {} and {}", &bitcoind_exe, &electrs_exe);
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
let conf = bitcoind::Conf {
|
||||
view_stdout: log_enabled!(Level::Debug),
|
||||
..Default::default()
|
||||
};
|
||||
let bitcoind = BitcoinD::with_conf(bitcoind_exe, &conf).unwrap();
|
||||
|
||||
let http_enabled = cfg!(feature = "test-esplora");
|
||||
|
||||
let electrsd = ElectrsD::new(electrs_exe, &bitcoind, false, http_enabled).unwrap();
|
||||
let conf = Conf {
|
||||
http_enabled,
|
||||
view_stderr: log_enabled!(Level::Debug),
|
||||
..Default::default()
|
||||
};
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &conf).unwrap();
|
||||
|
||||
let node_address = bitcoind.client.get_new_address(None, None).unwrap();
|
||||
bitcoind
|
||||
|
||||
43
src/types.rs
43
src/types.rs
@@ -10,6 +10,7 @@
|
||||
// licenses.
|
||||
|
||||
use std::convert::AsRef;
|
||||
use std::ops::Sub;
|
||||
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
||||
use bitcoin::{hash_types::Txid, util::psbt};
|
||||
@@ -65,10 +66,31 @@ impl FeeRate {
|
||||
FeeRate(1.0)
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and weight units (`wu`).
|
||||
pub fn from_wu(fee: u64, wu: usize) -> FeeRate {
|
||||
Self::from_vb(fee, wu.vbytes())
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and `vbytes`.
|
||||
pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate {
|
||||
let rate = fee as f32 / vbytes as f32;
|
||||
Self::from_sat_per_vb(rate)
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/vbyte
|
||||
pub fn as_sat_vb(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in weight units.
|
||||
pub fn fee_wu(&self, wu: usize) -> u64 {
|
||||
self.fee_vb(wu.vbytes())
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||
(self.as_sat_vb() * vbytes as f32).ceil() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for FeeRate {
|
||||
@@ -77,6 +99,27 @@ impl std::default::Default for FeeRate {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for FeeRate {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: FeeRate) -> Self::Output {
|
||||
FeeRate(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by types that can be used to measure weight units.
|
||||
pub trait Vbytes {
|
||||
/// Convert weight units to virtual bytes.
|
||||
fn vbytes(self) -> usize;
|
||||
}
|
||||
|
||||
impl Vbytes for usize {
|
||||
fn vbytes(self) -> usize {
|
||||
// ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
|
||||
(self as f32 / 4.0).ceil() as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// An unspent output owned by a [`Wallet`].
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
|
||||
@@ -115,8 +115,8 @@ mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||
use crate::wallet::AddressIndex::New;
|
||||
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestValidator;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::wallet::coin_selection::*;
|
||||
//! # use bdk::wallet::{self, coin_selection::*};
|
||||
//! # use bdk::database::Database;
|
||||
//! # use bdk::*;
|
||||
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
|
||||
@@ -41,7 +41,7 @@
|
||||
//! optional_utxos: Vec<WeightedUtxo>,
|
||||
//! fee_rate: FeeRate,
|
||||
//! amount_needed: u64,
|
||||
//! fee_amount: f32,
|
||||
//! fee_amount: u64,
|
||||
//! ) -> Result<CoinSelectionResult, bdk::Error> {
|
||||
//! let mut selected_amount = 0;
|
||||
//! let mut additional_weight = 0;
|
||||
@@ -57,9 +57,8 @@
|
||||
//! },
|
||||
//! )
|
||||
//! .collect::<Vec<_>>();
|
||||
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
|
||||
//! let amount_needed_with_fees =
|
||||
//! (fee_amount + additional_fees).ceil() as u64 + amount_needed;
|
||||
//! let additional_fees = fee_rate.fee_wu(additional_weight);
|
||||
//! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed;
|
||||
//! if amount_needed_with_fees > selected_amount {
|
||||
//! return Err(bdk::Error::InsufficientFunds {
|
||||
//! needed: amount_needed_with_fees,
|
||||
@@ -90,7 +89,6 @@
|
||||
//! ```
|
||||
|
||||
use crate::types::FeeRate;
|
||||
use crate::wallet::Vbytes;
|
||||
use crate::{database::Database, WeightedUtxo};
|
||||
use crate::{error::Error, Utxo};
|
||||
|
||||
@@ -118,7 +116,7 @@ pub struct CoinSelectionResult {
|
||||
/// List of outputs selected for use as inputs
|
||||
pub selected: Vec<Utxo>,
|
||||
/// Total fee amount in satoshi
|
||||
pub fee_amount: f32,
|
||||
pub fee_amount: u64,
|
||||
}
|
||||
|
||||
impl CoinSelectionResult {
|
||||
@@ -165,7 +163,7 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
|
||||
optional_utxos: Vec<WeightedUtxo>,
|
||||
fee_rate: FeeRate,
|
||||
amount_needed: u64,
|
||||
fee_amount: f32,
|
||||
fee_amount: u64,
|
||||
) -> Result<CoinSelectionResult, Error>;
|
||||
}
|
||||
|
||||
@@ -184,10 +182,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
||||
mut optional_utxos: Vec<WeightedUtxo>,
|
||||
fee_rate: FeeRate,
|
||||
amount_needed: u64,
|
||||
mut fee_amount: f32,
|
||||
mut fee_amount: u64,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
||||
|
||||
log::debug!(
|
||||
"amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
|
||||
amount_needed,
|
||||
@@ -212,9 +208,9 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
||||
.scan(
|
||||
(&mut selected_amount, &mut fee_amount),
|
||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
|
||||
if must_use || **selected_amount < amount_needed + **fee_amount {
|
||||
**fee_amount +=
|
||||
calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||
fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||
|
||||
log::debug!(
|
||||
@@ -231,7 +227,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let amount_needed_with_fees = amount_needed + (fee_amount.ceil() as u64);
|
||||
let amount_needed_with_fees = amount_needed + fee_amount;
|
||||
if selected_amount < amount_needed_with_fees {
|
||||
return Err(Error::InsufficientFunds {
|
||||
needed: amount_needed_with_fees,
|
||||
@@ -251,16 +247,15 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
||||
struct OutputGroup {
|
||||
weighted_utxo: WeightedUtxo,
|
||||
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate
|
||||
fee: f32,
|
||||
fee: u64,
|
||||
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
|
||||
effective_value: i64,
|
||||
}
|
||||
|
||||
impl OutputGroup {
|
||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||
let fee =
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight).vbytes() * fee_rate.as_sat_vb();
|
||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
|
||||
let fee = fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||
OutputGroup {
|
||||
weighted_utxo,
|
||||
fee,
|
||||
@@ -303,7 +298,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
||||
optional_utxos: Vec<WeightedUtxo>,
|
||||
fee_rate: FeeRate,
|
||||
amount_needed: u64,
|
||||
fee_amount: f32,
|
||||
fee_amount: u64,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
// Mapping every (UTXO, usize) to an output group
|
||||
let required_utxos: Vec<OutputGroup> = required_utxos
|
||||
@@ -325,7 +320,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let actual_target = fee_amount.ceil() as u64 + amount_needed;
|
||||
let actual_target = fee_amount + amount_needed;
|
||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
|
||||
|
||||
let expected = (curr_available_value + curr_value)
|
||||
@@ -345,6 +340,14 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
||||
.try_into()
|
||||
.expect("Bitcoin amount to fit into i64");
|
||||
|
||||
if curr_value > actual_target {
|
||||
return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
|
||||
vec![],
|
||||
required_utxos,
|
||||
fee_amount,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.bnb(
|
||||
required_utxos.clone(),
|
||||
@@ -378,7 +381,7 @@ impl BranchAndBoundCoinSelection {
|
||||
mut curr_value: i64,
|
||||
mut curr_available_value: i64,
|
||||
actual_target: i64,
|
||||
fee_amount: f32,
|
||||
fee_amount: u64,
|
||||
cost_of_change: f32,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
// current_selection[i] will contain true if we are using optional_utxos[i],
|
||||
@@ -486,7 +489,7 @@ impl BranchAndBoundCoinSelection {
|
||||
mut optional_utxos: Vec<OutputGroup>,
|
||||
curr_value: i64,
|
||||
actual_target: i64,
|
||||
fee_amount: f32,
|
||||
fee_amount: u64,
|
||||
) -> CoinSelectionResult {
|
||||
#[cfg(not(test))]
|
||||
optional_utxos.shuffle(&mut thread_rng());
|
||||
@@ -515,10 +518,10 @@ impl BranchAndBoundCoinSelection {
|
||||
fn calculate_cs_result(
|
||||
mut selected_utxos: Vec<OutputGroup>,
|
||||
mut required_utxos: Vec<OutputGroup>,
|
||||
mut fee_amount: f32,
|
||||
mut fee_amount: u64,
|
||||
) -> CoinSelectionResult {
|
||||
selected_utxos.append(&mut required_utxos);
|
||||
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
|
||||
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<u64>();
|
||||
let selected = selected_utxos
|
||||
.into_iter()
|
||||
.map(|u| u.weighted_utxo.utxo)
|
||||
@@ -540,6 +543,7 @@ mod test {
|
||||
use super::*;
|
||||
use crate::database::MemoryDatabase;
|
||||
use crate::types::*;
|
||||
use crate::wallet::Vbytes;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
@@ -547,52 +551,33 @@ mod test {
|
||||
|
||||
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
||||
|
||||
const FEE_AMOUNT: f32 = 50.0;
|
||||
const FEE_AMOUNT: u64 = 50;
|
||||
|
||||
fn utxo(value: u64, index: u32) -> WeightedUtxo {
|
||||
assert!(index < 10);
|
||||
let outpoint = OutPoint::from_str(&format!(
|
||||
"000000000000000000000000000000000000000000000000000000000000000{}:0",
|
||||
index
|
||||
))
|
||||
.unwrap();
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint,
|
||||
txout: TxOut {
|
||||
value,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_test_utxos() -> Vec<WeightedUtxo> {
|
||||
vec![
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"0000000000000000000000000000000000000000000000000000000000000000:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: 100_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
}),
|
||||
},
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: FEE_AMOUNT as u64 - 40,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
}),
|
||||
},
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"0000000000000000000000000000000000000000000000000000000000000002:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: 200_000,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::Internal,
|
||||
}),
|
||||
},
|
||||
utxo(100_000, 0),
|
||||
utxo(FEE_AMOUNT as u64 - 40, 1),
|
||||
utxo(200_000, 2),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -656,13 +641,13 @@ mod test {
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
250_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 254)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -677,13 +662,13 @@ mod test {
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
20_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 254);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -698,13 +683,13 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
20_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 1);
|
||||
assert_eq!(result.selected_amount(), 200_000);
|
||||
assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 118);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -720,7 +705,7 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
500_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -738,7 +723,7 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
250_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -758,13 +743,13 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
250_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 254);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -785,7 +770,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 254);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -806,7 +791,38 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.fee_amount, 254);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bnb_coin_selection_required_not_enough() {
|
||||
let utxos = get_test_utxos();
|
||||
let database = MemoryDatabase::default();
|
||||
|
||||
let required = vec![utxos[0].clone()];
|
||||
let mut optional = utxos[1..].to_vec();
|
||||
optional.push(utxo(500_000, 3));
|
||||
|
||||
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
|
||||
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
|
||||
assert_eq!(amount, 100_000);
|
||||
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
|
||||
assert!(amount > 150_000);
|
||||
|
||||
let result = BranchAndBoundCoinSelection::default()
|
||||
.coin_select(
|
||||
&database,
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
150_000,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount as f32 - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -822,7 +838,7 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
500_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -840,7 +856,7 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
250_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -857,7 +873,7 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
99932, // first utxo's effective value
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -865,7 +881,7 @@ mod test {
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_WITNESS_SIZE).vbytes();
|
||||
let epsilon = 0.5;
|
||||
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
|
||||
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < epsilon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -884,7 +900,7 @@ mod test {
|
||||
optional_utxos,
|
||||
FeeRate::from_sat_per_vb(0.0),
|
||||
target_amount,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.selected_amount(), target_amount);
|
||||
@@ -911,7 +927,7 @@ mod test {
|
||||
0,
|
||||
curr_available_value,
|
||||
20_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
cost_of_change,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -938,7 +954,7 @@ mod test {
|
||||
0,
|
||||
curr_available_value,
|
||||
20_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
cost_of_change,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -950,7 +966,6 @@ mod test {
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
|
||||
let fee_amount = 50.0;
|
||||
|
||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||
.into_iter()
|
||||
@@ -972,12 +987,12 @@ mod test {
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
fee_amount,
|
||||
FEE_AMOUNT,
|
||||
cost_of_change,
|
||||
)
|
||||
.unwrap();
|
||||
assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
assert_eq!(result.fee_amount, 186);
|
||||
}
|
||||
|
||||
// TODO: bnb() function should be optimized, and this test should be done with more utxos
|
||||
@@ -1009,7 +1024,7 @@ mod test {
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
0.0,
|
||||
0,
|
||||
0.0,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1035,12 +1050,10 @@ mod test {
|
||||
utxos,
|
||||
0,
|
||||
target_amount as i64,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
);
|
||||
|
||||
assert!(result.selected_amount() > target_amount);
|
||||
assert!(
|
||||
(result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
|
||||
);
|
||||
assert_eq!(result.fee_amount, (50 + result.selected.len() * 68) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
@@ -55,6 +56,7 @@ use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
||||
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LIMIT_SATOSHI};
|
||||
|
||||
use crate::blockchain::{Blockchain, Progress};
|
||||
use crate::database::memory::MemoryDatabase;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
@@ -66,6 +68,7 @@ use crate::descriptor::{
|
||||
use crate::error::Error;
|
||||
use crate::psbt::PsbtUtils;
|
||||
use crate::signer::SignerError;
|
||||
use crate::testutils;
|
||||
use crate::types::*;
|
||||
|
||||
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
|
||||
@@ -167,6 +170,11 @@ where
|
||||
secp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Bitcoin network the wallet is using.
|
||||
pub fn network(&self) -> Network {
|
||||
self.network
|
||||
}
|
||||
}
|
||||
|
||||
/// The address index selection strategy to use to derived an address from the wallet's external
|
||||
@@ -543,7 +551,7 @@ where
|
||||
});
|
||||
}
|
||||
}
|
||||
(FeeRate::from_sat_per_vb(0.0), *fee as f32)
|
||||
(FeeRate::from_sat_per_vb(0.0), *fee)
|
||||
}
|
||||
FeePolicy::FeeRate(rate) => {
|
||||
if let Some(previous_fee) = params.bumping_fee {
|
||||
@@ -554,7 +562,7 @@ where
|
||||
});
|
||||
}
|
||||
}
|
||||
(*rate, 0.0)
|
||||
(*rate, 0)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -573,8 +581,7 @@ where
|
||||
let mut outgoing: u64 = 0;
|
||||
let mut received: u64 = 0;
|
||||
|
||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
||||
fee_amount += calc_fee_bytes(tx.get_weight());
|
||||
fee_amount += fee_rate.fee_wu(tx.get_weight());
|
||||
|
||||
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
|
||||
|
||||
@@ -591,7 +598,7 @@ where
|
||||
script_pubkey: script_pubkey.clone(),
|
||||
value,
|
||||
};
|
||||
fee_amount += calc_fee_bytes(serialize(&new_out).len() * 4);
|
||||
fee_amount += fee_rate.fee_vb(serialize(&new_out).len());
|
||||
|
||||
tx.output.push(new_out);
|
||||
|
||||
@@ -649,9 +656,8 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
fee_amount += calc_fee_bytes(serialize(&drain_output).len() * 4);
|
||||
fee_amount += fee_rate.fee_vb(serialize(&drain_output).len());
|
||||
|
||||
let mut fee_amount = fee_amount.ceil() as u64;
|
||||
let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||
|
||||
if tx.output.is_empty() {
|
||||
@@ -754,8 +760,10 @@ where
|
||||
return Err(Error::IrreplaceableTransaction);
|
||||
}
|
||||
|
||||
let vbytes = tx.get_weight().vbytes();
|
||||
let feerate = details.fee.ok_or(Error::FeeRateUnavailable)? as f32 / vbytes;
|
||||
let feerate = FeeRate::from_wu(
|
||||
details.fee.ok_or(Error::FeeRateUnavailable)?,
|
||||
tx.get_weight(),
|
||||
);
|
||||
|
||||
// remove the inputs from the tx and process them
|
||||
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
|
||||
@@ -832,7 +840,7 @@ where
|
||||
utxos: original_utxos,
|
||||
bumping_fee: Some(tx_builder::PreviousFee {
|
||||
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
|
||||
rate: feerate,
|
||||
rate: feerate.as_sat_vb(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1534,11 +1542,6 @@ where
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Get the Bitcoin network the wallet is using.
|
||||
pub fn network(&self) -> Network {
|
||||
self.network
|
||||
}
|
||||
|
||||
/// Broadcast a transaction to the network
|
||||
#[maybe_async]
|
||||
pub fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
|
||||
@@ -1548,31 +1551,60 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by types that can be used to measure weight units.
|
||||
pub trait Vbytes {
|
||||
/// Convert weight units to virtual bytes.
|
||||
fn vbytes(self) -> f32;
|
||||
}
|
||||
/// Return a fake wallet that appears to be funded for testing.
|
||||
pub fn get_funded_wallet(
|
||||
descriptor: &str,
|
||||
) -> (
|
||||
Wallet<(), MemoryDatabase>,
|
||||
(String, Option<String>),
|
||||
bitcoin::Txid,
|
||||
) {
|
||||
let descriptors = testutils!(@descriptors (descriptor));
|
||||
let wallet = Wallet::new_offline(
|
||||
&descriptors.0,
|
||||
None,
|
||||
Network::Regtest,
|
||||
MemoryDatabase::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
impl Vbytes for usize {
|
||||
fn vbytes(self) -> f32 {
|
||||
self as f32 / 4.0
|
||||
}
|
||||
let funding_address_kix = 0;
|
||||
|
||||
let tx_meta = testutils! {
|
||||
@tx ( (@external descriptors, funding_address_kix) => 50_000 ) (@confirmations 1)
|
||||
};
|
||||
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_script_pubkey(
|
||||
&bitcoin::Address::from_str(&tx_meta.output.get(0).unwrap().to_address)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
KeychainKind::External,
|
||||
funding_address_kix,
|
||||
)
|
||||
.unwrap();
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_last_index(KeychainKind::External, funding_address_kix)
|
||||
.unwrap();
|
||||
|
||||
let txid = crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, Some(100));
|
||||
|
||||
(wallet, descriptors, txid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::{util::psbt, Network};
|
||||
|
||||
use crate::database::memory::MemoryDatabase;
|
||||
use crate::database::Database;
|
||||
use crate::types::KeychainKind;
|
||||
|
||||
use super::*;
|
||||
use crate::signer::{SignOptions, SignerError};
|
||||
use crate::testutils;
|
||||
use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
|
||||
|
||||
#[test]
|
||||
@@ -1684,50 +1716,6 @@ pub(crate) mod test {
|
||||
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
|
||||
}
|
||||
|
||||
pub(crate) fn get_funded_wallet(
|
||||
descriptor: &str,
|
||||
) -> (
|
||||
Wallet<(), MemoryDatabase>,
|
||||
(String, Option<String>),
|
||||
bitcoin::Txid,
|
||||
) {
|
||||
let descriptors = testutils!(@descriptors (descriptor));
|
||||
let wallet = Wallet::new_offline(
|
||||
&descriptors.0,
|
||||
None,
|
||||
Network::Regtest,
|
||||
MemoryDatabase::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let funding_address_kix = 0;
|
||||
|
||||
let tx_meta = testutils! {
|
||||
@tx ( (@external descriptors, funding_address_kix) => 50_000 ) (@confirmations 1)
|
||||
};
|
||||
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_script_pubkey(
|
||||
&bitcoin::Address::from_str(&tx_meta.output.get(0).unwrap().to_address)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
KeychainKind::External,
|
||||
funding_address_kix,
|
||||
)
|
||||
.unwrap();
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_last_index(KeychainKind::External, funding_address_kix)
|
||||
.unwrap();
|
||||
|
||||
let txid = crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, Some(100));
|
||||
|
||||
(wallet, descriptors, txid)
|
||||
}
|
||||
|
||||
macro_rules! assert_fee_rate {
|
||||
($tx:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
|
||||
let mut tx = $tx.clone();
|
||||
@@ -1746,13 +1734,13 @@ pub(crate) mod test {
|
||||
dust_change = true;
|
||||
)*
|
||||
|
||||
let tx_fee_rate = $fees as f32 / (tx.get_weight().vbytes());
|
||||
let fee_rate = $fee_rate.as_sat_vb();
|
||||
let tx_fee_rate = FeeRate::from_wu($fees, tx.get_weight());
|
||||
let fee_rate = $fee_rate;
|
||||
|
||||
if !dust_change {
|
||||
assert!((tx_fee_rate - fee_rate).abs() < 0.5, "Expected fee rate of {}, the tx has {}", fee_rate, tx_fee_rate);
|
||||
assert!((tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
} else {
|
||||
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {}, the tx has {}", fee_rate, tx_fee_rate);
|
||||
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user