Compare commits
1 Commits
v1.0.0-alp
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c256280eda |
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
security_audit:
|
security_audit:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/audit-check@v1
|
- uses: actions-rs/audit-check@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install lcov tools
|
- name: Install lcov tools
|
||||||
run: sudo apt-get install lcov -y
|
run: sudo apt-get install lcov -y
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
|
|||||||
10
.github/workflows/cont_integration.yml
vendored
10
.github/workflows/cont_integration.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- --all-features
|
- --all-features
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
CFLAGS: -I/usr/include
|
CFLAGS: -I/usr/include
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
# Install a recent version of clang that supports wasm32
|
# Install a recent version of clang that supports wasm32
|
||||||
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
||||||
- run: sudo apt-get update || exit 1
|
- run: sudo apt-get update || exit 1
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
clippy_check:
|
clippy_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|||||||
6
.github/workflows/nightly_docs.yml
vendored
6
.github/workflows/nightly_docs.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default nightly-2022-12-14
|
run: rustup default nightly-2022-12-14
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
@@ -17,8 +17,6 @@ jobs:
|
|||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.2.1
|
uses: Swatinem/rust-cache@v2.2.1
|
||||||
- name: Pin dependencies for MSRV
|
|
||||||
run: cargo update -p home --precise "0.5.5"
|
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cargo doc --no-deps
|
run: cargo doc --no-deps
|
||||||
env:
|
env:
|
||||||
@@ -36,7 +34,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout `bitcoindevkit.org`
|
- name: Checkout `bitcoindevkit.org`
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.DOCS_PUSH_SSH_KEY }}
|
ssh-key: ${{ secrets.DOCS_PUSH_SSH_KEY }}
|
||||||
repository: bitcoindevkit/bitcoindevkit.org
|
repository: bitcoindevkit/bitcoindevkit.org
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ members = [
|
|||||||
"crates/esplora",
|
"crates/esplora",
|
||||||
"crates/bitcoind_rpc",
|
"crates/bitcoind_rpc",
|
||||||
"crates/hwi",
|
"crates/hwi",
|
||||||
"crates/testenv",
|
|
||||||
"example-crates/example_cli",
|
"example-crates/example_cli",
|
||||||
"example-crates/example_electrum",
|
"example-crates/example_electrum",
|
||||||
"example-crates/example_esplora",
|
"example-crates/example_esplora",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
version = "1.0.0-alpha.8"
|
version = "1.0.0-alpha.7"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
documentation = "https://docs.rs/bdk"
|
documentation = "https://docs.rs/bdk"
|
||||||
description = "A modern, lightweight, descriptor-based wallet library"
|
description = "A modern, lightweight, descriptor-based wallet library"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
## `bdk`
|
## `bdk`
|
||||||
|
|
||||||
The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
|
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
|
||||||
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
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
|
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
|
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
|
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
||||||
can create and sign transactions.
|
can create and sign transactions.
|
||||||
|
|
||||||
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
|
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
|
||||||
|
|
||||||
### Blockchain data
|
### Blockchain data
|
||||||
|
|
||||||
In order to get blockchain data for `Wallet` to consume, you should configure a client from
|
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
|
||||||
an available chain source. Typically you make a request to the chain source and get a response
|
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
|
||||||
that the `Wallet` can use to update its view of the chain.
|
|
||||||
|
This can be created manually or from blockchain-scanning crates.
|
||||||
|
|
||||||
**Blockchain Data Sources**
|
**Blockchain Data Sources**
|
||||||
|
|
||||||
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
||||||
* [`bdk_electrum`]: Grabs blockchain data from Electrum 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**
|
**Examples**
|
||||||
|
|
||||||
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
|
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
|
||||||
* [`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_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
|
### Persistence
|
||||||
|
|
||||||
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
|
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.
|
||||||
|
|
||||||
**Implementations**
|
**Implementations**
|
||||||
|
|
||||||
* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
|
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
```rust
|
||||||
```rust,compile_fail
|
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
|
||||||
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Create a new file `Store`.
|
// a type that implements `Persist`
|
||||||
let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
|
let db = ();
|
||||||
|
|
||||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
|
||||||
let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
|
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
|
||||||
|
|
||||||
// Insert a single `TxOut` at `OutPoint` into the wallet.
|
// get a new address (this increments revealed derivation index)
|
||||||
let _ = wallet.insert_txout(outpoint, txout);
|
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
|
||||||
wallet.commit().expect("must write to database");
|
println!("staged changes: {:?}", wallet.staged());
|
||||||
|
// persist changes
|
||||||
|
wallet.commit().expect("must save");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -218,11 +218,9 @@ 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
|
license, shall be dual licensed as above, without any additional terms or
|
||||||
conditions.
|
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_chain`]: https://docs.rs/bdk_chain/latest
|
||||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||||
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
||||||
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
|
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
|
||||||
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
||||||
|
|||||||
@@ -11,10 +11,9 @@
|
|||||||
|
|
||||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||||
|
|
||||||
|
use crate::FeeRate;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||||
use bitcoin::Amount;
|
|
||||||
use bitcoin::FeeRate;
|
|
||||||
use bitcoin::TxOut;
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
// TODO upstream the functions here to `rust-bitcoin`?
|
// TODO upstream the functions here to `rust-bitcoin`?
|
||||||
@@ -66,7 +65,7 @@ impl PsbtUtils for Psbt {
|
|||||||
let fee_amount = self.fee_amount();
|
let fee_amount = self.fee_amount();
|
||||||
fee_amount.map(|fee| {
|
fee_amount.map(|fee| {
|
||||||
let weight = self.clone().extract_tx().weight();
|
let weight = self.clone().extract_tx().weight();
|
||||||
Amount::from_sat(fee) / weight
|
FeeRate::from_wu(fee, weight)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
|
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use core::convert::AsRef;
|
use core::convert::AsRef;
|
||||||
|
use core::ops::Sub;
|
||||||
|
|
||||||
use bdk_chain::ConfirmationTime;
|
use bdk_chain::ConfirmationTime;
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||||
use bitcoin::psbt;
|
use bitcoin::{psbt, Weight};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -46,6 +47,116 @@ 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`].
|
/// An unspent output owned by a [`Wallet`].
|
||||||
///
|
///
|
||||||
/// [`Wallet`]: crate::Wallet
|
/// [`Wallet`]: crate::Wallet
|
||||||
@@ -133,3 +244,73 @@ impl Utxo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_store_feerate_in_const() {
|
||||||
|
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_value() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_nan() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_inf() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_feerate_pos_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_btc_per_kvb() {
|
||||||
|
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_vbyte() {
|
||||||
|
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_default_min_relay_fee() {
|
||||||
|
let fee = FeeRate::default_min_relay_fee();
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kvb() {
|
||||||
|
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kwu() {
|
||||||
|
let fee = FeeRate::from_sat_per_kwu(250.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
assert_eq!(fee.sat_per_kwu(), 250.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
//! &self,
|
//! &self,
|
||||||
//! required_utxos: Vec<WeightedUtxo>,
|
//! required_utxos: Vec<WeightedUtxo>,
|
||||||
//! optional_utxos: Vec<WeightedUtxo>,
|
//! optional_utxos: Vec<WeightedUtxo>,
|
||||||
//! fee_rate: FeeRate,
|
//! fee_rate: bdk::FeeRate,
|
||||||
//! target_amount: u64,
|
//! target_amount: u64,
|
||||||
//! drain_script: &Script,
|
//! drain_script: &Script,
|
||||||
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
//! },
|
//! },
|
||||||
//! )
|
//! )
|
||||||
//! .collect::<Vec<_>>();
|
//! .collect::<Vec<_>>();
|
||||||
//! let additional_fees = (fee_rate * additional_weight).to_sat();
|
//! let additional_fees = fee_rate.fee_wu(additional_weight);
|
||||||
//! let amount_needed_with_fees = additional_fees + target_amount;
|
//! let amount_needed_with_fees = additional_fees + target_amount;
|
||||||
//! if selected_amount < amount_needed_with_fees {
|
//! if selected_amount < amount_needed_with_fees {
|
||||||
//! return Err(coin_selection::Error::InsufficientFunds {
|
//! return Err(coin_selection::Error::InsufficientFunds {
|
||||||
@@ -101,10 +101,10 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::chain::collections::HashSet;
|
use crate::chain::collections::HashSet;
|
||||||
|
use crate::types::FeeRate;
|
||||||
use crate::wallet::utils::IsDust;
|
use crate::wallet::utils::IsDust;
|
||||||
use crate::Utxo;
|
use crate::Utxo;
|
||||||
use crate::WeightedUtxo;
|
use crate::WeightedUtxo;
|
||||||
use bitcoin::FeeRate;
|
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::consensus::encode::serialize;
|
use bitcoin::consensus::encode::serialize;
|
||||||
@@ -313,8 +313,7 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
|
|||||||
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
|
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)
|
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
|
||||||
let drain_output_len = serialize(drain_script).len() + 8usize;
|
let drain_output_len = serialize(drain_script).len() + 8usize;
|
||||||
let change_fee =
|
let change_fee = fee_rate.fee_vb(drain_output_len);
|
||||||
(fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
|
|
||||||
let drain_val = remaining_amount.saturating_sub(change_fee);
|
let drain_val = remaining_amount.saturating_sub(change_fee);
|
||||||
|
|
||||||
if drain_val.is_dust(drain_script) {
|
if drain_val.is_dust(drain_script) {
|
||||||
@@ -345,12 +344,9 @@ fn select_sorted_utxos(
|
|||||||
(&mut selected_amount, &mut fee_amount),
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||||
if must_use || **selected_amount < target_amount + **fee_amount {
|
if must_use || **selected_amount < target_amount + **fee_amount {
|
||||||
**fee_amount += (fee_rate
|
**fee_amount += fee_rate.fee_wu(Weight::from_wu(
|
||||||
* Weight::from_wu(
|
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
));
|
||||||
))
|
|
||||||
.to_sat();
|
|
||||||
|
|
||||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
Some(weighted_utxo.utxo)
|
Some(weighted_utxo.utxo)
|
||||||
} else {
|
} else {
|
||||||
@@ -391,10 +387,9 @@ struct OutputGroup {
|
|||||||
|
|
||||||
impl OutputGroup {
|
impl OutputGroup {
|
||||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||||
let fee = (fee_rate
|
let fee = fee_rate.fee_wu(Weight::from_wu(
|
||||||
* Weight::from_wu((TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64))
|
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||||
.to_sat();
|
));
|
||||||
|
|
||||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||||
OutputGroup {
|
OutputGroup {
|
||||||
weighted_utxo,
|
weighted_utxo,
|
||||||
@@ -461,8 +456,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
|||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let cost_of_change =
|
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||||
(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
|
// `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
|
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
|
||||||
@@ -553,7 +547,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
mut curr_value: i64,
|
mut curr_value: i64,
|
||||||
mut curr_available_value: i64,
|
mut curr_available_value: i64,
|
||||||
target_amount: i64,
|
target_amount: i64,
|
||||||
cost_of_change: u64,
|
cost_of_change: f32,
|
||||||
drain_script: &Script,
|
drain_script: &Script,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
@@ -744,11 +738,12 @@ mod test {
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use bdk_chain::ConfirmationTime;
|
use bdk_chain::ConfirmationTime;
|
||||||
use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut};
|
use bitcoin::{OutPoint, ScriptBuf, TxOut};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use crate::wallet::coin_selection::filter_duplicates;
|
use crate::wallet::coin_selection::filter_duplicates;
|
||||||
|
use crate::wallet::Vbytes;
|
||||||
|
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
@@ -898,7 +893,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -919,7 +914,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -940,7 +935,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -962,7 +957,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -980,7 +975,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -997,7 +992,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1018,7 +1013,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1039,7 +1034,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1061,7 +1056,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1080,7 +1075,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1101,7 +1096,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1122,7 +1117,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
utxos.clone(),
|
utxos.clone(),
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1143,7 +1138,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1180,7 +1175,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
required,
|
required,
|
||||||
optional,
|
optional,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1202,7 +1197,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1220,7 +1215,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1232,18 +1227,22 @@ mod test {
|
|||||||
let utxos = get_test_utxos();
|
let utxos = get_test_utxos();
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
let target_amount = 99932; // first utxo's effective value
|
let target_amount = 99932; // first utxo's effective value
|
||||||
let feerate = FeeRate::BROADCAST_MIN;
|
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::new(0)
|
let result = BranchAndBoundCoinSelection::new(0)
|
||||||
.coin_select(vec![], utxos, feerate, target_amount, &drain_script)
|
.coin_select(
|
||||||
|
vec![],
|
||||||
|
utxos,
|
||||||
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
|
target_amount,
|
||||||
|
&drain_script,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount(), 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
let input_weight = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE) as u64;
|
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
|
||||||
// the final fee rate should be exactly the same as the fee rate given
|
// the final fee rate should be exactly the same as the fee rate given
|
||||||
let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
|
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
|
||||||
assert_eq!(result_feerate, feerate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1259,7 +1258,7 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
optional_utxos,
|
optional_utxos,
|
||||||
FeeRate::ZERO,
|
FeeRate::from_sat_per_vb(0.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
)
|
)
|
||||||
@@ -1271,7 +1270,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "BnBNoExactMatch")]
|
#[should_panic(expected = "BnBNoExactMatch")]
|
||||||
fn test_bnb_function_no_exact_match() {
|
fn test_bnb_function_no_exact_match() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = get_test_utxos()
|
let utxos: Vec<OutputGroup> = get_test_utxos()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1280,7 +1279,7 @@ mod test {
|
|||||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
let target_amount = 20_000 + FEE_AMOUNT;
|
let target_amount = 20_000 + FEE_AMOUNT;
|
||||||
@@ -1301,7 +1300,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
||||||
fn test_bnb_function_tries_exceeded() {
|
fn test_bnb_function_tries_exceeded() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1310,7 +1309,7 @@ mod test {
|
|||||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||||
let target_amount = 20_000 + FEE_AMOUNT;
|
let target_amount = 20_000 + FEE_AMOUNT;
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
@@ -1332,9 +1331,9 @@ mod test {
|
|||||||
// The match won't be exact but still in the range
|
// The match won't be exact but still in the range
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bnb_function_almost_exact_match_with_fees() {
|
fn test_bnb_function_almost_exact_match_with_fees() {
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
|
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||||
|
|
||||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1347,7 +1346,7 @@ mod test {
|
|||||||
|
|
||||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||||
// cost_of_change + 5.
|
// cost_of_change + 5.
|
||||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
|
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
||||||
|
|
||||||
let drain_script = ScriptBuf::default();
|
let drain_script = ScriptBuf::default();
|
||||||
|
|
||||||
@@ -1372,7 +1371,7 @@ mod test {
|
|||||||
fn test_bnb_function_exact_match_more_utxos() {
|
fn test_bnb_function_exact_match_more_utxos() {
|
||||||
let seed = [0; 32];
|
let seed = [0; 32];
|
||||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||||
let fee_rate = FeeRate::ZERO;
|
let fee_rate = FeeRate::from_sat_per_vb(0.0);
|
||||||
|
|
||||||
for _ in 0..200 {
|
for _ in 0..200 {
|
||||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||||
@@ -1398,7 +1397,7 @@ mod test {
|
|||||||
curr_value,
|
curr_value,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
target_amount,
|
target_amount,
|
||||||
0,
|
0.0,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
)
|
)
|
||||||
@@ -1414,7 +1413,7 @@ mod test {
|
|||||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||||
|
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let utxos: Vec<OutputGroup> = utxos
|
let utxos: Vec<OutputGroup> = utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
@@ -1443,7 +1442,7 @@ mod test {
|
|||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb_unchecked(10),
|
FeeRate::from_sat_per_vb(10.0),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
@@ -1469,7 +1468,7 @@ mod test {
|
|||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
required,
|
required,
|
||||||
optional,
|
optional,
|
||||||
FeeRate::from_sat_per_vb_unchecked(10),
|
FeeRate::from_sat_per_vb(10.0),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
@@ -1491,7 +1490,7 @@ mod test {
|
|||||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb_unchecked(10_000),
|
FeeRate::from_sat_per_vb(10_000.0),
|
||||||
500_000,
|
500_000,
|
||||||
&drain_script,
|
&drain_script,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
use crate::descriptor::policy::PolicyError;
|
use crate::descriptor::policy::PolicyError;
|
||||||
use crate::descriptor::DescriptorError;
|
use crate::descriptor::DescriptorError;
|
||||||
use crate::wallet::coin_selection;
|
use crate::wallet::coin_selection;
|
||||||
use crate::{descriptor, KeychainKind};
|
use crate::{descriptor, FeeRate, KeychainKind};
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
@@ -83,8 +83,8 @@ pub enum CreateTxError<P> {
|
|||||||
},
|
},
|
||||||
/// When bumping a tx the fee rate requested is lower than required
|
/// When bumping a tx the fee rate requested is lower than required
|
||||||
FeeRateTooLow {
|
FeeRateTooLow {
|
||||||
/// Required fee rate
|
/// Required fee rate (satoshi/vbyte)
|
||||||
required: bitcoin::FeeRate,
|
required: FeeRate,
|
||||||
},
|
},
|
||||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||||
NoUtxosSelected,
|
NoUtxosSelected,
|
||||||
@@ -168,10 +168,8 @@ where
|
|||||||
CreateTxError::FeeRateTooLow { required } => {
|
CreateTxError::FeeRateTooLow { required } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
|
"Fee rate too low: required {} sat/vbyte",
|
||||||
//"Fee rate too low: required {required:#}"
|
required.as_sat_per_vb()
|
||||||
"Fee rate too low: required {} sat/vb",
|
|
||||||
crate::floating_rate!(required)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CreateTxError::NoUtxosSelected => {
|
CreateTxError::NoUtxosSelected => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
//! # use bdk::signer::SignerOrdering;
|
//! # use bdk::signer::SignerOrdering;
|
||||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||||
//! # use bdk::wallet::AddressIndex::New;
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||||
//! # use hwi::HWIClient;
|
//! # use hwi::HWIClient;
|
||||||
//! # use std::sync::Arc;
|
//! # use std::sync::Arc;
|
||||||
//! #
|
//! #
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ use bdk_chain::{
|
|||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction,
|
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
|
||||||
TxOut, Txid, Weight, Witness,
|
Txid, Weight, Witness,
|
||||||
};
|
};
|
||||||
use bitcoin::{consensus::encode::serialize, BlockHash};
|
use bitcoin::{consensus::encode::serialize, BlockHash};
|
||||||
use bitcoin::{constants::genesis_block, psbt};
|
use bitcoin::{constants::genesis_block, psbt};
|
||||||
@@ -986,8 +986,10 @@ impl<D> Wallet<D> {
|
|||||||
/// ```
|
/// ```
|
||||||
/// [`insert_txout`]: Self::insert_txout
|
/// [`insert_txout`]: Self::insert_txout
|
||||||
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
|
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
|
||||||
self.calculate_fee(tx)
|
self.calculate_fee(tx).map(|fee| {
|
||||||
.map(|fee| bitcoin::Amount::from_sat(fee) / tx.weight())
|
let weight = tx.weight();
|
||||||
|
FeeRate::from_wu(fee, weight)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
/// Compute the `tx`'s sent and received amounts (in satoshis).
|
||||||
@@ -1430,31 +1432,32 @@ impl<D> Wallet<D> {
|
|||||||
(Some(rbf), _) => rbf.get_value(),
|
(Some(rbf), _) => rbf.get_value(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() {
|
let (fee_rate, mut fee_amount) = match params
|
||||||
|
.fee_policy
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&FeePolicy::FeeRate(FeeRate::default()))
|
||||||
|
{
|
||||||
//FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
|
//FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
|
||||||
FeePolicy::FeeAmount(fee) => {
|
FeePolicy::FeeAmount(fee) => {
|
||||||
if let Some(previous_fee) = params.bumping_fee {
|
if let Some(previous_fee) = params.bumping_fee {
|
||||||
if fee < previous_fee.absolute {
|
if *fee < previous_fee.absolute {
|
||||||
return Err(CreateTxError::FeeTooLow {
|
return Err(CreateTxError::FeeTooLow {
|
||||||
required: previous_fee.absolute,
|
required: previous_fee.absolute,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(FeeRate::ZERO, fee)
|
(FeeRate::from_sat_per_vb(0.0), *fee)
|
||||||
}
|
}
|
||||||
FeePolicy::FeeRate(rate) => {
|
FeePolicy::FeeRate(rate) => {
|
||||||
if let Some(previous_fee) = params.bumping_fee {
|
if let Some(previous_fee) = params.bumping_fee {
|
||||||
let required_feerate = FeeRate::from_sat_per_kwu(
|
let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0);
|
||||||
previous_fee.rate.to_sat_per_kwu()
|
if *rate < required_feerate {
|
||||||
+ FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb
|
|
||||||
);
|
|
||||||
if rate < required_feerate {
|
|
||||||
return Err(CreateTxError::FeeRateTooLow {
|
return Err(CreateTxError::FeeRateTooLow {
|
||||||
required: required_feerate,
|
required: required_feerate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(rate, 0)
|
(*rate, 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1497,7 +1500,7 @@ impl<D> Wallet<D> {
|
|||||||
outgoing += value;
|
outgoing += value;
|
||||||
}
|
}
|
||||||
|
|
||||||
fee_amount += (fee_rate * tx.weight()).to_sat();
|
fee_amount += fee_rate.fee_wu(tx.weight());
|
||||||
|
|
||||||
// Segwit transactions' header is 2WU larger than legacy txs' header,
|
// Segwit transactions' header is 2WU larger than legacy txs' header,
|
||||||
// as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144).
|
// as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144).
|
||||||
@@ -1508,7 +1511,7 @@ impl<D> Wallet<D> {
|
|||||||
// end up with a transaction with a slightly higher fee rate than the requested one.
|
// 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
|
// If, instead, we undershoot, we may end up with a feerate lower than the requested one
|
||||||
// - we might come up with non broadcastable txs!
|
// - we might come up with non broadcastable txs!
|
||||||
fee_amount += (fee_rate * Weight::from_wu(2)).to_sat();
|
fee_amount += fee_rate.fee_wu(Weight::from_wu(2));
|
||||||
|
|
||||||
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
||||||
&& internal_descriptor.is_none()
|
&& internal_descriptor.is_none()
|
||||||
@@ -1649,7 +1652,7 @@ impl<D> Wallet<D> {
|
|||||||
/// let mut psbt = {
|
/// let mut psbt = {
|
||||||
/// let mut builder = wallet.build_fee_bump(tx.txid())?;
|
/// let mut builder = wallet.build_fee_bump(tx.txid())?;
|
||||||
/// builder
|
/// builder
|
||||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));
|
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0));
|
||||||
/// builder.finish()?
|
/// builder.finish()?
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
@@ -1777,7 +1780,7 @@ impl<D> Wallet<D> {
|
|||||||
utxos: original_utxos,
|
utxos: original_utxos,
|
||||||
bumping_fee: Some(tx_builder::PreviousFee {
|
bumping_fee: Some(tx_builder::PreviousFee {
|
||||||
absolute: fee,
|
absolute: fee,
|
||||||
rate: fee_rate,
|
rate: fee_rate.as_sat_per_vb(),
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1971,15 +1974,6 @@ impl<D> Wallet<D> {
|
|||||||
if sign_options.remove_partial_sigs {
|
if sign_options.remove_partial_sigs {
|
||||||
psbt_input.partial_sigs.clear();
|
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,
|
Err(_) => finished = false,
|
||||||
}
|
}
|
||||||
@@ -1988,12 +1982,6 @@ impl<D> Wallet<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if finished && sign_options.remove_taproot_extras {
|
|
||||||
for output in &mut psbt.outputs {
|
|
||||||
output.tap_key_origins.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(finished)
|
Ok(finished)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2578,17 +2566,6 @@ fn create_signers<E: IntoWalletDescriptor>(
|
|||||||
Ok((signers, change_signers))
|
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]
|
#[macro_export]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Macro for getting a wallet for use in a doctest
|
/// Macro for getting a wallet for use in a doctest
|
||||||
|
|||||||
@@ -782,16 +782,6 @@ pub struct SignOptions {
|
|||||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||||
pub remove_partial_sigs: bool,
|
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.
|
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||||
///
|
///
|
||||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||||
@@ -837,7 +827,6 @@ impl Default for SignOptions {
|
|||||||
assume_height: None,
|
assume_height: None,
|
||||||
allow_all_sighashes: false,
|
allow_all_sighashes: false,
|
||||||
remove_partial_sigs: true,
|
remove_partial_sigs: true,
|
||||||
remove_taproot_extras: true,
|
|
||||||
try_finalize: true,
|
try_finalize: true,
|
||||||
tap_leaves_options: TapLeavesOptions::default(),
|
tap_leaves_options: TapLeavesOptions::default(),
|
||||||
sign_with_tap_internal_key: true,
|
sign_with_tap_internal_key: true,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||||
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||||
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||||
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||||
//! // Only spend non-change outputs
|
//! // Only spend non-change outputs
|
||||||
//! .do_not_spend_change()
|
//! .do_not_spend_change()
|
||||||
//! // Turn on RBF signaling
|
//! // Turn on RBF signaling
|
||||||
@@ -40,20 +40,22 @@
|
|||||||
//! # Ok::<(), anyhow::Error>(())
|
//! # Ok::<(), anyhow::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use crate::collections::BTreeMap;
|
||||||
|
use crate::collections::HashSet;
|
||||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||||
|
use bdk_chain::PersistBackend;
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
use bdk_chain::PersistBackend;
|
|
||||||
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||||
use bitcoin::script::PushBytes;
|
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||||
use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
|
||||||
|
|
||||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||||
use super::{ChangeSet, CreateTxError, Wallet};
|
use super::ChangeSet;
|
||||||
use crate::collections::{BTreeMap, HashSet};
|
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
|
||||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
use crate::wallet::CreateTxError;
|
||||||
|
use crate::{Utxo, Wallet};
|
||||||
|
|
||||||
/// Context in which the [`TxBuilder`] is valid
|
/// Context in which the [`TxBuilder`] is valid
|
||||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||||
@@ -161,7 +163,7 @@ pub(crate) struct TxParams {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct PreviousFee {
|
pub(crate) struct PreviousFee {
|
||||||
pub absolute: u64,
|
pub absolute: u64,
|
||||||
pub rate: FeeRate,
|
pub rate: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -172,7 +174,7 @@ pub(crate) enum FeePolicy {
|
|||||||
|
|
||||||
impl Default for FeePolicy {
|
impl Default for FeePolicy {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
|
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,12 +191,14 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
|||||||
|
|
||||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||||
/// Set a custom fee rate.
|
/// Set a custom fee rate
|
||||||
///
|
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
||||||
/// 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 this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
||||||
/// This means that the total fee paid is equal to `fee_rate` times the size
|
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
|
||||||
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
|
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
|
||||||
/// relay policy.
|
/// * 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)
|
||||||
///
|
///
|
||||||
/// Note that this is really a minimum feerate -- it's possible to
|
/// Note that this is really a minimum feerate -- it's possible to
|
||||||
/// overshoot it slightly since adding a change output to drain the remaining
|
/// overshoot it slightly since adding a change output to drain the remaining
|
||||||
@@ -777,7 +781,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
|||||||
/// .drain_wallet()
|
/// .drain_wallet()
|
||||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||||
/// .drain_to(to_address.script_pubkey())
|
/// .drain_to(to_address.script_pubkey())
|
||||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||||
/// .enable_rbf();
|
/// .enable_rbf();
|
||||||
/// let psbt = tx_builder.finish()?;
|
/// let psbt = tx_builder.finish()?;
|
||||||
/// # Ok::<(), anyhow::Error>(())
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
|||||||
use bdk_chain::indexed_tx_graph::Indexer;
|
use bdk_chain::indexed_tx_graph::Indexer;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
// Return a fake wallet that appears to be funded for testing.
|
// Return a fake wallet that appears to be funded for testing.
|
||||||
@@ -154,16 +154,3 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
|||||||
pub fn get_test_tr_dup_keys() -> &'static str {
|
pub fn get_test_tr_dup_keys() -> &'static str {
|
||||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
"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,8 +1,7 @@
|
|||||||
use bdk::bitcoin::FeeRate;
|
|
||||||
use bdk::bitcoin::TxIn;
|
use bdk::bitcoin::TxIn;
|
||||||
use bdk::wallet::AddressIndex;
|
use bdk::wallet::AddressIndex;
|
||||||
use bdk::wallet::AddressIndex::New;
|
use bdk::wallet::AddressIndex::New;
|
||||||
use bdk::{psbt, SignOptions};
|
use bdk::{psbt, FeeRate, SignOptions};
|
||||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
mod common;
|
mod common;
|
||||||
@@ -83,13 +82,13 @@ fn test_psbt_sign_with_finalized() {
|
|||||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
let expected_fee_rate = 1.2345;
|
||||||
|
|
||||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(expected_fee_rate);
|
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||||
let mut psbt = builder.finish().unwrap();
|
let mut psbt = builder.finish().unwrap();
|
||||||
let fee_amount = psbt.fee_amount();
|
let fee_amount = psbt.fee_amount();
|
||||||
assert!(fee_amount.is_some());
|
assert!(fee_amount.is_some());
|
||||||
@@ -100,21 +99,21 @@ fn test_psbt_fee_rate_with_witness_utxo() {
|
|||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
let expected_fee_rate = 1.2345;
|
||||||
|
|
||||||
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(expected_fee_rate);
|
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||||
let mut psbt = builder.finish().unwrap();
|
let mut psbt = builder.finish().unwrap();
|
||||||
let fee_amount = psbt.fee_amount();
|
let fee_amount = psbt.fee_amount();
|
||||||
assert!(fee_amount.is_some());
|
assert!(fee_amount.is_some());
|
||||||
@@ -124,21 +123,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
|||||||
assert!(finalized);
|
assert!(finalized);
|
||||||
|
|
||||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psbt_fee_rate_with_missing_txout() {
|
fn test_psbt_fee_rate_with_missing_txout() {
|
||||||
use psbt::PsbtUtils;
|
use psbt::PsbtUtils;
|
||||||
|
|
||||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
let expected_fee_rate = 1.2345;
|
||||||
|
|
||||||
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wpkh_wallet.get_address(New);
|
let addr = wpkh_wallet.get_address(New);
|
||||||
let mut builder = wpkh_wallet.build_tx();
|
let mut builder = wpkh_wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(expected_fee_rate);
|
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||||
let mut wpkh_psbt = builder.finish().unwrap();
|
let mut wpkh_psbt = builder.finish().unwrap();
|
||||||
|
|
||||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||||
@@ -150,7 +149,7 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
|||||||
let addr = pkh_wallet.get_address(New);
|
let addr = pkh_wallet.get_address(New);
|
||||||
let mut builder = pkh_wallet.build_tx();
|
let mut builder = pkh_wallet.build_tx();
|
||||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
builder.fee_rate(expected_fee_rate);
|
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||||
let mut pkh_psbt = builder.finish().unwrap();
|
let mut pkh_psbt = builder.finish().unwrap();
|
||||||
|
|
||||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||||
@@ -162,26 +161,16 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
|||||||
fn test_psbt_multiple_internalkey_signers() {
|
fn test_psbt_multiple_internalkey_signers() {
|
||||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||||
use bdk::KeychainKind;
|
use bdk::KeychainKind;
|
||||||
use bitcoin::key::TapTweak;
|
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
|
||||||
use bitcoin::secp256k1::{schnorr, KeyPair, Message, Secp256k1, XOnlyPublicKey};
|
use miniscript::psbt::PsbtExt;
|
||||||
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
|
||||||
use bitcoin::{PrivateKey, TxOut};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
|
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
|
||||||
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 send_to = wallet.get_address(AddressIndex::New);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.drain_to(send_to.script_pubkey()).drain_wallet();
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
let mut psbt = builder.finish().unwrap();
|
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
|
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
||||||
wallet.add_signer(
|
wallet.add_signer(
|
||||||
KeychainKind::External,
|
KeychainKind::External,
|
||||||
@@ -194,32 +183,10 @@ fn test_psbt_multiple_internalkey_signers() {
|
|||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||||
assert!(finalized);
|
// Checks that we signed using the right key
|
||||||
|
assert!(
|
||||||
// To verify, we need the signature, message, and pubkey
|
psbt.finalize_mut(&secp).is_ok(),
|
||||||
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
|
"The wrong internal key was used"
|
||||||
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::tx_builder::AddForeignUtxoError;
|
||||||
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
|
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
|
||||||
use bdk::wallet::{AddressIndex::*, NewError};
|
use bdk::wallet::{AddressIndex::*, NewError};
|
||||||
use bdk::KeychainKind;
|
use bdk::{FeeRate, KeychainKind};
|
||||||
use bdk_chain::COINBASE_MATURITY;
|
use bdk_chain::COINBASE_MATURITY;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::psbt;
|
|
||||||
use bitcoin::script::PushBytesBuf;
|
|
||||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||||
use bitcoin::taproot::TapNodeHash;
|
use bitcoin::ScriptBuf;
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Sequence,
|
absolute, script::PushBytesBuf, taproot::TapNodeHash, Address, OutPoint, Sequence, Transaction,
|
||||||
Transaction, TxIn, TxOut, Txid, Weight,
|
TxIn, TxOut, Weight,
|
||||||
};
|
};
|
||||||
|
use bitcoin::{psbt, Network};
|
||||||
|
use bitcoin::{BlockHash, Txid};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
@@ -246,11 +246,9 @@ 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
|
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||||
// sats are the transaction fee.
|
// sats are the transaction fee.
|
||||||
|
|
||||||
// tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113
|
// tx weight = 452 bytes, as vbytes = (452+3)/4 = 113
|
||||||
// fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212
|
// fee rate (sats per vbyte) = fee / vbytes = 1000 / 113 = 8.8495575221 rounded to 8.849558
|
||||||
// fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9
|
assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558);
|
||||||
assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212);
|
|
||||||
assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -304,15 +302,11 @@ macro_rules! assert_fee_rate {
|
|||||||
|
|
||||||
assert_eq!(fee_amount, $fees);
|
assert_eq!(fee_amount, $fees);
|
||||||
|
|
||||||
let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight())
|
let tx_fee_rate = FeeRate::from_wu($fees, tx.weight());
|
||||||
.to_sat_per_kwu();
|
let fee_rate = $fee_rate;
|
||||||
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 {
|
if !dust_change {
|
||||||
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);
|
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);
|
||||||
} else {
|
} else {
|
||||||
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||||
}
|
}
|
||||||
@@ -653,7 +647,7 @@ fn test_create_tx_default_fee_rate() {
|
|||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -663,11 +657,11 @@ fn test_create_tx_custom_fee_rate() {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -759,7 +753,7 @@ fn test_create_tx_drain_to_dust_amount() {
|
|||||||
builder
|
builder
|
||||||
.drain_to(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(454));
|
.fee_rate(FeeRate::from_sat_per_vb(453.0));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1487,6 +1481,7 @@ fn test_bump_fee_confirmed_tx() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[should_panic(expected = "FeeRateTooLow")]
|
||||||
fn test_bump_fee_low_fee_rate() {
|
fn test_bump_fee_low_fee_rate() {
|
||||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
@@ -1495,7 +1490,6 @@ fn test_bump_fee_low_fee_rate() {
|
|||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.enable_rbf();
|
.enable_rbf();
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let feerate = psbt.fee_rate().unwrap();
|
|
||||||
|
|
||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx();
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
@@ -1505,18 +1499,8 @@ fn test_bump_fee_low_fee_rate() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::BROADCAST_MIN);
|
builder.fee_rate(FeeRate::from_sat_per_vb(1.0));
|
||||||
let res = builder.finish();
|
builder.finish().unwrap();
|
||||||
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]
|
#[test]
|
||||||
@@ -1584,9 +1568,8 @@ fn test_bump_fee_reduce_change() {
|
|||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(feerate).enable_rbf();
|
builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf();
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
@@ -1617,7 +1600,7 @@ fn test_bump_fee_reduce_change() {
|
|||||||
sent_received.1
|
sent_received.1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_absolute(200);
|
builder.fee_absolute(200);
|
||||||
@@ -1680,10 +1663,9 @@ fn test_bump_fee_reduce_single_recipient() {
|
|||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.fee_rate(feerate)
|
.fee_rate(FeeRate::from_sat_per_vb(2.5))
|
||||||
.allow_shrinking(addr.script_pubkey())
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
@@ -1697,7 +1679,7 @@ fn test_bump_fee_reduce_single_recipient() {
|
|||||||
assert_eq!(tx.output.len(), 1);
|
assert_eq!(tx.output.len(), 1);
|
||||||
assert_eq!(tx.output[0].value + fee.unwrap_or(0), sent_received.0);
|
assert_eq!(tx.output[0].value + fee.unwrap_or(0), sent_received.0);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1792,7 +1774,7 @@ fn test_bump_fee_drain_wallet() {
|
|||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.allow_shrinking(addr.script_pubkey())
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.extract_tx());
|
||||||
|
|
||||||
@@ -1855,7 +1837,7 @@ fn test_bump_fee_remove_output_manually_selected_only() {
|
|||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.manually_selected_only()
|
.manually_selected_only()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(255));
|
.fee_rate(FeeRate::from_sat_per_vb(255.0));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1896,7 +1878,7 @@ fn test_bump_fee_add_input() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
@@ -1923,7 +1905,7 @@ fn test_bump_fee_add_input() {
|
|||||||
sent_received.1
|
sent_received.1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2006,7 +1988,7 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
|||||||
// now bump the fees without using `allow_shrinking`. the wallet should add an
|
// 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
|
// extra input and a change output, and leave the original output untouched
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
@@ -2038,7 +2020,7 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
|||||||
75_000 - original_send_all_amount - fee.unwrap_or(0)
|
75_000 - original_send_all_amount - fee.unwrap_or(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2083,7 +2065,7 @@ fn test_bump_fee_add_input_change_dust() {
|
|||||||
// two inputs (50k, 25k) and one output (45k) - epsilon
|
// two inputs (50k, 25k) and one output (45k) - epsilon
|
||||||
// We use epsilon here to avoid asking for a slightly too high feerate
|
// We use epsilon here to avoid asking for a slightly too high feerate
|
||||||
let fee_abs = 50_000 + 25_000 - 45_000 - 10;
|
let fee_abs = 50_000 + 25_000 - 45_000 - 10;
|
||||||
builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight);
|
builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
@@ -2106,7 +2088,7 @@ fn test_bump_fee_add_input_change_dust() {
|
|||||||
45_000
|
45_000
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2137,7 +2119,7 @@ fn test_bump_fee_force_add_input() {
|
|||||||
builder
|
builder
|
||||||
.add_utxo(incoming_op)
|
.add_utxo(incoming_op)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx());
|
||||||
let fee = check_fee!(wallet, psbt);
|
let fee = check_fee!(wallet, psbt);
|
||||||
@@ -2165,7 +2147,7 @@ fn test_bump_fee_force_add_input() {
|
|||||||
sent_received.1
|
sent_received.1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2261,7 +2243,7 @@ fn test_bump_fee_unconfirmed_inputs_only() {
|
|||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
|
builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2296,7 +2278,7 @@ fn test_bump_fee_unconfirmed_input() {
|
|||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(15))
|
.fee_rate(FeeRate::from_sat_per_vb(15.0))
|
||||||
.allow_shrinking(addr.script_pubkey())
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
@@ -2316,7 +2298,7 @@ fn test_fee_amount_negative_drain_val() {
|
|||||||
let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
|
let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.assume_checked();
|
.assume_checked();
|
||||||
let fee_rate = FeeRate::from_sat_per_kwu(500);
|
let fee_rate = FeeRate::from_sat_per_vb(2.01);
|
||||||
let incoming_op = receive_output_in_latest_block(&mut wallet, 8859);
|
let incoming_op = receive_output_in_latest_block(&mut wallet, 8859);
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
@@ -2831,32 +2813,6 @@ 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]
|
#[test]
|
||||||
fn test_taproot_psbt_populate_tap_key_origins() {
|
fn test_taproot_psbt_populate_tap_key_origins() {
|
||||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
||||||
@@ -3543,7 +3499,7 @@ fn test_fee_rate_sign_no_grinding_high_r() {
|
|||||||
// alright.
|
// alright.
|
||||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
let mut data = PushBytesBuf::try_from(vec![0]).unwrap();
|
let mut data = PushBytesBuf::try_from(vec![0]).unwrap();
|
||||||
builder
|
builder
|
||||||
@@ -3609,7 +3565,7 @@ fn test_fee_rate_sign_grinding_low_r() {
|
|||||||
// signature is 70 bytes.
|
// signature is 70 bytes.
|
||||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New);
|
let addr = wallet.get_address(New);
|
||||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.drain_to(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_bitcoind_rpc"
|
name = "bdk_bitcoind_rpc"
|
||||||
version = "0.7.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.63"
|
rust-version = "1.63"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -19,7 +19,7 @@ bitcoincore-rpc = { version = "0.17" }
|
|||||||
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bdk_testenv = { path = "../testenv", version = "0.1.0", default_features = false }
|
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -2,14 +2,160 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
|
|
||||||
use bdk_bitcoind_rpc::Emitter;
|
use bdk_bitcoind_rpc::Emitter;
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{Address, Amount, Txid},
|
bitcoin::{Address, Amount, BlockHash, Txid},
|
||||||
keychain::Balance,
|
keychain::Balance,
|
||||||
local_chain::{self, CheckPoint, LocalChain},
|
local_chain::{self, CheckPoint, LocalChain},
|
||||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||||
};
|
};
|
||||||
use bdk_testenv::TestEnv;
|
use bitcoin::{
|
||||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||||
use bitcoincore_rpc::RpcApi;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure that blocks are emitted in order even after reorg.
|
/// Ensure that blocks are emitted in order even after reorg.
|
||||||
///
|
///
|
||||||
@@ -20,22 +166,17 @@ use bitcoincore_rpc::RpcApi;
|
|||||||
#[test]
|
#[test]
|
||||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let network_tip = env.rpc_client().get_block_count()?;
|
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
||||||
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
|
|
||||||
|
|
||||||
// Mine some blocks and return the actual block hashes.
|
// mine some blocks and returned the actual block hashes
|
||||||
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
|
|
||||||
// returning block hashes.
|
|
||||||
let exp_hashes = {
|
let exp_hashes = {
|
||||||
let mut hashes = (0..=network_tip)
|
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
|
||||||
.map(|height| env.rpc_client().get_block_hash(height))
|
hashes.extend(env.mine_blocks(101, None)?);
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
|
|
||||||
hashes
|
hashes
|
||||||
};
|
};
|
||||||
|
|
||||||
// See if the emitter outputs the right blocks.
|
// see if the emitter outputs the right blocks
|
||||||
println!("first sync:");
|
println!("first sync:");
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
let height = emission.block_height();
|
let height = emission.block_height();
|
||||||
@@ -66,7 +207,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
"final local_chain state is unexpected",
|
"final local_chain state is unexpected",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Perform reorg.
|
// perform reorg
|
||||||
let reorged_blocks = env.reorg(6)?;
|
let reorged_blocks = env.reorg(6)?;
|
||||||
let exp_hashes = exp_hashes
|
let exp_hashes = exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -75,7 +216,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// See if the emitter outputs the right blocks.
|
// see if the emitter outputs the right blocks
|
||||||
println!("after reorg:");
|
println!("after reorg:");
|
||||||
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
@@ -131,25 +272,16 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
|
|
||||||
println!("getting new addresses!");
|
println!("getting new addresses!");
|
||||||
let addr_0 = env
|
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.rpc_client()
|
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.get_new_address(None, None)?
|
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.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!("got new addresses!");
|
||||||
|
|
||||||
println!("mining block!");
|
println!("mining block!");
|
||||||
env.mine_blocks(101, None)?;
|
env.mine_blocks(101, None)?;
|
||||||
println!("mined blocks!");
|
println!("mined blocks!");
|
||||||
|
|
||||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||||
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||||
let mut index = SpkTxOutIndex::<usize>::default();
|
let mut index = SpkTxOutIndex::<usize>::default();
|
||||||
index.insert_spk(0, addr_0.script_pubkey());
|
index.insert_spk(0, addr_0.script_pubkey());
|
||||||
@@ -158,7 +290,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
index
|
index
|
||||||
});
|
});
|
||||||
|
|
||||||
let emitter = &mut Emitter::new(env.rpc_client(), chain.tip(), 0);
|
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
|
||||||
|
|
||||||
while let Some(emission) = emitter.next_block()? {
|
while let Some(emission) = emitter.next_block()? {
|
||||||
let height = emission.block_height();
|
let height = emission.block_height();
|
||||||
@@ -174,7 +306,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
let exp_txids = {
|
let exp_txids = {
|
||||||
let mut txids = BTreeSet::new();
|
let mut txids = BTreeSet::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
txids.insert(env.rpc_client().send_to_address(
|
txids.insert(env.client.send_to_address(
|
||||||
&addr_0,
|
&addr_0,
|
||||||
Amount::from_sat(10_000),
|
Amount::from_sat(10_000),
|
||||||
None,
|
None,
|
||||||
@@ -210,7 +342,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// mine a block that confirms the 3 txs
|
// mine a block that confirms the 3 txs
|
||||||
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
||||||
let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32;
|
let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
|
||||||
let exp_anchors = exp_txids
|
let exp_anchors = exp_txids
|
||||||
.iter()
|
.iter()
|
||||||
.map({
|
.map({
|
||||||
@@ -254,10 +386,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
EMITTER_START_HEIGHT as _,
|
EMITTER_START_HEIGHT as _,
|
||||||
);
|
);
|
||||||
@@ -331,24 +463,21 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// setup addresses
|
// setup addresses
|
||||||
let addr_to_mine = env
|
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.rpc_client()
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked();
|
|
||||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||||
|
|
||||||
// setup receiver
|
// setup receiver
|
||||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||||
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||||
let mut recv_index = SpkTxOutIndex::default();
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
recv_index.insert_spk((), spk_to_track.clone());
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
@@ -364,7 +493,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// lock outputs that send to `addr_to_track`
|
// lock outputs that send to `addr_to_track`
|
||||||
let outpoints_to_lock = env
|
let outpoints_to_lock = env
|
||||||
.rpc_client()
|
.client
|
||||||
.get_transaction(&txid, None)?
|
.get_transaction(&txid, None)?
|
||||||
.transaction()?
|
.transaction()?
|
||||||
.output
|
.output
|
||||||
@@ -373,7 +502,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
||||||
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
env.rpc_client().lock_unspent(&outpoints_to_lock)?;
|
env.client.lock_unspent(&outpoints_to_lock)?;
|
||||||
|
|
||||||
let _ = env.mine_blocks(1, None)?;
|
let _ = env.mine_blocks(1, None)?;
|
||||||
}
|
}
|
||||||
@@ -422,19 +551,16 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks and sync up emitter
|
// mine blocks and sync up emitter
|
||||||
let addr = env
|
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.rpc_client()
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked();
|
|
||||||
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
||||||
while emitter.next_header()?.is_some() {}
|
while emitter.next_header()?.is_some() {}
|
||||||
|
|
||||||
@@ -487,19 +613,16 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks to get initial balance, sync emitter up to tip
|
// mine blocks to get initial balance, sync emitter up to tip
|
||||||
let addr = env
|
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.rpc_client()
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked();
|
|
||||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||||
while emitter.next_header()?.is_some() {}
|
while emitter.next_header()?.is_some() {}
|
||||||
|
|
||||||
@@ -575,19 +698,16 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let env = TestEnv::new()?;
|
let env = TestEnv::new()?;
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// mine blocks to get initial balance
|
// mine blocks to get initial balance
|
||||||
let addr = env
|
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||||
.rpc_client()
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked();
|
|
||||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||||
|
|
||||||
// introduce mempool tx at each block extension
|
// introduce mempool tx at each block extension
|
||||||
@@ -605,7 +725,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(tx, _)| tx.txid())
|
.map(|(tx, _)| tx.txid())
|
||||||
.collect::<BTreeSet<_>>(),
|
.collect::<BTreeSet<_>>(),
|
||||||
env.rpc_client()
|
env.client
|
||||||
.get_raw_mempool()?
|
.get_raw_mempool()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<BTreeSet<_>>(),
|
.collect::<BTreeSet<_>>(),
|
||||||
@@ -624,7 +744,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
|||||||
// emission.
|
// emission.
|
||||||
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
||||||
let tx_introductions = dbg!(env
|
let tx_introductions = dbg!(env
|
||||||
.rpc_client()
|
.client
|
||||||
.get_raw_mempool_verbose()?
|
.get_raw_mempool_verbose()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(txid, entry)| (txid, entry.height as usize))
|
.map(|(txid, entry)| (txid, entry.height as usize))
|
||||||
@@ -701,10 +821,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// start height is 99
|
// start height is 99
|
||||||
let mut emitter = Emitter::new(
|
let mut emitter = Emitter::new(
|
||||||
env.rpc_client(),
|
&env.client,
|
||||||
CheckPoint::new(BlockId {
|
CheckPoint::new(BlockId {
|
||||||
height: 0,
|
height: 0,
|
||||||
hash: env.rpc_client().get_block_hash(0)?,
|
hash: env.client.get_block_hash(0)?,
|
||||||
}),
|
}),
|
||||||
(PREMINE_COUNT - 2) as u32,
|
(PREMINE_COUNT - 2) as u32,
|
||||||
);
|
);
|
||||||
@@ -722,12 +842,12 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
|||||||
let block_hash_100a = block_header_100a.block_hash();
|
let block_hash_100a = block_header_100a.block_hash();
|
||||||
|
|
||||||
// get hash for block 101a
|
// get hash for block 101a
|
||||||
let block_hash_101a = env.rpc_client().get_block_hash(101)?;
|
let block_hash_101a = env.client.get_block_hash(101)?;
|
||||||
|
|
||||||
// invalidate blocks 99a, 100a, 101a
|
// invalidate blocks 99a, 100a, 101a
|
||||||
env.rpc_client().invalidate_block(&block_hash_99a)?;
|
env.client.invalidate_block(&block_hash_99a)?;
|
||||||
env.rpc_client().invalidate_block(&block_hash_100a)?;
|
env.client.invalidate_block(&block_hash_100a)?;
|
||||||
env.rpc_client().invalidate_block(&block_hash_101a)?;
|
env.client.invalidate_block(&block_hash_101a)?;
|
||||||
|
|
||||||
// mine new blocks 99b, 100b, 101b
|
// mine new blocks 99b, 100b, 101b
|
||||||
env.mine_blocks(3, None)?;
|
env.mine_blocks(3, None)?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_electrum"
|
name = "bdk_electrum"
|
||||||
version = "0.10.0"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -15,8 +15,3 @@ readme = "README.md"
|
|||||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||||
electrum-client = { version = "0.18" }
|
electrum-client = { version = "0.18" }
|
||||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
bdk_testenv = { path = "../testenv", version = "0.1.0", default-features = false }
|
|
||||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
|
||||||
anyhow = "1"
|
|
||||||
@@ -189,7 +189,7 @@ impl<A: ElectrumApi> ElectrumExt for A {
|
|||||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||||
let mut request_spks = keychain_spks
|
let mut request_spks = keychain_spks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, s)| (k, s.into_iter()))
|
.map(|(k, s)| (k.clone(), s.into_iter()))
|
||||||
.collect::<BTreeMap<K, _>>();
|
.collect::<BTreeMap<K, _>>();
|
||||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||||
|
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
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]
|
[package]
|
||||||
name = "bdk_esplora"
|
name = "bdk_esplora"
|
||||||
version = "0.10.0"
|
version = "0.9.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -21,8 +21,7 @@ futures = { version = "0.3.26", optional = true }
|
|||||||
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
||||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.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"] }
|
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
|||||||
@@ -52,19 +52,6 @@ pub trait EsploraAsyncExt {
|
|||||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
/// 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
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
///
|
|
||||||
/// ## Note
|
|
||||||
///
|
|
||||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
|
||||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
|
||||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
|
||||||
///
|
|
||||||
/// This follows the same approach as other Bitcoin-related software,
|
|
||||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
|
||||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
|
||||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
|
||||||
///
|
|
||||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
|
||||||
async fn full_scan<K: Ord + Clone + Send>(
|
async fn full_scan<K: Ord + Clone + Send>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<
|
keychain_spks: BTreeMap<
|
||||||
@@ -175,7 +162,6 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||||
let stop_gap = Ord::max(stop_gap, 1);
|
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
let mut spks = spks.into_iter();
|
let mut spks = spks.into_iter();
|
||||||
@@ -240,12 +226,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
let past_gap_limit = if let Some(i) = last_active_index {
|
||||||
last_index >= i.saturating_add(stop_gap as u32)
|
last_index > i.saturating_add(stop_gap as u32)
|
||||||
} else {
|
} else {
|
||||||
last_index + 1 >= stop_gap as u32
|
last_index >= stop_gap as u32
|
||||||
};
|
};
|
||||||
if gap_limit_reached {
|
if past_gap_limit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,19 +50,6 @@ pub trait EsploraExt {
|
|||||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
/// 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
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
///
|
|
||||||
/// ## Note
|
|
||||||
///
|
|
||||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
|
||||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
|
||||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
|
||||||
///
|
|
||||||
/// This follows the same approach as other Bitcoin-related software,
|
|
||||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
|
||||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
|
||||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
|
||||||
///
|
|
||||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
|
||||||
fn full_scan<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
@@ -162,7 +149,6 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||||
let stop_gap = Ord::max(stop_gap, 1);
|
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
let mut spks = spks.into_iter();
|
let mut spks = spks.into_iter();
|
||||||
@@ -230,12 +216,12 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||||
let gap_limit_reached = if let Some(i) = last_active_index {
|
let past_gap_limit = if let Some(i) = last_active_index {
|
||||||
last_index >= i.saturating_add(stop_gap as u32)
|
last_index > i.saturating_add(stop_gap as u32)
|
||||||
} else {
|
} else {
|
||||||
last_index + 1 >= stop_gap as u32
|
last_index >= stop_gap as u32
|
||||||
};
|
};
|
||||||
if gap_limit_reached {
|
if past_gap_limit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,68 @@
|
|||||||
use bdk_esplora::EsploraAsyncExt;
|
use bdk_esplora::EsploraAsyncExt;
|
||||||
use electrsd::bitcoind::anyhow;
|
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
use esplora_client::{self, Builder};
|
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||||
|
use electrsd::{Conf, ElectrsD};
|
||||||
|
use esplora_client::{self, AsyncClient, Builder};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||||
use bdk_testenv::TestEnv;
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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 =
|
let receive_address0 =
|
||||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||||
let receive_address1 =
|
let receive_address1 =
|
||||||
@@ -48,11 +95,12 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().await.unwrap() < 102 {
|
while env.client.get_height().await.unwrap() < 102 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update = client
|
let graph_update = env
|
||||||
|
.client
|
||||||
.sync(
|
.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -91,12 +139,10 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the bounds of the address scan depending on the `stop_gap`.
|
/// Test the bounds of the address scan depending on the gap limit.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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)?;
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
|
||||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||||
@@ -136,16 +182,16 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().await.unwrap() < 103 {
|
while env.client.get_height().await.unwrap() < 103 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
|
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
|
||||||
assert!(graph_update.full_txs().next().is_none());
|
assert!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
|
||||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -161,18 +207,18 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().await.unwrap() < 104 {
|
while env.client.get_height().await.unwrap() < 104 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||||
// The last active indice won't be updated in the first case but will in the second one.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1).await?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use bdk_chain::local_chain::LocalChain;
|
use bdk_chain::local_chain::LocalChain;
|
||||||
use bdk_chain::BlockId;
|
use bdk_chain::BlockId;
|
||||||
use bdk_esplora::EsploraExt;
|
use bdk_esplora::EsploraExt;
|
||||||
use electrsd::bitcoind::anyhow;
|
|
||||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||||
use esplora_client::{self, Builder};
|
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||||
|
use electrsd::{Conf, ElectrsD};
|
||||||
|
use esplora_client::{self, BlockingClient, Builder};
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||||
use bdk_testenv::TestEnv;
|
|
||||||
|
|
||||||
macro_rules! h {
|
macro_rules! h {
|
||||||
($index:literal) => {{
|
($index:literal) => {{
|
||||||
@@ -26,12 +26,73 @@ macro_rules! local_chain {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestEnv {
|
||||||
|
bitcoind: BitcoinD,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
electrsd: ElectrsD,
|
||||||
|
client: BlockingClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestEnv {
|
||||||
|
fn new() -> Result<Self, anyhow::Error> {
|
||||||
|
let bitcoind_exe =
|
||||||
|
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||||
|
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||||
|
|
||||||
|
let mut electrs_conf = Conf::default();
|
||||||
|
electrs_conf.http_enabled = true;
|
||||||
|
let electrs_exe =
|
||||||
|
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||||
|
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||||
|
|
||||||
|
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
bitcoind,
|
||||||
|
electrsd,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||||
|
let mut electrs_conf = Conf::default();
|
||||||
|
electrs_conf.http_enabled = true;
|
||||||
|
let electrs_exe =
|
||||||
|
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||||
|
let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
|
||||||
|
|
||||||
|
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||||
|
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||||
|
self.electrsd = electrsd;
|
||||||
|
self.client = client;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mine_blocks(
|
||||||
|
&self,
|
||||||
|
count: usize,
|
||||||
|
address: Option<Address>,
|
||||||
|
) -> anyhow::Result<Vec<BlockHash>> {
|
||||||
|
let coinbase_address = match address {
|
||||||
|
Some(address) => address,
|
||||||
|
None => self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_new_address(None, None)?
|
||||||
|
.assume_checked(),
|
||||||
|
};
|
||||||
|
let block_hashes = self
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.generate_to_address(count as _, &coinbase_address)?;
|
||||||
|
Ok(block_hashes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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 =
|
let receive_address0 =
|
||||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||||
let receive_address1 =
|
let receive_address1 =
|
||||||
@@ -64,11 +125,11 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().unwrap() < 102 {
|
while env.client.get_height().unwrap() < 102 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update = client.sync(
|
let graph_update = env.client.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -106,12 +167,10 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the bounds of the address scan depending on the `stop_gap`.
|
/// Test the bounds of the address scan depending on the gap limit.
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||||
let env = TestEnv::new()?;
|
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)?;
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
|
||||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||||
@@ -151,16 +210,16 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().unwrap() < 103 {
|
while env.client.get_height().unwrap() < 103 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
|
||||||
assert!(graph_update.full_txs().next().is_none());
|
assert!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
|
||||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -176,18 +235,18 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let _block_hashes = env.mine_blocks(1, None)?;
|
let _block_hashes = env.mine_blocks(1, None)?;
|
||||||
while client.get_height().unwrap() < 104 {
|
while env.client.get_height().unwrap() < 104 {
|
||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||||
// The last active indice won't be updated in the first case but will in the second one.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1)?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
|
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
@@ -214,8 +273,6 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
};
|
};
|
||||||
// so new blocks can be seen by Electrs
|
// so new blocks can be seen by Electrs
|
||||||
let env = env.reset_electrsd()?;
|
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 {
|
struct TestCase {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
@@ -318,7 +375,8 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
println!("Case {}: {}", i, t.name);
|
println!("Case {}: {}", i, t.name);
|
||||||
let mut chain = t.chain;
|
let mut chain = t.chain;
|
||||||
|
|
||||||
let update = client
|
let update = env
|
||||||
|
.client
|
||||||
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_file_store"
|
name = "bdk_file_store"
|
||||||
version = "0.8.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ where
|
|||||||
.create(true)
|
.create(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
|
||||||
.open(file_path)?;
|
.open(file_path)?;
|
||||||
f.write_all(magic)?;
|
f.write_all(magic)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_hwi"
|
name = "bdk_hwi"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
//! # use bdk::signer::SignerOrdering;
|
//! # use bdk::signer::SignerOrdering;
|
||||||
//! # use bdk_hwi::HWISigner;
|
//! # use bdk_hwi::HWISigner;
|
||||||
//! # use bdk::wallet::AddressIndex::New;
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||||
//! # use hwi::HWIClient;
|
//! # use hwi::HWIClient;
|
||||||
//! # use std::sync::Arc;
|
//! # use std::sync::Arc;
|
||||||
//! #
|
//! #
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
use bdk_chain::bitcoin::{
|
|
||||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
|
||||||
secp256k1::rand::random, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf,
|
|
||||||
ScriptHash, Transaction, TxIn, TxOut, Txid,
|
|
||||||
};
|
|
||||||
use bitcoincore_rpc::{
|
|
||||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
|
||||||
RpcApi,
|
|
||||||
};
|
|
||||||
use electrsd::electrum_client::ElectrumApi;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
|
|
||||||
/// instance connected to it.
|
|
||||||
pub struct TestEnv {
|
|
||||||
pub bitcoind: electrsd::bitcoind::BitcoinD,
|
|
||||||
pub electrsd: electrsd::ElectrsD,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestEnv {
|
|
||||||
/// Construct a new [`TestEnv`] instance with default configurations.
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let bitcoind = match std::env::var_os("BITCOIND_EXE") {
|
|
||||||
Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
|
|
||||||
None => {
|
|
||||||
let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
|
|
||||||
.expect(
|
|
||||||
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
|
|
||||||
);
|
|
||||||
electrsd::bitcoind::BitcoinD::with_conf(
|
|
||||||
bitcoind_exe,
|
|
||||||
&electrsd::bitcoind::Conf::default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let mut electrsd_conf = electrsd::Conf::default();
|
|
||||||
electrsd_conf.http_enabled = true;
|
|
||||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
|
||||||
Some(env_electrs_exe) => {
|
|
||||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let electrs_exe = electrsd::downloaded_exe_path()
|
|
||||||
.expect("electrs version feature must be enabled");
|
|
||||||
electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(Self { bitcoind, electrsd })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exposes the [`ElectrumApi`] calls from the Electrum client.
|
|
||||||
pub fn electrum_client(&self) -> &impl ElectrumApi {
|
|
||||||
&self.electrsd.client
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
|
|
||||||
pub fn rpc_client(&self) -> &impl RpcApi {
|
|
||||||
&self.bitcoind.client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset `electrsd` so that new blocks can be seen.
|
|
||||||
pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
|
||||||
let mut electrsd_conf = electrsd::Conf::default();
|
|
||||||
electrsd_conf.http_enabled = true;
|
|
||||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
|
||||||
Some(env_electrs_exe) => {
|
|
||||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let electrs_exe = electrsd::downloaded_exe_path()
|
|
||||||
.expect("electrs version feature must be enabled");
|
|
||||||
electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
self.electrsd = electrsd;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
|
|
||||||
/// `address`.
|
|
||||||
pub fn mine_blocks(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
address: Option<Address>,
|
|
||||||
) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let coinbase_address = match address {
|
|
||||||
Some(address) => address,
|
|
||||||
None => self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.get_new_address(None, None)?
|
|
||||||
.assume_checked(),
|
|
||||||
};
|
|
||||||
let block_hashes = self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.generate_to_address(count as _, &coinbase_address)?;
|
|
||||||
Ok(block_hashes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
|
|
||||||
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
|
||||||
let bt = self.bitcoind.client.get_block_template(
|
|
||||||
GetBlockTemplateModes::Template,
|
|
||||||
&[GetBlockTemplateRules::SegWit],
|
|
||||||
&[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let txdata = vec![Transaction {
|
|
||||||
version: 1,
|
|
||||||
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
|
|
||||||
input: vec![TxIn {
|
|
||||||
previous_output: bdk_chain::bitcoin::OutPoint::default(),
|
|
||||||
script_sig: ScriptBuf::builder()
|
|
||||||
.push_int(bt.height as _)
|
|
||||||
// randomn number so that re-mining creates unique block
|
|
||||||
.push_int(random())
|
|
||||||
.into_script(),
|
|
||||||
sequence: bdk_chain::bitcoin::Sequence::default(),
|
|
||||||
witness: bdk_chain::bitcoin::Witness::new(),
|
|
||||||
}],
|
|
||||||
output: vec![TxOut {
|
|
||||||
value: 0,
|
|
||||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
|
||||||
}],
|
|
||||||
}];
|
|
||||||
|
|
||||||
let bits: [u8; 4] = bt
|
|
||||||
.bits
|
|
||||||
.clone()
|
|
||||||
.try_into()
|
|
||||||
.expect("rpc provided us with invalid bits");
|
|
||||||
|
|
||||||
let mut block = Block {
|
|
||||||
header: Header {
|
|
||||||
version: bdk_chain::bitcoin::block::Version::default(),
|
|
||||||
prev_blockhash: bt.previous_block_hash,
|
|
||||||
merkle_root: TxMerkleNode::all_zeros(),
|
|
||||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
|
||||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
|
||||||
nonce: 0,
|
|
||||||
},
|
|
||||||
txdata,
|
|
||||||
};
|
|
||||||
|
|
||||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
|
||||||
|
|
||||||
for nonce in 0..=u32::MAX {
|
|
||||||
block.header.nonce = nonce;
|
|
||||||
if block.header.target().is_met_by(block.block_hash()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.bitcoind.client.submit_block(&block)?;
|
|
||||||
Ok((bt.height as usize, block.block_hash()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method waits for the Electrum notification indicating that a new block has been mined.
|
|
||||||
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
|
|
||||||
self.electrsd.client.block_headers_subscribe()?;
|
|
||||||
let mut delay = Duration::from_millis(64);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
self.electrsd.trigger()?;
|
|
||||||
self.electrsd.client.ping()?;
|
|
||||||
if self.electrsd.client.block_headers_pop()?.is_some() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if delay.as_millis() < 512 {
|
|
||||||
delay = delay.mul_f32(2.0);
|
|
||||||
}
|
|
||||||
std::thread::sleep(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invalidate a number of blocks of a given size `count`.
|
|
||||||
pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
|
||||||
let mut hash = self.bitcoind.client.get_best_block_hash()?;
|
|
||||||
for _ in 0..count {
|
|
||||||
let prev_hash = self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.get_block_info(&hash)?
|
|
||||||
.previousblockhash;
|
|
||||||
self.bitcoind.client.invalidate_block(&hash)?;
|
|
||||||
match prev_hash {
|
|
||||||
Some(prev_hash) => hash = prev_hash,
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reorg a number of blocks of a given size `count`.
|
|
||||||
/// Refer to [`TestEnv::mine_empty_block`] for more information.
|
|
||||||
pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
|
||||||
let start_height = self.bitcoind.client.get_block_count()?;
|
|
||||||
self.invalidate_blocks(count)?;
|
|
||||||
|
|
||||||
let res = self.mine_blocks(count, None);
|
|
||||||
assert_eq!(
|
|
||||||
self.bitcoind.client.get_block_count()?,
|
|
||||||
start_height,
|
|
||||||
"reorg should not result in height change"
|
|
||||||
);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reorg with a number of empty blocks of a given size `count`.
|
|
||||||
pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
|
||||||
let start_height = self.bitcoind.client.get_block_count()?;
|
|
||||||
self.invalidate_blocks(count)?;
|
|
||||||
|
|
||||||
let res = (0..count)
|
|
||||||
.map(|_| self.mine_empty_block())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
assert_eq!(
|
|
||||||
self.bitcoind.client.get_block_count()?,
|
|
||||||
start_height,
|
|
||||||
"reorg should not result in height change"
|
|
||||||
);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a tx of a given `amount` to a given `address`.
|
|
||||||
pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
|
||||||
let txid = self
|
|
||||||
.bitcoind
|
|
||||||
.client
|
|
||||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
|
||||||
Ok(txid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use crate::TestEnv;
|
|
||||||
use anyhow::Result;
|
|
||||||
use bitcoincore_rpc::RpcApi;
|
|
||||||
|
|
||||||
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
|
|
||||||
#[test]
|
|
||||||
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
|
|
||||||
let env = TestEnv::new()?;
|
|
||||||
|
|
||||||
// Mine some blocks.
|
|
||||||
env.mine_blocks(101, None)?;
|
|
||||||
env.wait_until_electrum_sees_block()?;
|
|
||||||
let height = env.bitcoind.client.get_block_count()?;
|
|
||||||
let blocks = (0..=height)
|
|
||||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
// Perform reorg on six blocks.
|
|
||||||
env.reorg(6)?;
|
|
||||||
env.wait_until_electrum_sees_block()?;
|
|
||||||
let reorged_height = env.bitcoind.client.get_block_count()?;
|
|
||||||
let reorged_blocks = (0..=height)
|
|
||||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
assert_eq!(height, reorged_height);
|
|
||||||
|
|
||||||
// Block hashes should not be equal on the six reorged blocks.
|
|
||||||
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
|
|
||||||
match i <= height as usize - 6 {
|
|
||||||
true => assert_eq!(block, reorged_block),
|
|
||||||
false => assert_ne!(block, reorged_block),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user