Compare commits
115 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 | ||
|
|
50c549b5ac | ||
|
|
8379839010 | ||
|
|
5489f905a4 | ||
|
|
420e929463 | ||
|
|
13ab5a835d | ||
|
|
728e26f223 | ||
|
|
dbbd514242 | ||
|
|
ae00e1ee7b | ||
|
|
adc95137ac | ||
|
|
022d5a21cf | ||
|
|
7aca88474a | ||
|
|
b3278a4c29 | ||
|
|
552f11cb5f | ||
|
|
d8f74dc5e4 | ||
|
|
8d93fad778 | ||
|
|
9bb39a3a3f | ||
|
|
9e098a5b6d | ||
|
|
c6b9ed3b76 | ||
|
|
1c15cb2f91 | ||
|
|
89a7ddca7f | ||
|
|
097d818d4c | ||
|
|
f11d663b7e | ||
|
|
4679ca1df7 | ||
|
|
64a90192d9 | ||
|
|
ba7624781d | ||
|
|
d597f4c761 | ||
|
|
f099b42005 | ||
|
|
ce8c617c9d | ||
|
|
8ad52f720f | ||
|
|
c5afbaa95d | ||
|
|
929b5ddb0c | ||
|
|
070fffb95c | ||
|
|
216648bcfd | ||
|
|
5299db34cb | ||
|
|
8375bb8d39 | ||
|
|
63fa710319 | ||
|
|
d4276a1c32 | ||
|
|
6a03e0f209 | ||
|
|
38b728ae52 | ||
|
|
d162208d95 | ||
|
|
e687c27096 | ||
|
|
5611c9e42a | ||
|
|
07116df541 | ||
|
|
48b28e3abc | ||
|
|
51bd01b3dd | ||
|
|
285ff46a49 | ||
|
|
8305e64849 | ||
|
|
66dc34e75a | ||
|
|
fbd1d65618 | ||
|
|
c4d5f2ccd8 | ||
|
|
52c77b8451 | ||
|
|
99661be5f3 | ||
|
|
914db84824 | ||
|
|
f8f371c8d8 | ||
|
|
232a172c32 | ||
|
|
8d916d7a10 | ||
|
|
3fa44a58ec | ||
|
|
6f824cf325 | ||
|
|
f05e8502e6 | ||
|
|
25653d71b8 | ||
|
|
e6433fb2c1 | ||
|
|
0bee46e75b | ||
|
|
08b745ec9f | ||
|
|
b6a58d4f9b | ||
|
|
cf0c333744 | ||
|
|
7c0f4653b2 | ||
|
|
3829fc18c7 | ||
|
|
d9501187ef | ||
|
|
a4f28c079e | ||
|
|
8ec65f0b8e | ||
|
|
a7d01dc39a | ||
|
|
e0512acf94 | ||
|
|
8f2d4d9d40 | ||
|
|
9467cad55d | ||
|
|
d3e5095df1 | ||
|
|
2b61a122ff | ||
|
|
c871764670 | ||
|
|
a3aa8b6682 | ||
|
|
ed91a4bdb4 | ||
|
|
179cfeff51 |
3
.github/workflows/cont_integration.yml
vendored
3
.github/workflows/cont_integration.yml
vendored
@@ -30,9 +30,8 @@ jobs:
|
||||
- name: Pin dependencies for MSRV
|
||||
if: matrix.rust.version == '1.63.0'
|
||||
run: |
|
||||
cargo update -p zip --precise "0.6.2"
|
||||
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",
|
||||
@@ -15,6 +16,7 @@ members = [
|
||||
"example-crates/wallet_electrum",
|
||||
"example-crates/wallet_esplora_blocking",
|
||||
"example-crates/wallet_esplora_async",
|
||||
"example-crates/wallet_rpc",
|
||||
"nursery/tmp_plan",
|
||||
"nursery/coin_select"
|
||||
]
|
||||
|
||||
@@ -48,6 +48,8 @@ The project is split up into several crates in the `/crates` directory:
|
||||
Fully working examples of how to use these components are in `/example-crates`:
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
|
||||
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk` library.
|
||||
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk` library.
|
||||
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
|
||||
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
|
||||
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.4"
|
||||
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.8.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
|
||||
|
||||
@@ -42,22 +42,16 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Computes the checksum bytes of a descriptor.
|
||||
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
|
||||
pub(crate) fn calc_checksum_bytes_internal(
|
||||
mut desc: &str,
|
||||
exclude_hash: bool,
|
||||
) -> Result<[u8; 8], DescriptorError> {
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
|
||||
let mut original_checksum = None;
|
||||
if exclude_hash {
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
@@ -95,39 +89,10 @@ pub(crate) fn calc_checksum_bytes_internal(
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
|
||||
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
|
||||
|
||||
/// Compute the checksum bytes of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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`?
|
||||
@@ -35,24 +36,16 @@ pub trait PsbtUtils {
|
||||
}
|
||||
|
||||
impl PsbtUtils for Psbt {
|
||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let input = self.inputs.get(input_index)?;
|
||||
|
||||
if input_index >= tx.input.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(input) = self.inputs.get(input_index) {
|
||||
if let Some(wit_utxo) = &input.witness_utxo {
|
||||
Some(wit_utxo.clone())
|
||||
} else if let Some(in_tx) = &input.non_witness_utxo {
|
||||
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
match (&input.witness_utxo, &input.non_witness_utxo) {
|
||||
(Some(_), _) => input.witness_utxo.clone(),
|
||||
(_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
|
||||
in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,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, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
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
|
||||
@@ -197,6 +86,8 @@ pub enum Utxo {
|
||||
Foreign {
|
||||
/// The location of the output.
|
||||
outpoint: OutPoint,
|
||||
/// The nSequence value to set for this input.
|
||||
sequence: Option<Sequence>,
|
||||
/// The information about the input we require to add it to a PSBT.
|
||||
// Box it to stop the type being too big.
|
||||
psbt_input: Box<psbt::Input>,
|
||||
@@ -219,6 +110,7 @@ impl Utxo {
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
..
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
@@ -232,74 +124,12 @@ 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);
|
||||
/// Get the sequence number if an explicit sequence number has to be set for this input.
|
||||
pub fn sequence(&self) -> Option<Sequence> {
|
||||
match self {
|
||||
Utxo::Local(_) => None,
|
||||
Utxo::Foreign { sequence, .. } => *sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -100,13 +100,15 @@
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::types::FeeRate;
|
||||
use crate::chain::collections::HashSet;
|
||||
use crate::wallet::utils::IsDust;
|
||||
use crate::Utxo;
|
||||
use crate::WeightedUtxo;
|
||||
use bitcoin::FeeRate;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::consensus::encode::serialize;
|
||||
use bitcoin::OutPoint;
|
||||
use bitcoin::{Script, Weight};
|
||||
|
||||
use core::convert::TryInto;
|
||||
@@ -311,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) {
|
||||
@@ -342,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 {
|
||||
@@ -385,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,
|
||||
@@ -454,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
|
||||
@@ -545,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> {
|
||||
@@ -711,17 +719,36 @@ impl BranchAndBoundCoinSelection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove duplicate UTXOs.
|
||||
///
|
||||
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
|
||||
pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
|
||||
where
|
||||
I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
|
||||
{
|
||||
let mut visited = HashSet::<OutPoint>::new();
|
||||
let required = required
|
||||
.into_iter()
|
||||
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
|
||||
.collect::<I>();
|
||||
let optional = optional
|
||||
.into_iter()
|
||||
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
|
||||
.collect::<I>();
|
||||
(required, optional)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
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::Vbytes;
|
||||
use crate::wallet::coin_selection::filter_duplicates;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
@@ -799,13 +826,14 @@ mod test {
|
||||
|
||||
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||
let mut res = Vec::new();
|
||||
for _ in 0..utxos_number {
|
||||
for i in 0..utxos_number {
|
||||
res.push(WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||
)
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
i
|
||||
))
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: rng.gen_range(0..200000000),
|
||||
@@ -829,24 +857,26 @@ mod test {
|
||||
}
|
||||
|
||||
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||
let utxo = WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: utxos_value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 42,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
}),
|
||||
};
|
||||
vec![utxo; utxos_number]
|
||||
(0..utxos_number)
|
||||
.map(|i| WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
i
|
||||
))
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: utxos_value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 42,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
||||
@@ -868,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,
|
||||
)
|
||||
@@ -889,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,
|
||||
)
|
||||
@@ -910,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,
|
||||
)
|
||||
@@ -932,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,
|
||||
)
|
||||
@@ -950,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,
|
||||
)
|
||||
@@ -967,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,
|
||||
)
|
||||
@@ -988,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,
|
||||
)
|
||||
@@ -1009,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,
|
||||
)
|
||||
@@ -1031,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,
|
||||
)
|
||||
@@ -1050,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,
|
||||
)
|
||||
@@ -1071,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,
|
||||
)
|
||||
@@ -1092,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,
|
||||
)
|
||||
@@ -1113,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,
|
||||
)
|
||||
@@ -1150,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,
|
||||
)
|
||||
@@ -1172,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,
|
||||
)
|
||||
@@ -1190,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,
|
||||
)
|
||||
@@ -1202,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]
|
||||
@@ -1233,7 +1259,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
optional_utxos,
|
||||
FeeRate::from_sat_per_vb(0.0),
|
||||
FeeRate::ZERO,
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1245,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))
|
||||
@@ -1254,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;
|
||||
@@ -1275,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))
|
||||
@@ -1284,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();
|
||||
@@ -1306,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()
|
||||
@@ -1321,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();
|
||||
|
||||
@@ -1346,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)
|
||||
@@ -1372,7 +1398,7 @@ mod test {
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
0.0,
|
||||
0,
|
||||
&drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
@@ -1388,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))
|
||||
@@ -1417,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,
|
||||
);
|
||||
@@ -1443,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,
|
||||
);
|
||||
@@ -1465,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,
|
||||
);
|
||||
@@ -1478,4 +1504,95 @@ mod test {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_duplicates() {
|
||||
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: 0,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
|
||||
txout: TxOut {
|
||||
value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 0,
|
||||
confirmation_time: ConfirmationTime::Confirmed {
|
||||
height: 12345,
|
||||
time: 12345,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
|
||||
let mut v = utxos
|
||||
.iter()
|
||||
.map(|&(txid, value)| utxo(txid, value))
|
||||
.collect::<Vec<_>>();
|
||||
v.sort_by_key(|u| u.utxo.outpoint());
|
||||
v
|
||||
}
|
||||
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
required: &'a [(&'a str, u64)],
|
||||
optional: &'a [(&'a str, u64)],
|
||||
exp_required: &'a [(&'a str, u64)],
|
||||
exp_optional: &'a [(&'a str, u64)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "no_duplicates",
|
||||
required: &[("A", 1000), ("B", 2100)],
|
||||
optional: &[("C", 1000)],
|
||||
exp_required: &[("A", 1000), ("B", 2100)],
|
||||
exp_optional: &[("C", 1000)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_required_utxos",
|
||||
required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
|
||||
optional: &[("D", 2100)],
|
||||
exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
|
||||
exp_optional: &[("D", 2100)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_optional_utxos",
|
||||
required: &[("A", 3000), ("B", 1200)],
|
||||
optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
|
||||
exp_required: &[("A", 3000), ("B", 1200)],
|
||||
exp_optional: &[("C", 5000), ("D", 1300)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_across_required_and_optional_utxos",
|
||||
required: &[("A", 3000), ("B", 1200), ("C", 2100)],
|
||||
optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
|
||||
exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
|
||||
exp_optional: &[("D", 1200), ("E", 5000)],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let (required, optional) =
|
||||
filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
|
||||
assert_eq!(
|
||||
required,
|
||||
to_utxo_vec(t.exp_required),
|
||||
"[{}:{}] unexpected `required` result",
|
||||
i,
|
||||
t.name
|
||||
);
|
||||
assert_eq!(
|
||||
optional,
|
||||
to_utxo_vec(t.exp_optional),
|
||||
"[{}:{}] unexpected `optional` result",
|
||||
i,
|
||||
t.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
//! #
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! Wallet
|
||||
//!
|
||||
//! This module defines the [`Wallet`].
|
||||
use crate::collections::{BTreeMap, HashMap, HashSet};
|
||||
use crate::collections::{BTreeMap, HashMap};
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
string::{String, ToString},
|
||||
@@ -23,7 +23,9 @@ pub use bdk_chain::keychain::Balance;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph,
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
|
||||
local_chain::{
|
||||
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
|
||||
},
|
||||
tx_graph::{CanonicalTx, TxGraph},
|
||||
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
||||
IndexedTxGraph, Persist, PersistBackend,
|
||||
@@ -31,8 +33,8 @@ use bdk_chain::{
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||
use bitcoin::{
|
||||
absolute, Address, 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};
|
||||
@@ -438,6 +440,55 @@ pub enum InsertTxError {
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for InsertTxError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
||||
tip_height,
|
||||
tx_height,
|
||||
} => {
|
||||
write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InsertTxError {}
|
||||
|
||||
/// An error that may occur when applying a block to [`Wallet`].
|
||||
#[derive(Debug)]
|
||||
pub enum ApplyBlockError {
|
||||
/// Occurs when the update chain cannot connect with original chain.
|
||||
CannotConnect(CannotConnectError),
|
||||
/// Occurs when the `connected_to` hash does not match the hash derived from `block`.
|
||||
UnexpectedConnectedToHash {
|
||||
/// Block hash of `connected_to`.
|
||||
connected_to_hash: BlockHash,
|
||||
/// Expected block hash of `connected_to`, as derived from `block`.
|
||||
expected_hash: BlockHash,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ApplyBlockError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ApplyBlockError::CannotConnect(err) => err.fmt(f),
|
||||
ApplyBlockError::UnexpectedConnectedToHash {
|
||||
expected_hash: block_hash,
|
||||
connected_to_hash: checkpoint_hash,
|
||||
} => write!(
|
||||
f,
|
||||
"`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
|
||||
checkpoint_hash, block_hash
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ApplyBlockError {}
|
||||
|
||||
impl<D> Wallet<D> {
|
||||
/// Initialize an empty [`Wallet`].
|
||||
pub fn new<E: IntoWalletDescriptor>(
|
||||
@@ -935,18 +986,15 @@ 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())
|
||||
}
|
||||
|
||||
/// Computes total input value going from script pubkeys in the index (sent) and the total output
|
||||
/// value going to script pubkeys in the index (received) in `tx`.
|
||||
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
||||
///
|
||||
/// For the `sent` to be computed correctly, the outputs being spent must have already been
|
||||
/// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
|
||||
/// so it will be correct even if it has not been scanned.
|
||||
/// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
|
||||
/// that spend from previous txouts tracked by this wallet. Received is the summation
|
||||
/// of this tx's outputs that send to script pubkeys tracked by this wallet.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -1297,7 +1345,7 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
Some(tx_builder::Version(x)) => x,
|
||||
None if requirements.csv.is_some() => 2,
|
||||
_ => 1,
|
||||
None => 1,
|
||||
};
|
||||
|
||||
// We use a match here instead of a unwrap_or_else as it's way more readable :)
|
||||
@@ -1350,6 +1398,7 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
};
|
||||
|
||||
// The nSequence to be by default for inputs unless an explicit sequence is specified.
|
||||
let n_sequence = match (params.rbf, requirements.csv) {
|
||||
// No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
|
||||
(None, None) if lock_time != absolute::LockTime::ZERO => {
|
||||
@@ -1381,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)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1449,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).
|
||||
@@ -1460,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()
|
||||
@@ -1468,15 +1516,8 @@ impl<D> Wallet<D> {
|
||||
return Err(CreateTxError::ChangePolicyDescriptor);
|
||||
}
|
||||
|
||||
let (required_utxos, optional_utxos) = self.preselect_utxos(
|
||||
params.change_policy,
|
||||
¶ms.unspendable,
|
||||
params.utxos.clone(),
|
||||
params.drain_wallet,
|
||||
params.manually_selected_only,
|
||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||
Some(current_height.to_consensus_u32()),
|
||||
);
|
||||
let (required_utxos, optional_utxos) =
|
||||
self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32()));
|
||||
|
||||
// get drain script
|
||||
let drain_script = match params.drain_to {
|
||||
@@ -1496,6 +1537,9 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
};
|
||||
|
||||
let (required_utxos, optional_utxos) =
|
||||
coin_selection::filter_duplicates(required_utxos, optional_utxos);
|
||||
|
||||
let coin_selection = coin_selection.coin_select(
|
||||
required_utxos,
|
||||
optional_utxos,
|
||||
@@ -1512,7 +1556,7 @@ impl<D> Wallet<D> {
|
||||
.map(|u| bitcoin::TxIn {
|
||||
previous_output: u.outpoint(),
|
||||
script_sig: ScriptBuf::default(),
|
||||
sequence: n_sequence,
|
||||
sequence: u.sequence().unwrap_or(n_sequence),
|
||||
witness: Witness::new(),
|
||||
})
|
||||
.collect();
|
||||
@@ -1605,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()?
|
||||
/// };
|
||||
///
|
||||
@@ -1692,6 +1736,7 @@ impl<D> Wallet<D> {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint: txin.previous_output,
|
||||
sequence: Some(txin.sequence),
|
||||
psbt_input: Box::new(psbt::Input {
|
||||
witness_utxo: Some(txout.clone()),
|
||||
non_witness_utxo: Some(prev_tx.clone()),
|
||||
@@ -1732,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()
|
||||
};
|
||||
@@ -1926,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,
|
||||
}
|
||||
@@ -1934,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)
|
||||
}
|
||||
|
||||
@@ -2010,17 +2070,26 @@ impl<D> Wallet<D> {
|
||||
|
||||
/// Given the options returns the list of utxos that must be used to form the
|
||||
/// transaction and any further that may be used if needed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn preselect_utxos(
|
||||
&self,
|
||||
change_policy: tx_builder::ChangeSpendPolicy,
|
||||
unspendable: &HashSet<OutPoint>,
|
||||
manually_selected: Vec<WeightedUtxo>,
|
||||
must_use_all_available: bool,
|
||||
manual_only: bool,
|
||||
must_only_use_confirmed_tx: bool,
|
||||
params: &TxParams,
|
||||
current_height: Option<u32>,
|
||||
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
|
||||
let TxParams {
|
||||
change_policy,
|
||||
unspendable,
|
||||
utxos,
|
||||
drain_wallet,
|
||||
manually_selected_only,
|
||||
bumping_fee,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let manually_selected = utxos.clone();
|
||||
// we mandate confirmed transactions if we're bumping the fee
|
||||
let must_only_use_confirmed_tx = bumping_fee.is_some();
|
||||
let must_use_all_available = *drain_wallet;
|
||||
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
@@ -2035,7 +2104,7 @@ impl<D> Wallet<D> {
|
||||
|
||||
// NOTE: we are intentionally ignoring `unspendable` here. i.e manual
|
||||
// selection overrides unspendable.
|
||||
if manual_only {
|
||||
if *manually_selected_only {
|
||||
return (must_spend, vec![]);
|
||||
}
|
||||
|
||||
@@ -2163,8 +2232,9 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
}
|
||||
Utxo::Foreign {
|
||||
psbt_input: foreign_psbt_input,
|
||||
outpoint,
|
||||
psbt_input: foreign_psbt_input,
|
||||
..
|
||||
} => {
|
||||
let is_taproot = foreign_psbt_input
|
||||
.witness_utxo
|
||||
@@ -2237,9 +2307,6 @@ impl<D> Wallet<D> {
|
||||
) -> Result<(), MiniscriptPsbtError> {
|
||||
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
|
||||
// the input utxos and outputs
|
||||
//
|
||||
// Clippy complains that the collect is not required, but that's wrong
|
||||
#[allow(clippy::needless_collect)]
|
||||
let utxos = (0..psbt.inputs.len())
|
||||
.filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
|
||||
.chain(
|
||||
@@ -2329,7 +2396,7 @@ impl<D> Wallet<D> {
|
||||
self.persist.commit().map(|c| c.is_some())
|
||||
}
|
||||
|
||||
/// Returns the changes that will be staged with the next call to [`commit`].
|
||||
/// Returns the changes that will be committed with the next call to [`commit`].
|
||||
///
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn staged(&self) -> &ChangeSet
|
||||
@@ -2353,6 +2420,86 @@ impl<D> Wallet<D> {
|
||||
pub fn local_chain(&self) -> &LocalChain {
|
||||
&self.chain
|
||||
}
|
||||
|
||||
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
|
||||
/// `prev_blockhash` of the block's header.
|
||||
///
|
||||
/// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
|
||||
/// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
|
||||
///
|
||||
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
|
||||
pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let connected_to = match height.checked_sub(1) {
|
||||
Some(prev_height) => BlockId {
|
||||
height: prev_height,
|
||||
hash: block.header.prev_blockhash,
|
||||
},
|
||||
None => BlockId {
|
||||
height,
|
||||
hash: block.block_hash(),
|
||||
},
|
||||
};
|
||||
self.apply_block_connected_to(block, height, connected_to)
|
||||
.map_err(|err| match err {
|
||||
ApplyHeaderError::InconsistentBlocks => {
|
||||
unreachable!("connected_to is derived from the block so must be consistent")
|
||||
}
|
||||
ApplyHeaderError::CannotConnect(err) => err,
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
|
||||
/// block to the internal chain.
|
||||
///
|
||||
/// The `connected_to` parameter informs the wallet how this block connects to the internal
|
||||
/// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
|
||||
/// internal [`TxGraph`].
|
||||
pub fn apply_block_connected_to(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
) -> Result<(), ApplyHeaderError>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.append(
|
||||
self.chain
|
||||
.apply_header_connected_to(&block.header, height, connected_to)?
|
||||
.into(),
|
||||
);
|
||||
changeset.append(
|
||||
self.indexed_graph
|
||||
.apply_block_relevant(block, height)
|
||||
.into(),
|
||||
);
|
||||
self.persist.stage(changeset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply relevant unconfirmed transactions to the wallet.
|
||||
///
|
||||
/// Transactions that are not relevant are filtered out.
|
||||
///
|
||||
/// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
|
||||
/// when the transaction was last seen in the mempool. This is used for conflict resolution
|
||||
/// when there is conflicting unconfirmed transactions. The transaction with the later
|
||||
/// `last_seen` is prioritized.
|
||||
pub fn apply_unconfirmed_txs<'t>(
|
||||
&mut self,
|
||||
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
|
||||
) where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let indexed_graph_changeset = self
|
||||
.indexed_graph
|
||||
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
|
||||
self.persist.stage(ChangeSet::from(indexed_graph_changeset));
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet<D> {
|
||||
@@ -2431,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.
|
||||
@@ -820,7 +830,6 @@ pub enum TapLeavesOptions {
|
||||
None,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for SignOptions {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
@@ -828,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
|
||||
@@ -389,6 +385,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
self.add_foreign_utxo_with_sequence(
|
||||
outpoint,
|
||||
psbt_input,
|
||||
satisfaction_weight,
|
||||
Sequence::MAX,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
|
||||
pub fn add_foreign_utxo_with_sequence(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
sequence: Sequence,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
if psbt_input.witness_utxo.is_none() {
|
||||
match psbt_input.non_witness_utxo.as_ref() {
|
||||
@@ -413,6 +425,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint,
|
||||
sequence: Some(sequence),
|
||||
psbt_input: Box::new(psbt_input),
|
||||
},
|
||||
});
|
||||
@@ -557,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`.
|
||||
@@ -617,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 {
|
||||
@@ -762,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.3.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.8", 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]
|
||||
|
||||
@@ -43,11 +43,13 @@ pub struct Emitter<'c, C> {
|
||||
}
|
||||
|
||||
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
/// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` and `start_height`.
|
||||
/// Construct a new [`Emitter`].
|
||||
///
|
||||
/// * `last_cp` is the check point used to find the latest block which is still part of the best
|
||||
/// chain.
|
||||
/// * `start_height` is the block height to start emitting blocks from.
|
||||
/// `last_cp` informs the emitter of the chain we are starting off with. This way, the emitter
|
||||
/// can start emission from a block that connects to the original chain.
|
||||
///
|
||||
/// `start_height` starts emission from a given height (if there are no conflicts with the
|
||||
/// original chain).
|
||||
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
|
||||
Self {
|
||||
client,
|
||||
@@ -127,13 +129,58 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
}
|
||||
|
||||
/// Emit the next block height and header (if any).
|
||||
pub fn next_header(&mut self) -> Result<Option<(u32, Header)>, bitcoincore_rpc::Error> {
|
||||
poll(self, |hash| self.client.get_block_header(hash))
|
||||
pub fn next_header(&mut self) -> Result<Option<BlockEvent<Header>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block_header(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
}
|
||||
|
||||
/// Emit the next block height and block (if any).
|
||||
pub fn next_block(&mut self) -> Result<Option<(u32, Block)>, bitcoincore_rpc::Error> {
|
||||
poll(self, |hash| self.client.get_block(hash))
|
||||
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A newly emitted block from [`Emitter`].
|
||||
#[derive(Debug)]
|
||||
pub struct BlockEvent<B> {
|
||||
/// Either a full [`Block`] or [`Header`] of the new block.
|
||||
pub block: B,
|
||||
|
||||
/// The checkpoint of the new block.
|
||||
///
|
||||
/// A [`CheckPoint`] is a node of a linked list of [`BlockId`]s. This checkpoint is linked to
|
||||
/// all [`BlockId`]s originally passed in [`Emitter::new`] as well as emitted blocks since then.
|
||||
/// These blocks are guaranteed to be of the same chain.
|
||||
///
|
||||
/// This is important as BDK structures require block-to-apply to be connected with another
|
||||
/// block in the original chain.
|
||||
pub checkpoint: CheckPoint,
|
||||
}
|
||||
|
||||
impl<B> BlockEvent<B> {
|
||||
/// The block height of this new block.
|
||||
pub fn block_height(&self) -> u32 {
|
||||
self.checkpoint.height()
|
||||
}
|
||||
|
||||
/// The block hash of this new block.
|
||||
pub fn block_hash(&self) -> BlockHash {
|
||||
self.checkpoint.hash()
|
||||
}
|
||||
|
||||
/// The [`BlockId`] of a previous block that this block connects to.
|
||||
///
|
||||
/// This either returns a [`BlockId`] of a previously emitted block or from the chain we started
|
||||
/// with (passed in as `last_cp` in [`Emitter::new`]).
|
||||
///
|
||||
/// This value is derived from [`BlockEvent::checkpoint`].
|
||||
pub fn connected_to(&self) -> BlockId {
|
||||
match self.checkpoint.prev() {
|
||||
Some(prev_cp) => prev_cp.block_id(),
|
||||
// there is no previous checkpoint, so just connect with itself
|
||||
None => self.checkpoint.block_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +250,7 @@ where
|
||||
fn poll<C, V, F>(
|
||||
emitter: &mut Emitter<C>,
|
||||
get_item: F,
|
||||
) -> Result<Option<(u32, V)>, bitcoincore_rpc::Error>
|
||||
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
F: Fn(&BlockHash) -> Result<V, bitcoincore_rpc::Error>,
|
||||
@@ -215,13 +262,14 @@ where
|
||||
let hash = res.hash;
|
||||
let item = get_item(&hash)?;
|
||||
|
||||
emitter.last_cp = emitter
|
||||
let new_cp = emitter
|
||||
.last_cp
|
||||
.clone()
|
||||
.push(BlockId { height, hash })
|
||||
.expect("must push");
|
||||
emitter.last_cp = new_cp.clone();
|
||||
emitter.last_block = Some(res);
|
||||
return Ok(Some((height, item)));
|
||||
return Ok(Some((new_cp, item)));
|
||||
}
|
||||
PollResponse::NoMoreBlocks => {
|
||||
emitter.last_block = None;
|
||||
|
||||
@@ -2,182 +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)
|
||||
}
|
||||
}
|
||||
|
||||
fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Update {
|
||||
let this_id = BlockId {
|
||||
height,
|
||||
hash: block.block_hash(),
|
||||
};
|
||||
let tip = if block.header.prev_blockhash == BlockHash::all_zeros() {
|
||||
CheckPoint::new(this_id)
|
||||
} else {
|
||||
CheckPoint::new(BlockId {
|
||||
height: height - 1,
|
||||
hash: block.header.prev_blockhash,
|
||||
})
|
||||
.extend(core::iter::once(this_id))
|
||||
.expect("must construct checkpoint")
|
||||
};
|
||||
|
||||
local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: false,
|
||||
}
|
||||
}
|
||||
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.
|
||||
///
|
||||
@@ -188,29 +20,38 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up
|
||||
#[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((height, block)) = emitter.next_block()? {
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
assert_eq!(
|
||||
block.block_hash(),
|
||||
emission.block_hash(),
|
||||
exp_hashes[height as usize],
|
||||
"emitted block hash is unexpected"
|
||||
);
|
||||
|
||||
let chain_update = block_to_chain_update(&block, height);
|
||||
assert_eq!(
|
||||
local_chain.apply_update(chain_update)?,
|
||||
BTreeMap::from([(height, Some(block.block_hash()))]),
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
BTreeMap::from([(height, Some(hash))]),
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
}
|
||||
@@ -225,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()
|
||||
@@ -234,30 +75,33 @@ 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((height, block)) = emitter.next_block()? {
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
assert_eq!(
|
||||
height, exp_height as u32,
|
||||
"emitted block has unexpected height"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
block.block_hash(),
|
||||
exp_hashes[height as usize],
|
||||
hash, exp_hashes[height as usize],
|
||||
"emitted block is unexpected"
|
||||
);
|
||||
|
||||
let chain_update = block_to_chain_update(&block, height);
|
||||
assert_eq!(
|
||||
local_chain.apply_update(chain_update)?,
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
if exp_height == exp_hashes.len() - reorged_blocks.len() {
|
||||
core::iter::once((height, Some(block.block_hash())))
|
||||
core::iter::once((height, Some(hash)))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
.collect::<bdk_chain::local_chain::ChangeSet>()
|
||||
} else {
|
||||
BTreeMap::from([(height, Some(block.block_hash()))])
|
||||
BTreeMap::from([(height, Some(hash))])
|
||||
},
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
@@ -287,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());
|
||||
@@ -305,11 +158,15 @@ 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((height, block)) = emitter.next_block()? {
|
||||
let _ = chain.apply_update(block_to_chain_update(&block, height))?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.is_empty());
|
||||
}
|
||||
|
||||
@@ -317,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,
|
||||
@@ -353,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({
|
||||
@@ -367,10 +224,13 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
|
||||
// must receive mined block which will confirm the transactions.
|
||||
{
|
||||
let (height, block) = emitter.next_block()?.expect("must get mined block");
|
||||
let _ = chain
|
||||
.apply_update(CheckPoint::from_header(&block.header, height).into_update(false))?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
|
||||
let emission = emitter.next_block()?.expect("must get mined block");
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.graph.txs.is_empty());
|
||||
assert!(indexed_additions.graph.txouts.is_empty());
|
||||
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
|
||||
@@ -394,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 _,
|
||||
);
|
||||
@@ -407,9 +267,12 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
|
||||
for reorg_count in 1..=10 {
|
||||
let replaced_blocks = env.reorg_empty_blocks(reorg_count)?;
|
||||
let (height, next_header) = emitter.next_header()?.expect("must emit block after reorg");
|
||||
let next_emission = emitter.next_header()?.expect("must emit block after reorg");
|
||||
assert_eq!(
|
||||
(height as usize, next_header.block_hash()),
|
||||
(
|
||||
next_emission.block_height() as usize,
|
||||
next_emission.block_hash()
|
||||
),
|
||||
replaced_blocks[0],
|
||||
"block emitted after reorg should be at the reorg height"
|
||||
);
|
||||
@@ -439,8 +302,9 @@ fn sync_from_emitter<C>(
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
{
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
process_block(recv_chain, recv_graph, block, height)?;
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
process_block(recv_chain, recv_graph, emission.block, height)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -467,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());
|
||||
@@ -497,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
|
||||
@@ -506,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)?;
|
||||
}
|
||||
@@ -555,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() {}
|
||||
|
||||
@@ -617,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() {}
|
||||
|
||||
@@ -660,7 +533,8 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
|
||||
// At this point, the emitter has seen all mempool transactions. It should only re-emit those
|
||||
// that have introduction heights less than the emitter's last-emitted block tip.
|
||||
while let Some((height, _)) = emitter.next_header()? {
|
||||
while let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
// We call `mempool()` twice.
|
||||
// The second call (at height `h`) should skip the tx introduced at height `h`.
|
||||
for try_index in 0..2 {
|
||||
@@ -701,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
|
||||
@@ -728,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<_>>(),
|
||||
@@ -747,14 +624,15 @@ 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))
|
||||
.collect::<BTreeMap<_, _>>());
|
||||
|
||||
// `next_header` emits the replacement block of the reorg
|
||||
if let Some((height, _)) = emitter.next_header()? {
|
||||
if let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
println!("\t- replacement height: {}", height);
|
||||
|
||||
// the mempool emission (that follows the first block emission after reorg) should only
|
||||
@@ -823,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,
|
||||
);
|
||||
@@ -835,27 +713,27 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
env.mine_blocks(PREMINE_COUNT, None)?;
|
||||
|
||||
// emit block 99a
|
||||
let (_, block_header_99a) = emitter.next_header()?.expect("block 99a header");
|
||||
let block_header_99a = emitter.next_header()?.expect("block 99a header").block;
|
||||
let block_hash_99a = block_header_99a.block_hash();
|
||||
let block_hash_98a = block_header_99a.prev_blockhash;
|
||||
|
||||
// emit block 100a
|
||||
let (_, block_header_100a) = emitter.next_header()?.expect("block 100a header");
|
||||
let block_header_100a = emitter.next_header()?.expect("block 100a header").block;
|
||||
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)?;
|
||||
|
||||
// emit block header 99b
|
||||
let (_, block_header_99b) = emitter.next_header()?.expect("block 99b header");
|
||||
let block_header_99b = emitter.next_header()?.expect("block 99b header").block;
|
||||
let block_hash_99b = block_header_99b.block_hash();
|
||||
let block_hash_98b = block_header_99b.prev_blockhash;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.8.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 }
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
|
||||
pub enum ChainPosition<A> {
|
||||
/// The chain data is seen as confirmed, and in anchored by `A`.
|
||||
Confirmed(A),
|
||||
/// The chain data is seen in mempool at this given timestamp.
|
||||
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
|
||||
Unconfirmed(u64),
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ impl<A: Anchor> ChainPosition<A> {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub enum ConfirmationTime {
|
||||
/// The confirmed variant.
|
||||
/// The transaction is confirmed
|
||||
Confirmed {
|
||||
/// Confirmation height.
|
||||
height: u32,
|
||||
/// Confirmation time in unix seconds.
|
||||
time: u64,
|
||||
},
|
||||
/// The unconfirmed variant.
|
||||
/// The transaction is unconfirmed
|
||||
Unconfirmed {
|
||||
/// The last-seen timestamp in unix seconds.
|
||||
last_seen: u64,
|
||||
@@ -81,7 +81,7 @@ impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
|
||||
height: a.confirmation_height,
|
||||
time: a.confirmation_time,
|
||||
},
|
||||
ChainPosition::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
|
||||
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,13 +157,12 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationHeightAnchor {
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
|
||||
/// The exact confirmation height of the transaction.
|
||||
///
|
||||
/// It is assumed that this value is never larger than the height of the anchor block.
|
||||
pub confirmation_height: u32,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationHeightAnchor {
|
||||
@@ -198,12 +197,12 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationTimeHeightAnchor {
|
||||
/// The confirmation height of the transaction being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the transaction being anchored.
|
||||
pub confirmation_time: u64,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
/// The confirmation height of the chain data being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the chain data being anchored.
|
||||
pub confirmation_time: u64,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
||||
@@ -229,12 +228,12 @@ impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
||||
/// A `TxOut` with as much data as we can retrieve about it
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FullTxOut<A> {
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The location of the `TxOut`.
|
||||
pub outpoint: OutPoint,
|
||||
/// The `TxOut`.
|
||||
pub txout: TxOut,
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The txid and chain position of the transaction (if any) that has spent this output.
|
||||
pub spent_by: Option<(ChainPosition<A>, Txid)>,
|
||||
/// Whether this output is on a coinbase transaction.
|
||||
@@ -299,3 +298,35 @@ impl<A: Anchor> FullTxOut<A> {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chain_position_ord() {
|
||||
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
|
||||
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
|
||||
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 9,
|
||||
anchor_block: BlockId {
|
||||
height: 20,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 12,
|
||||
anchor_block: BlockId {
|
||||
height: 15,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
|
||||
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
|
||||
assert!(
|
||||
conf2 > conf1,
|
||||
"confirmation_height is higher then it should be higher ord"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,20 +224,26 @@ where
|
||||
/// Irrelevant transactions in `txs` will be ignored.
|
||||
pub fn apply_block_relevant(
|
||||
&mut self,
|
||||
block: Block,
|
||||
block: &Block,
|
||||
height: u32,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
let block_id = BlockId {
|
||||
hash: block.block_hash(),
|
||||
height,
|
||||
};
|
||||
let txs = block.txdata.iter().enumerate().map(|(tx_pos, tx)| {
|
||||
(
|
||||
tx,
|
||||
core::iter::once(A::from_block_position(&block, block_id, tx_pos)),
|
||||
)
|
||||
});
|
||||
self.batch_insert_relevant(txs)
|
||||
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
changeset.indexer.append(self.index.index_tx(tx));
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.txid();
|
||||
let anchor = A::from_block_position(block, block_id, tx_pos);
|
||||
changeset.graph.append(self.graph.insert_tx(tx.clone()));
|
||||
changeset
|
||||
.graph
|
||||
.append(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
/// Batch insert all transactions of the given `block` of `height`.
|
||||
|
||||
@@ -20,7 +20,7 @@ pub use txout_index::*;
|
||||
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
|
||||
/// It maps each keychain `K` to its last revealed index.
|
||||
///
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet] are
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet`]s are
|
||||
/// monotone in that they will never decrease the revealed derivation index.
|
||||
///
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
|
||||
|
||||
@@ -13,7 +13,7 @@ use core::{
|
||||
|
||||
use crate::Append;
|
||||
|
||||
const DEFAULT_LOOKAHEAD: u32 = 1_000;
|
||||
const DEFAULT_LOOKAHEAD: u32 = 25;
|
||||
|
||||
/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and
|
||||
/// indexes [`TxOut`]s with them.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use core::convert::Infallible;
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::block::Header;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||
@@ -39,6 +40,28 @@ impl CheckPoint {
|
||||
Self(Arc::new(CPInner { block, prev: None }))
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method will error if any of the follow occurs:
|
||||
///
|
||||
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
|
||||
/// - The `blocks` iterator is not in ascending height order.
|
||||
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
|
||||
///
|
||||
/// The error type is the last successful checkpoint constructed (if any).
|
||||
pub fn from_block_ids(
|
||||
block_ids: impl IntoIterator<Item = BlockId>,
|
||||
) -> Result<Self, Option<Self>> {
|
||||
let mut blocks = block_ids.into_iter();
|
||||
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
|
||||
for id in blocks {
|
||||
acc = acc.push(id).map_err(Some)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from the given `header` and block `height`.
|
||||
///
|
||||
/// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,
|
||||
@@ -347,6 +370,95 @@ impl LocalChain {
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] at `height` which you claim is connected to a existing block in the chain.
|
||||
///
|
||||
/// This is useful when you have a block header that you want to record as part of the chain but
|
||||
/// don't necessarily know that the `prev_blockhash` is in the chain.
|
||||
///
|
||||
/// This will usually insert two new [`BlockId`]s into the chain: the header's block and the
|
||||
/// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed
|
||||
/// to be `prev_blockhash` (in which case only one new block id will be inserted).
|
||||
/// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update
|
||||
/// rules][`apply_update`] are satisfied.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
|
||||
/// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
|
||||
/// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
|
||||
/// height is greater than the header's `height`.
|
||||
///
|
||||
/// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
|
||||
///
|
||||
/// [`apply_update`]: Self::apply_update
|
||||
pub fn apply_header_connected_to(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
) -> Result<ChangeSet, ApplyHeaderError> {
|
||||
let this = BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
};
|
||||
let prev = height.checked_sub(1).map(|prev_height| BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
});
|
||||
let conn = match connected_to {
|
||||
// `connected_to` can be ignored if same as `this` or `prev` (duplicate)
|
||||
conn if conn == this || Some(conn) == prev => None,
|
||||
// this occurs if:
|
||||
// - `connected_to` height is the same as `prev`, but different hash
|
||||
// - `connected_to` height is the same as `this`, but different hash
|
||||
// - `connected_to` height is greater than `this` (this is not allowed)
|
||||
conn if conn.height >= height.saturating_sub(1) => {
|
||||
return Err(ApplyHeaderError::InconsistentBlocks)
|
||||
}
|
||||
conn => Some(conn),
|
||||
};
|
||||
|
||||
let update = Update {
|
||||
tip: CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
|
||||
.expect("block ids must be in order"),
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
|
||||
self.apply_update(update)
|
||||
.map_err(ApplyHeaderError::CannotConnect)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] connecting it with the previous block.
|
||||
///
|
||||
/// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
|
||||
/// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
|
||||
/// use the current block as `connected_to`.
|
||||
///
|
||||
/// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
|
||||
pub fn apply_header(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let connected_to = match height.checked_sub(1) {
|
||||
Some(prev_height) => BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
},
|
||||
None => BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
},
|
||||
};
|
||||
self.apply_header_connected_to(header, height, connected_to)
|
||||
.map_err(|err| match err {
|
||||
ApplyHeaderError::InconsistentBlocks => {
|
||||
unreachable!("connected_to is derived from the block so is always consistent")
|
||||
}
|
||||
ApplyHeaderError::CannotConnect(err) => err,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
@@ -557,6 +669,30 @@ impl core::fmt::Display for CannotConnectError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CannotConnectError {}
|
||||
|
||||
/// The error type for [`LocalChain::apply_header_connected_to`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ApplyHeaderError {
|
||||
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
|
||||
InconsistentBlocks,
|
||||
/// Occurs when the update cannot connect with the original chain.
|
||||
CannotConnect(CannotConnectError),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ApplyHeaderError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ApplyHeaderError::InconsistentBlocks => write!(
|
||||
f,
|
||||
"the `connected_to` block conflicts with either the current or previous block"
|
||||
),
|
||||
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ApplyHeaderError {}
|
||||
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
|
||||
@@ -55,6 +55,18 @@ where
|
||||
// if written successfully, take and return `self.stage`
|
||||
.map(|_| Some(core::mem::take(&mut self.stage)))
|
||||
}
|
||||
|
||||
/// Stages a new changeset and commits it (along with any other previously staged changes) to
|
||||
/// the persistence backend
|
||||
///
|
||||
/// Convenience method for calling [`stage`] and then [`commit`].
|
||||
///
|
||||
/// [`stage`]: Self::stage
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn stage_and_commit(&mut self, changeset: C) -> Result<Option<C>, B::WriteError> {
|
||||
self.stage(changeset);
|
||||
self.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/// A persistence backend for [`Persist`].
|
||||
|
||||
@@ -43,18 +43,24 @@ impl<D> SpkIterator<D>
|
||||
where
|
||||
D: Borrow<Descriptor<DescriptorPublicKey>>,
|
||||
{
|
||||
/// Creates a new script pubkey iterator starting at 0 from a descriptor.
|
||||
/// Create a new script pubkey iterator from `descriptor`.
|
||||
///
|
||||
/// This iterates from derivation index 0 and stops at index 0x7FFFFFFF (as specified in
|
||||
/// BIP-32). Non-wildcard descriptors will only return one script pubkey at derivation index 0.
|
||||
///
|
||||
/// Use [`new_with_range`](SpkIterator::new_with_range) to create an iterator with a specified
|
||||
/// derivation index range.
|
||||
pub fn new(descriptor: D) -> Self {
|
||||
SpkIterator::new_with_range(descriptor, 0..=BIP32_MAX_INDEX)
|
||||
}
|
||||
|
||||
// Creates a new script pubkey iterator from a descriptor with a given range.
|
||||
// If the descriptor doesn't have a wildcard, we shorten whichever range you pass in
|
||||
// to have length <= 1. This means that if you pass in 0..0 or 0..1 the range will
|
||||
// remain the same, but if you pass in 0..10, we'll shorten it to 0..1
|
||||
// Also note that if the descriptor doesn't have a wildcard, passing in a range starting
|
||||
// from n > 0, will return an empty iterator.
|
||||
pub(crate) fn new_with_range<R>(descriptor: D, range: R) -> Self
|
||||
/// Create a new script pubkey iterator from `descriptor` and a given `range`.
|
||||
///
|
||||
/// Non-wildcard descriptors will only emit a single script pubkey (at derivation index 0).
|
||||
/// Wildcard descriptors have an end-bound of 0x7FFFFFFF (inclusive).
|
||||
///
|
||||
/// Refer to [`new`](SpkIterator::new) for more.
|
||||
pub fn new_with_range<R>(descriptor: D, range: R) -> Self
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
@@ -73,13 +79,6 @@ where
|
||||
// Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
|
||||
end = end.min(BIP32_MAX_INDEX + 1);
|
||||
|
||||
if !descriptor.borrow().has_wildcard() {
|
||||
// The length of the range should be at most 1
|
||||
if end != start {
|
||||
end = start + 1;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
next_index: start,
|
||||
end,
|
||||
@@ -250,6 +249,14 @@ mod test {
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=2).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..11).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..=10).next(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
// The following dummy traits were created to test if SpkIterator is working properly.
|
||||
|
||||
@@ -40,20 +40,23 @@
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
//! # let tx_a = tx_from_hex(RAW_TX_1);
|
||||
//! let mut graph: TxGraph = TxGraph::default();
|
||||
//! let mut another_graph: TxGraph = TxGraph::default();
|
||||
//! let mut tx_graph: TxGraph = TxGraph::default();
|
||||
//!
|
||||
//! // insert a transaction
|
||||
//! let changeset = graph.insert_tx(tx_a);
|
||||
//! let changeset = tx_graph.insert_tx(tx_a);
|
||||
//!
|
||||
//! // the resulting changeset can be applied to another tx graph
|
||||
//! another_graph.apply_changeset(changeset);
|
||||
//! // We can restore the state of the `tx_graph` by applying all
|
||||
//! // the changesets obtained by mutating the original (the order doesn't matter).
|
||||
//! let mut restored_tx_graph: TxGraph = TxGraph::default();
|
||||
//! restored_tx_graph.apply_changeset(changeset);
|
||||
//!
|
||||
//! assert_eq!(tx_graph, restored_tx_graph);
|
||||
//! ```
|
||||
//!
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`].
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
|
||||
//!
|
||||
//! ```
|
||||
//! # use bdk_chain::BlockId;
|
||||
//! # use bdk_chain::{Append, BlockId};
|
||||
//! # use bdk_chain::tx_graph::TxGraph;
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
@@ -337,7 +340,7 @@ impl<A> TxGraph<A> {
|
||||
|
||||
/// The transactions spending from this output.
|
||||
///
|
||||
/// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in
|
||||
/// [`TxGraph`] allows conflicting transactions within the graph. Obviously the transactions in
|
||||
/// the returned set will never be in the same active-chain.
|
||||
pub fn outspends(&self, outpoint: OutPoint) -> &HashSet<Txid> {
|
||||
self.spends.get(&outpoint).unwrap_or(&self.empty_outspends)
|
||||
@@ -451,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 {
|
||||
@@ -1212,11 +1230,6 @@ impl<A> Default for ChangeSet<A> {
|
||||
}
|
||||
|
||||
impl<A> ChangeSet<A> {
|
||||
/// Returns true if the [`ChangeSet`] is empty (no transactions or txouts).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.txs.is_empty() && self.txouts.is_empty()
|
||||
}
|
||||
|
||||
/// Iterates over all outpoints contained within [`ChangeSet`].
|
||||
pub fn txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs
|
||||
@@ -1296,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use bdk_chain::local_chain::{
|
||||
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, MissingGenesisError, Update,
|
||||
use bdk_chain::{
|
||||
local_chain::{
|
||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||
LocalChain, MissingGenesisError, Update,
|
||||
},
|
||||
BlockId,
|
||||
};
|
||||
use bitcoin::BlockHash;
|
||||
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
@@ -288,6 +292,27 @@ fn update_local_chain() {
|
||||
],
|
||||
},
|
||||
},
|
||||
// Allow update that is shorter than original chain
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A C D E F
|
||||
// update | A C D'
|
||||
TestLocalChain {
|
||||
name: "allow update that is shorter than original chain",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D")), (4, h!("E")), (5, h!("F"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("C")), (3, h!("D'"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(3, Some(h!("D'"))),
|
||||
(4, None),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D'"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(TestLocalChain::run);
|
||||
@@ -423,3 +448,234 @@ fn local_chain_disconnect_from() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_from_block_ids() {
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
blocks: &'a [(u32, BlockHash)],
|
||||
exp_result: Result<(), Option<(u32, BlockHash)>>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "in_order",
|
||||
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
TestCase {
|
||||
name: "with_duplicates",
|
||||
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
|
||||
exp_result: Err(Some((2, h!("C")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "not_in_order",
|
||||
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
|
||||
exp_result: Err(Some((3, h!("D")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "empty",
|
||||
blocks: &[],
|
||||
exp_result: Err(None),
|
||||
},
|
||||
TestCase {
|
||||
name: "single",
|
||||
blocks: &[(21, h!("million"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let result = CheckPoint::from_block_ids(
|
||||
t.blocks
|
||||
.iter()
|
||||
.map(|&(height, hash)| BlockId { height, hash }),
|
||||
);
|
||||
match t.exp_result {
|
||||
Ok(_) => {
|
||||
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
|
||||
let result_vec = {
|
||||
let mut v = result
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<Vec<_>>();
|
||||
v.reverse();
|
||||
v
|
||||
};
|
||||
assert_eq!(
|
||||
&result_vec, t.blocks,
|
||||
"[{}:{}] not equal to original block ids",
|
||||
i, t.name
|
||||
);
|
||||
}
|
||||
Err(exp_last) => {
|
||||
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(
|
||||
err.as_ref()
|
||||
.map(|last_cp| (last_cp.height(), last_cp.hash())),
|
||||
exp_last,
|
||||
"[{}:{}] error's last cp height should be {:?}, got {:?}",
|
||||
i,
|
||||
t.name,
|
||||
exp_last,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_apply_header_connected_to() {
|
||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||
Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash,
|
||||
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
|
||||
time: 0,
|
||||
bits: bitcoin::CompactTarget::default(),
|
||||
nonce: 0,
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
header: Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("A"));
|
||||
let hash = header.block_hash();
|
||||
let height = 2;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "connected_to_self_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (height, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let hash = header.block_hash();
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: prev_hash,
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_prev_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![(height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(BlockHash::all_zeros());
|
||||
let hash = header.block_hash();
|
||||
let height = 0;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "genesis_applied_to_self",
|
||||
chain: local_chain![(0, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("Z"));
|
||||
let height = 10;
|
||||
let hash = header.block_hash();
|
||||
let prev_height = height - 1;
|
||||
let prev_hash = header.prev_blockhash;
|
||||
TestCase {
|
||||
name: "connect_at_connected_to",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
header,
|
||||
height: 10,
|
||||
connected_to: BlockId {
|
||||
height: 3,
|
||||
hash: h!("C"),
|
||||
},
|
||||
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: h!("not_prev_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_prev_hash",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
|
||||
header,
|
||||
height: prev_height + 1,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height,
|
||||
hash: h!("not_current_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_current_block",
|
||||
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("B"));
|
||||
let height = 3;
|
||||
let connected_to = BlockId {
|
||||
height: 4,
|
||||
hash: h!("D"),
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_is_greater",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
|
||||
let exp_result = t
|
||||
.exp_result
|
||||
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
|
||||
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -213,7 +215,8 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let _ = graph.insert_tx(tx);
|
||||
let changeset = graph.insert_tx(tx);
|
||||
assert!(!changeset.is_empty());
|
||||
assert!(graph.outspends(OutPoint::null()).is_empty());
|
||||
assert!(graph.tx_spends(Txid::all_zeros()).next().is_none());
|
||||
}
|
||||
@@ -289,7 +292,7 @@ fn insert_tx_displaces_txouts() {
|
||||
}],
|
||||
};
|
||||
|
||||
let _ = tx_graph.insert_txout(
|
||||
let changeset = tx_graph.insert_txout(
|
||||
OutPoint {
|
||||
txid: tx.txid(),
|
||||
vout: 0,
|
||||
@@ -300,6 +303,8 @@ fn insert_tx_displaces_txouts() {
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!changeset.is_empty());
|
||||
|
||||
let _ = tx_graph.insert_txout(
|
||||
OutPoint {
|
||||
txid: tx.txid(),
|
||||
@@ -653,7 +658,8 @@ fn test_walk_ancestors() {
|
||||
]);
|
||||
|
||||
[&tx_a0, &tx_b1].iter().for_each(|&tx| {
|
||||
let _ = graph.insert_anchor(tx.txid(), tip.block_id());
|
||||
let changeset = graph.insert_anchor(tx.txid(), tip.block_id());
|
||||
assert!(!changeset.is_empty());
|
||||
});
|
||||
|
||||
let ancestors = [
|
||||
@@ -1027,10 +1033,12 @@ fn test_changeset_last_seen_append() {
|
||||
last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!original.is_empty() || original_ls.is_none());
|
||||
let update = ChangeSet::<()> {
|
||||
last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!update.is_empty() || update_ls.is_none());
|
||||
|
||||
original.append(update);
|
||||
assert_eq!(
|
||||
@@ -1172,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.6.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.8.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"
|
||||
@@ -56,12 +56,14 @@ impl RelevantTxids {
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
/// Finalizes [`RelevantTxids`] with `new_txs` and anchors of type
|
||||
/// [`ConfirmationTimeHeightAnchor`].
|
||||
/// Finalizes the update by fetching `missing` txids from the `client`, where the
|
||||
/// resulting [`TxGraph`] has anchors of type [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
///
|
||||
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
|
||||
/// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
|
||||
/// use it.
|
||||
// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
|
||||
// use it.
|
||||
pub fn into_confirmation_time_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
@@ -177,7 +179,7 @@ pub trait ElectrumExt {
|
||||
) -> Result<ElectrumUpdate, Error>;
|
||||
}
|
||||
|
||||
impl ElectrumExt for Client {
|
||||
impl<A: ElectrumApi> ElectrumExt for A {
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
@@ -187,7 +189,7 @@ impl ElectrumExt for Client {
|
||||
) -> 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();
|
||||
|
||||
@@ -301,7 +303,7 @@ impl ElectrumExt for Client {
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &Client,
|
||||
client: &impl ElectrumApi,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
@@ -415,7 +417,7 @@ fn determine_tx_anchor(
|
||||
}
|
||||
|
||||
fn populate_with_outpoints(
|
||||
client: &Client,
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
@@ -476,7 +478,7 @@ fn populate_with_outpoints(
|
||||
}
|
||||
|
||||
fn populate_with_txids(
|
||||
client: &Client,
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
@@ -512,7 +514,7 @@ fn populate_with_txids(
|
||||
}
|
||||
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
client: &Client,
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
|
||||
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.6.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.8.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"] }
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ use bdk_esplora::EsploraExt;
|
||||
// use bdk_esplora::EsploraAsyncExt;
|
||||
```
|
||||
|
||||
For full examples, refer to [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora) (blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
|
||||
For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
|
||||
|
||||
[`esplora-client`]: https://docs.rs/esplora-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::{Error, TxStatus};
|
||||
use esplora_client::TxStatus;
|
||||
use futures::{stream::FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::AsyncClient`].
|
||||
///
|
||||
@@ -19,17 +22,22 @@ use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EsploraAsyncExt {
|
||||
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
|
||||
/// Prepare a [`LocalChain`] update with blocks fetched from Esplora.
|
||||
///
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
///
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## Consistency
|
||||
///
|
||||
/// The chain update returned is guaranteed to be consistent as long as there is not a *large* re-org
|
||||
/// during the call. The size of re-org we can tollerate is server dependent but will be at
|
||||
/// least 10.
|
||||
///
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
@@ -44,7 +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.
|
||||
#[allow(clippy::result_large_err)]
|
||||
///
|
||||
/// ## 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<
|
||||
@@ -67,7 +87,6 @@ pub trait EsploraAsyncExt {
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraAsyncExt::full_scan
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
@@ -85,21 +104,22 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
|
||||
let new_tip_height = self.get_height().await?;
|
||||
// Fetch latest N (server dependent) blocks from Esplora. The server guarantees these are
|
||||
// consistent.
|
||||
let mut fetched_blocks = self
|
||||
.get_blocks(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let new_tip_height = fetched_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.copied()
|
||||
.expect("must have atleast one block");
|
||||
|
||||
// atomically fetch blocks from esplora
|
||||
let mut fetched_blocks = {
|
||||
let heights = (0..=new_tip_height).rev();
|
||||
let hashes = self
|
||||
.get_blocks(Some(new_tip_height))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| b.id);
|
||||
heights.zip(hashes).collect::<BTreeMap<u32, BlockHash>>()
|
||||
};
|
||||
|
||||
// fetch heights that the caller is interested in
|
||||
// Fetch blocks of heights that the caller is interested in, skipping blocks that are
|
||||
// already fetched when constructing `fetched_blocks`.
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
@@ -107,81 +127,37 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
let hash = self.get_block_hash(height).await?;
|
||||
entry.insert(hash);
|
||||
// ❗The return value of `get_block_hash` is not strictly guaranteed to be consistent
|
||||
// with the chain at the time of `get_blocks` above (there could have been a deep
|
||||
// re-org). Since `get_blocks` returns 10 (or so) blocks we are assuming that it's
|
||||
// not possible to have a re-org deeper than that.
|
||||
entry.insert(self.get_block_hash(height).await?);
|
||||
}
|
||||
}
|
||||
|
||||
// find the earliest point of agreement between local chain and fetched chain
|
||||
let earliest_agreement_cp = {
|
||||
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
|
||||
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height).await?
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Ensure `fetched_blocks` can create an update that connects with the original chain by
|
||||
// finding a "Point of Agreement".
|
||||
for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) {
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
earliest_agreement_cp
|
||||
};
|
||||
|
||||
let tip = {
|
||||
// first checkpoint to use for the update chain
|
||||
let first_cp = match earliest_agreement_cp {
|
||||
Some(cp) => cp,
|
||||
None => {
|
||||
let (&height, &hash) = fetched_blocks
|
||||
.iter()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
CheckPoint::new(BlockId { height, hash })
|
||||
let fetched_hash = match fetched_blocks.entry(height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
*entry.insert(self.get_block_hash(height).await?)
|
||||
}
|
||||
};
|
||||
// transform fetched chain into the update chain
|
||||
fetched_blocks
|
||||
// we exclude anything at or below the first cp of the update chain otherwise
|
||||
// building the chain will fail
|
||||
.split_off(&(first_cp.height() + 1))
|
||||
.into_iter()
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(first_cp, |prev_cp, block| {
|
||||
prev_cp.push(block).expect("must extend checkpoint")
|
||||
})
|
||||
};
|
||||
|
||||
// We have found point of agreement so the update will connect!
|
||||
if fetched_hash == local_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip,
|
||||
tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from))
|
||||
.expect("must be in height order"),
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
@@ -199,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();
|
||||
@@ -241,16 +218,34 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,15 +1,18 @@
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::collections::{BTreeMap, BTreeSet};
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::{Error, TxStatus};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::BlockingClient`].
|
||||
///
|
||||
@@ -17,17 +20,22 @@ use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
||||
///
|
||||
/// [crate-level documentation]: crate
|
||||
pub trait EsploraExt {
|
||||
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
|
||||
/// Prepare a [`LocalChain`] update with blocks fetched from Esplora.
|
||||
///
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
///
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## Consistency
|
||||
///
|
||||
/// The chain update returned is guaranteed to be consistent as long as there is not a *large* re-org
|
||||
/// during the call. The size of re-org we can tollerate is server dependent but will be at
|
||||
/// least 10.
|
||||
///
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
@@ -42,7 +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.
|
||||
#[allow(clippy::result_large_err)]
|
||||
///
|
||||
/// ## 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)>>,
|
||||
@@ -62,7 +82,6 @@ pub trait EsploraExt {
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraExt::full_scan
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
@@ -78,20 +97,21 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<Item = u32>,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
|
||||
let new_tip_height = self.get_height()?;
|
||||
// Fetch latest N (server dependent) blocks from Esplora. The server guarantees these are
|
||||
// consistent.
|
||||
let mut fetched_blocks = self
|
||||
.get_blocks(None)?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let new_tip_height = fetched_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.copied()
|
||||
.expect("must atleast have one block");
|
||||
|
||||
// atomically fetch blocks from esplora
|
||||
let mut fetched_blocks = {
|
||||
let heights = (0..=new_tip_height).rev();
|
||||
let hashes = self
|
||||
.get_blocks(Some(new_tip_height))?
|
||||
.into_iter()
|
||||
.map(|b| b.id);
|
||||
heights.zip(hashes).collect::<BTreeMap<u32, BlockHash>>()
|
||||
};
|
||||
|
||||
// fetch heights that the caller is interested in
|
||||
// Fetch blocks of heights that the caller is interested in, skipping blocks that are
|
||||
// already fetched when constructing `fetched_blocks`.
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
@@ -99,81 +119,35 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
let hash = self.get_block_hash(height)?;
|
||||
entry.insert(hash);
|
||||
// ❗The return value of `get_block_hash` is not strictly guaranteed to be consistent
|
||||
// with the chain at the time of `get_blocks` above (there could have been a deep
|
||||
// re-org). Since `get_blocks` returns 10 (or so) blocks we are assuming that it's
|
||||
// not possible to have a re-org deeper than that.
|
||||
entry.insert(self.get_block_hash(height)?);
|
||||
}
|
||||
}
|
||||
|
||||
// find the earliest point of agreement between local chain and fetched chain
|
||||
let earliest_agreement_cp = {
|
||||
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
|
||||
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height)?
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Ensure `fetched_blocks` can create an update that connects with the original chain by
|
||||
// finding a "Point of Agreement".
|
||||
for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) {
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
earliest_agreement_cp
|
||||
};
|
||||
|
||||
let tip = {
|
||||
// first checkpoint to use for the update chain
|
||||
let first_cp = match earliest_agreement_cp {
|
||||
Some(cp) => cp,
|
||||
None => {
|
||||
let (&height, &hash) = fetched_blocks
|
||||
.iter()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
CheckPoint::new(BlockId { height, hash })
|
||||
}
|
||||
let fetched_hash = match fetched_blocks.entry(height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(self.get_block_hash(height)?),
|
||||
};
|
||||
// transform fetched chain into the update chain
|
||||
fetched_blocks
|
||||
// we exclude anything at or below the first cp of the update chain otherwise
|
||||
// building the chain will fail
|
||||
.split_off(&(first_cp.height() + 1))
|
||||
.into_iter()
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(first_cp, |prev_cp, block| {
|
||||
prev_cp.push(block).expect("must extend checkpoint")
|
||||
})
|
||||
};
|
||||
|
||||
// We have found point of agreement so the update will connect!
|
||||
if fetched_hash == local_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip,
|
||||
tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from))
|
||||
.expect("must be in height order"),
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
@@ -188,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();
|
||||
@@ -233,16 +208,34 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -286,7 +279,12 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
.map(|txid| {
|
||||
std::thread::spawn({
|
||||
let client = self.clone();
|
||||
move || client.get_tx_status(&txid).map(|s| (txid, s))
|
||||
move || {
|
||||
client
|
||||
.get_tx_status(&txid)
|
||||
.map_err(Box::new)
|
||||
.map(|s| (txid, s))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<JoinHandle<Result<(Txid, TxStatus), Error>>>>();
|
||||
|
||||
@@ -31,8 +31,6 @@ mod async_ext;
|
||||
#[cfg(feature = "async")]
|
||||
pub use async_ext::*;
|
||||
|
||||
const ASSUME_FINAL_DEPTH: u32 = 15;
|
||||
|
||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
|
||||
if let TxStatus {
|
||||
block_height: Some(height),
|
||||
|
||||
@@ -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(),
|
||||
@@ -109,6 +61,28 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
@@ -117,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.
|
||||
@@ -160,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);
|
||||
|
||||
@@ -185,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,68 +1,37 @@
|
||||
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 std::collections::{BTreeMap, HashSet};
|
||||
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;
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: BlockingClient,
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
}};
|
||||
}
|
||||
|
||||
#[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 =
|
||||
@@ -95,17 +64,39 @@ 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(),
|
||||
1,
|
||||
)?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
@@ -115,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.
|
||||
@@ -158,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);
|
||||
|
||||
@@ -183,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));
|
||||
@@ -202,3 +195,181 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_local_chain() -> anyhow::Result<()> {
|
||||
const TIP_HEIGHT: u32 = 50;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let blocks = {
|
||||
let bitcoind_client = &env.bitcoind.client;
|
||||
assert_eq!(bitcoind_client.get_block_count()?, 1);
|
||||
[
|
||||
(0, bitcoind_client.get_block_hash(0)?),
|
||||
(1, bitcoind_client.get_block_hash(1)?),
|
||||
]
|
||||
.into_iter()
|
||||
.chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
};
|
||||
// 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,
|
||||
chain: LocalChain,
|
||||
request_heights: &'static [u32],
|
||||
exp_update_heights: &'static [u32],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "request_later_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (21, blocks[&21])],
|
||||
request_heights: &[22, 25, 28],
|
||||
exp_update_heights: &[21, 22, 25, 28],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (5, blocks[&5])],
|
||||
request_heights: &[4],
|
||||
exp_update_heights: &[4, 5],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_2",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (10, blocks[&10])],
|
||||
request_heights: &[4, 6],
|
||||
exp_update_heights: &[4, 6, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (7, blocks[&7]), (11, blocks[&11])],
|
||||
request_heights: &[8, 9, 15],
|
||||
exp_update_heights: &[8, 9, 11, 15],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_tip_only",
|
||||
chain: local_chain![(0, blocks[&0]), (5, blocks[&5]), (49, blocks[&49])],
|
||||
request_heights: &[TIP_HEIGHT],
|
||||
exp_update_heights: &[49],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, blocks[&23])],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, h!("23"))],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[13, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg_2",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[17, 20],
|
||||
exp_update_heights: &[17, 20, 21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(9, blocks[&9]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[25, 27],
|
||||
exp_update_heights: &[9, 22, 23, 25, 27],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg_2",
|
||||
chain: local_chain![(0, blocks[&0]), (9, h!("9"))],
|
||||
request_heights: &[10],
|
||||
exp_update_heights: &[0, 9, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (9, h!("9"))],
|
||||
request_heights: &[8, 11],
|
||||
exp_update_heights: &[1, 8, 9, 11],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
||||
let update_blocks = update
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let exp_update_blocks = t
|
||||
.exp_update_heights
|
||||
.iter()
|
||||
.map(|&height| {
|
||||
let hash = blocks[&height];
|
||||
BlockId { height, hash }
|
||||
})
|
||||
.chain(
|
||||
// Electrs Esplora `get_block` call fetches 10 blocks which is included in the
|
||||
// update
|
||||
blocks
|
||||
.range(TIP_HEIGHT - 9..)
|
||||
.map(|(&height, &hash)| BlockId { height, hash }),
|
||||
)
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(
|
||||
update_blocks, exp_update_blocks,
|
||||
"[{}:{}] unexpected update",
|
||||
i, t.name
|
||||
);
|
||||
|
||||
let _ = chain
|
||||
.apply_update(update)
|
||||
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
|
||||
|
||||
// all requested heights must exist in the final chain
|
||||
for height in t.request_heights {
|
||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||
assert_eq!(
|
||||
chain.blocks().get(height),
|
||||
Some(exp_blockhash),
|
||||
"[{}:{}] block {}:{} must exist in final chain",
|
||||
i,
|
||||
t.name,
|
||||
height,
|
||||
exp_blockhash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.4.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.8.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Seek},
|
||||
io::{self, BufReader, Seek},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@ use crate::bincode_options;
|
||||
///
|
||||
/// [`next`]: Self::next
|
||||
pub struct EntryIter<'t, T> {
|
||||
db_file: Option<&'t mut File>,
|
||||
|
||||
/// Buffered reader around the file
|
||||
db_file: BufReader<&'t mut File>,
|
||||
finished: bool,
|
||||
/// The file position for the first read of `db_file`.
|
||||
start_pos: Option<u64>,
|
||||
types: PhantomData<T>,
|
||||
@@ -24,8 +25,9 @@ pub struct EntryIter<'t, T> {
|
||||
impl<'t, T> EntryIter<'t, T> {
|
||||
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
|
||||
Self {
|
||||
db_file: Some(db_file),
|
||||
db_file: BufReader::new(db_file),
|
||||
start_pos: Some(start_pos),
|
||||
finished: false,
|
||||
types: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -38,44 +40,44 @@ where
|
||||
type Item = Result<T, IterError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// closure which reads a single entry starting from `self.pos`
|
||||
let read_one = |f: &mut File, start_pos: Option<u64>| -> Result<Option<T>, IterError> {
|
||||
let pos = match start_pos {
|
||||
Some(pos) => f.seek(io::SeekFrom::Start(pos))?,
|
||||
None => f.stream_position()?,
|
||||
};
|
||||
if self.finished {
|
||||
return None;
|
||||
}
|
||||
(|| {
|
||||
if let Some(start) = self.start_pos.take() {
|
||||
self.db_file.seek(io::SeekFrom::Start(start))?;
|
||||
}
|
||||
|
||||
match bincode_options().deserialize_from(&*f) {
|
||||
Ok(changeset) => {
|
||||
f.stream_position()?;
|
||||
Ok(Some(changeset))
|
||||
}
|
||||
let pos_before_read = self.db_file.stream_position()?;
|
||||
match bincode_options().deserialize_from(&mut self.db_file) {
|
||||
Ok(changeset) => Ok(Some(changeset)),
|
||||
Err(e) => {
|
||||
self.finished = true;
|
||||
let pos_after_read = self.db_file.stream_position()?;
|
||||
// allow unexpected EOF if 0 bytes were read
|
||||
if let bincode::ErrorKind::Io(inner) = &*e {
|
||||
if inner.kind() == io::ErrorKind::UnexpectedEof {
|
||||
let eof = f.seek(io::SeekFrom::End(0))?;
|
||||
if pos == eof {
|
||||
return Ok(None);
|
||||
}
|
||||
if inner.kind() == io::ErrorKind::UnexpectedEof
|
||||
&& pos_after_read == pos_before_read
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
f.seek(io::SeekFrom::Start(pos))?;
|
||||
self.db_file.seek(io::SeekFrom::Start(pos_before_read))?;
|
||||
Err(IterError::Bincode(*e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let result = read_one(self.db_file.as_mut()?, self.start_pos.take());
|
||||
if result.is_err() {
|
||||
self.db_file = None;
|
||||
}
|
||||
result.transpose()
|
||||
})()
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for IterError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
IterError::Io(value)
|
||||
impl<'t, T> Drop for EntryIter<'t, T> {
|
||||
fn drop(&mut self) {
|
||||
// This syncs the underlying file's offset with the buffer's position. This way, we
|
||||
// maintain the correct position to start the next read/write.
|
||||
if let Ok(pos) = self.db_file.stream_position() {
|
||||
let _ = self.db_file.get_mut().seek(io::SeekFrom::Start(pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,4 +99,10 @@ impl core::fmt::Display for IterError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for IterError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
IterError::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IterError {}
|
||||
|
||||
@@ -13,14 +13,14 @@ pub(crate) fn bincode_options() -> impl bincode::Options {
|
||||
|
||||
/// Error that occurs due to problems encountered with the file.
|
||||
#[derive(Debug)]
|
||||
pub enum FileError<'a> {
|
||||
pub enum FileError {
|
||||
/// IO error, this may mean that the file is too short.
|
||||
Io(io::Error),
|
||||
/// Magic bytes do not match what is expected.
|
||||
InvalidMagicBytes { got: Vec<u8>, expected: &'a [u8] },
|
||||
InvalidMagicBytes { got: Vec<u8>, expected: Vec<u8> },
|
||||
}
|
||||
|
||||
impl<'a> core::fmt::Display for FileError<'a> {
|
||||
impl core::fmt::Display for FileError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "io error trying to read file: {}", e),
|
||||
@@ -33,10 +33,10 @@ impl<'a> core::fmt::Display for FileError<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<io::Error> for FileError<'a> {
|
||||
impl From<io::Error> for FileError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::error::Error for FileError<'a> {}
|
||||
impl std::error::Error for FileError {}
|
||||
|
||||
@@ -15,13 +15,13 @@ use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
///
|
||||
/// The changesets are the results of altering a tracker implementation (`T`).
|
||||
#[derive(Debug)]
|
||||
pub struct Store<'a, C> {
|
||||
magic: &'a [u8],
|
||||
pub struct Store<C> {
|
||||
magic_len: usize,
|
||||
db_file: File,
|
||||
marker: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, C> PersistBackend<C> for Store<'a, C>
|
||||
impl<C> PersistBackend<C> for Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
@@ -38,7 +38,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C> Store<'a, C>
|
||||
impl<C> Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
@@ -48,7 +48,7 @@ where
|
||||
/// the `Store` in the future with [`open`].
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
pub fn create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -64,10 +64,11 @@ where
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?;
|
||||
f.write_all(magic)?;
|
||||
Ok(Self {
|
||||
magic,
|
||||
magic_len: magic.len(),
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
@@ -83,7 +84,7 @@ where
|
||||
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
|
||||
///
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn open<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -94,12 +95,12 @@ where
|
||||
if magic_buf != magic {
|
||||
return Err(FileError::InvalidMagicBytes {
|
||||
got: magic_buf,
|
||||
expected: magic,
|
||||
expected: magic.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
magic_len: magic.len(),
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
@@ -111,7 +112,7 @@ where
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open_or_create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn open_or_create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -132,14 +133,14 @@ where
|
||||
/// always iterate over all entries until `None` is returned if you want your next write to go
|
||||
/// at the end; otherwise, you will write over existing entries.
|
||||
pub fn iter_changesets(&mut self) -> EntryIter<C> {
|
||||
EntryIter::new(self.magic.len() as u64, &mut self.db_file)
|
||||
EntryIter::new(self.magic_len as u64, &mut self.db_file)
|
||||
}
|
||||
|
||||
/// Loads all the changesets that have been stored as one giant changeset.
|
||||
///
|
||||
/// This function returns a tuple of the aggregate changeset and a result that indicates
|
||||
/// whether an error occurred while reading or deserializing one of the entries. If so the
|
||||
/// changeset will consist of all of those it was able to read.
|
||||
/// This function returns the aggregate changeset, or `None` if nothing was persisted.
|
||||
/// If reading or deserializing any of the entries fails, an error is returned that
|
||||
/// consists of all those it was able to read.
|
||||
///
|
||||
/// You should usually check the error. In many applications, it may make sense to do a full
|
||||
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
|
||||
@@ -219,6 +220,7 @@ mod test {
|
||||
|
||||
use bincode::DefaultOptions;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
io::{Read, Write},
|
||||
vec::Vec,
|
||||
};
|
||||
@@ -228,7 +230,7 @@ mod test {
|
||||
const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] =
|
||||
[98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49];
|
||||
|
||||
type TestChangeSet = Vec<String>;
|
||||
type TestChangeSet = BTreeSet<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestTracker;
|
||||
@@ -253,7 +255,7 @@ mod test {
|
||||
fn open_or_create_new() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let changeset = vec!["hello".to_string(), "world".to_string()];
|
||||
let changeset = BTreeSet::from(["hello".to_string(), "world".to_string()]);
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
@@ -304,7 +306,7 @@ mod test {
|
||||
let mut data = [255_u8; 2000];
|
||||
data[..TEST_MAGIC_BYTES_LEN].copy_from_slice(&TEST_MAGIC_BYTES);
|
||||
|
||||
let changeset = vec!["one".into(), "two".into(), "three!".into()];
|
||||
let changeset = TestChangeSet::from(["one".into(), "two".into(), "three!".into()]);
|
||||
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&data).expect("should write");
|
||||
@@ -340,4 +342,119 @@ mod test {
|
||||
|
||||
assert_eq!(got_bytes, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_write_is_short() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = [
|
||||
TestChangeSet::from(["1".into()]),
|
||||
TestChangeSet::from(["2".into(), "3".into()]),
|
||||
TestChangeSet::from(["4".into(), "5".into(), "6".into()]),
|
||||
];
|
||||
let last_changeset = TestChangeSet::from(["7".into(), "8".into(), "9".into()]);
|
||||
let last_changeset_bytes = bincode_options().serialize(&last_changeset).unwrap();
|
||||
|
||||
for short_write_len in 1..last_changeset_bytes.len() - 1 {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", short_write_len));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// simulate creating a file, writing data where the last write is incomplete
|
||||
{
|
||||
let mut db =
|
||||
Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
// this is the incomplete write
|
||||
db.db_file
|
||||
.write_all(&last_changeset_bytes[..short_write_len])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// load file again and aggregate changesets
|
||||
// write the last changeset again (this time it succeeds)
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let err = db
|
||||
.aggregate_changesets()
|
||||
.expect_err("should return error as last read is short");
|
||||
assert_eq!(
|
||||
err.changeset,
|
||||
changesets.iter().cloned().reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets that are written in full",
|
||||
);
|
||||
db.db_file.write_all(&last_changeset_bytes).unwrap();
|
||||
}
|
||||
|
||||
// load file again - this time we should successfully aggregate all changesets
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let aggregated_changesets = db
|
||||
.aggregate_changesets()
|
||||
.expect("aggregating all changesets should succeed");
|
||||
assert_eq!(
|
||||
aggregated_changesets,
|
||||
changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(core::iter::once(last_changeset.clone()))
|
||||
.reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_after_short_read() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = (0..20)
|
||||
.map(|n| TestChangeSet::from([format!("{}", n)]))
|
||||
.collect::<Vec<_>>();
|
||||
let last_changeset = TestChangeSet::from(["last".into()]);
|
||||
|
||||
for read_count in 0..changesets.len() {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", read_count));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// First, we create the file with all the changesets!
|
||||
let mut db = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
drop(db);
|
||||
|
||||
// We re-open the file and read `read_count` number of changesets.
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let mut exp_aggregation = db
|
||||
.iter_changesets()
|
||||
.take(read_count)
|
||||
.map(|r| r.expect("must read valid changeset"))
|
||||
.fold(TestChangeSet::default(), |mut acc, v| {
|
||||
Append::append(&mut acc, v);
|
||||
acc
|
||||
});
|
||||
// We write after a short read.
|
||||
db.write_changes(&last_changeset)
|
||||
.expect("last write must succeed");
|
||||
Append::append(&mut exp_aggregation, last_changeset.clone());
|
||||
drop(db);
|
||||
|
||||
// We open the file again and check whether aggregate changeset is expected.
|
||||
let aggregation = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.unwrap()
|
||||
.aggregate_changesets()
|
||||
.expect("must aggregate changesets")
|
||||
.unwrap_or_default();
|
||||
assert_eq!(aggregation, exp_aggregation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
68
example-crates/example_bitcoind_rpc_polling/README.md
Normal file
68
example-crates/example_bitcoind_rpc_polling/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Example RPC CLI
|
||||
|
||||
### Simple Regtest Test
|
||||
|
||||
1. Start local regtest bitcoind.
|
||||
```
|
||||
mkdir -p /tmp/regtest/bitcoind
|
||||
bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
|
||||
```
|
||||
2. Create a test bitcoind wallet and set bitcoind env.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
|
||||
export RPC_URL=127.0.0.1:18443
|
||||
export RPC_USER=<your-rpc-username>
|
||||
export RPC_PASS=<your-rpc-password>
|
||||
```
|
||||
3. Get test bitcoind wallet info.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
|
||||
```
|
||||
4. Get new test bitcoind wallet address.
|
||||
```
|
||||
BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
|
||||
echo $BITCOIND_ADDRESS
|
||||
```
|
||||
5. Generate 101 blocks with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
|
||||
```
|
||||
6. Verify test bitcoind wallet balance.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
|
||||
```
|
||||
7. Set descriptor env and get address from RPC CLI wallet.
|
||||
```
|
||||
export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
|
||||
cargo run -- --network regtest address next
|
||||
```
|
||||
8. Send 5 test bitcoin to RPC CLI wallet.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
|
||||
```
|
||||
9. Sync blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
10. Get RPC CLI wallet unconfirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
11. Generate 1 block with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
|
||||
```
|
||||
12. Sync the blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
13. Get RPC CLI wallet confirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
14. Get RPC CLI wallet transactions.
|
||||
```
|
||||
cargo run -- --network regtest txout list
|
||||
```
|
||||
@@ -14,7 +14,7 @@ use bdk_bitcoind_rpc::{
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Block, Transaction},
|
||||
indexed_tx_graph, keychain,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
local_chain::{self, LocalChain},
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
};
|
||||
use example_cli::{
|
||||
@@ -42,7 +42,7 @@ type ChangeSet = (
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Emission {
|
||||
Block { height: u32, block: Block },
|
||||
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
|
||||
Mempool(Vec<(Transaction, u64)>),
|
||||
Tip(u32),
|
||||
}
|
||||
@@ -110,9 +110,13 @@ enum RpcCommands {
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
println!(
|
||||
"[{:>10}s] loaded initial changeset from db",
|
||||
start.elapsed().as_secs_f32()
|
||||
@@ -147,7 +151,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let rpc_cmd = match args.command {
|
||||
example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
|
||||
general_cmd => {
|
||||
let res = example_cli::handle_commands(
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -160,8 +164,6 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd,
|
||||
);
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,17 +180,20 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Instant::now();
|
||||
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
let mut db = db.lock().unwrap();
|
||||
|
||||
let chain_update =
|
||||
CheckPoint::from_header(&block.header, height).into_update(false);
|
||||
let chain_changeset = chain
|
||||
.apply_update(chain_update)
|
||||
.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(block, height);
|
||||
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
|
||||
db.stage((chain_changeset, graph_changeset));
|
||||
|
||||
// commit staged db changes in intervals
|
||||
@@ -256,7 +261,8 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
loop {
|
||||
match emitter.next_block()? {
|
||||
Some((height, block)) => {
|
||||
Some(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
if sigterm_flag.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
@@ -264,7 +270,7 @@ fn main() -> anyhow::Result<()> {
|
||||
block_count = rpc_client.get_block_count()? as u32;
|
||||
tx.send(Emission::Tip(block_count))?;
|
||||
}
|
||||
tx.send(Emission::Block { height, block })?;
|
||||
tx.send(Emission::Block(block_emission))?;
|
||||
}
|
||||
None => {
|
||||
if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
|
||||
@@ -293,13 +299,17 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut chain = chain.lock().unwrap();
|
||||
|
||||
let changeset = match emission {
|
||||
Emission::Block { height, block } => {
|
||||
let chain_update =
|
||||
CheckPoint::from_header(&block.header, height).into_update(false);
|
||||
Emission::Block(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
let chain_update = local_chain::Update {
|
||||
tip: block_emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
let chain_changeset = chain
|
||||
.apply_update(chain_update)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(block, height);
|
||||
let graph_changeset =
|
||||
graph.apply_block_relevant(&block_emission.block, height);
|
||||
(chain_changeset, graph_changeset)
|
||||
}
|
||||
Emission::Mempool(mempool_txs) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ pub type KeychainChangeSet<A> = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
pub type Database<'m, C> = Persist<Store<'m, C>, C>;
|
||||
pub type Database<C> = Persist<Store<C>, C>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
@@ -53,7 +53,6 @@ pub struct Args<CS: clap::Subcommand, S: clap::Args> {
|
||||
pub command: Commands<CS, S>,
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
#[clap(flatten)]
|
||||
@@ -73,7 +72,9 @@ pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
},
|
||||
/// Send coins to an address.
|
||||
Send {
|
||||
/// Amount to send in satoshis
|
||||
value: u64,
|
||||
/// Destination address
|
||||
address: Address<address::NetworkUnchecked>,
|
||||
#[clap(short, default_value = "bnb")]
|
||||
coin_select: CoinSelectionAlgo,
|
||||
@@ -135,7 +136,6 @@ impl core::fmt::Display for CoinSelectionAlgo {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address.
|
||||
@@ -144,14 +144,17 @@ pub enum AddressCmd {
|
||||
New,
|
||||
/// List all addresses
|
||||
List {
|
||||
/// List change addresses
|
||||
#[clap(long)]
|
||||
change: bool,
|
||||
},
|
||||
/// Get last revealed address index for each keychain.
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum TxOutCmd {
|
||||
/// List transaction outputs.
|
||||
List {
|
||||
/// Return only spent outputs.
|
||||
#[clap(short, long)]
|
||||
@@ -185,7 +188,12 @@ impl core::fmt::Display for Keychain {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct CreateTxChange {
|
||||
pub index_changeset: keychain::ChangeSet<Keychain>,
|
||||
pub change_keychain: Keychain,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
@@ -193,10 +201,7 @@ pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
) -> anyhow::Result<(
|
||||
Transaction,
|
||||
Option<(keychain::ChangeSet<Keychain>, (Keychain, u32))>,
|
||||
)>
|
||||
) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
@@ -388,7 +393,11 @@ where
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some((changeset, (internal_keychain, change_index)))
|
||||
Some(CreateTxChange {
|
||||
index_changeset: changeset,
|
||||
change_keychain: internal_keychain,
|
||||
index: change_index,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -396,35 +405,34 @@ where
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
// Alias the elements of `Result` of `planned_utxos`
|
||||
pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
|
||||
|
||||
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
|
||||
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
.filter_map(
|
||||
#[allow(clippy::type_complexity)]
|
||||
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
},
|
||||
)
|
||||
.filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -457,11 +465,10 @@ where
|
||||
|
||||
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage(C::from((
|
||||
db.stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)));
|
||||
db.commit()?;
|
||||
)))?;
|
||||
let addr =
|
||||
Address::from_script(spk, network).context("failed to derive address")?;
|
||||
println!("[address @ {}] {}", spk_i, addr);
|
||||
@@ -595,17 +602,21 @@ where
|
||||
let (tx, change_info) =
|
||||
create_tx(graph, chain, keymap, coin_select, address, value)?;
|
||||
|
||||
if let Some((index_changeset, (change_keychain, index))) = change_info {
|
||||
if let Some(CreateTxChange {
|
||||
index_changeset,
|
||||
change_keychain,
|
||||
index,
|
||||
}) = change_info
|
||||
{
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this, then we don't want to broadcast.
|
||||
{
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage(C::from((
|
||||
db.stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)));
|
||||
db.commit()?;
|
||||
)))?;
|
||||
}
|
||||
|
||||
// We don't want other callers/threads to use this address while we're using it
|
||||
@@ -627,10 +638,10 @@ where
|
||||
// We know the tx is at least unconfirmed now. Note if persisting here fails,
|
||||
// it's not a big deal since we can always find it again form
|
||||
// blockchain.
|
||||
db.lock().unwrap().stage(C::from((
|
||||
db.lock().unwrap().stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
keychain_changeset,
|
||||
)));
|
||||
)))?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -645,17 +656,26 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn init<'m, CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &'m [u8],
|
||||
/// The initial state returned by [`init`].
|
||||
pub struct Init<CS: clap::Subcommand, S: clap::Args, C> {
|
||||
/// Arguments parsed by the cli.
|
||||
pub args: Args<CS, S>,
|
||||
/// Descriptor keymap.
|
||||
pub keymap: KeyMap,
|
||||
/// Keychain-txout index.
|
||||
pub index: KeychainTxOutIndex<Keychain>,
|
||||
/// Persistence backend.
|
||||
pub db: Mutex<Database<C>>,
|
||||
/// Initial changeset.
|
||||
pub init_changeset: C,
|
||||
}
|
||||
|
||||
/// Parses command line arguments and initializes all components, creating
|
||||
/// a file store with the given parameters, or loading one if it exists.
|
||||
pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &[u8],
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<(
|
||||
Args<CS, S>,
|
||||
KeyMap,
|
||||
KeychainTxOutIndex<Keychain>,
|
||||
Mutex<Database<'m, C>>,
|
||||
C,
|
||||
)>
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned,
|
||||
{
|
||||
@@ -681,7 +701,7 @@ where
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
let mut db_backend = match Store::<'m, C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
Ok(db_backend) => db_backend,
|
||||
// we cannot return `err` directly as it has lifetime `'m`
|
||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||
@@ -689,11 +709,11 @@ where
|
||||
|
||||
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||
|
||||
Ok((
|
||||
Ok(Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
Mutex::new(Database::new(db_backend)),
|
||||
db: Mutex::new(Database::new(db_backend)),
|
||||
init_changeset,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,8 +103,15 @@ type ChangeSet = (
|
||||
);
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
|
||||
example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let (disk_local_chain, disk_tx_graph) = init_changeset;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
@@ -122,7 +129,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let electrum_cmd = match &args.command {
|
||||
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||
general_cmd => {
|
||||
let res = example_cli::handle_commands(
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -135,9 +142,6 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -99,8 +99,13 @@ pub struct ScanOptions {
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
|
||||
@@ -125,7 +130,7 @@ fn main() -> anyhow::Result<()> {
|
||||
example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
|
||||
// These are general commands handled by example_cli. Execute the cmd and return.
|
||||
general_cmd => {
|
||||
let res = example_cli::handle_commands(
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -140,9 +145,6 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
15
example-crates/wallet_rpc/Cargo.toml
Normal file
15
example-crates/wallet_rpc/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "wallet_rpc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
|
||||
|
||||
anyhow = "1"
|
||||
clap = { version = "3.2.25", features = ["derive", "env"] }
|
||||
ctrlc = "2.0.1"
|
||||
45
example-crates/wallet_rpc/README.md
Normal file
45
example-crates/wallet_rpc/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Wallet RPC Example
|
||||
|
||||
```
|
||||
$ cargo run --bin wallet_rpc -- --help
|
||||
|
||||
wallet_rpc 0.1.0
|
||||
Bitcoind RPC example using `bdk::Wallet`
|
||||
|
||||
USAGE:
|
||||
wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]
|
||||
|
||||
ARGS:
|
||||
<DESCRIPTOR> Wallet descriptor [env: DESCRIPTOR=]
|
||||
<CHANGE_DESCRIPTOR> Wallet change descriptor [env: CHANGE_DESCRIPTOR=]
|
||||
|
||||
OPTIONS:
|
||||
--db-path <DB_PATH>
|
||||
Where to store wallet data [env: BDK_DB_PATH=] [default: .bdk_wallet_rpc_example.db]
|
||||
|
||||
-h, --help
|
||||
Print help information
|
||||
|
||||
--network <NETWORK>
|
||||
Bitcoin network to connect to [env: BITCOIN_NETWORK=] [default: testnet]
|
||||
|
||||
--rpc-cookie <RPC_COOKIE>
|
||||
RPC auth cookie file [env: RPC_COOKIE=]
|
||||
|
||||
--rpc-pass <RPC_PASS>
|
||||
RPC auth password [env: RPC_PASS=]
|
||||
|
||||
--rpc-user <RPC_USER>
|
||||
RPC auth username [env: RPC_USER=]
|
||||
|
||||
--start-height <START_HEIGHT>
|
||||
Earliest block height to start sync from [env: START_HEIGHT=] [default: 481824]
|
||||
|
||||
--url <URL>
|
||||
RPC URL [env: RPC_URL=] [default: 127.0.0.1:8332]
|
||||
|
||||
-V, --version
|
||||
Print version information
|
||||
|
||||
```
|
||||
|
||||
182
example-crates/wallet_rpc/src/main.rs
Normal file
182
example-crates/wallet_rpc/src/main.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use bdk::{
|
||||
bitcoin::{Block, Network, Transaction},
|
||||
wallet::Wallet,
|
||||
};
|
||||
use bdk_bitcoind_rpc::{
|
||||
bitcoincore_rpc::{Auth, Client, RpcApi},
|
||||
Emitter,
|
||||
};
|
||||
use bdk_file_store::Store;
|
||||
use clap::{self, Parser};
|
||||
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
|
||||
|
||||
const DB_MAGIC: &str = "bdk-rpc-wallet-example";
|
||||
|
||||
/// Bitcoind RPC example using `bdk::Wallet`.
|
||||
///
|
||||
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
|
||||
/// count.
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct Args {
|
||||
/// Wallet descriptor
|
||||
#[clap(env = "DESCRIPTOR")]
|
||||
pub descriptor: String,
|
||||
/// Wallet change descriptor
|
||||
#[clap(env = "CHANGE_DESCRIPTOR")]
|
||||
pub change_descriptor: Option<String>,
|
||||
/// Earliest block height to start sync from
|
||||
#[clap(env = "START_HEIGHT", long, default_value = "481824")]
|
||||
pub start_height: u32,
|
||||
/// Bitcoin network to connect to
|
||||
#[clap(env = "BITCOIN_NETWORK", long, default_value = "testnet")]
|
||||
pub network: Network,
|
||||
/// Where to store wallet data
|
||||
#[clap(
|
||||
env = "BDK_DB_PATH",
|
||||
long,
|
||||
default_value = ".bdk_wallet_rpc_example.db"
|
||||
)]
|
||||
pub db_path: PathBuf,
|
||||
|
||||
/// RPC URL
|
||||
#[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")]
|
||||
pub url: String,
|
||||
/// RPC auth cookie file
|
||||
#[clap(env = "RPC_COOKIE", long)]
|
||||
pub rpc_cookie: Option<PathBuf>,
|
||||
/// RPC auth username
|
||||
#[clap(env = "RPC_USER", long)]
|
||||
pub rpc_user: Option<String>,
|
||||
/// RPC auth password
|
||||
#[clap(env = "RPC_PASS", long)]
|
||||
pub rpc_pass: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn client(&self) -> anyhow::Result<Client> {
|
||||
Ok(Client::new(
|
||||
&self.url,
|
||||
match (&self.rpc_cookie, &self.rpc_user, &self.rpc_pass) {
|
||||
(None, None, None) => Auth::None,
|
||||
(Some(path), _, _) => Auth::CookieFile(path.clone()),
|
||||
(_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
|
||||
(_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
|
||||
(_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
|
||||
},
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Emission {
|
||||
SigTerm,
|
||||
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
|
||||
Mempool(Vec<(Transaction, u64)>),
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let rpc_client = args.client()?;
|
||||
println!(
|
||||
"Connected to Bitcoin Core RPC at {:?}",
|
||||
rpc_client.get_blockchain_info().unwrap()
|
||||
);
|
||||
|
||||
let start_load_wallet = Instant::now();
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
&args.descriptor,
|
||||
args.change_descriptor.as_ref(),
|
||||
Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?,
|
||||
args.network,
|
||||
)?;
|
||||
println!(
|
||||
"Loaded wallet in {}s",
|
||||
start_load_wallet.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
let balance = wallet.get_balance();
|
||||
println!("Wallet balance before syncing: {} sats", balance.total());
|
||||
|
||||
let wallet_tip = wallet.latest_checkpoint();
|
||||
println!(
|
||||
"Wallet tip: {} at height {}",
|
||||
wallet_tip.hash(),
|
||||
wallet_tip.height()
|
||||
);
|
||||
|
||||
let (sender, receiver) = sync_channel::<Emission>(21);
|
||||
|
||||
let signal_sender = sender.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
signal_sender
|
||||
.send(Emission::SigTerm)
|
||||
.expect("failed to send sigterm")
|
||||
});
|
||||
|
||||
let emitter_tip = wallet_tip.clone();
|
||||
spawn(move || -> Result<(), anyhow::Error> {
|
||||
let mut emitter = Emitter::new(&rpc_client, emitter_tip, args.start_height);
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
sender.send(Emission::Block(emission))?;
|
||||
}
|
||||
sender.send(Emission::Mempool(emitter.mempool()?))?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut blocks_received = 0_usize;
|
||||
for emission in receiver {
|
||||
match emission {
|
||||
Emission::SigTerm => {
|
||||
println!("Sigterm received, exiting...");
|
||||
break;
|
||||
}
|
||||
Emission::Block(block_emission) => {
|
||||
blocks_received += 1;
|
||||
let height = block_emission.block_height();
|
||||
let hash = block_emission.block_hash();
|
||||
let connected_to = block_emission.connected_to();
|
||||
let start_apply_block = Instant::now();
|
||||
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
|
||||
wallet.commit()?;
|
||||
let elapsed = start_apply_block.elapsed().as_secs_f32();
|
||||
println!(
|
||||
"Applied block {} at height {} in {}s",
|
||||
hash, height, elapsed
|
||||
);
|
||||
}
|
||||
Emission::Mempool(mempool_emission) => {
|
||||
let start_apply_mempool = Instant::now();
|
||||
wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
|
||||
wallet.commit()?;
|
||||
println!(
|
||||
"Applied unconfirmed transactions in {}s",
|
||||
start_apply_mempool.elapsed().as_secs_f32()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let wallet_tip_end = wallet.latest_checkpoint();
|
||||
let balance = wallet.get_balance();
|
||||
println!(
|
||||
"Synced {} blocks in {}s",
|
||||
blocks_received,
|
||||
start_load_wallet.elapsed().as_secs_f32(),
|
||||
);
|
||||
println!(
|
||||
"Wallet tip is '{}:{}'",
|
||||
wallet_tip_end.height(),
|
||||
wallet_tip_end.hash()
|
||||
);
|
||||
println!("Wallet balance is {} sats", balance.total());
|
||||
println!(
|
||||
"Wallet has {} transactions and {} utxos",
|
||||
wallet.transactions().count(),
|
||||
wallet.list_unspent().count()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user