Compare commits
37 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5557dce70 | ||
|
|
7b97c956c7 | ||
|
|
e5aa4fe9e6 | ||
|
|
2580013912 | ||
|
|
380bc4025a | ||
|
|
7c1861aab9 | ||
|
|
80e190b3e7 | ||
|
|
7c9ba3cfc8 | ||
|
|
2462e90415 | ||
|
|
04d0ab5a97 | ||
|
|
4edf533b67 | ||
|
|
6e648fd5af | ||
|
|
a837cd349b | ||
|
|
0eb1ac2bcb | ||
|
|
6e8a4a8966 | ||
|
|
475a77219a | ||
|
|
0d64beb040 | ||
|
|
89608ddd0f | ||
|
|
09bd86e2d8 | ||
|
|
004957dc29 | ||
|
|
fc637a7bcc | ||
|
|
ec1c5f4cf8 | ||
|
|
06d7dc5c3a | ||
|
|
c01983d02a | ||
|
|
fef70d5e8f | ||
|
|
c3544c9b8c | ||
|
|
5840ce473e | ||
|
|
b290b29502 | ||
|
|
8c78a42163 | ||
|
|
d77a7f2ff1 | ||
|
|
3d44ffaef2 | ||
|
|
2efa299d04 | ||
|
|
2647aff4bc | ||
|
|
c151d8fd23 | ||
|
|
2c324d3759 | ||
|
|
5489f905a4 | ||
|
|
022d5a21cf |
1
.github/workflows/cont_integration.yml
vendored
1
.github/workflows/cont_integration.yml
vendored
@@ -32,7 +32,6 @@ jobs:
|
||||
run: |
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
cargo update -p jobserver --precise "0.1.26"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
- name: Build
|
||||
run: cargo build ${{ matrix.features }}
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
run: rustup update
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Pin dependencies for MSRV
|
||||
run: cargo update -p home --precise "0.5.5"
|
||||
- name: Build docs
|
||||
run: cargo doc --no-deps
|
||||
env:
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
"crates/hwi",
|
||||
"crates/testenv",
|
||||
"example-crates/example_cli",
|
||||
"example-crates/example_electrum",
|
||||
"example-crates/example_esplora",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.6"
|
||||
version = "1.0.0-alpha.8"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -18,7 +18,7 @@ miniscript = { version = "10.0.0", features = ["serde"], default-features = fals
|
||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.10.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = ["miniscript", "serde"], default-features = false }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
## `bdk`
|
||||
|
||||
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
|
||||
The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
|
||||
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
||||
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
||||
construct a wallet. It has two keychains (external and internal) which are defined by
|
||||
@@ -34,51 +34,51 @@ construct a wallet. It has two keychains (external and internal) which are defin
|
||||
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
||||
can create and sign transactions.
|
||||
|
||||
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
|
||||
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
|
||||
|
||||
### Blockchain data
|
||||
|
||||
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
|
||||
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
|
||||
|
||||
This can be created manually or from blockchain-scanning crates.
|
||||
In order to get blockchain data for `Wallet` to consume, you should configure a client from
|
||||
an available chain source. Typically you make a request to the chain source and get a response
|
||||
that the `Wallet` can use to update its view of the chain.
|
||||
|
||||
**Blockchain Data Sources**
|
||||
|
||||
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
||||
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
|
||||
* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
|
||||
|
||||
**Examples**
|
||||
|
||||
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
|
||||
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
|
||||
* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
|
||||
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
||||
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
|
||||
|
||||
### Persistence
|
||||
|
||||
To persist the `Wallet` on disk, `Wallet` needs to be constructed with a
|
||||
[`Persist`](https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainPersist.html) implementation.
|
||||
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
|
||||
|
||||
**Implementations**
|
||||
|
||||
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
|
||||
* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
|
||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||
```rust,compile_fail
|
||||
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
||||
|
||||
fn main() {
|
||||
// a type that implements `Persist`
|
||||
let db = ();
|
||||
// Create a new file `Store`.
|
||||
let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
|
||||
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
|
||||
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||
let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
|
||||
|
||||
// get a new address (this increments revealed derivation index)
|
||||
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
|
||||
println!("staged changes: {:?}", wallet.staged());
|
||||
// persist changes
|
||||
wallet.commit().expect("must save");
|
||||
// Insert a single `TxOut` at `OutPoint` into the wallet.
|
||||
let _ = wallet.insert_txout(outpoint, txout);
|
||||
wallet.commit().expect("must write to database");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -218,9 +218,11 @@ submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
||||
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
|
||||
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
|
||||
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
|
||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||
|
||||
use crate::FeeRate;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::Amount;
|
||||
use bitcoin::FeeRate;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
// TODO upstream the functions here to `rust-bitcoin`?
|
||||
@@ -65,7 +66,7 @@ impl PsbtUtils for Psbt {
|
||||
let fee_amount = self.fee_amount();
|
||||
fee_amount.map(|fee| {
|
||||
let weight = self.clone().extract_tx().weight();
|
||||
FeeRate::from_wu(fee, weight)
|
||||
Amount::from_sat(fee) / weight
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
use core::ops::Sub;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
use bitcoin::psbt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -47,116 +46,6 @@ impl AsRef<[u8]> for KeychainKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fee rate
|
||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||
// Internally stored as satoshi/vbyte
|
||||
pub struct FeeRate(f32);
|
||||
|
||||
impl FeeRate {
|
||||
/// Create a new instance checking the value provided
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
fn new_checked(value: f32) -> Self {
|
||||
assert!(value.is_normal() || value == 0.0);
|
||||
assert!(value.is_sign_positive());
|
||||
|
||||
FeeRate(value)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
||||
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
||||
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
||||
FeeRate::new_checked(btc_per_kvb * 1e5)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_vb)
|
||||
}
|
||||
|
||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||
pub const fn default_min_relay_fee() -> Self {
|
||||
FeeRate(1.0)
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and weight units (`wu`).
|
||||
pub fn from_wu(fee: u64, wu: Weight) -> FeeRate {
|
||||
Self::from_vb(fee, wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// 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_per_vb(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/kwu
|
||||
pub fn sat_per_kwu(&self) -> f32 {
|
||||
self.0 * 250.0_f32
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in weight units.
|
||||
pub fn fee_wu(&self, wu: Weight) -> u64 {
|
||||
self.fee_vb(wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FeeRate {
|
||||
fn default() -> Self {
|
||||
FeeRate::default_min_relay_fee()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -244,73 +133,3 @@ impl Utxo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_store_feerate_in_const() {
|
||||
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_neg_zero() {
|
||||
let _ = FeeRate::from_sat_per_vb(-0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_neg_value() {
|
||||
let _ = FeeRate::from_sat_per_vb(-5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_nan() {
|
||||
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_inf() {
|
||||
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_feerate_pos_zero() {
|
||||
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_btc_per_kvb() {
|
||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_vbyte() {
|
||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_default_min_relay_fee() {
|
||||
let fee = FeeRate::default_min_relay_fee();
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_kvb() {
|
||||
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_kwu() {
|
||||
let fee = FeeRate::from_sat_per_kwu(250.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
assert_eq!(fee.sat_per_kwu(), 250.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
//! &self,
|
||||
//! required_utxos: Vec<WeightedUtxo>,
|
||||
//! optional_utxos: Vec<WeightedUtxo>,
|
||||
//! fee_rate: bdk::FeeRate,
|
||||
//! fee_rate: FeeRate,
|
||||
//! target_amount: u64,
|
||||
//! drain_script: &Script,
|
||||
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
||||
@@ -61,7 +61,7 @@
|
||||
//! },
|
||||
//! )
|
||||
//! .collect::<Vec<_>>();
|
||||
//! let additional_fees = fee_rate.fee_wu(additional_weight);
|
||||
//! let additional_fees = (fee_rate * additional_weight).to_sat();
|
||||
//! let amount_needed_with_fees = additional_fees + target_amount;
|
||||
//! if selected_amount < amount_needed_with_fees {
|
||||
//! return Err(coin_selection::Error::InsufficientFunds {
|
||||
@@ -101,10 +101,10 @@
|
||||
//! ```
|
||||
|
||||
use crate::chain::collections::HashSet;
|
||||
use crate::types::FeeRate;
|
||||
use crate::wallet::utils::IsDust;
|
||||
use crate::Utxo;
|
||||
use crate::WeightedUtxo;
|
||||
use bitcoin::FeeRate;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::consensus::encode::serialize;
|
||||
@@ -313,7 +313,8 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
|
||||
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
|
||||
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
|
||||
let drain_output_len = serialize(drain_script).len() + 8usize;
|
||||
let change_fee = fee_rate.fee_vb(drain_output_len);
|
||||
let change_fee =
|
||||
(fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
|
||||
let drain_val = remaining_amount.saturating_sub(change_fee);
|
||||
|
||||
if drain_val.is_dust(drain_script) {
|
||||
@@ -344,9 +345,12 @@ fn select_sorted_utxos(
|
||||
(&mut selected_amount, &mut fee_amount),
|
||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||
if must_use || **selected_amount < target_amount + **fee_amount {
|
||||
**fee_amount += fee_rate.fee_wu(Weight::from_wu(
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
));
|
||||
**fee_amount += (fee_rate
|
||||
* Weight::from_wu(
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
))
|
||||
.to_sat();
|
||||
|
||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||
Some(weighted_utxo.utxo)
|
||||
} else {
|
||||
@@ -387,9 +391,10 @@ struct OutputGroup {
|
||||
|
||||
impl OutputGroup {
|
||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||
let fee = fee_rate.fee_wu(Weight::from_wu(
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
));
|
||||
let fee = (fee_rate
|
||||
* Weight::from_wu((TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64))
|
||||
.to_sat();
|
||||
|
||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||
OutputGroup {
|
||||
weighted_utxo,
|
||||
@@ -456,7 +461,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||
let cost_of_change =
|
||||
(Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
|
||||
|
||||
// `curr_value` and `curr_available_value` are both the sum of *effective_values* of
|
||||
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
|
||||
@@ -547,7 +553,7 @@ impl BranchAndBoundCoinSelection {
|
||||
mut curr_value: i64,
|
||||
mut curr_available_value: i64,
|
||||
target_amount: i64,
|
||||
cost_of_change: f32,
|
||||
cost_of_change: u64,
|
||||
drain_script: &Script,
|
||||
fee_rate: FeeRate,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
@@ -738,12 +744,11 @@ mod test {
|
||||
use core::str::FromStr;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::{OutPoint, ScriptBuf, TxOut};
|
||||
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut};
|
||||
|
||||
use super::*;
|
||||
use crate::types::*;
|
||||
use crate::wallet::coin_selection::filter_duplicates;
|
||||
use crate::wallet::Vbytes;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
@@ -893,7 +898,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -914,7 +919,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -935,7 +940,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -957,7 +962,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -975,7 +980,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -992,7 +997,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1013,7 +1018,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1034,7 +1039,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1056,7 +1061,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1075,7 +1080,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1096,7 +1101,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1117,7 +1122,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos.clone(),
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1138,7 +1143,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1175,7 +1180,7 @@ mod test {
|
||||
.coin_select(
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1197,7 +1202,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1215,7 +1220,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1227,22 +1232,18 @@ mod test {
|
||||
let utxos = get_test_utxos();
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 99932; // first utxo's effective value
|
||||
let feerate = FeeRate::BROADCAST_MIN;
|
||||
|
||||
let result = BranchAndBoundCoinSelection::new(0)
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
.coin_select(vec![], utxos, feerate, target_amount, &drain_script)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 1);
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
|
||||
let input_weight = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE) as u64;
|
||||
// the final fee rate should be exactly the same as the fee rate given
|
||||
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
|
||||
let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
|
||||
assert_eq!(result_feerate, feerate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1258,7 +1259,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
optional_utxos,
|
||||
FeeRate::from_sat_per_vb(0.0),
|
||||
FeeRate::ZERO,
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1270,7 +1271,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "BnBNoExactMatch")]
|
||||
fn test_bnb_function_no_exact_match() {
|
||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
||||
let utxos: Vec<OutputGroup> = get_test_utxos()
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1279,7 +1280,7 @@ mod test {
|
||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
@@ -1300,7 +1301,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
||||
fn test_bnb_function_tries_exceeded() {
|
||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
||||
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1309,7 +1310,7 @@ mod test {
|
||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
@@ -1331,9 +1332,9 @@ mod test {
|
||||
// The match won't be exact but still in the range
|
||||
#[test]
|
||||
fn test_bnb_function_almost_exact_match_with_fees() {
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
||||
|
||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||
.into_iter()
|
||||
@@ -1346,7 +1347,7 @@ mod test {
|
||||
|
||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||
// cost_of_change + 5.
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
@@ -1371,7 +1372,7 @@ mod test {
|
||||
fn test_bnb_function_exact_match_more_utxos() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let fee_rate = FeeRate::from_sat_per_vb(0.0);
|
||||
let fee_rate = FeeRate::ZERO;
|
||||
|
||||
for _ in 0..200 {
|
||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||
@@ -1397,7 +1398,7 @@ mod test {
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
0.0,
|
||||
0,
|
||||
&drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
@@ -1413,7 +1414,7 @@ mod test {
|
||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let utxos: Vec<OutputGroup> = utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1442,7 +1443,7 @@ mod test {
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(10),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
@@ -1468,7 +1469,7 @@ mod test {
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(10),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
@@ -1490,7 +1491,7 @@ mod test {
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(10_000.0),
|
||||
FeeRate::from_sat_per_vb_unchecked(10_000),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
use crate::descriptor::policy::PolicyError;
|
||||
use crate::descriptor::DescriptorError;
|
||||
use crate::wallet::coin_selection;
|
||||
use crate::{descriptor, FeeRate, KeychainKind};
|
||||
use crate::{descriptor, KeychainKind};
|
||||
use alloc::string::String;
|
||||
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
||||
use core::fmt;
|
||||
@@ -83,8 +83,8 @@ pub enum CreateTxError<P> {
|
||||
},
|
||||
/// When bumping a tx the fee rate requested is lower than required
|
||||
FeeRateTooLow {
|
||||
/// Required fee rate (satoshi/vbyte)
|
||||
required: FeeRate,
|
||||
/// Required fee rate
|
||||
required: bitcoin::FeeRate,
|
||||
},
|
||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||
NoUtxosSelected,
|
||||
@@ -168,8 +168,10 @@ where
|
||||
CreateTxError::FeeRateTooLow { required } => {
|
||||
write!(
|
||||
f,
|
||||
"Fee rate too low: required {} sat/vbyte",
|
||||
required.as_sat_per_vb()
|
||||
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
|
||||
//"Fee rate too low: required {required:#}"
|
||||
"Fee rate too low: required {} sat/vb",
|
||||
crate::floating_rate!(required)
|
||||
)
|
||||
}
|
||||
CreateTxError::NoUtxosSelected => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
|
||||
@@ -33,8 +33,8 @@ use bdk_chain::{
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||
use bitcoin::{
|
||||
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
|
||||
Txid, Weight, Witness,
|
||||
absolute, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction,
|
||||
TxOut, Txid, Weight, Witness,
|
||||
};
|
||||
use bitcoin::{consensus::encode::serialize, BlockHash};
|
||||
use bitcoin::{constants::genesis_block, psbt};
|
||||
@@ -986,10 +986,8 @@ impl<D> Wallet<D> {
|
||||
/// ```
|
||||
/// [`insert_txout`]: Self::insert_txout
|
||||
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
|
||||
self.calculate_fee(tx).map(|fee| {
|
||||
let weight = tx.weight();
|
||||
FeeRate::from_wu(fee, weight)
|
||||
})
|
||||
self.calculate_fee(tx)
|
||||
.map(|fee| bitcoin::Amount::from_sat(fee) / tx.weight())
|
||||
}
|
||||
|
||||
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
||||
@@ -1432,32 +1430,31 @@ impl<D> Wallet<D> {
|
||||
(Some(rbf), _) => rbf.get_value(),
|
||||
};
|
||||
|
||||
let (fee_rate, mut fee_amount) = match params
|
||||
.fee_policy
|
||||
.as_ref()
|
||||
.unwrap_or(&FeePolicy::FeeRate(FeeRate::default()))
|
||||
{
|
||||
let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() {
|
||||
//FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
|
||||
FeePolicy::FeeAmount(fee) => {
|
||||
if let Some(previous_fee) = params.bumping_fee {
|
||||
if *fee < previous_fee.absolute {
|
||||
if fee < previous_fee.absolute {
|
||||
return Err(CreateTxError::FeeTooLow {
|
||||
required: previous_fee.absolute,
|
||||
});
|
||||
}
|
||||
}
|
||||
(FeeRate::from_sat_per_vb(0.0), *fee)
|
||||
(FeeRate::ZERO, fee)
|
||||
}
|
||||
FeePolicy::FeeRate(rate) => {
|
||||
if let Some(previous_fee) = params.bumping_fee {
|
||||
let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0);
|
||||
if *rate < required_feerate {
|
||||
let required_feerate = FeeRate::from_sat_per_kwu(
|
||||
previous_fee.rate.to_sat_per_kwu()
|
||||
+ FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb
|
||||
);
|
||||
if rate < required_feerate {
|
||||
return Err(CreateTxError::FeeRateTooLow {
|
||||
required: required_feerate,
|
||||
});
|
||||
}
|
||||
}
|
||||
(*rate, 0)
|
||||
(rate, 0)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1500,7 +1497,7 @@ impl<D> Wallet<D> {
|
||||
outgoing += value;
|
||||
}
|
||||
|
||||
fee_amount += fee_rate.fee_wu(tx.weight());
|
||||
fee_amount += (fee_rate * tx.weight()).to_sat();
|
||||
|
||||
// Segwit transactions' header is 2WU larger than legacy txs' header,
|
||||
// as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144).
|
||||
@@ -1511,7 +1508,7 @@ impl<D> Wallet<D> {
|
||||
// end up with a transaction with a slightly higher fee rate than the requested one.
|
||||
// If, instead, we undershoot, we may end up with a feerate lower than the requested one
|
||||
// - we might come up with non broadcastable txs!
|
||||
fee_amount += fee_rate.fee_wu(Weight::from_wu(2));
|
||||
fee_amount += (fee_rate * Weight::from_wu(2)).to_sat();
|
||||
|
||||
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
||||
&& internal_descriptor.is_none()
|
||||
@@ -1652,7 +1649,7 @@ impl<D> Wallet<D> {
|
||||
/// let mut psbt = {
|
||||
/// let mut builder = wallet.build_fee_bump(tx.txid())?;
|
||||
/// builder
|
||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0));
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
@@ -1780,7 +1777,7 @@ impl<D> Wallet<D> {
|
||||
utxos: original_utxos,
|
||||
bumping_fee: Some(tx_builder::PreviousFee {
|
||||
absolute: fee,
|
||||
rate: fee_rate.as_sat_per_vb(),
|
||||
rate: fee_rate,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1974,6 +1971,15 @@ impl<D> Wallet<D> {
|
||||
if sign_options.remove_partial_sigs {
|
||||
psbt_input.partial_sigs.clear();
|
||||
}
|
||||
if sign_options.remove_taproot_extras {
|
||||
// We just constructed the final witness, clear these fields.
|
||||
psbt_input.tap_key_sig = None;
|
||||
psbt_input.tap_script_sigs.clear();
|
||||
psbt_input.tap_scripts.clear();
|
||||
psbt_input.tap_key_origins.clear();
|
||||
psbt_input.tap_internal_key = None;
|
||||
psbt_input.tap_merkle_root = None;
|
||||
}
|
||||
}
|
||||
Err(_) => finished = false,
|
||||
}
|
||||
@@ -1982,6 +1988,12 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
}
|
||||
|
||||
if finished && sign_options.remove_taproot_extras {
|
||||
for output in &mut psbt.outputs {
|
||||
output.tap_key_origins.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(finished)
|
||||
}
|
||||
|
||||
@@ -2566,6 +2578,17 @@ fn create_signers<E: IntoWalletDescriptor>(
|
||||
Ok((signers, change_signers))
|
||||
}
|
||||
|
||||
/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! floating_rate {
|
||||
($rate:expr) => {{
|
||||
use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
|
||||
// sat_kwu / 250.0 -> sat_vb
|
||||
$rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64)
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
/// Macro for getting a wallet for use in a doctest
|
||||
|
||||
@@ -782,6 +782,16 @@ pub struct SignOptions {
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to remove taproot specific fields from the PSBT on finalization.
|
||||
///
|
||||
/// For inputs this includes the taproot internal key, merkle root, and individual
|
||||
/// scripts and signatures. For both inputs and outputs it includes key origin info.
|
||||
///
|
||||
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
|
||||
///
|
||||
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
|
||||
pub remove_taproot_extras: bool,
|
||||
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||
@@ -827,6 +837,7 @@ impl Default for SignOptions {
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
remove_taproot_extras: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||
//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
//! // Only spend non-change outputs
|
||||
//! .do_not_spend_change()
|
||||
//! // Turn on RBF signaling
|
||||
@@ -40,22 +40,20 @@
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::collections::HashSet;
|
||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use bdk_chain::PersistBackend;
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bdk_chain::PersistBackend;
|
||||
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use super::ChangeSet;
|
||||
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
|
||||
use crate::wallet::CreateTxError;
|
||||
use crate::{Utxo, Wallet};
|
||||
use super::{ChangeSet, CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
|
||||
/// Context in which the [`TxBuilder`] is valid
|
||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||
@@ -163,7 +161,7 @@ pub(crate) struct TxParams {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct PreviousFee {
|
||||
pub absolute: u64,
|
||||
pub rate: f32,
|
||||
pub rate: FeeRate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -174,7 +172,7 @@ pub(crate) enum FeePolicy {
|
||||
|
||||
impl Default for FeePolicy {
|
||||
fn default() -> Self {
|
||||
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
|
||||
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,15 +188,13 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate
|
||||
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
||||
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
|
||||
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
|
||||
/// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb
|
||||
/// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
|
||||
/// Default is 1 sat/vB (see min_relay_fee)
|
||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate.
|
||||
///
|
||||
/// This method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to `fee_rate` times the size
|
||||
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
|
||||
/// relay policy.
|
||||
///
|
||||
/// Note that this is really a minimum feerate -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
@@ -574,20 +570,6 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
@@ -634,6 +616,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
|
||||
pub enum AddUtxoError {
|
||||
@@ -779,7 +777,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
/// .enable_rbf();
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||
use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||
use std::str::FromStr;
|
||||
|
||||
// Return a fake wallet that appears to be funded for testing.
|
||||
@@ -154,3 +154,16 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
||||
pub fn get_test_tr_dup_keys() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
|
||||
/// useful in cases where we want to create a feerate from a `f64`, as the
|
||||
/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
|
||||
///
|
||||
/// **Note** this 'quick and dirty' conversion should only be used when the input
|
||||
/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
|
||||
/// or else the resulting value will be inaccurate.
|
||||
pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
||||
// 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
|
||||
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||
FeeRate::from_sat_per_kwu(sat_kwu)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use bdk::bitcoin::FeeRate;
|
||||
use bdk::bitcoin::TxIn;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{psbt, FeeRate, SignOptions};
|
||||
use bdk::{psbt, SignOptions};
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use core::str::FromStr;
|
||||
mod common;
|
||||
@@ -82,13 +83,13 @@ fn test_psbt_sign_with_finalized() {
|
||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
@@ -99,21 +100,21 @@ fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
@@ -123,21 +124,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_missing_txout() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wpkh_wallet.get_address(New);
|
||||
let mut builder = wpkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
let mut wpkh_psbt = builder.finish().unwrap();
|
||||
|
||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||
@@ -149,7 +150,7 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
let addr = pkh_wallet.get_address(New);
|
||||
let mut builder = pkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
let mut pkh_psbt = builder.finish().unwrap();
|
||||
|
||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
@@ -161,16 +162,26 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
fn test_psbt_multiple_internalkey_signers() {
|
||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk::KeychainKind;
|
||||
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
|
||||
use miniscript::psbt::PsbtExt;
|
||||
use bitcoin::key::TapTweak;
|
||||
use bitcoin::secp256k1::{schnorr, KeyPair, Message, Secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
||||
use bitcoin::{PrivateKey, TxOut};
|
||||
use std::sync::Arc;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
|
||||
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
|
||||
let desc = format!("tr({})", wif);
|
||||
let prv = PrivateKey::from_wif(wif).unwrap();
|
||||
let keypair = KeyPair::from_secret_key(&secp, &prv.inner);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(&desc);
|
||||
let to_spend = wallet.get_balance().total();
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
builder.drain_to(send_to.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let unsigned_tx = psbt.unsigned_tx.clone();
|
||||
|
||||
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
||||
wallet.add_signer(
|
||||
KeychainKind::External,
|
||||
@@ -183,10 +194,32 @@ fn test_psbt_multiple_internalkey_signers() {
|
||||
},
|
||||
)),
|
||||
);
|
||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
// Checks that we signed using the right key
|
||||
assert!(
|
||||
psbt.finalize_mut(&secp).is_ok(),
|
||||
"The wrong internal key was used"
|
||||
);
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
// To verify, we need the signature, message, and pubkey
|
||||
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
|
||||
assert!(!witness.is_empty());
|
||||
let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
|
||||
|
||||
// the prevout we're spending
|
||||
let prevouts = &[TxOut {
|
||||
script_pubkey: send_to.script_pubkey(),
|
||||
value: to_spend,
|
||||
}];
|
||||
let prevouts = Prevouts::All(prevouts);
|
||||
let input_index = 0;
|
||||
let mut sighash_cache = SighashCache::new(unsigned_tx);
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
|
||||
.unwrap();
|
||||
let message = Message::from(sighash);
|
||||
|
||||
// add tweak. this was taken from `signer::sign_psbt_schnorr`
|
||||
let keypair = keypair.tap_tweak(&secp, None).to_inner();
|
||||
let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
|
||||
|
||||
// Must verify if we used the correct key to sign
|
||||
let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
|
||||
assert!(verify_res.is_ok(), "The wrong internal key was used");
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ use bdk::wallet::error::CreateTxError;
|
||||
use bdk::wallet::tx_builder::AddForeignUtxoError;
|
||||
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
|
||||
use bdk::wallet::{AddressIndex::*, NewError};
|
||||
use bdk::{FeeRate, KeychainKind};
|
||||
use bdk::KeychainKind;
|
||||
use bdk_chain::COINBASE_MATURITY;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::psbt;
|
||||
use bitcoin::script::PushBytesBuf;
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||
use bitcoin::ScriptBuf;
|
||||
use bitcoin::taproot::TapNodeHash;
|
||||
use bitcoin::{
|
||||
absolute, script::PushBytesBuf, taproot::TapNodeHash, Address, OutPoint, Sequence, Transaction,
|
||||
TxIn, TxOut, Weight,
|
||||
absolute, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Sequence,
|
||||
Transaction, TxIn, TxOut, Txid, Weight,
|
||||
};
|
||||
use bitcoin::{psbt, Network};
|
||||
use bitcoin::{BlockHash, Txid};
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
@@ -246,9 +246,11 @@ fn test_get_funded_wallet_tx_fee_rate() {
|
||||
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||
// sats are the transaction fee.
|
||||
|
||||
// tx weight = 452 bytes, as vbytes = (452+3)/4 = 113
|
||||
// fee rate (sats per vbyte) = fee / vbytes = 1000 / 113 = 8.8495575221 rounded to 8.849558
|
||||
assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558);
|
||||
// tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113
|
||||
// fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212
|
||||
// fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9
|
||||
assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212);
|
||||
assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,11 +304,15 @@ macro_rules! assert_fee_rate {
|
||||
|
||||
assert_eq!(fee_amount, $fees);
|
||||
|
||||
let tx_fee_rate = FeeRate::from_wu($fees, tx.weight());
|
||||
let fee_rate = $fee_rate;
|
||||
let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight())
|
||||
.to_sat_per_kwu();
|
||||
let fee_rate = $fee_rate.to_sat_per_kwu();
|
||||
let half_default = FeeRate::BROADCAST_MIN.checked_div(2)
|
||||
.unwrap()
|
||||
.to_sat_per_kwu();
|
||||
|
||||
if !dust_change {
|
||||
assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "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);
|
||||
}
|
||||
@@ -647,7 +653,7 @@ fn test_create_tx_default_fee_rate() {
|
||||
let psbt = builder.finish().unwrap();
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -657,11 +663,11 @@ fn test_create_tx_custom_fee_rate() {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 25_000)
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -753,7 +759,7 @@ fn test_create_tx_drain_to_dust_amount() {
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(453.0));
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(454));
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -1481,7 +1487,6 @@ fn test_bump_fee_confirmed_tx() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "FeeRateTooLow")]
|
||||
fn test_bump_fee_low_fee_rate() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_address(New);
|
||||
@@ -1490,6 +1495,7 @@ fn test_bump_fee_low_fee_rate() {
|
||||
.add_recipient(addr.script_pubkey(), 25_000)
|
||||
.enable_rbf();
|
||||
let psbt = builder.finish().unwrap();
|
||||
let feerate = psbt.fee_rate().unwrap();
|
||||
|
||||
let tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
@@ -1499,8 +1505,18 @@ fn test_bump_fee_low_fee_rate() {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(1.0));
|
||||
builder.finish().unwrap();
|
||||
builder.fee_rate(FeeRate::BROADCAST_MIN);
|
||||
let res = builder.finish();
|
||||
assert_matches!(
|
||||
res,
|
||||
Err(CreateTxError::FeeRateTooLow { .. }),
|
||||
"expected FeeRateTooLow error"
|
||||
);
|
||||
|
||||
let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb
|
||||
let sat_vb = required as f64 / 250.0;
|
||||
let expect = format!("Fee rate too low: required {} sat/vb", sat_vb);
|
||||
assert_eq!(res.unwrap_err().to_string(), expect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1568,8 +1584,9 @@ fn test_bump_fee_reduce_change() {
|
||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||
.unwrap();
|
||||
|
||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf();
|
||||
builder.fee_rate(feerate).enable_rbf();
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
@@ -1600,7 +1617,7 @@ fn test_bump_fee_reduce_change() {
|
||||
sent_received.1
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_absolute(200);
|
||||
@@ -1663,9 +1680,10 @@ fn test_bump_fee_reduce_single_recipient() {
|
||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||
.unwrap();
|
||||
|
||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.fee_rate(FeeRate::from_sat_per_vb(2.5))
|
||||
.fee_rate(feerate)
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap();
|
||||
let psbt = builder.finish().unwrap();
|
||||
@@ -1679,7 +1697,7 @@ fn test_bump_fee_reduce_single_recipient() {
|
||||
assert_eq!(tx.output.len(), 1);
|
||||
assert_eq!(tx.output[0].value + fee.unwrap_or(0), sent_received.0);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1774,7 +1792,7 @@ fn test_bump_fee_drain_wallet() {
|
||||
.drain_wallet()
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.extract_tx());
|
||||
|
||||
@@ -1837,7 +1855,7 @@ fn test_bump_fee_remove_output_manually_selected_only() {
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.manually_selected_only()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(255.0));
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(255));
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -1878,7 +1896,7 @@ fn test_bump_fee_add_input() {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
@@ -1905,7 +1923,7 @@ fn test_bump_fee_add_input() {
|
||||
sent_received.1
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1988,7 +2006,7 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
||||
// now bump the fees without using `allow_shrinking`. the wallet should add an
|
||||
// extra input and a change output, and leave the original output untouched
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
@@ -2020,7 +2038,7 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
||||
75_000 - original_send_all_amount - fee.unwrap_or(0)
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2065,7 +2083,7 @@ fn test_bump_fee_add_input_change_dust() {
|
||||
// two inputs (50k, 25k) and one output (45k) - epsilon
|
||||
// We use epsilon here to avoid asking for a slightly too high feerate
|
||||
let fee_abs = 50_000 + 25_000 - 45_000 - 10;
|
||||
builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight));
|
||||
builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight);
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
@@ -2088,7 +2106,7 @@ fn test_bump_fee_add_input_change_dust() {
|
||||
45_000
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2119,7 +2137,7 @@ fn test_bump_fee_force_add_input() {
|
||||
builder
|
||||
.add_utxo(incoming_op)
|
||||
.unwrap()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
@@ -2147,7 +2165,7 @@ fn test_bump_fee_force_add_input() {
|
||||
sent_received.1
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2243,7 +2261,7 @@ fn test_bump_fee_unconfirmed_inputs_only() {
|
||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||
.unwrap();
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -2278,7 +2296,7 @@ fn test_bump_fee_unconfirmed_input() {
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.fee_rate(FeeRate::from_sat_per_vb(15.0))
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(15))
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap();
|
||||
builder.finish().unwrap();
|
||||
@@ -2298,7 +2316,7 @@ fn test_fee_amount_negative_drain_val() {
|
||||
let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
|
||||
.unwrap()
|
||||
.assume_checked();
|
||||
let fee_rate = FeeRate::from_sat_per_vb(2.01);
|
||||
let fee_rate = FeeRate::from_sat_per_kwu(500);
|
||||
let incoming_op = receive_output_in_latest_block(&mut wallet, 8859);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
@@ -2813,6 +2831,32 @@ fn test_get_address_no_reuse_single_descriptor() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_remove_tapfields_after_finalize_sign_option() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
|
||||
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
// removes tap_* from inputs
|
||||
for input in &psbt.inputs {
|
||||
assert!(input.tap_key_sig.is_none());
|
||||
assert!(input.tap_script_sigs.is_empty());
|
||||
assert!(input.tap_scripts.is_empty());
|
||||
assert!(input.tap_key_origins.is_empty());
|
||||
assert!(input.tap_internal_key.is_none());
|
||||
assert!(input.tap_merkle_root.is_none());
|
||||
}
|
||||
// removes key origins from outputs
|
||||
for output in &psbt.outputs {
|
||||
assert!(output.tap_key_origins.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_psbt_populate_tap_key_origins() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
||||
@@ -3499,7 +3543,7 @@ fn test_fee_rate_sign_no_grinding_high_r() {
|
||||
// alright.
|
||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let mut builder = wallet.build_tx();
|
||||
let mut data = PushBytesBuf::try_from(vec![0]).unwrap();
|
||||
builder
|
||||
@@ -3565,7 +3609,7 @@ fn test_fee_rate_sign_grinding_low_r() {
|
||||
// signature is 70 bytes.
|
||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -16,10 +16,10 @@ readme = "README.md"
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.17" }
|
||||
bdk_chain = { path = "../chain", version = "0.10", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||
bdk_testenv = { path = "../testenv", version = "0.1.0", default_features = false }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2,160 +2,14 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, BlockHash, Txid},
|
||||
bitcoin::{Address, Amount, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
|
||||
TxIn, TxOut, WScriptHash,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
|
||||
struct TestEnv {
|
||||
#[allow(dead_code)]
|
||||
daemon: bitcoind::BitcoinD,
|
||||
client: bitcoincore_rpc::Client,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let daemon = match std::env::var_os("TEST_BITCOIND") {
|
||||
Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
|
||||
None => bitcoind::BitcoinD::from_downloaded(),
|
||||
}?;
|
||||
let client = bitcoincore_rpc::Client::new(
|
||||
&daemon.rpc_url(),
|
||||
bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
|
||||
)?;
|
||||
Ok(Self { daemon, client })
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self.client.get_new_address(None, None)?.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
|
||||
fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||
let bt = self.client.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let txdata = vec![Transaction {
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::from_height(0)?,
|
||||
input: vec![TxIn {
|
||||
previous_output: bitcoin::OutPoint::default(),
|
||||
script_sig: ScriptBuf::builder()
|
||||
.push_int(bt.height as _)
|
||||
// randomn number so that re-mining creates unique block
|
||||
.push_int(random())
|
||||
.into_script(),
|
||||
sequence: bitcoin::Sequence::default(),
|
||||
witness: bitcoin::Witness::new(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 0,
|
||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
||||
}],
|
||||
}];
|
||||
|
||||
let bits: [u8; 4] = bt
|
||||
.bits
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("rpc provided us with invalid bits");
|
||||
|
||||
let mut block = Block {
|
||||
header: Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash: bt.previous_block_hash,
|
||||
merkle_root: TxMerkleNode::all_zeros(),
|
||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
||||
nonce: 0,
|
||||
},
|
||||
txdata,
|
||||
};
|
||||
|
||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
||||
|
||||
for nonce in 0..=u32::MAX {
|
||||
block.header.nonce = nonce;
|
||||
if block.header.target().is_met_by(block.block_hash()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.client.submit_block(&block)?;
|
||||
Ok((bt.height as usize, block.block_hash()))
|
||||
}
|
||||
|
||||
fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||
let mut hash = self.client.get_best_block_hash()?;
|
||||
for _ in 0..count {
|
||||
let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
|
||||
self.client.invalidate_block(&hash)?;
|
||||
match prev_hash {
|
||||
Some(prev_hash) => hash = prev_hash,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = self.mine_blocks(count, None);
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = (0..count)
|
||||
.map(|_| self.mine_empty_block())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||
let txid = self
|
||||
.client
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
use bdk_testenv::TestEnv;
|
||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
|
||||
/// Ensure that blocks are emitted in order even after reorg.
|
||||
///
|
||||
@@ -166,17 +20,22 @@ impl TestEnv {
|
||||
#[test]
|
||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
||||
let network_tip = env.rpc_client().get_block_count()?;
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
|
||||
|
||||
// mine some blocks and returned the actual block hashes
|
||||
// Mine some blocks and return the actual block hashes.
|
||||
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
|
||||
// returning block hashes.
|
||||
let exp_hashes = {
|
||||
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
|
||||
hashes.extend(env.mine_blocks(101, None)?);
|
||||
let mut hashes = (0..=network_tip)
|
||||
.map(|height| env.rpc_client().get_block_hash(height))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
|
||||
hashes
|
||||
};
|
||||
|
||||
// see if the emitter outputs the right blocks
|
||||
// See if the emitter outputs the right blocks.
|
||||
println!("first sync:");
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
@@ -207,7 +66,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
"final local_chain state is unexpected",
|
||||
);
|
||||
|
||||
// perform reorg
|
||||
// Perform reorg.
|
||||
let reorged_blocks = env.reorg(6)?;
|
||||
let exp_hashes = exp_hashes
|
||||
.iter()
|
||||
@@ -216,7 +75,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// see if the emitter outputs the right blocks
|
||||
// See if the emitter outputs the right blocks.
|
||||
println!("after reorg:");
|
||||
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
@@ -272,16 +131,25 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
println!("getting new addresses!");
|
||||
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_0 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_1 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_2 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
println!("got new addresses!");
|
||||
|
||||
println!("mining block!");
|
||||
env.mine_blocks(101, None)?;
|
||||
println!("mined blocks!");
|
||||
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut index = SpkTxOutIndex::<usize>::default();
|
||||
index.insert_spk(0, addr_0.script_pubkey());
|
||||
@@ -290,7 +158,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
index
|
||||
});
|
||||
|
||||
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
|
||||
let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
|
||||
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
@@ -306,7 +174,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let exp_txids = {
|
||||
let mut txids = BTreeSet::new();
|
||||
for _ in 0..3 {
|
||||
txids.insert(env.client.send_to_address(
|
||||
txids.insert(env.rpc_client().send_to_address(
|
||||
&addr_0,
|
||||
Amount::from_sat(10_000),
|
||||
None,
|
||||
@@ -342,7 +210,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
|
||||
// mine a block that confirms the 3 txs
|
||||
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
||||
let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
|
||||
let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
|
||||
let exp_anchors = exp_txids
|
||||
.iter()
|
||||
.map({
|
||||
@@ -386,10 +254,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
EMITTER_START_HEIGHT as _,
|
||||
);
|
||||
@@ -463,21 +331,24 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// setup addresses
|
||||
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_to_mine = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||
|
||||
// setup receiver
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
@@ -493,7 +364,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
// lock outputs that send to `addr_to_track`
|
||||
let outpoints_to_lock = env
|
||||
.client
|
||||
.rpc_client()
|
||||
.get_transaction(&txid, None)?
|
||||
.transaction()?
|
||||
.output
|
||||
@@ -502,7 +373,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
||||
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
||||
.collect::<Vec<_>>();
|
||||
env.client.lock_unspent(&outpoints_to_lock)?;
|
||||
env.rpc_client().lock_unspent(&outpoints_to_lock)?;
|
||||
|
||||
let _ = env.mine_blocks(1, None)?;
|
||||
}
|
||||
@@ -551,16 +422,19 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks and sync up emitter
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
@@ -613,16 +487,19 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance, sync emitter up to tip
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
@@ -698,16 +575,19 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
|
||||
// introduce mempool tx at each block extension
|
||||
@@ -725,7 +605,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
env.client
|
||||
env.rpc_client()
|
||||
.get_raw_mempool()?
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
@@ -744,7 +624,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
// emission.
|
||||
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
||||
let tx_introductions = dbg!(env
|
||||
.client
|
||||
.rpc_client()
|
||||
.get_raw_mempool_verbose()?
|
||||
.into_iter()
|
||||
.map(|(txid, entry)| (txid, entry.height as usize))
|
||||
@@ -821,10 +701,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
|
||||
// start height is 99
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
env.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
(PREMINE_COUNT - 2) as u32,
|
||||
);
|
||||
@@ -842,12 +722,12 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
let block_hash_100a = block_header_100a.block_hash();
|
||||
|
||||
// get hash for block 101a
|
||||
let block_hash_101a = env.client.get_block_hash(101)?;
|
||||
let block_hash_101a = env.rpc_client().get_block_hash(101)?;
|
||||
|
||||
// invalidate blocks 99a, 100a, 101a
|
||||
env.client.invalidate_block(&block_hash_99a)?;
|
||||
env.client.invalidate_block(&block_hash_100a)?;
|
||||
env.client.invalidate_block(&block_hash_101a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_99a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_100a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_101a)?;
|
||||
|
||||
// mine new blocks 99b, 100b, 101b
|
||||
env.mine_blocks(3, None)?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -18,7 +18,6 @@ bitcoin = { version = "0.30.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||
# note versions > 0.9.1 breaks ours 1.57.0 MSRV.
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
|
||||
@@ -326,12 +326,17 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
self.lookahead
|
||||
}
|
||||
|
||||
/// Store lookahead scripts until `target_index`.
|
||||
/// Store lookahead scripts until `target_index` (inclusive).
|
||||
///
|
||||
/// This does not change the `lookahead` setting.
|
||||
/// This does not change the global `lookahead` setting.
|
||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||
let next_index = self.next_store_index(keychain);
|
||||
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
|
||||
let (next_index, _) = self.next_index(keychain);
|
||||
|
||||
let temp_lookahead = (target_index + 1)
|
||||
.checked_sub(next_index)
|
||||
.filter(|&index| index > 0);
|
||||
|
||||
if let Some(temp_lookahead) = temp_lookahead {
|
||||
self.replenish_lookahead(keychain, temp_lookahead);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +454,21 @@ impl<A> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Transform the [`TxGraph`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> TxGraph<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
let mut new_graph = TxGraph::<A2>::default();
|
||||
new_graph.apply_changeset(self.initial_changeset().map_anchors(f));
|
||||
new_graph
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Construct a new [`TxGraph`] from a list of transactions.
|
||||
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
|
||||
@@ -1294,6 +1309,26 @@ impl<A: Ord> Append for ChangeSet<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> ChangeSet<A> {
|
||||
/// Transform the [`ChangeSet`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Ord, F>(self, mut f: F) -> ChangeSet<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
ChangeSet {
|
||||
txs: self.txs,
|
||||
txouts: self.txouts,
|
||||
anchors: BTreeSet::<(A2, Txid)>::from_iter(
|
||||
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
|
||||
),
|
||||
last_seen: self.last_seen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
self
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
|
||||
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
||||
TxIn, TxOut, Txid, Witness,
|
||||
@@ -49,11 +49,11 @@ impl TxOutTemplate {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init_graph<'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
|
||||
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
let mut graph = TxGraph::<A>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
spk_index.insert_spk(
|
||||
@@ -126,7 +126,7 @@ pub fn init_graph<'a>(
|
||||
spk_index.scan(&tx);
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.txid(), *anchor);
|
||||
let _ = graph.insert_anchor(tx.txid(), anchor.clone());
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||
|
||||
@@ -386,3 +386,103 @@ fn test_non_wildcard_derivations() {
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that calling `lookahead_to_target` stores the expected spks.
|
||||
#[test]
|
||||
fn lookahead_to_target() {
|
||||
#[derive(Default)]
|
||||
struct TestCase {
|
||||
/// Global lookahead value.
|
||||
lookahead: u32,
|
||||
/// Last revealed index for external keychain.
|
||||
external_last_revealed: Option<u32>,
|
||||
/// Last revealed index for internal keychain.
|
||||
internal_last_revealed: Option<u32>,
|
||||
/// Call `lookahead_to_target(External, u32)`.
|
||||
external_target: Option<u32>,
|
||||
/// Call `lookahead_to_target(Internal, u32)`.
|
||||
internal_target: Option<u32>,
|
||||
}
|
||||
|
||||
let test_cases = &[
|
||||
TestCase {
|
||||
lookahead: 0,
|
||||
external_target: Some(100),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 10,
|
||||
internal_target: Some(99),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 100,
|
||||
internal_target: Some(9),
|
||||
external_target: Some(10),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 12,
|
||||
external_last_revealed: Some(2),
|
||||
internal_last_revealed: Some(2),
|
||||
internal_target: Some(15),
|
||||
external_target: Some(13),
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 13,
|
||||
external_last_revealed: Some(100),
|
||||
internal_last_revealed: Some(21),
|
||||
internal_target: Some(120),
|
||||
external_target: Some(130),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases {
|
||||
let (mut index, _, _) = init_txout_index(t.lookahead);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||
}
|
||||
if let Some(last_revealed) = t.internal_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
|
||||
}
|
||||
|
||||
let keychain_test_cases = [
|
||||
(
|
||||
TestKeychain::External,
|
||||
t.external_last_revealed,
|
||||
t.external_target,
|
||||
),
|
||||
(
|
||||
TestKeychain::Internal,
|
||||
t.internal_last_revealed,
|
||||
t.internal_target,
|
||||
),
|
||||
];
|
||||
for (keychain, last_revealed, target) in keychain_test_cases {
|
||||
if let Some(target) = target {
|
||||
let original_last_stored_index = match last_revealed {
|
||||
Some(last_revealed) => Some(last_revealed + t.lookahead),
|
||||
None => t.lookahead.checked_sub(1),
|
||||
};
|
||||
let exp_last_stored_index = match original_last_stored_index {
|
||||
Some(original_last_stored_index) => {
|
||||
Ord::max(target, original_last_stored_index)
|
||||
}
|
||||
None => target,
|
||||
};
|
||||
index.lookahead_to_target(&keychain, target);
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let exp_keys = core::iter::repeat(keychain)
|
||||
.zip(0_u32..=exp_last_stored_index)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys, exp_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ use bdk_chain::{
|
||||
use bitcoin::{
|
||||
absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use common::*;
|
||||
use core::iter;
|
||||
use rand::RngCore;
|
||||
use std::vec;
|
||||
|
||||
#[test]
|
||||
@@ -1178,3 +1180,86 @@ fn test_missing_blocks() {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The `map_anchors` allow a caller to pass a function to reconstruct the [`TxGraph`] with any [`Anchor`],
|
||||
/// even though the function is non-deterministic.
|
||||
fn call_map_anchors_with_non_deterministic_anchor() {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
/// A non-deterministic anchor
|
||||
pub struct NonDeterministicAnchor {
|
||||
pub anchor_block: BlockId,
|
||||
pub non_deterministic_field: u32,
|
||||
}
|
||||
|
||||
let template = [
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(1))],
|
||||
anchors: &[block_id!(1, "A")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(2, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx2", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
anchors: &[block_id!(3, "C"), block_id!(4, "D")],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let (graph, _, _) = init_graph(&template);
|
||||
let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor {
|
||||
anchor_block: a,
|
||||
// A non-deterministic value
|
||||
non_deterministic_field: rand::thread_rng().next_u32(),
|
||||
});
|
||||
|
||||
// Check all the details in new_graph reconstruct as well
|
||||
|
||||
let mut full_txs_vec: Vec<_> = graph.full_txs().collect();
|
||||
full_txs_vec.sort();
|
||||
let mut new_txs_vec: Vec<_> = new_graph.full_txs().collect();
|
||||
new_txs_vec.sort();
|
||||
let mut new_txs = new_txs_vec.iter();
|
||||
|
||||
for tx_node in full_txs_vec.iter() {
|
||||
let new_txnode = new_txs.next().unwrap();
|
||||
assert_eq!(new_txnode.txid, tx_node.txid);
|
||||
assert_eq!(new_txnode.tx, tx_node.tx);
|
||||
assert_eq!(
|
||||
new_txnode.last_seen_unconfirmed,
|
||||
tx_node.last_seen_unconfirmed
|
||||
);
|
||||
assert_eq!(new_txnode.anchors.len(), tx_node.anchors.len());
|
||||
|
||||
let mut new_anchors: Vec<_> = new_txnode.anchors.iter().map(|a| a.anchor_block).collect();
|
||||
new_anchors.sort();
|
||||
let mut old_anchors: Vec<_> = tx_node.anchors.iter().copied().collect();
|
||||
old_anchors.sort();
|
||||
assert_eq!(new_anchors, old_anchors);
|
||||
}
|
||||
assert!(new_txs.next().is_none());
|
||||
|
||||
let new_graph_anchors: Vec<_> = new_graph
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|i| i.0.anchor_block)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
new_graph_anchors,
|
||||
vec![
|
||||
block_id!(1, "A"),
|
||||
block_id!(2, "B"),
|
||||
block_id!(3, "C"),
|
||||
block_id!(4, "D"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,6 +12,11 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.10.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
electrum-client = { version = "0.18" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", version = "0.1.0", default-features = false }
|
||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = "1"
|
||||
@@ -189,7 +189,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||
let mut request_spks = keychain_spks
|
||||
.into_iter()
|
||||
.map(|(k, s)| (k.clone(), s.into_iter()))
|
||||
.map(|(k, s)| (k, s.into_iter()))
|
||||
.collect::<BTreeMap<K, _>>();
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
|
||||
192
crates/electrum/tests/test_electrum.rs
Normal file
192
crates/electrum/tests/test_electrum.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use anyhow::Result;
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||
keychain::Balance,
|
||||
local_chain::LocalChain,
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_electrum::{ElectrumExt, ElectrumUpdate};
|
||||
use bdk_testenv::TestEnv;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||
) -> Result<Balance> {
|
||||
let chain_tip = recv_chain.tip().block_id();
|
||||
let outpoints = recv_graph.index.outpoints().clone();
|
||||
let balance = recv_graph
|
||||
.graph()
|
||||
.balance(recv_chain, chain_tip, outpoints, |_, _| true);
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
/// Ensure that [`ElectrumExt`] can sync properly.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
/// 2. Send a tx.
|
||||
/// 3. Mine extra block to confirm sent tx.
|
||||
/// 4. Check [`Balance`] to ensure tx is confirmed.
|
||||
#[test]
|
||||
fn scan_detects_confirmed_tx() -> Result<()> {
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
});
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||
|
||||
// Create transaction that is tracked by our receiver.
|
||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
|
||||
// Mine a block to confirm sent tx.
|
||||
env.mine_blocks(1, None)?;
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
|
||||
// Check to see if tx is confirmed.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat(),
|
||||
..Balance::default()
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that confirmed txs that are reorged become unconfirmed.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
/// 2. Mine 8 blocks with a confirmed tx in each.
|
||||
/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
|
||||
/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
|
||||
#[test]
|
||||
fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
|
||||
const REORG_COUNT: usize = 8;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
});
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||
|
||||
// Create transactions that are tracked by our receiver.
|
||||
for _ in 0..REORG_COUNT {
|
||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
}
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update.clone());
|
||||
|
||||
// Retain a snapshot of all anchors before reorg process.
|
||||
let initial_anchors = graph_update.all_anchors();
|
||||
|
||||
// Check if initial balance is correct.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
);
|
||||
|
||||
// Perform reorgs with different depths.
|
||||
for depth in 1..=REORG_COUNT {
|
||||
env.reorg_empty_blocks(depth)?;
|
||||
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update =
|
||||
relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
|
||||
// Check to see if a new anchor is added during current reorg.
|
||||
if !initial_anchors.is_superset(graph_update.all_anchors()) {
|
||||
println!("New anchor added at reorg depth {}", depth);
|
||||
}
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * depth as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
depth,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.10.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
esplora-client = { version = "0.6.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
@@ -21,7 +21,8 @@ futures = { version = "0.3.26", optional = true }
|
||||
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", version = "0.1.0", default_features = false }
|
||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -52,6 +52,19 @@ pub trait EsploraAsyncExt {
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||
///
|
||||
/// This follows the same approach as other Bitcoin-related software,
|
||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||
///
|
||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
@@ -162,6 +175,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
let stop_gap = Ord::max(stop_gap, 1);
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
@@ -226,12 +240,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let past_gap_limit = if let Some(i) = last_active_index {
|
||||
last_index > i.saturating_add(stop_gap as u32)
|
||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index >= stop_gap as u32
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if past_gap_limit {
|
||||
if gap_limit_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,19 @@ pub trait EsploraExt {
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||
///
|
||||
/// This follows the same approach as other Bitcoin-related software,
|
||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||
///
|
||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
@@ -149,6 +162,7 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
let stop_gap = Ord::max(stop_gap, 1);
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
@@ -216,12 +230,12 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let past_gap_limit = if let Some(i) = last_active_index {
|
||||
last_index > i.saturating_add(stop_gap as u32)
|
||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index >= stop_gap as u32
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if past_gap_limit {
|
||||
if gap_limit_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,21 @@
|
||||
use bdk_esplora::EsploraAsyncExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, AsyncClient, Builder};
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: AsyncClient,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Result<Self, anyhow::Error> {
|
||||
let bitcoind_exe =
|
||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
Ok(Self {
|
||||
bitcoind,
|
||||
electrsd,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
@@ -95,12 +48,11 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 102 {
|
||||
while client.get_height().await.unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = env
|
||||
.client
|
||||
let graph_update = client
|
||||
.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
@@ -139,10 +91,12 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the bounds of the address scan depending on the gap limit.
|
||||
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||
#[tokio::test]
|
||||
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
@@ -182,16 +136,16 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 103 {
|
||||
while client.get_height().await.unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
@@ -207,18 +161,18 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 104 {
|
||||
while client.get_height().await.unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use bdk_chain::local_chain::LocalChain;
|
||||
use bdk_chain::BlockId;
|
||||
use bdk_esplora::EsploraExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, BlockingClient, Builder};
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
@@ -26,73 +26,12 @@ macro_rules! local_chain {
|
||||
}};
|
||||
}
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: BlockingClient,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Result<Self, anyhow::Error> {
|
||||
let bitcoind_exe =
|
||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
|
||||
Ok(Self {
|
||||
bitcoind,
|
||||
electrsd,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
self.electrsd = electrsd;
|
||||
self.client = client;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
@@ -125,11 +64,11 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 102 {
|
||||
while client.get_height().unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = env.client.sync(
|
||||
let graph_update = client.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
@@ -167,10 +106,12 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the bounds of the address scan depending on the gap limit.
|
||||
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
@@ -210,16 +151,16 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 103 {
|
||||
while client.get_height().unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
@@ -235,18 +176,18 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 104 {
|
||||
while client.get_height().unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
|
||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
@@ -273,6 +214,8 @@ fn update_local_chain() -> anyhow::Result<()> {
|
||||
};
|
||||
// so new blocks can be seen by Electrs
|
||||
let env = env.reset_electrsd()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
@@ -375,8 +318,7 @@ fn update_local_chain() -> anyhow::Result<()> {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
|
||||
let update = env
|
||||
.client
|
||||
let update = client
|
||||
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
||||
.map_err(|err| {
|
||||
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.10.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ where
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?;
|
||||
f.write_all(magic)?;
|
||||
Ok(Self {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_hwi"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
|
||||
24
crates/testenv/Cargo.toml
Normal file
24
crates/testenv/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_testenv"
|
||||
description = "Testing framework for BDK chain sources."
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitcoincore-rpc = { version = "0.17" }
|
||||
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bdk_chain/std"]
|
||||
serde = ["bdk_chain/serde"]
|
||||
6
crates/testenv/README.md
Normal file
6
crates/testenv/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# BDK TestEnv
|
||||
|
||||
This crate sets up a regtest environment with a single [`bitcoind`] node
|
||||
connected to an [`electrs`] instance. This framework provides the infrastructure
|
||||
for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
|
||||
[`bdk_esplora`], etc.
|
||||
278
crates/testenv/src/lib.rs
Normal file
278
crates/testenv/src/lib.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use bdk_chain::bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf,
|
||||
ScriptHash, Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
use electrsd::electrum_client::ElectrumApi;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
|
||||
/// instance connected to it.
|
||||
pub struct TestEnv {
|
||||
pub bitcoind: electrsd::bitcoind::BitcoinD,
|
||||
pub electrsd: electrsd::ElectrsD,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
/// Construct a new [`TestEnv`] instance with default configurations.
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let bitcoind = match std::env::var_os("BITCOIND_EXE") {
|
||||
Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
|
||||
None => {
|
||||
let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
|
||||
.expect(
|
||||
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
|
||||
);
|
||||
electrsd::bitcoind::BitcoinD::with_conf(
|
||||
bitcoind_exe,
|
||||
&electrsd::bitcoind::Conf::default(),
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
let mut electrsd_conf = electrsd::Conf::default();
|
||||
electrsd_conf.http_enabled = true;
|
||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||
Some(env_electrs_exe) => {
|
||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
|
||||
}
|
||||
None => {
|
||||
let electrs_exe = electrsd::downloaded_exe_path()
|
||||
.expect("electrs version feature must be enabled");
|
||||
electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(Self { bitcoind, electrsd })
|
||||
}
|
||||
|
||||
/// Exposes the [`ElectrumApi`] calls from the Electrum client.
|
||||
pub fn electrum_client(&self) -> &impl ElectrumApi {
|
||||
&self.electrsd.client
|
||||
}
|
||||
|
||||
/// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
|
||||
pub fn rpc_client(&self) -> &impl RpcApi {
|
||||
&self.bitcoind.client
|
||||
}
|
||||
|
||||
// Reset `electrsd` so that new blocks can be seen.
|
||||
pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||
let mut electrsd_conf = electrsd::Conf::default();
|
||||
electrsd_conf.http_enabled = true;
|
||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||
Some(env_electrs_exe) => {
|
||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||
}
|
||||
None => {
|
||||
let electrs_exe = electrsd::downloaded_exe_path()
|
||||
.expect("electrs version feature must be enabled");
|
||||
electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||
}
|
||||
}?;
|
||||
self.electrsd = electrsd;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
|
||||
/// `address`.
|
||||
pub fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
|
||||
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
|
||||
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||
let bt = self.bitcoind.client.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let txdata = vec![Transaction {
|
||||
version: 1,
|
||||
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
|
||||
input: vec![TxIn {
|
||||
previous_output: bdk_chain::bitcoin::OutPoint::default(),
|
||||
script_sig: ScriptBuf::builder()
|
||||
.push_int(bt.height as _)
|
||||
// randomn number so that re-mining creates unique block
|
||||
.push_int(random())
|
||||
.into_script(),
|
||||
sequence: bdk_chain::bitcoin::Sequence::default(),
|
||||
witness: bdk_chain::bitcoin::Witness::new(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 0,
|
||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
||||
}],
|
||||
}];
|
||||
|
||||
let bits: [u8; 4] = bt
|
||||
.bits
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("rpc provided us with invalid bits");
|
||||
|
||||
let mut block = Block {
|
||||
header: Header {
|
||||
version: bdk_chain::bitcoin::block::Version::default(),
|
||||
prev_blockhash: bt.previous_block_hash,
|
||||
merkle_root: TxMerkleNode::all_zeros(),
|
||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
||||
nonce: 0,
|
||||
},
|
||||
txdata,
|
||||
};
|
||||
|
||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
||||
|
||||
for nonce in 0..=u32::MAX {
|
||||
block.header.nonce = nonce;
|
||||
if block.header.target().is_met_by(block.block_hash()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.bitcoind.client.submit_block(&block)?;
|
||||
Ok((bt.height as usize, block.block_hash()))
|
||||
}
|
||||
|
||||
/// This method waits for the Electrum notification indicating that a new block has been mined.
|
||||
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
|
||||
self.electrsd.client.block_headers_subscribe()?;
|
||||
let mut delay = Duration::from_millis(64);
|
||||
|
||||
loop {
|
||||
self.electrsd.trigger()?;
|
||||
self.electrsd.client.ping()?;
|
||||
if self.electrsd.client.block_headers_pop()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if delay.as_millis() < 512 {
|
||||
delay = delay.mul_f32(2.0);
|
||||
}
|
||||
std::thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidate a number of blocks of a given size `count`.
|
||||
pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||
let mut hash = self.bitcoind.client.get_best_block_hash()?;
|
||||
for _ in 0..count {
|
||||
let prev_hash = self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_block_info(&hash)?
|
||||
.previousblockhash;
|
||||
self.bitcoind.client.invalidate_block(&hash)?;
|
||||
match prev_hash {
|
||||
Some(prev_hash) => hash = prev_hash,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reorg a number of blocks of a given size `count`.
|
||||
/// Refer to [`TestEnv::mine_empty_block`] for more information.
|
||||
pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let start_height = self.bitcoind.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = self.mine_blocks(count, None);
|
||||
assert_eq!(
|
||||
self.bitcoind.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
/// Reorg with a number of empty blocks of a given size `count`.
|
||||
pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||
let start_height = self.bitcoind.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = (0..count)
|
||||
.map(|_| self.mine_empty_block())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
self.bitcoind.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Send a tx of a given `amount` to a given `address`.
|
||||
pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||
let txid = self
|
||||
.bitcoind
|
||||
.client
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::TestEnv;
|
||||
use anyhow::Result;
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
|
||||
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
|
||||
#[test]
|
||||
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, None)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let height = env.bitcoind.client.get_block_count()?;
|
||||
let blocks = (0..=height)
|
||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Perform reorg on six blocks.
|
||||
env.reorg(6)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let reorged_height = env.bitcoind.client.get_block_count()?;
|
||||
let reorged_blocks = (0..=height)
|
||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
assert_eq!(height, reorged_height);
|
||||
|
||||
// Block hashes should not be equal on the six reorged blocks.
|
||||
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
|
||||
match i <= height as usize - 6 {
|
||||
true => assert_eq!(block, reorged_block),
|
||||
false => assert_ne!(block, reorged_block),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user