Compare commits
1 Commits
v1.0.0-alp
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d040b7057 |
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Run simulator image
|
||||
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install python dependencies
|
||||
|
||||
51
.github/workflows/cont_integration.yml
vendored
51
.github/workflows/cont_integration.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
rust:
|
||||
- version: stable
|
||||
clippy: true
|
||||
- version: 1.63.0 # MSRV
|
||||
- version: 1.57.0 # MSRV
|
||||
features:
|
||||
- --no-default-features
|
||||
- --all-features
|
||||
@@ -28,13 +28,28 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Pin dependencies for MSRV
|
||||
if: matrix.rust.version == '1.63.0'
|
||||
if: matrix.rust.version == '1.57.0'
|
||||
run: |
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
cargo update -p url --precise "2.5.0"
|
||||
cargo update -p log --precise "0.4.18"
|
||||
cargo update -p tempfile --precise "3.6.0"
|
||||
cargo update -p reqwest --precise "0.11.18"
|
||||
cargo update -p hyper-rustls --precise 0.24.0
|
||||
cargo update -p rustls:0.21.9 --precise "0.21.1"
|
||||
cargo update -p rustls:0.20.9 --precise "0.20.8"
|
||||
cargo update -p tokio --precise "1.29.1"
|
||||
cargo update -p tokio-util --precise "0.7.8"
|
||||
cargo update -p flate2 --precise "1.0.26"
|
||||
cargo update -p h2 --precise "0.3.20"
|
||||
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
|
||||
cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
|
||||
cargo update -p zip --precise "0.6.2"
|
||||
cargo update -p time --precise "0.3.13"
|
||||
cargo update -p byteorder --precise "1.4.3"
|
||||
cargo update -p webpki --precise "0.22.2"
|
||||
cargo update -p os_str_bytes --precise 6.5.1
|
||||
cargo update -p sct --precise 0.7.0
|
||||
cargo update -p cc --precise "1.0.81"
|
||||
cargo update -p jobserver --precise "0.1.26"
|
||||
- name: Build
|
||||
run: cargo build ${{ matrix.features }}
|
||||
- name: Test
|
||||
@@ -58,15 +73,15 @@ jobs:
|
||||
- name: Check bdk_chain
|
||||
working-directory: ./crates/chain
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features miniscript/no-std,hashbrown
|
||||
- name: Check bdk wallet
|
||||
working-directory: ./crates/wallet
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
|
||||
|
||||
check-wasm:
|
||||
name: Check WASM
|
||||
@@ -90,12 +105,12 @@ jobs:
|
||||
target: "wasm32-unknown-unknown"
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Check bdk wallet
|
||||
working-directory: ./crates/wallet
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,async
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
@@ -119,7 +134,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.78.0
|
||||
# we pin clippy instead of using "stable" so that our CI doesn't break
|
||||
# at each new cargo release
|
||||
toolchain: "1.67.0"
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Rust Cache
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2024-05-12
|
||||
run: rustup default nightly-2022-12-14
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,3 @@ Cargo.lock
|
||||
|
||||
# Example persisted files.
|
||||
*.db
|
||||
*.sqlite*
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/wallet",
|
||||
"crates/bdk",
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/sqlite",
|
||||
"crates/electrum",
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
"crates/hwi",
|
||||
"crates/testenv",
|
||||
"example-crates/example_cli",
|
||||
"example-crates/example_electrum",
|
||||
"example-crates/example_esplora",
|
||||
@@ -17,7 +14,6 @@ members = [
|
||||
"example-crates/wallet_electrum",
|
||||
"example-crates/wallet_esplora_blocking",
|
||||
"example-crates/wallet_esplora_async",
|
||||
"example-crates/wallet_rpc",
|
||||
"nursery/tmp_plan",
|
||||
"nursery/coin_select"
|
||||
]
|
||||
|
||||
64
README.md
64
README.md
@@ -10,19 +10,19 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk_wallet"><img alt="Wallet API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html"><img alt="Rustc Version 1.57.0+" src="https://img.shields.io/badge/rustc-1.57.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
<a href="https://bitcoindevkit.org">Project Homepage</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.rs/bdk_wallet">Documentation</a>
|
||||
<a href="https://docs.rs/bdk">Documentation</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@@ -39,18 +39,15 @@ It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
|
||||
|
||||
The project is split up into several crates in the `/crates` directory:
|
||||
|
||||
- [`wallet`](./crates/wallet): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`chain`](./crates/chain): Tools for storing and indexing chain data
|
||||
- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet
|
||||
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
|
||||
- [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
|
||||
Fully working examples of how to use these components are in `/example-crates`:
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
|
||||
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
|
||||
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
|
||||
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
|
||||
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
|
||||
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
|
||||
@@ -63,16 +60,51 @@ Fully working examples of how to use these components are in `/example-crates`:
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
This library should compile with any combination of features with Rust 1.63.0.
|
||||
This library should compile with any combination of features with Rust 1.57.0.
|
||||
|
||||
To build with the MSRV you will need to pin dependencies as follows:
|
||||
|
||||
```shell
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
cargo update -p url --precise "2.5.0"
|
||||
# log 0.4.19 has MSRV 1.60.0+
|
||||
cargo update -p log --precise "0.4.18"
|
||||
# tempfile 3.7.0 has MSRV 1.63.0+
|
||||
cargo update -p tempfile --precise "3.6.0"
|
||||
# reqwest 0.11.19 has MSRV 1.63.0+
|
||||
cargo update -p reqwest --precise "0.11.18"
|
||||
# hyper-rustls 0.24.1 has MSRV 1.60.0+
|
||||
cargo update -p hyper-rustls --precise 0.24.0
|
||||
# rustls 0.21.7 has MSRV 1.60.0+
|
||||
cargo update -p rustls:0.21.9 --precise "0.21.1"
|
||||
# rustls 0.20.9 has MSRV 1.60.0+
|
||||
cargo update -p rustls:0.20.9 --precise "0.20.8"
|
||||
# tokio 1.33 has MSRV 1.63.0+
|
||||
cargo update -p tokio --precise "1.29.1"
|
||||
# tokio-util 0.7.9 doesn't build with MSRV 1.57.0
|
||||
cargo update -p tokio-util --precise "0.7.8"
|
||||
# flate2 1.0.27 has MSRV 1.63.0+
|
||||
cargo update -p flate2 --precise "1.0.26"
|
||||
# h2 0.3.21 has MSRV 1.63.0+
|
||||
cargo update -p h2 --precise "0.3.20"
|
||||
# rustls-webpki 0.100.3 has MSRV 1.60.0+
|
||||
cargo update -p rustls-webpki:0.100.3 --precise "0.100.1"
|
||||
# rustls-webpki 0.101.2 has MSRV 1.60.0+
|
||||
cargo update -p rustls-webpki:0.101.7 --precise "0.101.1"
|
||||
# zip 0.6.6 has MSRV 1.59.0+
|
||||
cargo update -p zip --precise "0.6.2"
|
||||
# time 0.3.14 has MSRV 1.59.0+
|
||||
cargo update -p time --precise "0.3.13"
|
||||
# byteorder 1.5.0 has MSRV 1.60.0+
|
||||
cargo update -p byteorder --precise "1.4.3"
|
||||
# webpki 0.22.4 requires `ring:0.17.2` which has MSRV 1.61.0+
|
||||
cargo update -p webpki --precise "0.22.2"
|
||||
# os_str_bytes 6.6.0 has MSRV 1.61.0+
|
||||
cargo update -p os_str_bytes --precise 6.5.1
|
||||
# sct 0.7.1 has MSRV 1.61.0+
|
||||
cargo update -p sct --precise 0.7.0
|
||||
# cc 1.0.82 has MSRV 1.61.0+
|
||||
cargo update -p cc --precise "1.0.81"
|
||||
# jobserver 0.1.27 has MSRV 1.66.0+
|
||||
cargo update -p jobserver --precise "0.1.26"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -1 +1 @@
|
||||
msrv="1.63.0"
|
||||
msrv="1.57.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk_wallet"
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.13"
|
||||
version = "1.0.0-alpha.2"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -10,18 +10,19 @@ readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.57"
|
||||
|
||||
[dependencies]
|
||||
rand = "^0.8"
|
||||
miniscript = { version = "12.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.32.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
miniscript = { version = "10.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.6.0", features = ["miniscript", "serde"], default-features = false }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
hwi = { version = "0.7.0", optional = true, features = [ "miniscript"] }
|
||||
bip39 = { version = "1.0.1", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
@@ -33,6 +34,8 @@ std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
|
||||
compiler = ["miniscript/compiler"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
hardware-signer = ["hwi"]
|
||||
test-hardware-signer = ["hardware-signer"]
|
||||
|
||||
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
|
||||
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||
@@ -43,7 +46,6 @@ dev-getrandom-wasm = ["getrandom/js"]
|
||||
lazy_static = "1.4"
|
||||
assert_matches = "1.5.0"
|
||||
tempfile = "3"
|
||||
bdk_sqlite = { path = "../sqlite" }
|
||||
bdk_file_store = { path = "../file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk_wallet"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html"><img alt="Rustc Version 1.57.0+" src="https://img.shields.io/badge/rustc-1.57.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
<a href="https://bitcoindevkit.org">Project Homepage</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.rs/bdk_wallet">Documentation</a>
|
||||
<a href="https://docs.rs/bdk">Documentation</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
# BDK Wallet
|
||||
## `bdk`
|
||||
|
||||
The `bdk_wallet` 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
|
||||
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
||||
construct a wallet. It has two keychains (external and internal) which are defined by
|
||||
@@ -34,78 +34,64 @@ construct a wallet. It has two keychains (external and internal) which are defin
|
||||
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
||||
can create and sign transactions.
|
||||
|
||||
For 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
|
||||
an available chain source. Typically you make a request to the chain source and get a response
|
||||
that the `Wallet` can use to update its view of the chain.
|
||||
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
|
||||
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
|
||||
|
||||
This can be created manually or from blockchain-scanning crates.
|
||||
|
||||
**Blockchain Data Sources**
|
||||
|
||||
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
||||
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
|
||||
* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
|
||||
|
||||
**Examples**
|
||||
|
||||
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
|
||||
* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
|
||||
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
|
||||
* [`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 `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
|
||||
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**
|
||||
|
||||
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
|
||||
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
|
||||
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
|
||||
|
||||
**Example**
|
||||
|
||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||
```rust,no_run
|
||||
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
|
||||
```rust
|
||||
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
|
||||
|
||||
fn main() {
|
||||
// Open or create a new file store for wallet data.
|
||||
let mut db =
|
||||
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
|
||||
.expect("create store");
|
||||
// a type that implements `Persist`
|
||||
let db = ();
|
||||
|
||||
// Create a wallet with initial wallet data read from the file store.
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
|
||||
let changeset = db.aggregate_changesets().expect("changeset loaded");
|
||||
let mut wallet =
|
||||
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
|
||||
.expect("create or load wallet");
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
|
||||
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
|
||||
|
||||
// Get a new address to receive bitcoin.
|
||||
let receive_address = wallet.reveal_next_address(KeychainKind::External);
|
||||
// Persist staged wallet data changes to the file store.
|
||||
let staged_changeset = wallet.take_staged();
|
||||
if let Some(changeset) = staged_changeset {
|
||||
db.append_changeset(&changeset)
|
||||
.expect("must commit changes to database");
|
||||
}
|
||||
println!("Your new receive address is: {}", receive_address.address);
|
||||
// get a new address (this increments revealed derivation index)
|
||||
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
|
||||
println!("staged changes: {:?}", wallet.staged());
|
||||
// persist changes
|
||||
wallet.commit().expect("must save");
|
||||
}
|
||||
```
|
||||
|
||||
<!-- ### Sync the balance of a descriptor -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk_wallet::Wallet; -->
|
||||
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk_wallet::SyncOptions; -->
|
||||
<!-- use bdk_wallet::electrum_client::Client; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk::SyncOptions; -->
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
@@ -115,7 +101,7 @@ fn main() {
|
||||
|
||||
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
|
||||
|
||||
<!-- println!("Descriptor balance: {} SAT", wallet.balance()?); -->
|
||||
<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
|
||||
|
||||
<!-- Ok(()) -->
|
||||
<!-- } -->
|
||||
@@ -123,12 +109,12 @@ fn main() {
|
||||
<!-- ### Generate a few addresses -->
|
||||
|
||||
<!-- ```rust -->
|
||||
<!-- use bdk_wallet::Wallet; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
@@ -145,19 +131,19 @@ fn main() {
|
||||
<!-- ### Create a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk_wallet::{FeeRate, Wallet, SyncOptions}; -->
|
||||
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
|
||||
<!-- use bdk_wallet::electrum_client::Client; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
<!-- use bdk::bitcoin::consensus::serialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
@@ -186,14 +172,14 @@ fn main() {
|
||||
<!-- ### Sign a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk_wallet::{Wallet, SignOptions}; -->
|
||||
<!-- use bdk::{Wallet, SignOptions}; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk_wallet::bitcoin::consensus::deserialize; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
<!-- use bdk::bitcoin::consensus::deserialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
@@ -216,7 +202,7 @@ fn main() {
|
||||
cargo test
|
||||
```
|
||||
|
||||
# License
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
@@ -225,18 +211,16 @@ Licensed under either of
|
||||
|
||||
at your option.
|
||||
|
||||
# Contribution
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/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
|
||||
66
crates/bdk/examples/compiler.rs
Normal file
66
crates/bdk/examples/compiler.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bitcoin;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
|
||||
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
|
||||
/// can be derived from the policy.
|
||||
///
|
||||
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// We start with a generic miniscript policy string
|
||||
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
|
||||
println!("Compiling policy: \n{}", policy_str);
|
||||
|
||||
// Parse the string as a [`Concrete`] type miniscript policy.
|
||||
let policy = Concrete::<String>::from_str(policy_str)?;
|
||||
|
||||
// Create a `wsh` type descriptor from the policy.
|
||||
// `policy.compile()` returns the resulting miniscript from the policy.
|
||||
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
|
||||
|
||||
println!("Compiled into following Descriptor: \n{}", descriptor);
|
||||
|
||||
// Create a new wallet from this descriptor
|
||||
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
|
||||
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.get_address(New)
|
||||
);
|
||||
|
||||
// BDK also has it's own `Policy` structure to represent the spending condition in a more
|
||||
// human readable json format.
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
println!(
|
||||
"The BDK spending policy: \n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -7,14 +7,14 @@
|
||||
// licenses.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bdk_wallet::bitcoin::bip32::DerivationPath;
|
||||
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::descriptor;
|
||||
use bdk_wallet::descriptor::IntoWalletDescriptor;
|
||||
use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk_wallet::miniscript::Tap;
|
||||
use bdk::bitcoin::bip32::DerivationPath;
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor;
|
||||
use bdk::descriptor::IntoWalletDescriptor;
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::Tap;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates how to generate a mnemonic phrase
|
||||
@@ -33,8 +33,8 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
let mnemonic_with_passphrase = (mnemonic, None);
|
||||
|
||||
// define external and internal derivation key path
|
||||
let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
|
||||
let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
|
||||
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
|
||||
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
|
||||
|
||||
// generate external and internal descriptor from mnemonic
|
||||
let (external_descriptor, ext_keymap) =
|
||||
@@ -9,14 +9,14 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk_wallet;
|
||||
extern crate bdk;
|
||||
use std::error::Error;
|
||||
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk_wallet::wallet::signer::SignersContainer;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk::wallet::signer::SignersContainer;
|
||||
|
||||
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
|
||||
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
|
||||
///
|
||||
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
|
||||
/// This is useful to express complex miniscript spending conditions into more human readable form.
|
||||
@@ -34,11 +34,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
|
||||
// Use the descriptor string to derive the full descriptor and a keymap.
|
||||
// The wallet descriptor can be used to create a new bdk_wallet::wallet.
|
||||
// The wallet descriptor can be used to create a new bdk::wallet.
|
||||
// While the `keymap` can be used to create a `SignerContainer`.
|
||||
//
|
||||
// The `SignerContainer` can sign for `PSBT`s.
|
||||
// a bdk_wallet::wallet internally uses these to handle transaction signing.
|
||||
// a bdk::wallet internally uses these to handle transaction signing.
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
@@ -42,16 +42,22 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
/// Computes the checksum bytes of a descriptor.
|
||||
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
|
||||
pub(crate) fn calc_checksum_bytes_internal(
|
||||
mut desc: &str,
|
||||
exclude_hash: bool,
|
||||
) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
|
||||
let mut original_checksum = None;
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
if exclude_hash {
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
@@ -89,10 +95,39 @@ pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
|
||||
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
|
||||
|
||||
/// Compute the checksum bytes of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -274,13 +274,14 @@ macro_rules! impl_sortedmulti {
|
||||
#[macro_export]
|
||||
macro_rules! parse_tap_tree {
|
||||
( @merge $tree_a:expr, $tree_b:expr) => {{
|
||||
use $crate::alloc::sync::Arc;
|
||||
use $crate::miniscript::descriptor::TapTree;
|
||||
|
||||
$tree_a
|
||||
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
|
||||
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
|
||||
a_keymap.extend(b_keymap.into_iter());
|
||||
Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||
})
|
||||
|
||||
}};
|
||||
@@ -423,7 +424,7 @@ macro_rules! apply_modifier {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// let (my_descriptor, my_keys_map, networks) = bdk_wallet::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
|
||||
/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -444,7 +445,7 @@ macro_rules! apply_modifier {
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
/// let my_timelock = 50;
|
||||
///
|
||||
/// let (descriptor_a, key_map_a, networks) = bdk_wallet::descriptor! {
|
||||
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
|
||||
/// wsh (
|
||||
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
|
||||
/// )
|
||||
@@ -452,12 +453,11 @@ macro_rules! apply_modifier {
|
||||
///
|
||||
/// #[rustfmt::skip]
|
||||
/// let b_items = vec![
|
||||
/// bdk_wallet::fragment!(pk(my_key_1))?,
|
||||
/// bdk_wallet::fragment!(s:pk(my_key_2))?,
|
||||
/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?,
|
||||
/// bdk::fragment!(pk(my_key_1))?,
|
||||
/// bdk::fragment!(s:pk(my_key_2))?,
|
||||
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
|
||||
/// ];
|
||||
/// let (descriptor_b, mut key_map_b, networks) =
|
||||
/// bdk_wallet::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||
///
|
||||
/// assert_eq!(descriptor_a, descriptor_b);
|
||||
/// assert_eq!(key_map_a.len(), key_map_b.len());
|
||||
@@ -476,7 +476,7 @@ macro_rules! apply_modifier {
|
||||
/// let my_key_2 =
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
///
|
||||
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! {
|
||||
/// let (descriptor, key_map, networks) = bdk::descriptor! {
|
||||
/// wsh (
|
||||
/// multi(2, my_key_1, my_key_2)
|
||||
/// )
|
||||
@@ -492,7 +492,7 @@ macro_rules! apply_modifier {
|
||||
/// let my_key =
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
///
|
||||
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?;
|
||||
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -703,10 +703,10 @@ macro_rules! fragment {
|
||||
$crate::keys::make_pkh($key, &secp)
|
||||
});
|
||||
( after ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value).expect("valid `AbsLockTime`"))
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
|
||||
});
|
||||
( older ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::miniscript::RelLockTime::from_consensus($value).expect("valid `RelLockTime`")) // TODO!!
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||
});
|
||||
( sha256 ( $hash:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
||||
@@ -757,8 +757,7 @@ macro_rules! fragment {
|
||||
(keys_acc, net_acc)
|
||||
});
|
||||
|
||||
let thresh = $crate::miniscript::Threshold::new($thresh, items).expect("valid threshold and pks collection");
|
||||
$crate::impl_leaf_opcode_value!(Thresh, thresh)
|
||||
$crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items)
|
||||
.map(|(minisc, _, _)| (minisc, key_maps, valid_networks))
|
||||
});
|
||||
( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
|
||||
@@ -770,12 +769,7 @@ macro_rules! fragment {
|
||||
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||
|
||||
let fun = |k, pks| {
|
||||
let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection");
|
||||
$crate::miniscript::Terminal::Multi(thresh)
|
||||
};
|
||||
|
||||
$crate::keys::make_multi($thresh, fun, $keys, &secp)
|
||||
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
|
||||
});
|
||||
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||
$crate::group_multi_keys!( $( $key ),* )
|
||||
@@ -784,12 +778,7 @@ macro_rules! fragment {
|
||||
( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||
|
||||
let fun = |k, pks| {
|
||||
let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection");
|
||||
$crate::miniscript::Terminal::MultiA(thresh)
|
||||
};
|
||||
|
||||
$crate::keys::make_multi($thresh, fun, $keys, &secp)
|
||||
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
|
||||
});
|
||||
( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||
$crate::group_multi_keys!( $( $key ),* )
|
||||
@@ -817,7 +806,7 @@ mod test {
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||
use bitcoin::PrivateKey;
|
||||
|
||||
// test the descriptor!() macro
|
||||
@@ -947,7 +936,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_bip32_legacy_descriptors() {
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
@@ -992,7 +981,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_bip32_segwitv0_descriptors() {
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
@@ -1049,10 +1038,10 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_dsl_sortedmulti() {
|
||||
let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let key_1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let key_2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
|
||||
|
||||
let desc_key1 = (key_1, path_1);
|
||||
@@ -1108,7 +1097,7 @@ mod test {
|
||||
// - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
|
||||
#[test]
|
||||
fn test_valid_networks() {
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1118,7 +1107,7 @@ mod test {
|
||||
[Testnet, Regtest, Signet].iter().cloned().collect()
|
||||
);
|
||||
|
||||
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1131,15 +1120,15 @@ mod test {
|
||||
fn test_key_maps_merged() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1163,7 +1152,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_script_context_validation() {
|
||||
// this compiles
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -23,6 +23,7 @@ pub enum Error {
|
||||
HardenedDerivationXpub,
|
||||
/// The descriptor contains multipath keys
|
||||
MultiPath,
|
||||
|
||||
/// Error thrown while working with [`keys`](crate::keys)
|
||||
Key(crate::keys::KeyError),
|
||||
/// Error while extracting and manipulating policies
|
||||
@@ -36,13 +37,11 @@ pub enum Error {
|
||||
/// Error during base58 decoding
|
||||
Base58(bitcoin::base58::Error),
|
||||
/// Key-related error
|
||||
Pk(bitcoin::key::ParsePublicKeyError),
|
||||
Pk(bitcoin::key::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// Hex decoding error
|
||||
Hex(bitcoin::hex::HexToBytesError),
|
||||
/// The provided wallet descriptors are identical
|
||||
ExternalAndInternalAreTheSame,
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
}
|
||||
|
||||
impl From<crate::keys::KeyError> for Error {
|
||||
@@ -80,9 +79,6 @@ impl fmt::Display for Error {
|
||||
Self::Pk(err) => write!(f, "Key-related error: {}", err),
|
||||
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
|
||||
Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
|
||||
Self::ExternalAndInternalAreTheSame => {
|
||||
write!(f, "External and internal descriptors are the same")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,8 +98,8 @@ impl From<bitcoin::base58::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::key::ParsePublicKeyError> for Error {
|
||||
fn from(err: bitcoin::key::ParsePublicKeyError) -> Self {
|
||||
impl From<bitcoin::key::Error> for Error {
|
||||
fn from(err: bitcoin::key::Error) -> Self {
|
||||
Error::Pk(err)
|
||||
}
|
||||
}
|
||||
@@ -114,8 +110,8 @@ impl From<miniscript::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::hex::HexToBytesError> for Error {
|
||||
fn from(err: bitcoin::hex::HexToBytesError) -> Self {
|
||||
impl From<bitcoin::hashes::hex::Error> for Error {
|
||||
fn from(err: bitcoin::hashes::hex::Error) -> Self {
|
||||
Error::Hex(err)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use crate::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
|
||||
use bitcoin::{psbt, taproot};
|
||||
use bitcoin::{Network, TxOut};
|
||||
@@ -229,7 +229,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
let pk = match pk {
|
||||
DescriptorPublicKey::XPub(ref xpub) => {
|
||||
let mut xpub = xpub.clone();
|
||||
xpub.xkey.network = self.network.into();
|
||||
xpub.xkey.network = self.network;
|
||||
|
||||
DescriptorPublicKey::XPub(xpub)
|
||||
}
|
||||
@@ -264,11 +264,11 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
.map(|(mut k, mut v)| {
|
||||
match (&mut k, &mut v) {
|
||||
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
|
||||
xpub.xkey.network = network.into();
|
||||
xprv.xkey.network = network.into();
|
||||
xpub.xkey.network = network;
|
||||
xprv.xkey.network = network;
|
||||
}
|
||||
(_, DescriptorSecretKey::Single(key)) => {
|
||||
key.key.network = network.into();
|
||||
key.key.network = network;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -377,7 +377,7 @@ where
|
||||
pub(crate) trait DescriptorMeta {
|
||||
fn is_witness(&self) -> bool;
|
||||
fn is_taproot(&self) -> bool;
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>>;
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>>;
|
||||
fn derive_from_hd_keypaths(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
@@ -418,7 +418,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
self.desc_type() == DescriptorType::Tr
|
||||
}
|
||||
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>> {
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>> {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
self.for_each_key(|pk| {
|
||||
@@ -438,20 +438,21 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||
let verify_key =
|
||||
|xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
|
||||
let derived = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, path)
|
||||
.expect("The path should never contain hardened derivation steps")
|
||||
.public_key;
|
||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||
path: &DerivationPath,
|
||||
expected: &SinglePubKey| {
|
||||
let derived = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, path)
|
||||
.expect("The path should never contain hardened derivation steps")
|
||||
.public_key;
|
||||
|
||||
match expected {
|
||||
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
match expected {
|
||||
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
|
||||
let mut path_found = None;
|
||||
|
||||
@@ -604,10 +605,10 @@ mod test {
|
||||
use core::str::FromStr;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::hex::FromHex;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::{bip32, Psbt};
|
||||
use bitcoin::{NetworkKind, ScriptBuf};
|
||||
use bitcoin::ScriptBuf;
|
||||
use bitcoin::{bip32, psbt::Psbt};
|
||||
|
||||
use super::*;
|
||||
use crate::psbt::PsbtUtils;
|
||||
@@ -726,7 +727,7 @@ mod test {
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||
@@ -743,9 +744,9 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
let mut xprv_testnet = xprv;
|
||||
xprv_testnet.network = NetworkKind::Test;
|
||||
xprv_testnet.network = Network::Testnet;
|
||||
|
||||
let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet);
|
||||
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
||||
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||
xkey: xpub_testnet,
|
||||
origin: None,
|
||||
@@ -835,7 +836,7 @@ mod test {
|
||||
fn test_descriptor_from_str_from_output_of_macro() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
||||
let key = (tpub, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -894,7 +895,7 @@ mod test {
|
||||
.update_with_descriptor_unchecked(&descriptor)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh()));
|
||||
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
|
||||
assert_eq!(psbt_input.witness_script, Some(script));
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use bdk_wallet::descriptor::*;
|
||||
//! # use bdk_wallet::wallet::signer::*;
|
||||
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
|
||||
//! # use bdk::descriptor::*;
|
||||
//! # use bdk::wallet::signer::*;
|
||||
//! # use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk::descriptor::policy::BuildSatisfaction;
|
||||
//! let secp = Secp256k1::new();
|
||||
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
|
||||
//!
|
||||
@@ -40,7 +40,6 @@ use crate::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::max;
|
||||
use miniscript::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG};
|
||||
|
||||
use core::fmt;
|
||||
|
||||
@@ -49,12 +48,12 @@ use serde::{Serialize, Serializer};
|
||||
|
||||
use bitcoin::bip32::Fingerprint;
|
||||
use bitcoin::hashes::{hash160, ripemd160, sha256};
|
||||
use bitcoin::{absolute, key::XOnlyPublicKey, relative, PublicKey, Sequence};
|
||||
use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
|
||||
};
|
||||
use miniscript::{hash256, Threshold};
|
||||
use miniscript::hash256;
|
||||
use miniscript::{
|
||||
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
|
||||
};
|
||||
@@ -138,7 +137,7 @@ pub enum SatisfiableItem {
|
||||
/// Relative timelock locktime
|
||||
RelativeTimelock {
|
||||
/// The timelock value
|
||||
value: relative::LockTime,
|
||||
value: Sequence,
|
||||
},
|
||||
/// Multi-signature public keys with threshold count
|
||||
Multisig {
|
||||
@@ -587,25 +586,30 @@ impl Policy {
|
||||
Ok(Some(policy))
|
||||
}
|
||||
|
||||
fn make_multi<Ctx: ScriptContext + 'static, const MAX: usize>(
|
||||
threshold: &Threshold<DescriptorPublicKey, MAX>,
|
||||
fn make_multisig<Ctx: ScriptContext + 'static>(
|
||||
keys: &[DescriptorPublicKey],
|
||||
signers: &SignersContainer,
|
||||
build_sat: BuildSatisfaction,
|
||||
threshold: usize,
|
||||
sorted: bool,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, PolicyError> {
|
||||
let parsed_keys = threshold.iter().map(|k| PkOrF::from_key(k, secp)).collect();
|
||||
if threshold == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect();
|
||||
|
||||
let mut contribution = Satisfaction::Partial {
|
||||
n: threshold.n(),
|
||||
m: threshold.k(),
|
||||
n: keys.len(),
|
||||
m: threshold,
|
||||
items: vec![],
|
||||
conditions: Default::default(),
|
||||
sorted: Some(sorted),
|
||||
};
|
||||
let mut satisfaction = contribution.clone();
|
||||
|
||||
for (index, key) in threshold.iter().enumerate() {
|
||||
for (index, key) in keys.iter().enumerate() {
|
||||
if signers.find(signer_id(key, secp)).is_some() {
|
||||
contribution.add(
|
||||
&Satisfaction::Complete {
|
||||
@@ -614,6 +618,7 @@ impl Policy {
|
||||
index,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(psbt) = build_sat.psbt() {
|
||||
if Ctx::find_signature(psbt, key, secp) {
|
||||
satisfaction.add(
|
||||
@@ -630,11 +635,12 @@ impl Policy {
|
||||
|
||||
let mut policy: Policy = SatisfiableItem::Multisig {
|
||||
keys: parsed_keys,
|
||||
threshold: threshold.k(),
|
||||
threshold,
|
||||
}
|
||||
.into();
|
||||
policy.contribution = contribution;
|
||||
policy.satisfaction = satisfaction;
|
||||
|
||||
Ok(Some(policy))
|
||||
}
|
||||
|
||||
@@ -719,7 +725,7 @@ impl Policy {
|
||||
timelock: Some(*value),
|
||||
}),
|
||||
SatisfiableItem::RelativeTimelock { value } => Ok(Condition {
|
||||
csv: Some((*value).into()),
|
||||
csv: Some(*value),
|
||||
timelock: None,
|
||||
}),
|
||||
_ => Ok(Condition::default()),
|
||||
@@ -946,14 +952,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
Some(policy)
|
||||
}
|
||||
Terminal::Older(value) => {
|
||||
let mut policy: Policy = SatisfiableItem::RelativeTimelock {
|
||||
value: (*value).into(),
|
||||
}
|
||||
.into();
|
||||
let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into();
|
||||
policy.contribution = Satisfaction::Complete {
|
||||
condition: Condition {
|
||||
timelock: None,
|
||||
csv: Some((*value).into()),
|
||||
csv: Some(*value),
|
||||
},
|
||||
};
|
||||
if let BuildSatisfaction::PsbtTimelocks {
|
||||
@@ -963,11 +966,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
} = build_sat
|
||||
{
|
||||
let older = Older::new(Some(current_height), Some(input_max_height), false);
|
||||
let older_sat =
|
||||
Satisfier::<bitcoin::PublicKey>::check_older(&older, (*value).into());
|
||||
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
|
||||
Satisfier::<bitcoin::PublicKey>::check_older(&sat, (*value).into())
|
||||
});
|
||||
let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
|
||||
let inputs_sat = psbt_inputs_sat(psbt)
|
||||
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
|
||||
if older_sat && inputs_sat {
|
||||
policy.satisfaction = policy.contribution.clone();
|
||||
}
|
||||
@@ -985,12 +986,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
Terminal::Hash160(hash) => {
|
||||
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
|
||||
}
|
||||
Terminal::Multi(threshold) => Policy::make_multi::<Ctx, MAX_PUBKEYS_PER_MULTISIG>(
|
||||
threshold, signers, build_sat, false, secp,
|
||||
)?,
|
||||
Terminal::MultiA(threshold) => Policy::make_multi::<Ctx, MAX_PUBKEYS_IN_CHECKSIGADD>(
|
||||
threshold, signers, build_sat, false, secp,
|
||||
)?,
|
||||
Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
|
||||
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
|
||||
}
|
||||
// Identities
|
||||
Terminal::Alt(inner)
|
||||
| Terminal::Swap(inner)
|
||||
@@ -1018,9 +1016,8 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
a.extract_policy(signers, build_sat, secp)?,
|
||||
b.extract_policy(signers, build_sat, secp)?,
|
||||
)?,
|
||||
Terminal::Thresh(threshold) => {
|
||||
let mut k = threshold.k();
|
||||
let nodes = threshold.data();
|
||||
Terminal::Thresh(k, nodes) => {
|
||||
let mut threshold = *k;
|
||||
let mapped: Vec<_> = nodes
|
||||
.iter()
|
||||
.map(|n| n.extract_policy(signers, build_sat, secp))
|
||||
@@ -1030,13 +1027,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
.collect();
|
||||
|
||||
if mapped.len() < nodes.len() {
|
||||
k = match k.checked_sub(nodes.len() - mapped.len()) {
|
||||
threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
|
||||
None => return Ok(None),
|
||||
Some(x) => x,
|
||||
};
|
||||
}
|
||||
|
||||
Policy::make_thresh(mapped, k)?
|
||||
Policy::make_thresh(mapped, threshold)?
|
||||
}
|
||||
|
||||
// Unsupported
|
||||
@@ -1090,10 +1087,13 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
||||
build_sat: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, Error> {
|
||||
let threshold = Threshold::new(keys.k(), keys.pks().to_vec())
|
||||
.expect("valid threshold and pks collection");
|
||||
Ok(Policy::make_multi::<Ctx, MAX_PUBKEYS_PER_MULTISIG>(
|
||||
&threshold, signers, build_sat, true, secp,
|
||||
Ok(Policy::make_multisig::<Ctx>(
|
||||
keys.pks.as_ref(),
|
||||
signers,
|
||||
build_sat,
|
||||
keys.k,
|
||||
true,
|
||||
secp,
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -1137,7 +1137,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
||||
let key_spend_sig =
|
||||
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
|
||||
|
||||
if tr.tap_tree().is_none() {
|
||||
if tr.taptree().is_none() {
|
||||
Ok(Some(key_spend_sig))
|
||||
} else {
|
||||
let mut items = vec![key_spend_sig];
|
||||
@@ -1184,8 +1184,8 @@ mod test {
|
||||
secp: &SecpCtx,
|
||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||
let path = bip32::DerivationPath::from_str(path).unwrap();
|
||||
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||
let tpub = bip32::Xpub::from_priv(secp, &tprv);
|
||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
|
||||
let fingerprint = tprv.fingerprint(secp);
|
||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||
@@ -36,17 +36,17 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::descriptor::error::Error as DescriptorError;
|
||||
/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk_wallet::miniscript::Legacy;
|
||||
/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bdk::descriptor::error::Error as DescriptorError;
|
||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk::miniscript::Legacy;
|
||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bitcoin::Network;
|
||||
///
|
||||
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
||||
///
|
||||
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
||||
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
/// Ok(bdk_wallet::descriptor!(pkh(self.0))?)
|
||||
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -72,21 +72,17 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Pkh;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Pkh;
|
||||
///
|
||||
/// let key_external =
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)
|
||||
/// .to_string(),
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -104,25 +100,17 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Wpkh_P2Sh;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// use bdk::template::P2Wpkh_P2Sh;
|
||||
/// use bdk::wallet::AddressIndex;
|
||||
///
|
||||
/// let key_external =
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// P2Wpkh_P2Sh(key_external),
|
||||
/// P2Wpkh_P2Sh(key_internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)
|
||||
/// .to_string(),
|
||||
/// wallet.get_address(AddressIndex::New).to_string(),
|
||||
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -141,21 +129,17 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet};
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Wpkh;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// use bdk::template::P2Wpkh;
|
||||
/// use bdk::wallet::AddressIndex::New;
|
||||
///
|
||||
/// let key_external =
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)
|
||||
/// .to_string(),
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -173,21 +157,17 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2TR;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2TR;
|
||||
///
|
||||
/// let key_external =
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
/// .next_unused_address(KeychainKind::External)
|
||||
/// .to_string(),
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -210,19 +190,20 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip44;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip44(key.clone(), KeychainKind::External),
|
||||
/// Bip44(key, KeychainKind::Internal),
|
||||
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
@@ -246,20 +227,21 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip44Public;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip44Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
@@ -283,19 +265,20 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip49(key.clone(), KeychainKind::External),
|
||||
/// Bip49(key, KeychainKind::Internal),
|
||||
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
@@ -319,20 +302,21 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49Public;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip49Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
@@ -356,19 +340,20 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip84(key.clone(), KeychainKind::External),
|
||||
/// Bip84(key, KeychainKind::Internal),
|
||||
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
@@ -392,20 +377,21 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84Public;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip84Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
@@ -429,19 +415,20 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip86(key.clone(), KeychainKind::External),
|
||||
/// Bip86(key, KeychainKind::Internal),
|
||||
/// Some(Bip86(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
|
||||
@@ -465,20 +452,21 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86Public;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip86Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
@@ -579,29 +567,29 @@ mod test {
|
||||
fn test_bip44_template_cointype() {
|
||||
use bitcoin::bip32::ChildNumber::{self, Hardened};
|
||||
|
||||
let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||
assert!(xprvkey.network.is_mainnet());
|
||||
let xprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||
assert_eq!(Network::Bitcoin, xprvkey.network);
|
||||
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
||||
.build(Network::Bitcoin)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||
let purpose = path.first().unwrap();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert_matches!(coin_type, Hardened { index: 0 });
|
||||
}
|
||||
|
||||
let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
assert!(!tprvkey.network.is_mainnet());
|
||||
let tprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
assert_eq!(Network::Testnet, tprvkey.network);
|
||||
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
||||
.build(Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||
let purpose = path.first().unwrap();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert_matches!(coin_type, Hardened { index: 1 });
|
||||
@@ -752,7 +740,7 @@ mod test {
|
||||
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_template() {
|
||||
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
@@ -782,7 +770,7 @@ mod test {
|
||||
// BIP44 public `pkh(key/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_public_template() {
|
||||
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
@@ -813,7 +801,7 @@ mod test {
|
||||
// BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
||||
#[test]
|
||||
fn test_bip49_template() {
|
||||
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
@@ -843,7 +831,7 @@ mod test {
|
||||
// BIP49 public `sh(wpkh(key/{0,1}/*))`
|
||||
#[test]
|
||||
fn test_bip49_public_template() {
|
||||
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
@@ -874,7 +862,7 @@ mod test {
|
||||
// BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip84_template() {
|
||||
let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
@@ -904,7 +892,7 @@ mod test {
|
||||
// BIP84 public `wpkh(key/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip84_public_template() {
|
||||
let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
@@ -936,7 +924,7 @@ mod test {
|
||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
#[test]
|
||||
fn test_bip86_template() {
|
||||
let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
|
||||
check(
|
||||
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
@@ -967,7 +955,7 @@ mod test {
|
||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
#[test]
|
||||
fn test_bip86_public_template() {
|
||||
let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
|
||||
check(
|
||||
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
@@ -57,7 +57,7 @@ pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
|
||||
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into())
|
||||
}
|
||||
|
||||
fn into_descriptor_key(
|
||||
@@ -97,7 +97,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used internally by `bdk_wallet::fragment!` and `bdk_wallet::descriptor!`. It has to be
|
||||
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
|
||||
// public because it is effectively called by external crates once the macros are expanded,
|
||||
// but since it is not meant to be part of the public api we hide it from the docs.
|
||||
#[doc(hidden)]
|
||||
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
Ok((public, KeyMap::default(), valid_networks))
|
||||
}
|
||||
DescriptorKey::Secret(secret, valid_networks, _) => {
|
||||
let mut key_map = KeyMap::new();
|
||||
let mut key_map = KeyMap::with_capacity(1);
|
||||
|
||||
let public = secret
|
||||
.to_public(secp)
|
||||
@@ -206,9 +206,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type valid in any context:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
@@ -224,9 +224,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type that is only valid on mainnet:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk_wallet::keys::{
|
||||
/// use bdk::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||
/// ScriptContext, SinglePub, SinglePubKey,
|
||||
/// };
|
||||
@@ -251,11 +251,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk_wallet::keys::{
|
||||
/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// is_legacy: bool,
|
||||
@@ -281,17 +279,17 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// makes the compiler (correctly) fail.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use core::str::FromStr;
|
||||
///
|
||||
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
///
|
||||
/// pub struct MySegwitOnlyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
/// }
|
||||
///
|
||||
/// impl IntoDescriptorKey<bdk_wallet::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk_wallet::miniscript::Segwitv0>, KeyError> {
|
||||
/// impl IntoDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
|
||||
/// self.pubkey.into_descriptor_key()
|
||||
/// }
|
||||
/// }
|
||||
@@ -299,8 +297,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// let key = MySegwitOnlyKeyType {
|
||||
/// pubkey: PublicKey::from_str("...")?,
|
||||
/// };
|
||||
/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?;
|
||||
/// // ^^^^^ changing this to `wpkh` would make it compile
|
||||
/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?;
|
||||
/// // ^^^^^ changing this to `wpkh` would make it compile
|
||||
///
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -311,15 +309,15 @@ pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
|
||||
|
||||
/// Enum for extended keys that can be either `xprv` or `xpub`
|
||||
///
|
||||
/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
|
||||
/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
|
||||
/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey)
|
||||
/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait.
|
||||
///
|
||||
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
|
||||
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
|
||||
/// A private extended key, aka an `xprv`
|
||||
Private((bip32::Xpriv, PhantomData<Ctx>)),
|
||||
Private((bip32::ExtendedPrivKey, PhantomData<Ctx>)),
|
||||
/// A public extended key, aka an `xpub`
|
||||
Public((bip32::Xpub, PhantomData<Ctx>)),
|
||||
Public((bip32::ExtendedPubKey, PhantomData<Ctx>)),
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
@@ -331,43 +329,43 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
|
||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the
|
||||
/// given [`Network`], if the key contains the private data
|
||||
pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
|
||||
pub fn into_xprv(self, network: Network) -> Option<bip32::ExtendedPrivKey> {
|
||||
match self {
|
||||
ExtendedKey::Private((mut xprv, _)) => {
|
||||
xprv.network = network.into();
|
||||
xprv.network = network;
|
||||
Some(xprv)
|
||||
}
|
||||
ExtendedKey::Public(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
|
||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the
|
||||
/// given [`Network`]
|
||||
pub fn into_xpub<C: Signing>(
|
||||
self,
|
||||
network: bitcoin::Network,
|
||||
secp: &Secp256k1<C>,
|
||||
) -> bip32::Xpub {
|
||||
) -> bip32::ExtendedPubKey {
|
||||
let mut xpub = match self {
|
||||
ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
|
||||
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
|
||||
ExtendedKey::Public((xpub, _)) => xpub,
|
||||
};
|
||||
|
||||
xpub.network = network.into();
|
||||
xpub.network = network;
|
||||
xpub
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
|
||||
fn from(xpub: bip32::Xpub) -> Self {
|
||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPubKey> for ExtendedKey<Ctx> {
|
||||
fn from(xpub: bip32::ExtendedPubKey) -> Self {
|
||||
ExtendedKey::Public((xpub, PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
fn from(xprv: bip32::Xpriv) -> Self {
|
||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
fn from(xprv: bip32::ExtendedPrivKey) -> Self {
|
||||
ExtendedKey::Private((xprv, PhantomData))
|
||||
}
|
||||
}
|
||||
@@ -385,13 +383,13 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// Key types that can be directly converted into an [`Xpriv`] or
|
||||
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
|
||||
/// Key types that can be directly converted into an [`ExtendedPrivKey`] or
|
||||
/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method.
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::bitcoin;
|
||||
/// use bdk_wallet::bitcoin::bip32;
|
||||
/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// struct MyCustomKeyType {
|
||||
/// key_data: bitcoin::PrivateKey,
|
||||
@@ -401,8 +399,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
///
|
||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
/// let xprv = bip32::Xpriv {
|
||||
/// network: self.network.into(),
|
||||
/// let xprv = bip32::ExtendedPrivKey {
|
||||
/// network: self.network,
|
||||
/// depth: 0,
|
||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||
/// private_key: self.key_data.inner,
|
||||
@@ -417,12 +415,12 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
///
|
||||
/// Types that don't internally encode the [`Network`] in which they are valid need some extra
|
||||
/// steps to override the set of valid networks, otherwise only the network specified in the
|
||||
/// [`Xpriv`] or [`Xpub`] will be considered valid.
|
||||
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_wallet::bitcoin;
|
||||
/// use bdk_wallet::bitcoin::bip32;
|
||||
/// use bdk_wallet::keys::{
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{
|
||||
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
///
|
||||
@@ -433,8 +431,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
///
|
||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
/// let xprv = bip32::Xpriv {
|
||||
/// network: bitcoin::Network::Bitcoin.into(), // pick an arbitrary network here
|
||||
/// let xprv = bip32::ExtendedPrivKey {
|
||||
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
||||
/// depth: 0,
|
||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||
/// private_key: self.key_data.inner,
|
||||
@@ -461,8 +459,8 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
/// ```
|
||||
///
|
||||
/// [`DerivationPath`]: (bip32::DerivationPath)
|
||||
/// [`Xpriv`]: (bip32::Xpriv)
|
||||
/// [`Xpub`]: (bip32::Xpub)
|
||||
/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey)
|
||||
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
|
||||
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||
/// Consume `self` and turn it into an [`ExtendedKey`]
|
||||
#[cfg_attr(
|
||||
@@ -471,9 +469,9 @@ pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||
This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
|
||||
like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
|
||||
```rust
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::keys::{DerivableKey, ExtendedKey};
|
||||
use bdk_wallet::keys::bip39::{Mnemonic, Language};
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::keys::{DerivableKey, ExtendedKey};
|
||||
use bdk::keys::bip39::{Mnemonic, Language};
|
||||
|
||||
# fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let xkey: ExtendedKey =
|
||||
@@ -522,13 +520,13 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(self.into())
|
||||
}
|
||||
@@ -672,7 +670,7 @@ where
|
||||
{
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
|
||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||
type Entropy = [u8; 32];
|
||||
|
||||
type Options = ();
|
||||
@@ -683,7 +681,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
// pick a arbitrary network here, but say that we support all of them
|
||||
let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
|
||||
let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, entropy.as_ref())?;
|
||||
Ok(GeneratedKey::new(xprv, any_network()))
|
||||
}
|
||||
}
|
||||
@@ -717,7 +715,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
|
||||
let inner = secp256k1::SecretKey::from_slice(&entropy)?;
|
||||
let private_key = PrivateKey {
|
||||
compressed: options.compressed,
|
||||
network: Network::Bitcoin.into(),
|
||||
network: Network::Bitcoin,
|
||||
inner,
|
||||
};
|
||||
|
||||
@@ -766,7 +764,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((pks, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk_wallet::fragment!` to build `pk_k()` fragments
|
||||
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
descriptor_key: Pk,
|
||||
@@ -780,7 +778,7 @@ pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk_wallet::fragment!` to build `pk_h()` fragments
|
||||
// Used internally by `bdk::fragment!` to build `pk_h()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
descriptor_key: Pk,
|
||||
@@ -794,7 +792,7 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments
|
||||
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_multi<
|
||||
Pk: IntoDescriptorKey<Ctx>,
|
||||
@@ -814,7 +812,7 @@ pub fn make_multi<
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments
|
||||
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_sortedmulti<Pk, Ctx, F>(
|
||||
thresh: usize,
|
||||
@@ -836,7 +834,7 @@ where
|
||||
Ok((descriptor, key_map, valid_networks))
|
||||
}
|
||||
|
||||
/// The "identity" conversion is used internally by some `bdk_wallet::fragment`s
|
||||
/// The "identity" conversion is used internally by some `bdk::fragment`s
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
Ok(self)
|
||||
@@ -847,7 +845,9 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match self {
|
||||
DescriptorPublicKey::Single(_) => any_network(),
|
||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) if xkey.network.is_mainnet() => {
|
||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||
if xkey.network == Network::Bitcoin =>
|
||||
{
|
||||
mainnet_network()
|
||||
}
|
||||
_ => test_networks(),
|
||||
@@ -880,8 +880,12 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match &self {
|
||||
DescriptorSecretKey::Single(sk) if sk.key.network.is_mainnet() => mainnet_network(),
|
||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) if xkey.network.is_mainnet() => {
|
||||
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
|
||||
mainnet_network()
|
||||
}
|
||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
||||
if xkey.network == Network::Bitcoin =>
|
||||
{
|
||||
mainnet_network()
|
||||
}
|
||||
_ => test_networks(),
|
||||
@@ -967,7 +971,7 @@ pub mod test {
|
||||
#[test]
|
||||
fn test_keys_generate_xprv() {
|
||||
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
||||
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
||||
|
||||
assert_eq!(generated_xprv.valid_networks, any_network());
|
||||
assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
|
||||
@@ -997,6 +1001,6 @@ pub mod test {
|
||||
.unwrap();
|
||||
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
|
||||
|
||||
assert_eq!(xprv.network, Network::Testnet.into());
|
||||
assert_eq!(xprv.network, Network::Testnet);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ extern crate std;
|
||||
pub extern crate alloc;
|
||||
|
||||
pub extern crate bitcoin;
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
pub extern crate hwi;
|
||||
pub extern crate miniscript;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
@@ -9,12 +9,11 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Additional functions on the `rust-bitcoin` `Psbt` structure.
|
||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||
|
||||
use crate::FeeRate;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::Amount;
|
||||
use bitcoin::FeeRate;
|
||||
use bitcoin::Psbt;
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
// TODO upstream the functions here to `rust-bitcoin`?
|
||||
@@ -26,36 +25,44 @@ pub trait PsbtUtils {
|
||||
|
||||
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_amount(&self) -> Option<Amount>;
|
||||
fn fee_amount(&self) -> Option<u64>;
|
||||
|
||||
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
|
||||
/// `Psbt` is finalized and all witness/signature data is added to the
|
||||
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
|
||||
/// transaction.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_rate(&self) -> Option<FeeRate>;
|
||||
}
|
||||
|
||||
impl PsbtUtils for Psbt {
|
||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let input = self.inputs.get(input_index)?;
|
||||
|
||||
match (&input.witness_utxo, &input.non_witness_utxo) {
|
||||
(Some(_), _) => input.witness_utxo.clone(),
|
||||
(_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
|
||||
in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
|
||||
}),
|
||||
_ => None,
|
||||
if input_index >= tx.input.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(input) = self.inputs.get(input_index) {
|
||||
if let Some(wit_utxo) = &input.witness_utxo {
|
||||
Some(wit_utxo.clone())
|
||||
} else if let Some(in_tx) = &input.non_witness_utxo {
|
||||
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn fee_amount(&self) -> Option<Amount> {
|
||||
fn fee_amount(&self) -> Option<u64> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
|
||||
|
||||
utxos.map(|inputs| {
|
||||
let input_amount: Amount = inputs.iter().map(|i| i.value).sum();
|
||||
let output_amount: Amount = self.unsigned_tx.output.iter().map(|o| o.value).sum();
|
||||
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
|
||||
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
|
||||
input_amount
|
||||
.checked_sub(output_amount)
|
||||
.expect("input amount must be greater than output amount")
|
||||
@@ -64,7 +71,9 @@ impl PsbtUtils for Psbt {
|
||||
|
||||
fn fee_rate(&self) -> Option<FeeRate> {
|
||||
let fee_amount = self.fee_amount();
|
||||
let weight = self.clone().extract_tx().ok()?.weight();
|
||||
fee_amount.map(|fee| fee / weight)
|
||||
fee_amount.map(|fee| {
|
||||
let weight = self.clone().extract_tx().weight();
|
||||
FeeRate::from_wu(fee, weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
305
crates/bdk/src/types.rs
Normal file
305
crates/bdk/src/types.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
use core::ops::Sub;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of keychains
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub enum KeychainKind {
|
||||
/// External keychain, used for deriving recipient addresses.
|
||||
External = 0,
|
||||
/// Internal keychain, used for deriving change addresses.
|
||||
Internal = 1,
|
||||
}
|
||||
|
||||
impl KeychainKind {
|
||||
/// Return [`KeychainKind`] as a byte
|
||||
pub fn as_byte(&self) -> u8 {
|
||||
match self {
|
||||
KeychainKind::External => b'e',
|
||||
KeychainKind::Internal => b'i',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for KeychainKind {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
KeychainKind::External => b"e",
|
||||
KeychainKind::Internal => b"i",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fee rate
|
||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||
// Internally stored as satoshi/vbyte
|
||||
pub struct FeeRate(f32);
|
||||
|
||||
impl FeeRate {
|
||||
/// Create a new instance checking the value provided
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
fn new_checked(value: f32) -> Self {
|
||||
assert!(value.is_normal() || value == 0.0);
|
||||
assert!(value.is_sign_positive());
|
||||
|
||||
FeeRate(value)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
||||
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
||||
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
||||
FeeRate::new_checked(btc_per_kvb * 1e5)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_vb)
|
||||
}
|
||||
|
||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||
pub const fn default_min_relay_fee() -> Self {
|
||||
FeeRate(1.0)
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and weight units (`wu`).
|
||||
pub fn from_wu(fee: u64, wu: Weight) -> FeeRate {
|
||||
Self::from_vb(fee, wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and `vbytes`.
|
||||
pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate {
|
||||
let rate = fee as f32 / vbytes as f32;
|
||||
Self::from_sat_per_vb(rate)
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/vbyte
|
||||
pub fn as_sat_per_vb(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/kwu
|
||||
pub fn sat_per_kwu(&self) -> f32 {
|
||||
self.0 * 250.0_f32
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in weight units.
|
||||
pub fn fee_wu(&self, wu: Weight) -> u64 {
|
||||
self.fee_vb(wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FeeRate {
|
||||
fn default() -> Self {
|
||||
FeeRate::default_min_relay_fee()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for FeeRate {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: FeeRate) -> Self::Output {
|
||||
FeeRate(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by types that can be used to measure weight units.
|
||||
pub trait Vbytes {
|
||||
/// Convert weight units to virtual bytes.
|
||||
fn vbytes(self) -> usize;
|
||||
}
|
||||
|
||||
impl Vbytes for usize {
|
||||
fn vbytes(self) -> usize {
|
||||
// ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
|
||||
(self as f32 / 4.0).ceil() as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// An unspent output owned by a [`Wallet`].
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LocalOutput {
|
||||
/// Reference to a transaction output
|
||||
pub outpoint: OutPoint,
|
||||
/// Transaction output
|
||||
pub txout: TxOut,
|
||||
/// Type of keychain
|
||||
pub keychain: KeychainKind,
|
||||
/// Whether this UTXO is spent or not
|
||||
pub is_spent: bool,
|
||||
/// The derivation index for the script pubkey in the wallet
|
||||
pub derivation_index: u32,
|
||||
/// The confirmation time for transaction containing this utxo
|
||||
pub confirmation_time: ConfirmationTime,
|
||||
}
|
||||
|
||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WeightedUtxo {
|
||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
///
|
||||
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
|
||||
pub satisfaction_weight: usize,
|
||||
/// The UTXO
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
Local(LocalOutput),
|
||||
/// A UTXO owned by another wallet.
|
||||
Foreign {
|
||||
/// The location of the output.
|
||||
outpoint: OutPoint,
|
||||
/// The information about the input we require to add it to a PSBT.
|
||||
// Box it to stop the type being too big.
|
||||
psbt_input: Box<psbt::Input>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Utxo {
|
||||
/// Get the location of the UTXO
|
||||
pub fn outpoint(&self) -> OutPoint {
|
||||
match &self {
|
||||
Utxo::Local(local) => local.outpoint,
|
||||
Utxo::Foreign { outpoint, .. } => *outpoint,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `TxOut` of the UTXO
|
||||
pub fn txout(&self) -> &TxOut {
|
||||
match &self {
|
||||
Utxo::Local(local) => &local.txout,
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
}
|
||||
|
||||
if let Some(txout) = &psbt_input.witness_utxo {
|
||||
return txout;
|
||||
}
|
||||
|
||||
unreachable!("Foreign UTXOs will always have one of these set")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,13 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::coin_selection::decide_change;
|
||||
//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk_chain::PersistBackend;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::coin_selection::decide_change;
|
||||
//! # use anyhow::Error;
|
||||
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
|
||||
//! #[derive(Debug)]
|
||||
//! struct AlwaysSpendEverything;
|
||||
//!
|
||||
@@ -39,7 +41,7 @@
|
||||
//! &self,
|
||||
//! required_utxos: Vec<WeightedUtxo>,
|
||||
//! optional_utxos: Vec<WeightedUtxo>,
|
||||
//! fee_rate: FeeRate,
|
||||
//! fee_rate: bdk::FeeRate,
|
||||
//! target_amount: u64,
|
||||
//! drain_script: &Script,
|
||||
//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
|
||||
@@ -51,17 +53,15 @@
|
||||
//! .scan(
|
||||
//! (&mut selected_amount, &mut additional_weight),
|
||||
//! |(selected_amount, additional_weight), weighted_utxo| {
|
||||
//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
||||
//! **additional_weight += Weight::from_wu(
|
||||
//! (TxIn::default().segwit_weight().to_wu()
|
||||
//! + weighted_utxo.satisfaction_weight as u64)
|
||||
//! as u64,
|
||||
//! (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
//! );
|
||||
//! Some(weighted_utxo.utxo)
|
||||
//! },
|
||||
//! )
|
||||
//! .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;
|
||||
//! if selected_amount < amount_needed_with_fees {
|
||||
//! return Err(coin_selection::Error::InsufficientFunds {
|
||||
@@ -91,7 +91,7 @@
|
||||
//! .unwrap();
|
||||
//! let psbt = {
|
||||
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
|
||||
//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
|
||||
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
|
||||
//! builder.finish()?
|
||||
//! };
|
||||
//!
|
||||
@@ -100,16 +100,13 @@
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::chain::collections::HashSet;
|
||||
use crate::types::FeeRate;
|
||||
use crate::wallet::utils::IsDust;
|
||||
use crate::Utxo;
|
||||
use crate::WeightedUtxo;
|
||||
use bitcoin::FeeRate;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::consensus::encode::serialize;
|
||||
use bitcoin::OutPoint;
|
||||
use bitcoin::TxIn;
|
||||
use bitcoin::{Script, Weight};
|
||||
|
||||
use core::convert::TryInto;
|
||||
@@ -120,6 +117,10 @@ use rand::seq::SliceRandom;
|
||||
/// overridden
|
||||
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
|
||||
|
||||
// Base weight of a Txin, not counting the weight needed for satisfying it.
|
||||
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
|
||||
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
|
||||
|
||||
/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@@ -192,7 +193,7 @@ pub struct CoinSelectionResult {
|
||||
impl CoinSelectionResult {
|
||||
/// The total value of the inputs selected.
|
||||
pub fn selected_amount(&self) -> u64 {
|
||||
self.selected.iter().map(|u| u.txout().value.to_sat()).sum()
|
||||
self.selected.iter().map(|u| u.txout().value).sum()
|
||||
}
|
||||
|
||||
/// The total value of the inputs selected from the local wallet.
|
||||
@@ -200,7 +201,7 @@ impl CoinSelectionResult {
|
||||
self.selected
|
||||
.iter()
|
||||
.filter_map(|u| match u {
|
||||
Utxo::Local(_) => Some(u.txout().value.to_sat()),
|
||||
Utxo::Local(_) => Some(u.txout().value),
|
||||
_ => None,
|
||||
})
|
||||
.sum()
|
||||
@@ -310,12 +311,11 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
|
||||
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
|
||||
// drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
|
||||
let drain_output_len = serialize(drain_script).len() + 8usize;
|
||||
let change_fee =
|
||||
(fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
|
||||
let change_fee = fee_rate.fee_vb(drain_output_len);
|
||||
let drain_val = remaining_amount.saturating_sub(change_fee);
|
||||
|
||||
if drain_val.is_dust(drain_script) {
|
||||
let dust_threshold = drain_script.minimal_non_dust().to_sat();
|
||||
let dust_threshold = drain_script.dust_value().to_sat();
|
||||
Excess::NoChange {
|
||||
dust_threshold,
|
||||
change_fee,
|
||||
@@ -342,13 +342,10 @@ fn select_sorted_utxos(
|
||||
(&mut selected_amount, &mut fee_amount),
|
||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||
if must_use || **selected_amount < target_amount + **fee_amount {
|
||||
**fee_amount += (fee_rate
|
||||
* Weight::from_wu(
|
||||
TxIn::default().segwit_weight().to_wu()
|
||||
+ weighted_utxo.satisfaction_weight as u64,
|
||||
))
|
||||
.to_sat();
|
||||
**selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||
**fee_amount += fee_rate.fee_wu(Weight::from_wu(
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
));
|
||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||
Some(weighted_utxo.utxo)
|
||||
} else {
|
||||
None
|
||||
@@ -388,12 +385,10 @@ struct OutputGroup {
|
||||
|
||||
impl OutputGroup {
|
||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||
let fee = (fee_rate
|
||||
* Weight::from_wu(
|
||||
TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
|
||||
))
|
||||
.to_sat();
|
||||
let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
|
||||
let fee = fee_rate.fee_wu(Weight::from_wu(
|
||||
(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
|
||||
));
|
||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||
OutputGroup {
|
||||
weighted_utxo,
|
||||
fee,
|
||||
@@ -459,8 +454,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let cost_of_change =
|
||||
(Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
|
||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
|
||||
|
||||
// `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
|
||||
@@ -484,7 +478,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
||||
.chain(optional_utxos.iter())
|
||||
.fold((0, 0), |(mut fees, mut value), utxo| {
|
||||
fees += utxo.fee;
|
||||
value += utxo.weighted_utxo.utxo.txout().value.to_sat();
|
||||
value += utxo.weighted_utxo.utxo.txout().value;
|
||||
|
||||
(fees, value)
|
||||
});
|
||||
@@ -551,7 +545,7 @@ impl BranchAndBoundCoinSelection {
|
||||
mut curr_value: i64,
|
||||
mut curr_available_value: i64,
|
||||
target_amount: i64,
|
||||
cost_of_change: u64,
|
||||
cost_of_change: f32,
|
||||
drain_script: &Script,
|
||||
fee_rate: FeeRate,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
@@ -588,7 +582,7 @@ impl BranchAndBoundCoinSelection {
|
||||
// If we found a solution better than the previous one, or if there wasn't previous
|
||||
// solution, update the best solution
|
||||
if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
|
||||
best_selection.clone_from(¤t_selection);
|
||||
best_selection = current_selection.clone();
|
||||
best_selection_value = Some(curr_value);
|
||||
}
|
||||
|
||||
@@ -717,44 +711,25 @@ impl BranchAndBoundCoinSelection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove duplicate UTXOs.
|
||||
///
|
||||
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
|
||||
pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
|
||||
where
|
||||
I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
|
||||
{
|
||||
let mut visited = HashSet::<OutPoint>::new();
|
||||
let required = required
|
||||
.into_iter()
|
||||
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
|
||||
.collect::<I>();
|
||||
let optional = optional
|
||||
.into_iter()
|
||||
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
|
||||
.collect::<I>();
|
||||
(required, optional)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
|
||||
use bitcoin::{OutPoint, ScriptBuf, TxOut};
|
||||
|
||||
use super::*;
|
||||
use crate::types::*;
|
||||
use crate::wallet::coin_selection::filter_duplicates;
|
||||
use crate::wallet::Vbytes;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::{Rng, RngCore, SeedableRng};
|
||||
|
||||
// signature len (1WU) + signature and sighash (72WU)
|
||||
// + pubkey len (1WU) + pubkey (33WU)
|
||||
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
|
||||
// n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
|
||||
// + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
|
||||
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
|
||||
|
||||
const FEE_AMOUNT: u64 = 50;
|
||||
|
||||
@@ -770,7 +745,7 @@ mod test {
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint,
|
||||
txout: TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
@@ -824,17 +799,16 @@ mod test {
|
||||
|
||||
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||
let mut res = Vec::new();
|
||||
for i in 0..utxos_number {
|
||||
for _ in 0..utxos_number {
|
||||
res.push(WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
i
|
||||
))
|
||||
outpoint: OutPoint::from_str(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: Amount::from_sat(rng.gen_range(0..200000000)),
|
||||
value: rng.gen_range(0..200000000),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
@@ -855,26 +829,24 @@ mod test {
|
||||
}
|
||||
|
||||
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||
(0..utxos_number)
|
||||
.map(|i| WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
i
|
||||
))
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: Amount::from_sat(utxos_value),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 42,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
let utxo = WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: utxos_value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 42,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
}),
|
||||
};
|
||||
vec![utxo; utxos_number]
|
||||
}
|
||||
|
||||
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
||||
@@ -882,7 +854,7 @@ mod test {
|
||||
utxos.shuffle(&mut rng);
|
||||
utxos[..utxos_picked_len]
|
||||
.iter()
|
||||
.map(|u| u.utxo.txout().value.to_sat())
|
||||
.map(|u| u.utxo.txout().value)
|
||||
.sum()
|
||||
}
|
||||
|
||||
@@ -896,7 +868,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -917,7 +889,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -938,7 +910,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -960,7 +932,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -978,7 +950,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -995,7 +967,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1016,7 +988,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1037,7 +1009,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1059,7 +1031,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1071,18 +1043,14 @@ mod test {
|
||||
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
|
||||
let utxos = get_oldest_first_test_utxos();
|
||||
|
||||
let target_amount: u64 = utxos
|
||||
.iter()
|
||||
.map(|wu| wu.utxo.txout().value.to_sat())
|
||||
.sum::<u64>()
|
||||
- 50;
|
||||
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1103,7 +1071,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1124,7 +1092,7 @@ mod test {
|
||||
.coin_select(
|
||||
utxos.clone(),
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1145,7 +1113,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1170,9 +1138,9 @@ mod test {
|
||||
));
|
||||
|
||||
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
|
||||
let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
|
||||
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
|
||||
assert_eq!(amount, 100_000);
|
||||
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
|
||||
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
|
||||
assert!(amount > 150_000);
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
@@ -1182,7 +1150,7 @@ mod test {
|
||||
.coin_select(
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1204,7 +1172,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1),
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1222,7 +1190,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(1000),
|
||||
FeeRate::from_sat_per_vb(1000.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1234,19 +1202,22 @@ mod test {
|
||||
let utxos = get_test_utxos();
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 99932; // first utxo's effective value
|
||||
let feerate = FeeRate::BROADCAST_MIN;
|
||||
|
||||
let result = BranchAndBoundCoinSelection::new(0)
|
||||
.coin_select(vec![], utxos, feerate, target_amount, &drain_script)
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 1);
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
let input_weight =
|
||||
TxIn::default().segwit_weight().to_wu() + 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
|
||||
let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
|
||||
assert_eq!(result_feerate, feerate);
|
||||
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1262,7 +1233,7 @@ mod test {
|
||||
.coin_select(
|
||||
vec![],
|
||||
optional_utxos,
|
||||
FeeRate::ZERO,
|
||||
FeeRate::from_sat_per_vb(0.0),
|
||||
target_amount,
|
||||
&drain_script,
|
||||
)
|
||||
@@ -1274,7 +1245,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "BnBNoExactMatch")]
|
||||
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()
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1283,7 +1254,7 @@ mod test {
|
||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = (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 target_amount = 20_000 + FEE_AMOUNT;
|
||||
@@ -1304,7 +1275,7 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "BnBTotalTriesExceeded")]
|
||||
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)
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1313,7 +1284,7 @@ mod test {
|
||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let size_of_change = 31;
|
||||
let cost_of_change = (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 drain_script = ScriptBuf::default();
|
||||
@@ -1335,9 +1306,9 @@ mod test {
|
||||
// The match won't be exact but still in the range
|
||||
#[test]
|
||||
fn test_bnb_function_almost_exact_match_with_fees() {
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
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)
|
||||
.into_iter()
|
||||
@@ -1350,7 +1321,7 @@ mod test {
|
||||
|
||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||
// cost_of_change + 5.
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
@@ -1375,7 +1346,7 @@ mod test {
|
||||
fn test_bnb_function_exact_match_more_utxos() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let fee_rate = FeeRate::ZERO;
|
||||
let fee_rate = FeeRate::from_sat_per_vb(0.0);
|
||||
|
||||
for _ in 0..200 {
|
||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||
@@ -1401,7 +1372,7 @@ mod test {
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
0,
|
||||
0.0,
|
||||
&drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
@@ -1417,7 +1388,7 @@ mod test {
|
||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let utxos: Vec<OutputGroup> = utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
@@ -1446,7 +1417,7 @@ mod test {
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb_unchecked(10),
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
@@ -1465,14 +1436,14 @@ mod test {
|
||||
let utxos = get_test_utxos();
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
let (required, optional) = utxos.into_iter().partition(
|
||||
|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
|
||||
);
|
||||
let (required, optional) = utxos
|
||||
.into_iter()
|
||||
.partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
|
||||
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb_unchecked(10),
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
@@ -1494,7 +1465,7 @@ mod test {
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb_unchecked(10_000),
|
||||
FeeRate::from_sat_per_vb(10_000.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
@@ -1507,95 +1478,4 @@ mod test {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_duplicates() {
|
||||
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: 0,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
|
||||
txout: TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
derivation_index: 0,
|
||||
confirmation_time: ConfirmationTime::Confirmed {
|
||||
height: 12345,
|
||||
time: 12345,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
|
||||
let mut v = utxos
|
||||
.iter()
|
||||
.map(|&(txid, value)| utxo(txid, value))
|
||||
.collect::<Vec<_>>();
|
||||
v.sort_by_key(|u| u.utxo.outpoint());
|
||||
v
|
||||
}
|
||||
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
required: &'a [(&'a str, u64)],
|
||||
optional: &'a [(&'a str, u64)],
|
||||
exp_required: &'a [(&'a str, u64)],
|
||||
exp_optional: &'a [(&'a str, u64)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "no_duplicates",
|
||||
required: &[("A", 1000), ("B", 2100)],
|
||||
optional: &[("C", 1000)],
|
||||
exp_required: &[("A", 1000), ("B", 2100)],
|
||||
exp_optional: &[("C", 1000)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_required_utxos",
|
||||
required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
|
||||
optional: &[("D", 2100)],
|
||||
exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
|
||||
exp_optional: &[("D", 2100)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_optional_utxos",
|
||||
required: &[("A", 3000), ("B", 1200)],
|
||||
optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
|
||||
exp_required: &[("A", 3000), ("B", 1200)],
|
||||
exp_optional: &[("C", 5000), ("D", 1300)],
|
||||
},
|
||||
TestCase {
|
||||
name: "duplicate_across_required_and_optional_utxos",
|
||||
required: &[("A", 3000), ("B", 1200), ("C", 2100)],
|
||||
optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
|
||||
exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
|
||||
exp_optional: &[("D", 1200), ("E", 5000)],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let (required, optional) =
|
||||
filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
|
||||
assert_eq!(
|
||||
required,
|
||||
to_utxo_vec(t.exp_required),
|
||||
"[{}:{}] unexpected `required` result",
|
||||
i,
|
||||
t.name
|
||||
);
|
||||
assert_eq!(
|
||||
optional,
|
||||
to_utxo_vec(t.exp_optional),
|
||||
"[{}:{}] unexpected `optional` result",
|
||||
i,
|
||||
t.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
use crate::descriptor::policy::PolicyError;
|
||||
use crate::descriptor::DescriptorError;
|
||||
use crate::wallet::coin_selection;
|
||||
use crate::{descriptor, KeychainKind};
|
||||
use crate::{descriptor, FeeRate, KeychainKind};
|
||||
use alloc::string::String;
|
||||
use bitcoin::{absolute, psbt, Amount, OutPoint, Sequence, Txid};
|
||||
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
||||
use core::fmt;
|
||||
|
||||
/// Errors returned by miniscript when updating inconsistent PSBTs
|
||||
@@ -47,9 +47,11 @@ impl std::error::Error for MiniscriptPsbtError {}
|
||||
/// Error returned from [`TxBuilder::finish`]
|
||||
///
|
||||
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
|
||||
pub enum CreateTxError {
|
||||
pub enum CreateTxError<P> {
|
||||
/// There was a problem with the descriptors passed in
|
||||
Descriptor(DescriptorError),
|
||||
/// We were unable to write wallet data to the persistence backend
|
||||
Persist(P),
|
||||
/// There was a problem while extracting and manipulating policies
|
||||
Policy(PolicyError),
|
||||
/// Spending policy is not compatible with this [`KeychainKind`]
|
||||
@@ -76,20 +78,29 @@ pub enum CreateTxError {
|
||||
},
|
||||
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
|
||||
FeeTooLow {
|
||||
/// Required fee absolute value [`Amount`]
|
||||
required: Amount,
|
||||
/// Required fee absolute value (satoshi)
|
||||
required: u64,
|
||||
},
|
||||
/// When bumping a tx the fee rate requested is lower than required
|
||||
FeeRateTooLow {
|
||||
/// Required fee rate
|
||||
required: bitcoin::FeeRate,
|
||||
/// Required fee rate (satoshi/vbyte)
|
||||
required: FeeRate,
|
||||
},
|
||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||
NoUtxosSelected,
|
||||
/// Output created is under the dust limit, 546 satoshis
|
||||
OutputBelowDustLimit(usize),
|
||||
/// The `change_policy` was set but the wallet does not have a change_descriptor
|
||||
ChangePolicyDescriptor,
|
||||
/// There was an error with coin selection
|
||||
CoinSelection(coin_selection::Error),
|
||||
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee
|
||||
InsufficientFunds {
|
||||
/// Sats needed for some transaction
|
||||
needed: u64,
|
||||
/// Sats available for spending
|
||||
available: u64,
|
||||
},
|
||||
/// Cannot build a tx without recipients
|
||||
NoRecipients,
|
||||
/// Partially signed bitcoin transaction error
|
||||
@@ -108,10 +119,20 @@ pub enum CreateTxError {
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
}
|
||||
|
||||
impl fmt::Display for CreateTxError {
|
||||
impl<P> fmt::Display for CreateTxError<P>
|
||||
where
|
||||
P: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Descriptor(e) => e.fmt(f),
|
||||
Self::Persist(e) => {
|
||||
write!(
|
||||
f,
|
||||
"failed to write wallet data to persistence backend: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
Self::Policy(e) => e.fmt(f),
|
||||
CreateTxError::SpendingPolicyRequired(keychain_kind) => {
|
||||
write!(f, "Spending policy required: {:?}", keychain_kind)
|
||||
@@ -142,15 +163,13 @@ impl fmt::Display for CreateTxError {
|
||||
)
|
||||
}
|
||||
CreateTxError::FeeTooLow { required } => {
|
||||
write!(f, "Fee to low: required {}", required.display_dynamic())
|
||||
write!(f, "Fee to low: required {} sat", required)
|
||||
}
|
||||
CreateTxError::FeeRateTooLow { required } => {
|
||||
write!(
|
||||
f,
|
||||
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
|
||||
//"Fee rate too low: required {required:#}"
|
||||
"Fee rate too low: required {} sat/vb",
|
||||
crate::floating_rate!(required)
|
||||
"Fee rate too low: required {} sat/vbyte",
|
||||
required.as_sat_per_vb()
|
||||
)
|
||||
}
|
||||
CreateTxError::NoUtxosSelected => {
|
||||
@@ -159,7 +178,20 @@ impl fmt::Display for CreateTxError {
|
||||
CreateTxError::OutputBelowDustLimit(limit) => {
|
||||
write!(f, "Output below the dust limit: {}", limit)
|
||||
}
|
||||
CreateTxError::ChangePolicyDescriptor => {
|
||||
write!(
|
||||
f,
|
||||
"The `change_policy` can be set only if the wallet has a change_descriptor"
|
||||
)
|
||||
}
|
||||
CreateTxError::CoinSelection(e) => e.fmt(f),
|
||||
CreateTxError::InsufficientFunds { needed, available } => {
|
||||
write!(
|
||||
f,
|
||||
"Insufficient funds: {} sat available of {} sat needed",
|
||||
available, needed
|
||||
)
|
||||
}
|
||||
CreateTxError::NoRecipients => {
|
||||
write!(f, "Cannot build tx without recipients")
|
||||
}
|
||||
@@ -180,38 +212,38 @@ impl fmt::Display for CreateTxError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<descriptor::error::Error> for CreateTxError {
|
||||
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
|
||||
fn from(err: descriptor::error::Error) -> Self {
|
||||
CreateTxError::Descriptor(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PolicyError> for CreateTxError {
|
||||
impl<P> From<PolicyError> for CreateTxError<P> {
|
||||
fn from(err: PolicyError) -> Self {
|
||||
CreateTxError::Policy(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MiniscriptPsbtError> for CreateTxError {
|
||||
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
|
||||
fn from(err: MiniscriptPsbtError) -> Self {
|
||||
CreateTxError::MiniscriptPsbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<psbt::Error> for CreateTxError {
|
||||
impl<P> From<psbt::Error> for CreateTxError<P> {
|
||||
fn from(err: psbt::Error) -> Self {
|
||||
CreateTxError::Psbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<coin_selection::Error> for CreateTxError {
|
||||
impl<P> From<coin_selection::Error> for CreateTxError<P> {
|
||||
fn from(err: coin_selection::Error) -> Self {
|
||||
CreateTxError::CoinSelection(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CreateTxError {}
|
||||
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`Wallet::build_fee_bump`]
|
||||
@@ -20,8 +20,8 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! let import = r#"{
|
||||
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
|
||||
//! "blockheight":1782088,
|
||||
@@ -29,9 +29,9 @@
|
||||
//! }"#;
|
||||
//!
|
||||
//! let import = FullyNodedExport::from_str(import)?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! let wallet = Wallet::new_no_persist(
|
||||
//! &import.descriptor(),
|
||||
//! &import.change_descriptor().expect("change descriptor"),
|
||||
//! import.change_descriptor().as_ref(),
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -40,11 +40,11 @@
|
||||
//! ### Export a `Wallet`
|
||||
//! ```
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! let wallet = Wallet::new(
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! let wallet = Wallet::new_no_persist(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
|
||||
@@ -53,9 +53,9 @@
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
use alloc::string::String;
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use miniscript::descriptor::{ShInner, WshInner};
|
||||
@@ -80,9 +80,9 @@ pub struct FullyNodedExport {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for FullyNodedExport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap())
|
||||
impl ToString for FullyNodedExport {
|
||||
fn to_string(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ impl FullyNodedExport {
|
||||
///
|
||||
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
|
||||
/// returned will be `0`.
|
||||
pub fn export_wallet(
|
||||
wallet: &Wallet,
|
||||
pub fn export_wallet<D>(
|
||||
wallet: &Wallet<D>,
|
||||
label: &str,
|
||||
include_blockheight: bool,
|
||||
) -> Result<Self, &'static str> {
|
||||
@@ -142,17 +142,19 @@ impl FullyNodedExport {
|
||||
blockheight,
|
||||
};
|
||||
|
||||
let change_descriptor = {
|
||||
let descriptor = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::Internal)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::Internal)
|
||||
.as_key_map(wallet.secp_ctx()),
|
||||
);
|
||||
Some(remove_checksum(descriptor))
|
||||
let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
|
||||
false => None,
|
||||
true => {
|
||||
let descriptor = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::Internal)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::Internal)
|
||||
.as_key_map(wallet.secp_ctx()),
|
||||
);
|
||||
Some(remove_checksum(descriptor))
|
||||
}
|
||||
};
|
||||
|
||||
if export.change_descriptor() != change_descriptor {
|
||||
return Err("Incompatible change descriptor");
|
||||
}
|
||||
@@ -164,7 +166,7 @@ impl FullyNodedExport {
|
||||
fn check_ms<Ctx: ScriptContext>(
|
||||
terminal: &Terminal<String, Ctx>,
|
||||
) -> Result<(), &'static str> {
|
||||
if let Terminal::Multi(_) = terminal {
|
||||
if let Terminal::Multi(_, _) = terminal {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("The descriptor contains operators not supported by Bitcoin Core")
|
||||
@@ -187,7 +189,6 @@ impl FullyNodedExport {
|
||||
WshInner::SortedMulti(_) => Ok(()),
|
||||
WshInner::Ms(ms) => check_ms(&ms.node),
|
||||
},
|
||||
Descriptor::Tr(_) => Ok(()),
|
||||
_ => Err("The descriptor is not compatible with Bitcoin Core"),
|
||||
}
|
||||
}
|
||||
@@ -213,20 +214,23 @@ impl FullyNodedExport {
|
||||
mod test {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::std::string::ToString;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||
use bitcoin::{BlockHash, Network, Transaction};
|
||||
|
||||
use super::*;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
|
||||
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
|
||||
fn get_test_wallet(
|
||||
descriptor: &str,
|
||||
change_descriptor: Option<&str>,
|
||||
network: Network,
|
||||
) -> Wallet<()> {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
|
||||
let transaction = Transaction {
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
version: transaction::Version::non_standard(0),
|
||||
version: 0,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
};
|
||||
wallet
|
||||
@@ -252,7 +256,7 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
@@ -264,14 +268,13 @@ mod test {
|
||||
#[test]
|
||||
#[should_panic(expected = "Incompatible change descriptor")]
|
||||
fn test_export_no_change() {
|
||||
// The wallet's change descriptor has no wildcard. It should be impossible to
|
||||
// This wallet explicitly doesn't have a change descriptor. It should be impossible to
|
||||
// export, because exporting this kind of external descriptor normally implies the
|
||||
// existence of a compatible internal descriptor
|
||||
// existence of an internal descriptor
|
||||
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/0)";
|
||||
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
|
||||
let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
@@ -284,7 +287,7 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
|
||||
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
@@ -301,7 +304,7 @@ mod test {
|
||||
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
|
||||
))";
|
||||
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet);
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
@@ -310,24 +313,12 @@ mod test {
|
||||
assert_eq!(export.label, "Test Label");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_tr() {
|
||||
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
||||
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||
assert_eq!(export.blockheight, 5000);
|
||||
assert_eq!(export.label, "Test Label");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_to_json() {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
|
||||
@@ -14,11 +14,11 @@
|
||||
//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk_wallet::wallet::AddressIndex::New;
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
@@ -30,7 +30,7 @@
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new(
|
||||
//! # let mut wallet = Wallet::new_no_persist(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
@@ -48,8 +48,8 @@
|
||||
//! ```
|
||||
|
||||
use bitcoin::bip32::Fingerprint;
|
||||
use bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::Psbt;
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
@@ -87,7 +87,7 @@ impl SignerCommon for HWISigner {
|
||||
impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut PartiallySignedTransaction,
|
||||
_sign_options: &crate::SignOptions,
|
||||
_secp: &crate::wallet::utils::SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,13 @@
|
||||
//! # use core::str::FromStr;
|
||||
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::signer::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bitcoin::psbt;
|
||||
//! # use bdk::signer::*;
|
||||
//! # use bdk::*;
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct CustomHSM;
|
||||
//! # impl CustomHSM {
|
||||
//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
|
||||
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn connect() -> Self {
|
||||
@@ -54,7 +55,7 @@
|
||||
//! impl InputSigner for CustomSigner {
|
||||
//! fn sign_input(
|
||||
//! &self,
|
||||
//! psbt: &mut Psbt,
|
||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||
//! input_index: usize,
|
||||
//! _sign_options: &SignOptions,
|
||||
//! _secp: &Secp256k1<All>,
|
||||
@@ -67,9 +68,8 @@
|
||||
//!
|
||||
//! let custom_signer = CustomSigner::connect();
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
|
||||
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
|
||||
//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
@@ -80,20 +80,19 @@
|
||||
//! ```
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::Ordering;
|
||||
use core::fmt;
|
||||
use core::ops::{Bound::Included, Deref};
|
||||
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||
use bitcoin::hashes::hash160;
|
||||
use bitcoin::secp256k1::Message;
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
|
||||
use bitcoin::{ecdsa, psbt, sighash, taproot, transaction};
|
||||
use bitcoin::{ecdsa, psbt, sighash, taproot};
|
||||
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
|
||||
use bitcoin::{PrivateKey, Psbt, PublicKey};
|
||||
use bitcoin::{PrivateKey, PublicKey};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
||||
@@ -159,35 +158,25 @@ pub enum SignerError {
|
||||
NonStandardSighash,
|
||||
/// Invalid SIGHASH for the signing context in use
|
||||
InvalidSighash,
|
||||
/// Error while computing the hash to sign a P2WPKH input.
|
||||
SighashP2wpkh(sighash::P2wpkhError),
|
||||
/// Error while computing the hash to sign a Taproot input.
|
||||
SighashTaproot(sighash::TaprootError),
|
||||
/// Error while computing the hash, out of bounds access on the transaction inputs.
|
||||
TxInputsIndexError(transaction::InputsIndexError),
|
||||
/// Error while computing the hash to sign
|
||||
SighashError(sighash::Error),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
/// To be used only by external libraries implementing [`InputSigner`] or
|
||||
/// [`TransactionSigner`], so that they can return their own custom errors, without having to
|
||||
/// modify [`SignerError`] in BDK.
|
||||
External(String),
|
||||
/// Error while signing using hardware wallets
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
HWIError(hwi::error::Error),
|
||||
}
|
||||
|
||||
impl From<transaction::InputsIndexError> for SignerError {
|
||||
fn from(v: transaction::InputsIndexError) -> Self {
|
||||
Self::TxInputsIndexError(v)
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
impl From<hwi::error::Error> for SignerError {
|
||||
fn from(e: hwi::error::Error) -> Self {
|
||||
SignerError::HWIError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sighash::P2wpkhError> for SignerError {
|
||||
fn from(e: sighash::P2wpkhError) -> Self {
|
||||
Self::SighashP2wpkh(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sighash::TaprootError> for SignerError {
|
||||
fn from(e: sighash::TaprootError) -> Self {
|
||||
Self::SighashTaproot(e)
|
||||
impl From<sighash::Error> for SignerError {
|
||||
fn from(e: sighash::Error) -> Self {
|
||||
SignerError::SighashError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,11 +194,10 @@ impl fmt::Display for SignerError {
|
||||
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
|
||||
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
|
||||
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
|
||||
Self::SighashP2wpkh(err) => write!(f, "Error while computing the hash to sign a P2WPKH input: {}", err),
|
||||
Self::SighashTaproot(err) => write!(f, "Error while computing the hash to sign a Taproot input: {}", err),
|
||||
Self::TxInputsIndexError(err) => write!(f, "Error while computing the hash, out of bounds access on the transaction inputs: {}", err),
|
||||
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
|
||||
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
|
||||
Self::External(err) => write!(f, "{}", err),
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +221,7 @@ pub enum SignerContext {
|
||||
},
|
||||
}
|
||||
|
||||
/// Wrapper to pair a signer with its context
|
||||
/// Wrapper structure to pair a signer with its context
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
|
||||
signer: S,
|
||||
@@ -282,7 +270,7 @@ pub trait InputSigner: SignerCommon {
|
||||
/// Sign a single psbt input
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -297,7 +285,7 @@ pub trait TransactionSigner: SignerCommon {
|
||||
/// Sign all the inputs of the psbt
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError>;
|
||||
@@ -306,7 +294,7 @@ pub trait TransactionSigner: SignerCommon {
|
||||
impl<T: InputSigner> TransactionSigner for T {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
@@ -318,7 +306,7 @@ impl<T: InputSigner> TransactionSigner for T {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
@@ -328,10 +316,10 @@ impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -414,7 +402,7 @@ fn multikey_to_xkeys<K: InnerXKey + Clone>(
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
@@ -424,10 +412,10 @@ impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||
impl InputSigner for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -456,7 +444,7 @@ impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -567,24 +555,21 @@ fn sign_psbt_ecdsa(
|
||||
secret_key: &secp256k1::SecretKey,
|
||||
pubkey: PublicKey,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: impl bitcoin::hashes::Hash<Bytes = [u8; 32]>,
|
||||
sighash_type: EcdsaSighashType,
|
||||
hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash,
|
||||
hash_ty: EcdsaSighashType,
|
||||
secp: &SecpCtx,
|
||||
allow_grinding: bool,
|
||||
) {
|
||||
let msg = &Message::from_digest(hash.to_byte_array());
|
||||
let signature = if allow_grinding {
|
||||
let msg = &Message::from(hash);
|
||||
let sig = if allow_grinding {
|
||||
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||
} else {
|
||||
secp.sign_ecdsa(msg, secret_key)
|
||||
};
|
||||
secp.verify_ecdsa(msg, &signature, &pubkey.inner)
|
||||
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||
.expect("invalid or corrupted ecdsa signature");
|
||||
|
||||
let final_signature = ecdsa::Signature {
|
||||
signature,
|
||||
sighash_type,
|
||||
};
|
||||
let final_signature = ecdsa::Signature { sig, hash_ty };
|
||||
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
||||
}
|
||||
|
||||
@@ -595,10 +580,10 @@ fn sign_psbt_schnorr(
|
||||
leaf_hash: Option<taproot::TapLeafHash>,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: TapSighash,
|
||||
sighash_type: TapSighashType,
|
||||
hash_ty: TapSighashType,
|
||||
secp: &SecpCtx,
|
||||
) {
|
||||
let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
||||
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
||||
let keypair = match leaf_hash {
|
||||
None => keypair
|
||||
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||
@@ -607,14 +592,11 @@ fn sign_psbt_schnorr(
|
||||
};
|
||||
|
||||
let msg = &Message::from(hash);
|
||||
let signature = secp.sign_schnorr(msg, &keypair);
|
||||
secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||
let sig = secp.sign_schnorr(msg, &keypair);
|
||||
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||
.expect("invalid or corrupted schnorr signature");
|
||||
|
||||
let final_signature = taproot::Signature {
|
||||
signature,
|
||||
sighash_type,
|
||||
};
|
||||
let final_signature = taproot::Signature { sig, hash_ty };
|
||||
|
||||
if let Some(lh) = leaf_hash {
|
||||
psbt_input
|
||||
@@ -806,16 +788,6 @@ pub struct SignOptions {
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to remove taproot specific fields from the PSBT on finalization.
|
||||
///
|
||||
/// For inputs this includes the taproot internal key, merkle root, and individual
|
||||
/// scripts and signatures. For both inputs and outputs it includes key origin info.
|
||||
///
|
||||
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
|
||||
///
|
||||
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
|
||||
pub remove_taproot_extras: bool,
|
||||
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||
@@ -840,10 +812,9 @@ pub struct SignOptions {
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TapLeavesOptions {
|
||||
/// The signer will sign all the leaves it has a key for.
|
||||
#[default]
|
||||
All,
|
||||
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
||||
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||
@@ -854,6 +825,13 @@ pub enum TapLeavesOptions {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for TapLeavesOptions {
|
||||
fn default() -> Self {
|
||||
TapLeavesOptions::All
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for SignOptions {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
@@ -861,7 +839,6 @@ impl Default for SignOptions {
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
remove_taproot_extras: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
@@ -876,7 +853,7 @@ pub(crate) trait ComputeSighash {
|
||||
type SighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||
@@ -888,7 +865,7 @@ impl ComputeSighash for Legacy {
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
@@ -937,7 +914,7 @@ impl ComputeSighash for Segwitv0 {
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
@@ -948,7 +925,7 @@ impl ComputeSighash for Segwitv0 {
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||
|
||||
let sighash_type = psbt_input
|
||||
let sighash = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||
.ecdsa_hash_ty()
|
||||
@@ -957,7 +934,7 @@ impl ComputeSighash for Segwitv0 {
|
||||
// Always try first with the non-witness utxo
|
||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
// Check the provided prev-tx
|
||||
if prev_tx.compute_txid() != tx_input.previous_output.txid {
|
||||
if prev_tx.txid() != tx_input.previous_output.txid {
|
||||
return Err(SignerError::InvalidNonWitnessUtxo);
|
||||
}
|
||||
|
||||
@@ -976,39 +953,40 @@ impl ComputeSighash for Segwitv0 {
|
||||
};
|
||||
let value = utxo.value;
|
||||
|
||||
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
|
||||
let sighash = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => {
|
||||
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
|
||||
}
|
||||
let script = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => witness_script.clone(),
|
||||
None => {
|
||||
if utxo.script_pubkey.is_p2wpkh() {
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
&utxo.script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
utxo.script_pubkey
|
||||
.p2wpkh_script_code()
|
||||
.expect("We check above that the spk is a p2wpkh")
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.map(|s| s.is_p2wpkh())
|
||||
.map(|s| s.is_v0_p2wpkh())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.p2wpkh_script_code()
|
||||
.expect("We check above that the spk is a p2wpkh")
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessScript);
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok((sighash, sighash_type))
|
||||
|
||||
Ok((
|
||||
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
|
||||
input_index,
|
||||
&script,
|
||||
value,
|
||||
sighash,
|
||||
)?,
|
||||
sighash,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,7 +996,7 @@ impl ComputeSighash for Tap {
|
||||
type SighashType = TapSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
|
||||
@@ -1189,7 +1167,7 @@ mod signers_container_tests {
|
||||
impl TransactionSigner for DummySigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
_psbt: &mut Psbt,
|
||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||
_sign_options: &SignOptions,
|
||||
_secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
@@ -1207,8 +1185,8 @@ mod signers_container_tests {
|
||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||
let secp: Secp256k1<All> = Secp256k1::new();
|
||||
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
||||
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||
let tpub = bip32::Xpub::from_priv(&secp, &tprv);
|
||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
|
||||
let fingerprint = tprv.fingerprint(&secp);
|
||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||
@@ -16,9 +16,11 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::ChangeSet;
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::ChangeSet;
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk::wallet::tx_builder::CreateTx;
|
||||
//! # use bdk_chain::PersistBackend;
|
||||
//! # use anyhow::Error;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
//! # let mut wallet = doctest_wallet!();
|
||||
@@ -27,9 +29,9 @@
|
||||
//!
|
||||
//! tx_builder
|
||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||
//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
|
||||
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
//! // 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
|
||||
//! .do_not_spend_change()
|
||||
//! // Turn on RBF signaling
|
||||
@@ -38,18 +40,36 @@
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::collections::HashSet;
|
||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use bdk_chain::PersistBackend;
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
|
||||
use super::coin_selection::CoinSelectionAlgorithm;
|
||||
use super::{CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use super::ChangeSet;
|
||||
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
|
||||
use crate::wallet::CreateTxError;
|
||||
use crate::{Utxo, Wallet};
|
||||
|
||||
/// Context in which the [`TxBuilder`] is valid
|
||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||
|
||||
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
|
||||
/// to bumping the fee of an existing one).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CreateTx;
|
||||
impl TxBuilderContext for CreateTx {}
|
||||
|
||||
/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BumpFee;
|
||||
impl TxBuilderContext for BumpFee {}
|
||||
|
||||
/// A transaction builder
|
||||
///
|
||||
@@ -61,12 +81,13 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
/// as in the following example:
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::tx_builder::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::tx_builder::*;
|
||||
/// # use bitcoin::*;
|
||||
/// # use core::str::FromStr;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
@@ -76,8 +97,8 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder
|
||||
/// .ordering(TxOrdering::Untouched)
|
||||
/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
|
||||
/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
|
||||
/// .add_recipient(addr1.script_pubkey(), 50_000)
|
||||
/// .add_recipient(addr2.script_pubkey(), 50_000);
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
@@ -86,7 +107,7 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder.ordering(TxOrdering::Untouched);
|
||||
/// for addr in &[addr1, addr2] {
|
||||
/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
|
||||
/// builder.add_recipient(addr.script_pubkey(), 50_000);
|
||||
/// }
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
@@ -105,10 +126,11 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`coin_selection`]: Self::coin_selection
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, Cs> {
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet>>,
|
||||
pub struct TxBuilder<'a, D, Cs, Ctx> {
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet<D>>>,
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
/// The parameters for transaction creation sans coin selection algorithm.
|
||||
@@ -141,7 +163,7 @@ pub(crate) struct TxParams {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct PreviousFee {
|
||||
pub absolute: u64,
|
||||
pub rate: FeeRate,
|
||||
pub rate: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -152,28 +174,31 @@ pub(crate) enum FeePolicy {
|
||||
|
||||
impl Default for FeePolicy {
|
||||
fn default() -> Self {
|
||||
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
|
||||
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> {
|
||||
impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
fn clone(&self) -> Self {
|
||||
TxBuilder {
|
||||
wallet: self.wallet.clone(),
|
||||
params: self.params.clone(),
|
||||
coin_selection: self.coin_selection.clone(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Methods supported for any CoinSelectionAlgorithm.
|
||||
impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
/// Set a custom fee rate.
|
||||
///
|
||||
/// This method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to `fee_rate` times the size
|
||||
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
|
||||
/// relay policy.
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate
|
||||
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
||||
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
|
||||
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
|
||||
/// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb
|
||||
/// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
|
||||
/// Default is 1 sat/vB (see min_relay_fee)
|
||||
///
|
||||
/// Note that this is really a minimum feerate -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
@@ -184,16 +209,16 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
}
|
||||
|
||||
/// Set an absolute fee
|
||||
/// The fee_absolute method refers to the absolute transaction fee in [`Amount`].
|
||||
/// If anyone sets both the `fee_absolute` method and the `fee_rate` method,
|
||||
/// the `FeePolicy` enum will be set by whichever method was called last,
|
||||
/// as the [`FeeRate`] and `FeeAmount` are mutually exclusive.
|
||||
/// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
|
||||
/// If anyone sets both the fee_absolute method and the fee_rate method,
|
||||
/// the FeePolicy enum will be set by whichever method was called last,
|
||||
/// as the FeeRate and FeeAmount are mutually exclusive.
|
||||
///
|
||||
/// Note that this is really a minimum absolute fee -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
/// excess might not be viable.
|
||||
pub fn fee_absolute(&mut self, fee_amount: Amount) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount.to_sat()));
|
||||
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -243,7 +268,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk::*;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
@@ -254,7 +279,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
///
|
||||
/// let builder = wallet
|
||||
/// .build_tx()
|
||||
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
|
||||
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
/// .policy_path(path, KeychainKind::External);
|
||||
///
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
@@ -293,9 +318,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
|
||||
for utxo in utxos {
|
||||
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||
|
||||
let satisfaction_weight =
|
||||
descriptor.max_weight_to_satisfy().unwrap().to_wu() as usize;
|
||||
#[allow(deprecated)]
|
||||
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
||||
self.params.utxos.push(WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
@@ -336,9 +360,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
||||
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
||||
///
|
||||
/// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
|
||||
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
|
||||
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
||||
/// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
|
||||
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
|
||||
///
|
||||
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||
///
|
||||
@@ -359,35 +383,19 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
///
|
||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
|
||||
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||
pub fn add_foreign_utxo(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
self.add_foreign_utxo_with_sequence(
|
||||
outpoint,
|
||||
psbt_input,
|
||||
satisfaction_weight,
|
||||
Sequence::MAX,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
|
||||
pub fn add_foreign_utxo_with_sequence(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
sequence: Sequence,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
if psbt_input.witness_utxo.is_none() {
|
||||
match psbt_input.non_witness_utxo.as_ref() {
|
||||
Some(tx) => {
|
||||
if tx.compute_txid() != outpoint.txid {
|
||||
if tx.txid() != outpoint.txid {
|
||||
return Err(AddForeignUtxoError::InvalidTxid {
|
||||
input_txid: tx.compute_txid(),
|
||||
input_txid: tx.txid(),
|
||||
foreign_utxo: outpoint,
|
||||
});
|
||||
}
|
||||
@@ -405,7 +413,6 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint,
|
||||
sequence: Some(sequence),
|
||||
psbt_input: Box::new(psbt_input),
|
||||
},
|
||||
});
|
||||
@@ -535,17 +542,35 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
|
||||
/// Choose the coin selection algorithm
|
||||
///
|
||||
/// Overrides the [`CoinSelectionAlgorithm`].
|
||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`].
|
||||
///
|
||||
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(self, coin_selection: P) -> TxBuilder<'a, P> {
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(
|
||||
self,
|
||||
coin_selection: P,
|
||||
) -> TxBuilder<'a, D, P, Ctx> {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
params: self.params,
|
||||
coin_selection,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
@@ -590,96 +615,6 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
self.params.allow_dust = allow_dust;
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
|
||||
self.params.recipients = recipients
|
||||
.into_iter()
|
||||
.map(|(script, amount)| (script, amount.to_sat()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
|
||||
self.params
|
||||
.recipients
|
||||
.push((script_pubkey, amount.to_sat()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, Amount::ZERO);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to *drain* excess coins to.
|
||||
///
|
||||
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
|
||||
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
|
||||
/// coins are too small) it will not be included in the resulting transaction. The only
|
||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||
///
|
||||
/// If you choose not to set any recipients, you should provide the utxos that the
|
||||
/// transaction should spend via [`add_utxos`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||
/// single address.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut tx_builder = wallet.build_tx();
|
||||
///
|
||||
/// tx_builder
|
||||
/// // Spend all outputs in this wallet.
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
/// .enable_rbf();
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
///
|
||||
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
|
||||
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError> {
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -744,11 +679,141 @@ impl fmt::Display for AddForeignUtxoError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AddForeignUtxoError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::allow_shrinking`]
|
||||
pub enum AllowShrinkingError {
|
||||
/// Script/PubKey was not in the original transaction
|
||||
MissingScriptPubKey(ScriptBuf),
|
||||
}
|
||||
|
||||
impl fmt::Display for AllowShrinkingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingScriptPubKey(script_buf) => write!(
|
||||
f,
|
||||
"Script/PubKey was not in the original transaction: {}",
|
||||
script_buf,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AllowShrinkingError {}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
|
||||
self.params.recipients = recipients;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: u64) -> &mut Self {
|
||||
self.params.recipients.push((script_pubkey, amount));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, 0u64);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to *drain* excess coins to.
|
||||
///
|
||||
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
|
||||
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
|
||||
/// coins are too small) it will not be included in the resulting transaction. The only
|
||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||
///
|
||||
/// If you choose not to set any recipients, you should either provide the utxos that the
|
||||
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
|
||||
///
|
||||
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||
/// single address.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk::wallet::tx_builder::CreateTx;
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut tx_builder = wallet.build_tx();
|
||||
///
|
||||
/// tx_builder
|
||||
/// // Spend all outputs in this wallet.
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||
/// .enable_rbf();
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
|
||||
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||
/// will attempt to find a change output to shrink instead.
|
||||
///
|
||||
/// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is
|
||||
/// preserved then it is currently not guaranteed to be in the same position as it was
|
||||
/// originally.
|
||||
///
|
||||
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
|
||||
/// transaction we are bumping.
|
||||
pub fn allow_shrinking(
|
||||
&mut self,
|
||||
script_pubkey: ScriptBuf,
|
||||
) -> Result<&mut Self, AllowShrinkingError> {
|
||||
match self
|
||||
.params
|
||||
.recipients
|
||||
.iter()
|
||||
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
|
||||
{
|
||||
Some(position) => {
|
||||
self.params.recipients.remove(position);
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
Ok(self)
|
||||
}
|
||||
None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordering of the transaction's inputs and outputs
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum TxOrdering {
|
||||
/// Randomized (default)
|
||||
#[default]
|
||||
Shuffle,
|
||||
/// Unchanged
|
||||
Untouched,
|
||||
@@ -756,6 +821,12 @@ pub enum TxOrdering {
|
||||
Bip69Lexicographic,
|
||||
}
|
||||
|
||||
impl Default for TxOrdering {
|
||||
fn default() -> Self {
|
||||
TxOrdering::Shuffle
|
||||
}
|
||||
}
|
||||
|
||||
impl TxOrdering {
|
||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
|
||||
pub fn sort_tx(&self, tx: &mut Transaction) {
|
||||
@@ -809,10 +880,9 @@ impl RbfValue {
|
||||
}
|
||||
|
||||
/// Policy regarding the use of change outputs when creating a transaction
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum ChangeSpendPolicy {
|
||||
/// Use both change and non-change outputs (default)
|
||||
#[default]
|
||||
ChangeAllowed,
|
||||
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
|
||||
OnlyChange,
|
||||
@@ -820,6 +890,12 @@ pub enum ChangeSpendPolicy {
|
||||
ChangeForbidden,
|
||||
}
|
||||
|
||||
impl Default for ChangeSpendPolicy {
|
||||
fn default() -> Self {
|
||||
ChangeSpendPolicy::ChangeAllowed
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeSpendPolicy {
|
||||
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
|
||||
match self {
|
||||
@@ -847,8 +923,7 @@ mod test {
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::consensus::deserialize;
|
||||
use bitcoin::hex::FromHex;
|
||||
use bitcoin::TxOut;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -919,7 +994,7 @@ mod test {
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(tx.output[0].value.to_sat(), 800);
|
||||
assert_eq!(tx.output[0].value, 800);
|
||||
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
|
||||
assert_eq!(
|
||||
tx.output[2].script_pubkey,
|
||||
@@ -936,7 +1011,7 @@ mod test {
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 0,
|
||||
},
|
||||
txout: TxOut::NULL,
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
@@ -947,7 +1022,7 @@ mod test {
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 1,
|
||||
},
|
||||
txout: TxOut::NULL,
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::Internal,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Confirmed {
|
||||
@@ -10,7 +10,7 @@
|
||||
// licenses.
|
||||
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::{absolute, relative, Script, Sequence};
|
||||
use bitcoin::{absolute, Script, Sequence};
|
||||
|
||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||
|
||||
@@ -26,7 +26,7 @@ pub trait IsDust {
|
||||
|
||||
impl IsDust for u64 {
|
||||
fn is_dust(&self, script: &Script) -> bool {
|
||||
*self < script.minimal_non_dust().to_sat()
|
||||
*self < script.dust_value().to_sat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Older {
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||
fn check_older(&self, n: relative::LockTime) -> bool {
|
||||
fn check_older(&self, n: Sequence) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
// TODO: test >= / >
|
||||
current_height
|
||||
@@ -138,7 +138,7 @@ mod test {
|
||||
.require_network(Network::Bitcoin)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
assert!(script_p2wpkh.is_p2wpkh());
|
||||
assert!(script_p2wpkh.is_v0_p2wpkh());
|
||||
assert!(293.is_dust(&script_p2wpkh));
|
||||
assert!(!294.is_dust(&script_p2wpkh));
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{
|
||||
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
|
||||
Txid,
|
||||
};
|
||||
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Return a fake wallet that appears to be funded for testing.
|
||||
///
|
||||
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
|
||||
/// sats are the transaction fee.
|
||||
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
|
||||
let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
|
||||
let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
|
||||
// Return a fake wallet that appears to be funded for testing.
|
||||
//
|
||||
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||
// sats are the transaction fee.
|
||||
pub fn get_funded_wallet_with_change(
|
||||
descriptor: &str,
|
||||
change: Option<&str>,
|
||||
) -> (Wallet, bitcoin::Txid) {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
|
||||
let change_address = wallet.get_address(AddressIndex::New).address;
|
||||
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
|
||||
.expect("address")
|
||||
.require_network(Network::Regtest)
|
||||
.unwrap();
|
||||
|
||||
let tx0 = Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
@@ -36,17 +36,17 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
||||
witness: Default::default(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(76_000),
|
||||
script_pubkey: receive_address.script_pubkey(),
|
||||
value: 76_000,
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
}],
|
||||
};
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx0.compute_txid(),
|
||||
txid: tx0.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Default::default(),
|
||||
@@ -55,11 +55,11 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: Amount::from_sat(50_000),
|
||||
script_pubkey: receive_address.script_pubkey(),
|
||||
value: 50_000,
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
},
|
||||
TxOut {
|
||||
value: Amount::from_sat(25_000),
|
||||
value: 25_000,
|
||||
script_pubkey: sendto_address.script_pubkey(),
|
||||
},
|
||||
],
|
||||
@@ -96,42 +96,22 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(wallet, tx1.compute_txid())
|
||||
(wallet, tx1.txid())
|
||||
}
|
||||
|
||||
/// Return a fake wallet that appears to be funded for testing.
|
||||
///
|
||||
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
|
||||
/// sats are the transaction fee.
|
||||
///
|
||||
/// Note: the change descriptor will have script type `p2wpkh`. If passing some other script type
|
||||
/// as argument, make sure you're ok with getting a wallet where the keychains have potentially
|
||||
/// different script types. Otherwise, use `get_funded_wallet_with_change`.
|
||||
// Return a fake wallet that appears to be funded for testing.
|
||||
//
|
||||
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||
// sats are the transaction fee.
|
||||
pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
|
||||
let change = get_test_wpkh_change();
|
||||
get_funded_wallet_with_change(descriptor, change)
|
||||
}
|
||||
|
||||
pub fn get_funded_wallet_wpkh() -> (Wallet, bitcoin::Txid) {
|
||||
get_funded_wallet_with_change(get_test_wpkh(), get_test_wpkh_change())
|
||||
get_funded_wallet_with_change(descriptor, None)
|
||||
}
|
||||
|
||||
pub fn get_test_wpkh() -> &'static str {
|
||||
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
|
||||
}
|
||||
|
||||
pub fn get_test_wpkh_with_change_desc() -> (&'static str, &'static str) {
|
||||
(
|
||||
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
|
||||
get_test_wpkh_change(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_test_wpkh_change() -> &'static str {
|
||||
"wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/0)"
|
||||
}
|
||||
|
||||
pub fn get_test_single_sig_csv() -> &'static str {
|
||||
// and(pk(Alice),older(6))
|
||||
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
|
||||
@@ -167,11 +147,6 @@ pub fn get_test_tr_single_sig_xprv() -> &'static str {
|
||||
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_single_sig_xprv_with_change_desc() -> (&'static str, &'static str) {
|
||||
("tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/0/*)",
|
||||
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/1/*)")
|
||||
}
|
||||
|
||||
pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
@@ -179,16 +154,3 @@ pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
||||
pub fn get_test_tr_dup_keys() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
|
||||
/// useful in cases where we want to create a feerate from a `f64`, as the
|
||||
/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
|
||||
///
|
||||
/// **Note** this 'quick and dirty' conversion should only be used when the input
|
||||
/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
|
||||
/// or else the resulting value will be inaccurate.
|
||||
pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
||||
// 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
|
||||
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||
FeeRate::from_sat_per_kwu(sat_kwu)
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||
use bdk_wallet::{psbt, KeychainKind, SignOptions};
|
||||
use bdk::bitcoin::TxIn;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{psbt, FeeRate, SignOptions};
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use core::str::FromStr;
|
||||
mod common;
|
||||
use common::*;
|
||||
@@ -12,9 +15,9 @@ const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6
|
||||
fn test_psbt_malformed_psbt_input_legacy() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
let options = SignOptions {
|
||||
@@ -29,9 +32,9 @@ fn test_psbt_malformed_psbt_input_legacy() {
|
||||
fn test_psbt_malformed_psbt_input_segwit() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[1].clone());
|
||||
let options = SignOptions {
|
||||
@@ -45,9 +48,9 @@ fn test_psbt_malformed_psbt_input_segwit() {
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_tx_input() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.unsigned_tx.input.push(TxIn::default());
|
||||
let options = SignOptions {
|
||||
@@ -61,9 +64,9 @@ fn test_psbt_malformed_tx_input() {
|
||||
fn test_psbt_sign_with_finalized() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
|
||||
// add a finalized input
|
||||
@@ -79,13 +82,13 @@ fn test_psbt_sign_with_finalized() {
|
||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
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 addr = wallet.peek_address(KeychainKind::External, 0);
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
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 fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
@@ -96,21 +99,21 @@ fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
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 addr = wallet.peek_address(KeychainKind::External, 0);
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
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 fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
@@ -120,21 +123,21 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_missing_txout() {
|
||||
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 addr = wpkh_wallet.peek_address(KeychainKind::External, 0);
|
||||
let addr = wpkh_wallet.get_address(New);
|
||||
let mut builder = wpkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut wpkh_psbt = builder.finish().unwrap();
|
||||
|
||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||
@@ -142,13 +145,11 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
assert!(wpkh_psbt.fee_amount().is_none());
|
||||
assert!(wpkh_psbt.fee_rate().is_none());
|
||||
|
||||
let desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)";
|
||||
let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)";
|
||||
let (mut pkh_wallet, _) = get_funded_wallet_with_change(desc, change_desc);
|
||||
let addr = pkh_wallet.peek_address(KeychainKind::External, 0);
|
||||
let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = pkh_wallet.get_address(New);
|
||||
let mut builder = pkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(expected_fee_rate);
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut pkh_psbt = builder.finish().unwrap();
|
||||
|
||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
@@ -158,29 +159,18 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
|
||||
#[test]
|
||||
fn test_psbt_multiple_internalkey_signers() {
|
||||
use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk_wallet::KeychainKind;
|
||||
use bitcoin::key::TapTweak;
|
||||
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
||||
use bitcoin::{PrivateKey, TxOut};
|
||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk::KeychainKind;
|
||||
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
|
||||
use miniscript::psbt::PsbtExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
|
||||
let desc = format!("tr({})", wif);
|
||||
let prv = PrivateKey::from_wif(wif).unwrap();
|
||||
let keypair = Keypair::from_secret_key(&secp, &prv.inner);
|
||||
|
||||
let change_desc = "tr(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
|
||||
let (mut wallet, _) = get_funded_wallet_with_change(&desc, change_desc);
|
||||
let to_spend = wallet.balance().total();
|
||||
let send_to = wallet.peek_address(KeychainKind::External, 0);
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
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 unsigned_tx = psbt.unsigned_tx.clone();
|
||||
|
||||
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
||||
wallet.add_signer(
|
||||
KeychainKind::External,
|
||||
@@ -193,32 +183,10 @@ fn test_psbt_multiple_internalkey_signers() {
|
||||
},
|
||||
)),
|
||||
);
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
// To verify, we need the signature, message, and pubkey
|
||||
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
|
||||
assert!(!witness.is_empty());
|
||||
let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
|
||||
|
||||
// the prevout we're spending
|
||||
let prevouts = &[TxOut {
|
||||
script_pubkey: send_to.script_pubkey(),
|
||||
value: to_spend,
|
||||
}];
|
||||
let prevouts = Prevouts::All(prevouts);
|
||||
let input_index = 0;
|
||||
let mut sighash_cache = SighashCache::new(unsigned_tx);
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
|
||||
.unwrap();
|
||||
let message = Message::from(sighash);
|
||||
|
||||
// add tweak. this was taken from `signer::sign_psbt_schnorr`
|
||||
let keypair = keypair.tap_tweak(&secp, None).to_inner();
|
||||
let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
|
||||
|
||||
// Must verify if we used the correct key to sign
|
||||
let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
|
||||
assert!(verify_res.is_ok(), "The wrong internal key was used");
|
||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
// Checks that we signed using the right key
|
||||
assert!(
|
||||
psbt.finalize_mut(&secp).is_ok(),
|
||||
"The wrong internal key was used"
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.12.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.57"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_bitcoind_rpc"
|
||||
@@ -13,12 +13,14 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.32.0", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.19.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.17" }
|
||||
bdk_chain = { path = "../chain", version = "0.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
|
||||
@@ -14,7 +14,7 @@ use bitcoin::{block::Header, Block, BlockHash, Transaction};
|
||||
pub use bitcoincore_rpc;
|
||||
use bitcoincore_rpc::bitcoincore_rpc_json;
|
||||
|
||||
/// The [`Emitter`] is used to emit data sourced from [`bitcoincore_rpc::Client`].
|
||||
/// A structure that emits data sourced from [`bitcoincore_rpc::Client`].
|
||||
///
|
||||
/// Refer to [module-level documentation] for more.
|
||||
///
|
||||
@@ -43,13 +43,11 @@ pub struct Emitter<'c, C> {
|
||||
}
|
||||
|
||||
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
/// Construct a new [`Emitter`].
|
||||
/// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` and `start_height`.
|
||||
///
|
||||
/// `last_cp` informs the emitter of the chain we are starting off with. This way, the emitter
|
||||
/// can start emission from a block that connects to the original chain.
|
||||
///
|
||||
/// `start_height` starts emission from a given height (if there are no conflicts with the
|
||||
/// original chain).
|
||||
/// * `last_cp` is the check point used to find the latest block which is still part of the best
|
||||
/// chain.
|
||||
/// * `start_height` is the block height to start emitting blocks from.
|
||||
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
|
||||
Self {
|
||||
client,
|
||||
@@ -129,58 +127,13 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
}
|
||||
|
||||
/// Emit the next block height and header (if any).
|
||||
pub fn next_header(&mut self) -> Result<Option<BlockEvent<Header>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block_header(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
pub fn next_header(&mut self) -> Result<Option<(u32, Header)>, bitcoincore_rpc::Error> {
|
||||
poll(self, |hash| self.client.get_block_header(hash))
|
||||
}
|
||||
|
||||
/// Emit the next block height and block (if any).
|
||||
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A newly emitted block from [`Emitter`].
|
||||
#[derive(Debug)]
|
||||
pub struct BlockEvent<B> {
|
||||
/// Either a full [`Block`] or [`Header`] of the new block.
|
||||
pub block: B,
|
||||
|
||||
/// The checkpoint of the new block.
|
||||
///
|
||||
/// A [`CheckPoint`] is a node of a linked list of [`BlockId`]s. This checkpoint is linked to
|
||||
/// all [`BlockId`]s originally passed in [`Emitter::new`] as well as emitted blocks since then.
|
||||
/// These blocks are guaranteed to be of the same chain.
|
||||
///
|
||||
/// This is important as BDK structures require block-to-apply to be connected with another
|
||||
/// block in the original chain.
|
||||
pub checkpoint: CheckPoint,
|
||||
}
|
||||
|
||||
impl<B> BlockEvent<B> {
|
||||
/// The block height of this new block.
|
||||
pub fn block_height(&self) -> u32 {
|
||||
self.checkpoint.height()
|
||||
}
|
||||
|
||||
/// The block hash of this new block.
|
||||
pub fn block_hash(&self) -> BlockHash {
|
||||
self.checkpoint.hash()
|
||||
}
|
||||
|
||||
/// The [`BlockId`] of a previous block that this block connects to.
|
||||
///
|
||||
/// This either returns a [`BlockId`] of a previously emitted block or from the chain we started
|
||||
/// with (passed in as `last_cp` in [`Emitter::new`]).
|
||||
///
|
||||
/// This value is derived from [`BlockEvent::checkpoint`].
|
||||
pub fn connected_to(&self) -> BlockId {
|
||||
match self.checkpoint.prev() {
|
||||
Some(prev_cp) => prev_cp.block_id(),
|
||||
// there is no previous checkpoint, so just connect with itself
|
||||
None => self.checkpoint.block_id(),
|
||||
}
|
||||
pub fn next_block(&mut self) -> Result<Option<(u32, Block)>, bitcoincore_rpc::Error> {
|
||||
poll(self, |hash| self.client.get_block(hash))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +203,7 @@ where
|
||||
fn poll<C, V, F>(
|
||||
emitter: &mut Emitter<C>,
|
||||
get_item: F,
|
||||
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
|
||||
) -> Result<Option<(u32, V)>, bitcoincore_rpc::Error>
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
F: Fn(&BlockHash) -> Result<V, bitcoincore_rpc::Error>,
|
||||
@@ -262,14 +215,13 @@ where
|
||||
let hash = res.hash;
|
||||
let item = get_item(&hash)?;
|
||||
|
||||
let new_cp = emitter
|
||||
emitter.last_cp = emitter
|
||||
.last_cp
|
||||
.clone()
|
||||
.push(BlockId { height, hash })
|
||||
.expect("must push");
|
||||
emitter.last_cp = new_cp.clone();
|
||||
emitter.last_block = Some(res);
|
||||
return Ok(Some((new_cp, item)));
|
||||
return Ok(Some((height, item)));
|
||||
}
|
||||
PollResponse::NoMoreBlocks => {
|
||||
emitter.last_block = None;
|
||||
|
||||
@@ -2,14 +2,182 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, Txid},
|
||||
bitcoin::{Address, Amount, BlockHash, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{CheckPoint, LocalChain},
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_testenv::{anyhow, TestEnv};
|
||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
use bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
|
||||
TxIn, TxOut, WScriptHash,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
|
||||
struct TestEnv {
|
||||
#[allow(dead_code)]
|
||||
daemon: bitcoind::BitcoinD,
|
||||
client: bitcoincore_rpc::Client,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let daemon = match std::env::var_os("TEST_BITCOIND") {
|
||||
Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
|
||||
None => bitcoind::BitcoinD::from_downloaded(),
|
||||
}?;
|
||||
let client = bitcoincore_rpc::Client::new(
|
||||
&daemon.rpc_url(),
|
||||
bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
|
||||
)?;
|
||||
Ok(Self { daemon, client })
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self.client.get_new_address(None, None)?.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
|
||||
fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||
let bt = self.client.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let txdata = vec![Transaction {
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::from_height(0)?,
|
||||
input: vec![TxIn {
|
||||
previous_output: bitcoin::OutPoint::default(),
|
||||
script_sig: ScriptBuf::builder()
|
||||
.push_int(bt.height as _)
|
||||
// randomn number so that re-mining creates unique block
|
||||
.push_int(random())
|
||||
.into_script(),
|
||||
sequence: bitcoin::Sequence::default(),
|
||||
witness: bitcoin::Witness::new(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 0,
|
||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
||||
}],
|
||||
}];
|
||||
|
||||
let bits: [u8; 4] = bt
|
||||
.bits
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("rpc provided us with invalid bits");
|
||||
|
||||
let mut block = Block {
|
||||
header: Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash: bt.previous_block_hash,
|
||||
merkle_root: TxMerkleNode::all_zeros(),
|
||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
||||
nonce: 0,
|
||||
},
|
||||
txdata,
|
||||
};
|
||||
|
||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
||||
|
||||
for nonce in 0..=u32::MAX {
|
||||
block.header.nonce = nonce;
|
||||
if block.header.target().is_met_by(block.block_hash()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.client.submit_block(&block)?;
|
||||
Ok((bt.height as usize, block.block_hash()))
|
||||
}
|
||||
|
||||
fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||
let mut hash = self.client.get_best_block_hash()?;
|
||||
for _ in 0..count {
|
||||
let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
|
||||
self.client.invalidate_block(&hash)?;
|
||||
match prev_hash {
|
||||
Some(prev_hash) => hash = prev_hash,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = self.mine_blocks(count, None);
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = (0..count)
|
||||
.map(|_| self.mine_empty_block())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||
let txid = self
|
||||
.client
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Update {
|
||||
let this_id = BlockId {
|
||||
height,
|
||||
hash: block.block_hash(),
|
||||
};
|
||||
let tip = if block.header.prev_blockhash == BlockHash::all_zeros() {
|
||||
CheckPoint::new(this_id)
|
||||
} else {
|
||||
CheckPoint::new(BlockId {
|
||||
height: height - 1,
|
||||
hash: block.header.prev_blockhash,
|
||||
})
|
||||
.extend(core::iter::once(this_id))
|
||||
.expect("must construct checkpoint")
|
||||
};
|
||||
|
||||
local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that blocks are emitted in order even after reorg.
|
||||
///
|
||||
@@ -20,53 +188,44 @@ use bitcoincore_rpc::RpcApi;
|
||||
#[test]
|
||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let network_tip = env.rpc_client().get_block_count()?;
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
||||
|
||||
// Mine some blocks and return the actual block hashes.
|
||||
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
|
||||
// returning block hashes.
|
||||
// mine some blocks and returned the actual block hashes
|
||||
let exp_hashes = {
|
||||
let mut hashes = (0..=network_tip)
|
||||
.map(|height| env.rpc_client().get_block_hash(height))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
hashes.extend(env.mine_blocks(101 - network_tip as usize, None)?);
|
||||
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
|
||||
hashes.extend(env.mine_blocks(101, None)?);
|
||||
hashes
|
||||
};
|
||||
|
||||
// See if the emitter outputs the right blocks.
|
||||
// see if the emitter outputs the right blocks
|
||||
println!("first sync:");
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
assert_eq!(
|
||||
emission.block_hash(),
|
||||
block.block_hash(),
|
||||
exp_hashes[height as usize],
|
||||
"emitted block hash is unexpected"
|
||||
);
|
||||
|
||||
let chain_update = block_to_chain_update(&block, height);
|
||||
assert_eq!(
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
BTreeMap::from([(height, Some(hash))]),
|
||||
local_chain.apply_update(chain_update)?,
|
||||
BTreeMap::from([(height, Some(block.block_hash()))]),
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
exp_hashes
|
||||
local_chain.blocks(),
|
||||
&exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
.collect(),
|
||||
"final local_chain state is unexpected",
|
||||
);
|
||||
|
||||
// Perform reorg.
|
||||
// perform reorg
|
||||
let reorged_blocks = env.reorg(6)?;
|
||||
let exp_hashes = exp_hashes
|
||||
.iter()
|
||||
@@ -75,30 +234,30 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// See if the emitter outputs the right blocks.
|
||||
// see if the emitter outputs the right blocks
|
||||
println!("after reorg:");
|
||||
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
assert_eq!(
|
||||
height, exp_height as u32,
|
||||
"emitted block has unexpected height"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hash, exp_hashes[height as usize],
|
||||
block.block_hash(),
|
||||
exp_hashes[height as usize],
|
||||
"emitted block is unexpected"
|
||||
);
|
||||
|
||||
let chain_update = block_to_chain_update(&block, height);
|
||||
assert_eq!(
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
local_chain.apply_update(chain_update)?,
|
||||
if exp_height == exp_hashes.len() - reorged_blocks.len() {
|
||||
core::iter::once((height, Some(hash)))
|
||||
core::iter::once((height, Some(block.block_hash())))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
.collect::<bdk_chain::local_chain::ChangeSet>()
|
||||
} else {
|
||||
BTreeMap::from([(height, Some(hash))])
|
||||
BTreeMap::from([(height, Some(block.block_hash()))])
|
||||
},
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
@@ -107,15 +266,12 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
exp_hashes
|
||||
local_chain.blocks(),
|
||||
&exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
.collect(),
|
||||
"final local_chain state is unexpected after reorg",
|
||||
);
|
||||
|
||||
@@ -131,25 +287,16 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
println!("getting new addresses!");
|
||||
let addr_0 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_1 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_2 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
println!("got new addresses!");
|
||||
|
||||
println!("mining block!");
|
||||
env.mine_blocks(101, None)?;
|
||||
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 index = SpkTxOutIndex::<usize>::default();
|
||||
index.insert_spk(0, addr_0.script_pubkey());
|
||||
@@ -158,12 +305,11 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
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()? {
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(emission.checkpoint)?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
let _ = chain.apply_update(block_to_chain_update(&block, height))?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
|
||||
assert!(indexed_additions.is_empty());
|
||||
}
|
||||
|
||||
@@ -171,7 +317,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let exp_txids = {
|
||||
let mut txids = BTreeSet::new();
|
||||
for _ in 0..3 {
|
||||
txids.insert(env.rpc_client().send_to_address(
|
||||
txids.insert(env.client.send_to_address(
|
||||
&addr_0,
|
||||
Amount::from_sat(10_000),
|
||||
None,
|
||||
@@ -197,7 +343,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
.graph
|
||||
.txs
|
||||
.iter()
|
||||
.map(|tx| tx.compute_txid())
|
||||
.map(|tx| tx.txid())
|
||||
.collect::<BTreeSet<Txid>>(),
|
||||
exp_txids,
|
||||
"changeset should have the 3 mempool transactions",
|
||||
@@ -207,7 +353,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
|
||||
// mine a block that confirms the 3 txs
|
||||
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
||||
let exp_block_height = env.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
|
||||
.iter()
|
||||
.map({
|
||||
@@ -221,10 +367,10 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
|
||||
// must receive mined block which will confirm the transactions.
|
||||
{
|
||||
let emission = emitter.next_block()?.expect("must get mined block");
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(emission.checkpoint)?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
let (height, block) = emitter.next_block()?.expect("must get mined block");
|
||||
let _ = chain
|
||||
.apply_update(CheckPoint::from_header(&block.header, height).into_update(false))?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(block, height);
|
||||
assert!(indexed_additions.graph.txs.is_empty());
|
||||
assert!(indexed_additions.graph.txouts.is_empty());
|
||||
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
|
||||
@@ -248,10 +394,10 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
EMITTER_START_HEIGHT as _,
|
||||
);
|
||||
@@ -261,12 +407,9 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
|
||||
for reorg_count in 1..=10 {
|
||||
let replaced_blocks = env.reorg_empty_blocks(reorg_count)?;
|
||||
let next_emission = emitter.next_header()?.expect("must emit block after reorg");
|
||||
let (height, next_header) = emitter.next_header()?.expect("must emit block after reorg");
|
||||
assert_eq!(
|
||||
(
|
||||
next_emission.block_height() as usize,
|
||||
next_emission.block_hash()
|
||||
),
|
||||
(height as usize, next_header.block_hash()),
|
||||
replaced_blocks[0],
|
||||
"block emitted after reorg should be at the reorg height"
|
||||
);
|
||||
@@ -282,7 +425,8 @@ fn process_block(
|
||||
block: Block,
|
||||
block_height: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
recv_chain.apply_update(CheckPoint::from_header(&block.header, block_height))?;
|
||||
recv_chain
|
||||
.apply_update(CheckPoint::from_header(&block.header, block_height).into_update(false))?;
|
||||
let _ = recv_graph.apply_block(block, block_height);
|
||||
Ok(())
|
||||
}
|
||||
@@ -295,9 +439,8 @@ fn sync_from_emitter<C>(
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
{
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
process_block(recv_chain, recv_graph, emission.block, height)?;
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
process_block(recv_chain, recv_graph, block, height)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,24 +467,21 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// setup addresses
|
||||
let addr_to_mine = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||
|
||||
// setup receiver
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.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_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
@@ -357,7 +497,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
// lock outputs that send to `addr_to_track`
|
||||
let outpoints_to_lock = env
|
||||
.rpc_client()
|
||||
.client
|
||||
.get_transaction(&txid, None)?
|
||||
.transaction()?
|
||||
.output
|
||||
@@ -366,7 +506,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
||||
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
||||
.collect::<Vec<_>>();
|
||||
env.rpc_client().lock_unspent(&outpoints_to_lock)?;
|
||||
env.client.lock_unspent(&outpoints_to_lock)?;
|
||||
|
||||
let _ = env.mine_blocks(1, None)?;
|
||||
}
|
||||
@@ -377,7 +517,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * ADDITIONAL_COUNT as u64,
|
||||
confirmed: SEND_AMOUNT.to_sat() * ADDITIONAL_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
@@ -391,8 +531,8 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT * reorg_count as u64,
|
||||
confirmed: SEND_AMOUNT.to_sat() * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * reorg_count as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
@@ -415,19 +555,16 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks and sync up emitter
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
@@ -440,7 +577,7 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
||||
let emitted_txids = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<Txid>>();
|
||||
assert_eq!(
|
||||
emitted_txids, exp_txids,
|
||||
@@ -480,19 +617,16 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance, sync emitter up to tip
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
@@ -509,7 +643,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
|
||||
"first mempool emission should include all txs",
|
||||
@@ -518,7 +652,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
|
||||
"second mempool emission should still include all txs",
|
||||
@@ -526,8 +660,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
|
||||
// At this point, the emitter has seen all mempool transactions. It should only re-emit those
|
||||
// that have introduction heights less than the emitter's last-emitted block tip.
|
||||
while let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
while let Some((height, _)) = emitter.next_header()? {
|
||||
// We call `mempool()` twice.
|
||||
// The second call (at height `h`) should skip the tx introduced at height `h`.
|
||||
for try_index in 0..2 {
|
||||
@@ -538,7 +671,7 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
let emitted_txids = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
emitted_txids, exp_txids,
|
||||
@@ -568,19 +701,16 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance
|
||||
let addr = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
|
||||
// introduce mempool tx at each block extension
|
||||
@@ -596,9 +726,9 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
env.rpc_client()
|
||||
env.client
|
||||
.get_raw_mempool()?
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
@@ -617,15 +747,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
// emission.
|
||||
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
||||
let tx_introductions = dbg!(env
|
||||
.rpc_client()
|
||||
.client
|
||||
.get_raw_mempool_verbose()?
|
||||
.into_iter()
|
||||
.map(|(txid, entry)| (txid, entry.height as usize))
|
||||
.collect::<BTreeMap<_, _>>());
|
||||
|
||||
// `next_header` emits the replacement block of the reorg
|
||||
if let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
if let Some((height, _)) = emitter.next_header()? {
|
||||
println!("\t- replacement height: {}", height);
|
||||
|
||||
// the mempool emission (that follows the first block emission after reorg) should only
|
||||
@@ -633,7 +762,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
let mempool = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_mempool = tx_introductions
|
||||
.iter()
|
||||
@@ -648,7 +777,7 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
let mempool = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.compute_txid())
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_mempool = tx_introductions
|
||||
.iter()
|
||||
@@ -694,10 +823,10 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
|
||||
// start height is 99
|
||||
let mut emitter = Emitter::new(
|
||||
env.rpc_client(),
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
(PREMINE_COUNT - 2) as u32,
|
||||
);
|
||||
@@ -706,27 +835,27 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
env.mine_blocks(PREMINE_COUNT, None)?;
|
||||
|
||||
// emit block 99a
|
||||
let block_header_99a = emitter.next_header()?.expect("block 99a header").block;
|
||||
let (_, block_header_99a) = emitter.next_header()?.expect("block 99a header");
|
||||
let block_hash_99a = block_header_99a.block_hash();
|
||||
let block_hash_98a = block_header_99a.prev_blockhash;
|
||||
|
||||
// emit block 100a
|
||||
let block_header_100a = emitter.next_header()?.expect("block 100a header").block;
|
||||
let (_, block_header_100a) = emitter.next_header()?.expect("block 100a header");
|
||||
let block_hash_100a = block_header_100a.block_hash();
|
||||
|
||||
// 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
|
||||
env.rpc_client().invalidate_block(&block_hash_99a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_100a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_101a)?;
|
||||
env.client.invalidate_block(&block_hash_99a)?;
|
||||
env.client.invalidate_block(&block_hash_100a)?;
|
||||
env.client.invalidate_block(&block_hash_101a)?;
|
||||
|
||||
// mine new blocks 99b, 100b, 101b
|
||||
env.mine_blocks(3, None)?;
|
||||
|
||||
// emit block header 99b
|
||||
let block_header_99b = emitter.next_header()?.expect("block 99b header").block;
|
||||
let (_, block_header_99b) = emitter.next_header()?.expect("block 99b header");
|
||||
let block_hash_99b = block_header_99b.block_hash();
|
||||
let block_hash_98b = block_header_99b.prev_blockhash;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.16.0"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
rust-version = "1.57"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_chain"
|
||||
@@ -13,18 +13,19 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.32.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] }
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||
# note versions > 0.9.1 breaks ours 1.57.0 MSRV.
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "12.0.0", optional = true, default-features = false }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
proptest = "1.2.0"
|
||||
|
||||
[features]
|
||||
default = ["std", "miniscript"]
|
||||
std = ["bitcoin/std", "miniscript?/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "miniscript/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde"]
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
|
||||
pub enum ChainPosition<A> {
|
||||
/// The chain data is seen as confirmed, and in anchored by `A`.
|
||||
Confirmed(A),
|
||||
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
|
||||
/// The chain data is seen in mempool at this given timestamp.
|
||||
Unconfirmed(u64),
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ impl<A: Anchor> ChainPosition<A> {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub enum ConfirmationTime {
|
||||
/// The transaction is confirmed
|
||||
/// The confirmed variant.
|
||||
Confirmed {
|
||||
/// Confirmation height.
|
||||
height: u32,
|
||||
/// Confirmation time in unix seconds.
|
||||
time: u64,
|
||||
},
|
||||
/// The transaction is unconfirmed
|
||||
/// The unconfirmed variant.
|
||||
Unconfirmed {
|
||||
/// The last-seen timestamp in unix seconds.
|
||||
last_seen: u64,
|
||||
@@ -81,7 +81,7 @@ impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
|
||||
height: a.confirmation_height,
|
||||
time: a.confirmation_time,
|
||||
},
|
||||
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
||||
ChainPosition::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,8 +147,6 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
@@ -157,12 +155,13 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationHeightAnchor {
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
|
||||
/// The exact confirmation height of the transaction.
|
||||
///
|
||||
/// It is assumed that this value is never larger than the height of the anchor block.
|
||||
pub confirmation_height: u32,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationHeightAnchor {
|
||||
@@ -187,8 +186,6 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
||||
/// transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
@@ -197,12 +194,12 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationTimeHeightAnchor {
|
||||
/// The confirmation height of the transaction being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the transaction being anchored.
|
||||
pub confirmation_time: u64,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
/// The confirmation height of the chain data being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the chain data being anchored.
|
||||
pub confirmation_time: u64,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
||||
@@ -228,12 +225,12 @@ impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
||||
/// A `TxOut` with as much data as we can retrieve about it
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FullTxOut<A> {
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The location of the `TxOut`.
|
||||
pub outpoint: OutPoint,
|
||||
/// The `TxOut`.
|
||||
pub txout: TxOut,
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The txid and chain position of the transaction (if any) that has spent this output.
|
||||
pub spent_by: Option<(ChainPosition<A>, Txid)>,
|
||||
/// Whether this output is on a coinbase transaction.
|
||||
@@ -298,35 +295,3 @@ impl<A: Anchor> FullTxOut<A> {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chain_position_ord() {
|
||||
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
|
||||
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
|
||||
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 9,
|
||||
anchor_block: BlockId {
|
||||
height: 20,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 12,
|
||||
anchor_block: BlockId {
|
||||
height: 15,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
|
||||
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
|
||||
assert!(
|
||||
conf2 > conf1,
|
||||
"confirmation_height is higher then it should be higher ord"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::BlockId;
|
||||
/// Represents a service that tracks the blockchain.
|
||||
///
|
||||
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
|
||||
/// is an ancestor of the `chain_tip`.
|
||||
/// is an ancestor of another "static block".
|
||||
///
|
||||
/// [`is_block_in_chain`]: Self::is_block_in_chain
|
||||
pub trait ChainOracle {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/// A changeset containing [`crate`] structures typically persisted together.
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(crate::serde::Deserialize, crate::serde::Serialize),
|
||||
serde(
|
||||
crate = "crate::serde",
|
||||
bound(
|
||||
deserialize = "A: Ord + crate::serde::Deserialize<'de>, K: Ord + crate::serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + crate::serde::Serialize, K: Ord + crate::serde::Serialize",
|
||||
),
|
||||
)
|
||||
)]
|
||||
pub struct CombinedChangeSet<K, A> {
|
||||
/// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub chain: crate::local_chain::ChangeSet,
|
||||
/// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph).
|
||||
pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
|
||||
/// Stores the network type of the transaction data.
|
||||
pub network: Option<bitcoin::Network>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chain: core::default::Default::default(),
|
||||
indexed_tx_graph: core::default::Default::default(),
|
||||
network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K: Ord, A: crate::Anchor> crate::Append for CombinedChangeSet<K, A> {
|
||||
fn append(&mut self, other: Self) {
|
||||
crate::Append::append(&mut self.chain, other.chain);
|
||||
crate::Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network type must either be just introduced or remain the same"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
|
||||
fn from(chain: crate::local_chain::ChangeSet) -> Self {
|
||||
Self {
|
||||
chain,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>>
|
||||
for CombinedChangeSet<K, A>
|
||||
{
|
||||
fn from(
|
||||
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::keychain::ChangeSet<K>> for CombinedChangeSet<K, A> {
|
||||
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
|
||||
indexer,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,10 @@
|
||||
use crate::{
|
||||
alloc::{string::ToString, vec::Vec},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
use bitcoin::hashes::{hash_newtype, sha256, Hash};
|
||||
|
||||
hash_newtype! {
|
||||
/// Represents the ID of a descriptor, defined as the sha256 hash of
|
||||
/// the descriptor string, checksum excluded.
|
||||
///
|
||||
/// This is useful for having a fixed-length unique representation of a descriptor,
|
||||
/// in particular, we use it to persist application state changes related to the
|
||||
/// descriptor without having to re-write the whole descriptor each time.
|
||||
///
|
||||
pub struct DescriptorId(pub sha256::Hash);
|
||||
}
|
||||
use crate::miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
/// A trait to extend the functionality of a miniscript descriptor.
|
||||
pub trait DescriptorExt {
|
||||
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
|
||||
/// Panics if the descriptor wildcard is hardened.
|
||||
fn dust_value(&self) -> u64;
|
||||
|
||||
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
|
||||
/// included.
|
||||
fn descriptor_id(&self) -> DescriptorId;
|
||||
}
|
||||
|
||||
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
@@ -31,14 +12,7 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
self.at_derivation_index(0)
|
||||
.expect("descriptor can't have hardened derivation")
|
||||
.script_pubkey()
|
||||
.minimal_non_dust()
|
||||
.dust_value()
|
||||
.to_sat()
|
||||
}
|
||||
|
||||
fn descriptor_id(&self) -> DescriptorId {
|
||||
let desc = self.to_string();
|
||||
let desc_without_checksum = desc.split('#').next().expect("Must be here");
|
||||
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
|
||||
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
|
||||
//! [`IndexedTxGraph`] documentation for more.
|
||||
//! Contains the [`IndexedTxGraph`] structure and associated types.
|
||||
//!
|
||||
//! This is essentially a [`TxGraph`] combined with an indexer.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
||||
|
||||
use crate::{
|
||||
keychain,
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
||||
};
|
||||
|
||||
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
|
||||
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
|
||||
///
|
||||
/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
||||
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
||||
#[derive(Debug)]
|
||||
pub struct IndexedTxGraph<A, I> {
|
||||
/// Transaction index.
|
||||
@@ -143,7 +146,7 @@ where
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx, anchors) in txs {
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.compute_txid();
|
||||
let txid = tx.txid();
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
for anchor in anchors {
|
||||
graph.append(self.graph.insert_anchor(txid, anchor));
|
||||
@@ -223,26 +226,20 @@ where
|
||||
/// Irrelevant transactions in `txs` will be ignored.
|
||||
pub fn apply_block_relevant(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
block: Block,
|
||||
height: u32,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
let block_id = BlockId {
|
||||
hash: block.block_hash(),
|
||||
height,
|
||||
};
|
||||
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
changeset.indexer.append(self.index.index_tx(tx));
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.compute_txid();
|
||||
let anchor = A::from_block_position(block, block_id, tx_pos);
|
||||
changeset.graph.append(self.graph.insert_tx(tx.clone()));
|
||||
changeset
|
||||
.graph
|
||||
.append(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
changeset
|
||||
let txs = block.txdata.iter().enumerate().map(|(tx_pos, tx)| {
|
||||
(
|
||||
tx,
|
||||
core::iter::once(A::from_block_position(&block, block_id, tx_pos)),
|
||||
)
|
||||
});
|
||||
self.batch_insert_relevant(txs)
|
||||
}
|
||||
|
||||
/// Batch insert all transactions of the given `block` of `height`.
|
||||
@@ -261,7 +258,7 @@ where
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
let anchor = A::from_block_position(&block, block_id, tx_pos);
|
||||
graph.append(self.graph.insert_anchor(tx.compute_txid(), anchor));
|
||||
graph.append(self.graph.insert_anchor(tx.txid(), anchor));
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
}
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
@@ -269,7 +266,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents changes to an [`IndexedTxGraph`].
|
||||
/// A structure that represents changes to an [`IndexedTxGraph`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
@@ -319,9 +316,8 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
|
||||
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
|
||||
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
|
||||
fn from(indexer: keychain::ChangeSet<K>) -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
indexer,
|
||||
@@ -352,9 +348,3 @@ pub trait Indexer {
|
||||
/// Determines whether the transaction should be included in the index.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
|
||||
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
&self.graph
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,76 @@
|
||||
//!
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{collections::BTreeMap, Append};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod txout_index;
|
||||
use bitcoin::{Amount, ScriptBuf};
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use txout_index::*;
|
||||
|
||||
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
|
||||
/// It maps each keychain `K` to its last revealed index.
|
||||
///
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet] are
|
||||
/// monotone in that they will never decrease the revealed derivation index.
|
||||
///
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
|
||||
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "K: Ord + serde::Deserialize<'de>",
|
||||
serialize = "K: Ord + serde::Serialize"
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct ChangeSet<K>(pub BTreeMap<K, u32>);
|
||||
|
||||
impl<K> ChangeSet<K> {
|
||||
/// Get the inner map of the keychain to its new derivation index.
|
||||
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> Append for ChangeSet<K> {
|
||||
/// Append another [`ChangeSet`] into self.
|
||||
///
|
||||
/// If the keychain already exists, increase the index when the other's index > self's index.
|
||||
/// If the keychain did not exist, append the new keychain.
|
||||
fn append(&mut self, mut other: Self) {
|
||||
self.0.iter_mut().for_each(|(key, index)| {
|
||||
if let Some(other_index) = other.0.remove(key) {
|
||||
*index = other_index.max(*index);
|
||||
}
|
||||
});
|
||||
|
||||
self.0.append(&mut other.0);
|
||||
}
|
||||
|
||||
/// Returns whether the changeset are empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for ChangeSet<K> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
|
||||
fn as_ref(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance, differentiated into various categories.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[cfg_attr(
|
||||
@@ -25,13 +89,13 @@ pub use txout_index::*;
|
||||
)]
|
||||
pub struct Balance {
|
||||
/// All coinbase outputs not yet matured
|
||||
pub immature: Amount,
|
||||
pub immature: u64,
|
||||
/// Unconfirmed UTXOs generated by a wallet tx
|
||||
pub trusted_pending: Amount,
|
||||
pub trusted_pending: u64,
|
||||
/// Unconfirmed UTXOs received from an external wallet
|
||||
pub untrusted_pending: Amount,
|
||||
pub untrusted_pending: u64,
|
||||
/// Confirmed and immediately spendable balance
|
||||
pub confirmed: Amount,
|
||||
pub confirmed: u64,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
@@ -39,21 +103,16 @@ impl Balance {
|
||||
///
|
||||
/// This is the balance you can spend right now that shouldn't get cancelled via another party
|
||||
/// double spending it.
|
||||
pub fn trusted_spendable(&self) -> Amount {
|
||||
pub fn trusted_spendable(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending
|
||||
}
|
||||
|
||||
/// Get the whole balance visible to the wallet.
|
||||
pub fn total(&self) -> Amount {
|
||||
pub fn total(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||
}
|
||||
}
|
||||
|
||||
/// A tuple of keychain index and `T` representing the indexed value.
|
||||
pub type Indexed<T> = (u32, T);
|
||||
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
|
||||
pub type KeychainIndexed<K, T> = ((K, u32), T);
|
||||
|
||||
impl core::fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
@@ -76,3 +135,40 @@ impl core::ops::Add for Balance {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn append_keychain_derivation_indices() {
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
|
||||
enum Keychain {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
lhs_di.insert(Keychain::One, 7);
|
||||
lhs_di.insert(Keychain::Two, 0);
|
||||
rhs_di.insert(Keychain::One, 3);
|
||||
rhs_di.insert(Keychain::Two, 5);
|
||||
lhs_di.insert(Keychain::Three, 3);
|
||||
rhs_di.insert(Keychain::Four, 4);
|
||||
|
||||
let mut lhs = ChangeSet(lhs_di);
|
||||
let rhs = ChangeSet(rhs_di);
|
||||
lhs.append(rhs);
|
||||
|
||||
// Exiting index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
|
||||
// Existing index updates if the new index in `other` is higher than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
|
||||
// Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
|
||||
// New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
//! This crate is a collection of core structures for [Bitcoin Dev Kit].
|
||||
//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release).
|
||||
//!
|
||||
//! The goal of this crate is to give wallets the mechanisms needed to:
|
||||
//!
|
||||
@@ -12,8 +12,9 @@
|
||||
//! you do it synchronously or asynchronously. If you know a fact about the blockchain, you can just
|
||||
//! tell `bdk_chain`'s APIs about it, and that information will be integrated, if it can be done
|
||||
//! consistently.
|
||||
//! 2. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
|
||||
//! cache or how you retrieve it from persistent storage.
|
||||
//! 2. Error-free APIs.
|
||||
//! 3. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
|
||||
//! cache or how you fetch it.
|
||||
//!
|
||||
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
|
||||
|
||||
@@ -28,7 +29,6 @@ pub use chain_data::*;
|
||||
pub mod indexed_tx_graph;
|
||||
pub use indexed_tx_graph::IndexedTxGraph;
|
||||
pub mod keychain;
|
||||
pub use keychain::{Indexed, KeychainIndexed};
|
||||
pub mod local_chain;
|
||||
mod tx_data_traits;
|
||||
pub mod tx_graph;
|
||||
@@ -36,6 +36,8 @@ pub use tx_data_traits::*;
|
||||
pub use tx_graph::TxGraph;
|
||||
mod chain_oracle;
|
||||
pub use chain_oracle::*;
|
||||
mod persist;
|
||||
pub use persist::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod example_utils;
|
||||
@@ -45,14 +47,11 @@ pub use miniscript;
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod descriptor_ext;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use descriptor_ext::{DescriptorExt, DescriptorId};
|
||||
pub use descriptor_ext::DescriptorExt;
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod spk_iter;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use spk_iter::*;
|
||||
mod changeset;
|
||||
pub use changeset::*;
|
||||
pub mod spk_client;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
@@ -61,6 +60,9 @@ extern crate alloc;
|
||||
#[cfg(feature = "serde")]
|
||||
pub extern crate serde_crate as serde;
|
||||
|
||||
#[cfg(feature = "bincode")]
|
||||
extern crate bincode;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[macro_use]
|
||||
extern crate std;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
||||
|
||||
use core::convert::Infallible;
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::block::Header;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||
/// A structure that represents changes to [`LocalChain`].
|
||||
///
|
||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||
@@ -35,42 +33,12 @@ struct CPInner {
|
||||
prev: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl PartialEq for CheckPoint {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_cps = self.iter().map(|cp| cp.block_id());
|
||||
let other_cps = other.iter().map(|cp| cp.block_id());
|
||||
self_cps.eq(other_cps)
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckPoint {
|
||||
/// Construct a new base block at the front of a linked list.
|
||||
pub fn new(block: BlockId) -> Self {
|
||||
Self(Arc::new(CPInner { block, prev: None }))
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method will error if any of the follow occurs:
|
||||
///
|
||||
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
|
||||
/// - The `blocks` iterator is not in ascending height order.
|
||||
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
|
||||
///
|
||||
/// The error type is the last successful checkpoint constructed (if any).
|
||||
pub fn from_block_ids(
|
||||
block_ids: impl IntoIterator<Item = BlockId>,
|
||||
) -> Result<Self, Option<Self>> {
|
||||
let mut blocks = block_ids.into_iter();
|
||||
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
|
||||
for id in blocks {
|
||||
acc = acc.push(id).map_err(Some)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from the given `header` and block `height`.
|
||||
///
|
||||
/// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,
|
||||
@@ -96,6 +64,16 @@ impl CheckPoint {
|
||||
.expect("must construct checkpoint")
|
||||
}
|
||||
|
||||
/// Convenience method to convert the [`CheckPoint`] into an [`Update`].
|
||||
///
|
||||
/// For more information, refer to [`Update`].
|
||||
pub fn into_update(self, introduce_older_blocks: bool) -> Update {
|
||||
Update {
|
||||
tip: self,
|
||||
introduce_older_blocks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts another checkpoint onto the linked list representing the blockchain.
|
||||
///
|
||||
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
|
||||
@@ -147,82 +125,237 @@ impl CheckPoint {
|
||||
pub fn iter(&self) -> CheckPointIter {
|
||||
self.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get checkpoint at `height`.
|
||||
/// A structure that iterates over checkpoints backwards.
|
||||
pub struct CheckPointIter {
|
||||
current: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl Iterator for CheckPointIter {
|
||||
type Item = CheckPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current = current.prev.clone();
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for CheckPoint {
|
||||
type Item = CheckPoint;
|
||||
type IntoIter = CheckPointIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct to update [`LocalChain`].
|
||||
///
|
||||
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
|
||||
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
|
||||
/// blocks to the original chain.
|
||||
///
|
||||
/// Block-by-block syncing mechanisms would typically create updates that builds upon the previous
|
||||
/// tip. In this case, `introduce_older_blocks` would be `false`.
|
||||
///
|
||||
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
|
||||
/// so some updates require introducing older blocks (to anchor older transactions). For
|
||||
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// The update chain's new tip.
|
||||
pub tip: CheckPoint,
|
||||
|
||||
/// Whether the update allows for introducing older blocks.
|
||||
///
|
||||
/// Returns `None` if checkpoint at `height` does not exist`.
|
||||
pub fn get(&self, height: u32) -> Option<Self> {
|
||||
self.range(height..=height).next()
|
||||
/// Refer to [struct-level documentation] for more.
|
||||
///
|
||||
/// [struct-level documentation]: Update
|
||||
pub introduce_older_blocks: bool,
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalChain {
|
||||
tip: CheckPoint,
|
||||
index: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
impl PartialEq for LocalChain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.index
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
if block.height > chain_tip.height {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(
|
||||
match (
|
||||
self.index.get(&block.height),
|
||||
self.index.get(&chain_tip.height),
|
||||
) {
|
||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Iterate checkpoints over a height range.
|
||||
///
|
||||
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||
/// height).
|
||||
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
let start_bound = range.start_bound().cloned();
|
||||
let end_bound = range.end_bound().cloned();
|
||||
self.iter()
|
||||
.skip_while(move |cp| match end_bound {
|
||||
core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound,
|
||||
core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound,
|
||||
core::ops::Bound::Unbounded => false,
|
||||
})
|
||||
.take_while(move |cp| match start_bound {
|
||||
core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound,
|
||||
core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound,
|
||||
core::ops::Bound::Unbounded => true,
|
||||
})
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||
Ok(self.tip.block_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Get the genesis hash.
|
||||
pub fn genesis_hash(&self) -> BlockHash {
|
||||
self.index.get(&0).copied().expect("must have genesis hash")
|
||||
}
|
||||
|
||||
/// Inserts `block_id` at its height within the chain.
|
||||
///
|
||||
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
|
||||
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
|
||||
/// it. If the height already existed and has a conflicting block hash then it will be purged
|
||||
/// along with all block followin it. The returned chain will have a tip of the `block_id`
|
||||
/// passed in. Of course, if the `block_id` was already present then this just returns `self`.
|
||||
/// Construct [`LocalChain`] from genesis `hash`.
|
||||
#[must_use]
|
||||
pub fn insert(self, block_id: BlockId) -> Self {
|
||||
assert_ne!(block_id.height, 0, "cannot insert the genesis block");
|
||||
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
|
||||
let height = 0;
|
||||
let chain = Self {
|
||||
tip: CheckPoint::new(BlockId { height, hash }),
|
||||
index: core::iter::once((height, hash)).collect(),
|
||||
};
|
||||
let changeset = chain.initial_changeset();
|
||||
(chain, changeset)
|
||||
}
|
||||
|
||||
let mut cp = self.clone();
|
||||
let mut tail = vec![];
|
||||
let base = loop {
|
||||
if cp.height() == block_id.height {
|
||||
if cp.hash() == block_id.hash {
|
||||
return self;
|
||||
}
|
||||
// if we have a conflict we just return the inserted block because the tail is by
|
||||
// implication invalid.
|
||||
tail = vec![];
|
||||
break cp.prev().expect("can't be called on genesis block");
|
||||
}
|
||||
|
||||
if cp.height() < block_id.height {
|
||||
break cp;
|
||||
}
|
||||
|
||||
tail.push(cp.block_id());
|
||||
cp = cp.prev().expect("will break before genesis block");
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_entry = changeset.get(&0).copied().flatten();
|
||||
let genesis_hash = match genesis_entry {
|
||||
Some(hash) => hash,
|
||||
None => return Err(MissingGenesisError),
|
||||
};
|
||||
|
||||
base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
|
||||
.expect("tail is in order")
|
||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&changeset)?;
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Apply `changeset` to the checkpoint.
|
||||
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||
let mut chain = Self {
|
||||
tip,
|
||||
index: BTreeMap::new(),
|
||||
};
|
||||
chain.reindex(0);
|
||||
|
||||
if chain.index.get(&0).copied().is_none() {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||
if !blocks.contains_key(&0) {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
|
||||
for block in &blocks {
|
||||
match tip {
|
||||
Some(curr) => {
|
||||
tip = Some(
|
||||
curr.push(BlockId::from(block))
|
||||
.expect("BTreeMap is ordered"),
|
||||
)
|
||||
}
|
||||
None => tip = Some(CheckPoint::new(BlockId::from(block))),
|
||||
}
|
||||
}
|
||||
|
||||
let chain = Self {
|
||||
index: blocks,
|
||||
tip: tip.expect("already checked to have genesis"),
|
||||
};
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> CheckPoint {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
|
||||
///
|
||||
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
|
||||
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
|
||||
/// the existing chain and invalidate the block after it (if it exists) by including a block at
|
||||
/// the same height but with a different hash to explicitly exclude it as a connection point.
|
||||
///
|
||||
/// Additionally, an empty chain can be updated with any chain, and a chain with a single block
|
||||
/// can have it's block invalidated by an update chain with a block at the same height but
|
||||
/// different hash.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// Refer to [`Update`] for more about the update struct.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
let changeset = merge_chains(
|
||||
self.tip.clone(),
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
// `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
|
||||
// `.apply_changeset`
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
|
||||
for cp in self.iter() {
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
@@ -248,257 +381,13 @@ impl CheckPoint {
|
||||
.expect("extension is strictly greater than base"),
|
||||
None => LocalChain::from_blocks(extension)?.tip(),
|
||||
};
|
||||
self = new_tip;
|
||||
self.tip = new_tip;
|
||||
self.reindex(start_height);
|
||||
|
||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over checkpoints backwards.
|
||||
pub struct CheckPointIter {
|
||||
current: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl Iterator for CheckPointIter {
|
||||
type Item = CheckPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current.clone_from(¤t.prev);
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for CheckPoint {
|
||||
type Item = CheckPoint;
|
||||
type IntoIter = CheckPointIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalChain {
|
||||
tip: CheckPoint,
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
let chain_tip_cp = match self.tip.get(chain_tip.height) {
|
||||
// we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
|
||||
// be identified in chain
|
||||
Some(cp) if cp.hash() == chain_tip.hash => cp,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
match chain_tip_cp.get(block.height) {
|
||||
Some(cp) => Ok(Some(cp.hash() == block.hash)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||
Ok(self.tip.block_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Get the genesis hash.
|
||||
pub fn genesis_hash(&self) -> BlockHash {
|
||||
self.tip.get(0).expect("genesis must exist").hash()
|
||||
}
|
||||
|
||||
/// Construct [`LocalChain`] from genesis `hash`.
|
||||
#[must_use]
|
||||
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
|
||||
let height = 0;
|
||||
let chain = Self {
|
||||
tip: CheckPoint::new(BlockId { height, hash }),
|
||||
};
|
||||
let changeset = chain.initial_changeset();
|
||||
(chain, changeset)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_entry = changeset.get(&0).copied().flatten();
|
||||
let genesis_hash = match genesis_entry {
|
||||
Some(hash) => hash,
|
||||
None => return Err(MissingGenesisError),
|
||||
};
|
||||
|
||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&changeset)?;
|
||||
|
||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_cp = tip.iter().last().expect("must have at least one element");
|
||||
if genesis_cp.height() != 0 {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
Ok(Self { tip })
|
||||
}
|
||||
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||
if !blocks.contains_key(&0) {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
for block in &blocks {
|
||||
match tip {
|
||||
Some(curr) => {
|
||||
tip = Some(
|
||||
curr.push(BlockId::from(block))
|
||||
.expect("BTreeMap is ordered"),
|
||||
)
|
||||
}
|
||||
None => tip = Some(CheckPoint::new(BlockId::from(block))),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tip: tip.expect("already checked to have genesis"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> CheckPoint {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
|
||||
///
|
||||
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
|
||||
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
|
||||
/// the existing chain and invalidate the block after it (if it exists) by including a block at
|
||||
/// the same height but with a different hash to explicitly exclude it as a connection point.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
|
||||
let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
|
||||
self.tip = new_tip;
|
||||
self._check_changeset_is_applied(&changeset);
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] at `height` which you claim is connected to a existing block in the chain.
|
||||
///
|
||||
/// This is useful when you have a block header that you want to record as part of the chain but
|
||||
/// don't necessarily know that the `prev_blockhash` is in the chain.
|
||||
///
|
||||
/// This will usually insert two new [`BlockId`]s into the chain: the header's block and the
|
||||
/// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed
|
||||
/// to be `prev_blockhash` (in which case only one new block id will be inserted).
|
||||
/// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update
|
||||
/// rules][`apply_update`] are satisfied.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
|
||||
/// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
|
||||
/// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
|
||||
/// height is greater than the header's `height`.
|
||||
///
|
||||
/// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
|
||||
///
|
||||
/// [`apply_update`]: Self::apply_update
|
||||
pub fn apply_header_connected_to(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
) -> Result<ChangeSet, ApplyHeaderError> {
|
||||
let this = BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
};
|
||||
let prev = height.checked_sub(1).map(|prev_height| BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
});
|
||||
let conn = match connected_to {
|
||||
// `connected_to` can be ignored if same as `this` or `prev` (duplicate)
|
||||
conn if conn == this || Some(conn) == prev => None,
|
||||
// this occurs if:
|
||||
// - `connected_to` height is the same as `prev`, but different hash
|
||||
// - `connected_to` height is the same as `this`, but different hash
|
||||
// - `connected_to` height is greater than `this` (this is not allowed)
|
||||
conn if conn.height >= height.saturating_sub(1) => {
|
||||
return Err(ApplyHeaderError::InconsistentBlocks)
|
||||
}
|
||||
conn => Some(conn),
|
||||
};
|
||||
|
||||
let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
|
||||
.expect("block ids must be in order");
|
||||
|
||||
self.apply_update(update)
|
||||
.map_err(ApplyHeaderError::CannotConnect)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] connecting it with the previous block.
|
||||
///
|
||||
/// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
|
||||
/// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
|
||||
/// use the current block as `connected_to`.
|
||||
///
|
||||
/// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
|
||||
pub fn apply_header(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let connected_to = match height.checked_sub(1) {
|
||||
Some(prev_height) => BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
},
|
||||
None => BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
},
|
||||
};
|
||||
self.apply_header_connected_to(header, height, connected_to)
|
||||
.map_err(|err| match err {
|
||||
ApplyHeaderError::InconsistentBlocks => {
|
||||
unreachable!("connected_to is derived from the block so is always consistent")
|
||||
}
|
||||
ApplyHeaderError::CannotConnect(err) => err,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
let old_tip = self.tip.clone();
|
||||
let new_tip = old_tip.apply_changeset(changeset)?;
|
||||
self.tip = new_tip;
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -508,16 +397,16 @@ impl LocalChain {
|
||||
///
|
||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
||||
if let Some(original_cp) = self.tip.get(block_id.height) {
|
||||
let original_hash = original_cp.hash();
|
||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
||||
if original_hash != block_id.hash {
|
||||
return Err(AlterCheckPointError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: Some(block_id.hash),
|
||||
});
|
||||
} else {
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
|
||||
let mut changeset = ChangeSet::default();
|
||||
@@ -531,51 +420,21 @@ impl LocalChain {
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Removes blocks from (and inclusive of) the given `block_id`.
|
||||
///
|
||||
/// This will remove blocks with a height equal or greater than `block_id`, but only if
|
||||
/// `block_id` exists in the chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
||||
/// genesis block.
|
||||
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
||||
let mut remove_from = Option::<CheckPoint>::None;
|
||||
let mut changeset = ChangeSet::default();
|
||||
for cp in self.tip().iter() {
|
||||
let cp_id = cp.block_id();
|
||||
if cp_id.height < block_id.height {
|
||||
/// Reindex the heights in the chain from (and including) `from` height
|
||||
fn reindex(&mut self, from: u32) {
|
||||
let _ = self.index.split_off(&from);
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() < from {
|
||||
break;
|
||||
}
|
||||
changeset.insert(cp_id.height, None);
|
||||
if cp_id == block_id {
|
||||
remove_from = Some(cp);
|
||||
}
|
||||
self.index.insert(cp.height(), cp.hash());
|
||||
}
|
||||
self.tip = match remove_from.map(|cp| cp.prev()) {
|
||||
// The checkpoint below the earliest checkpoint to remove will be the new tip.
|
||||
Some(Some(new_tip)) => new_tip,
|
||||
// If there is no checkpoint below the earliest checkpoint to remove, it means the
|
||||
// "earliest checkpoint to remove" is the genesis block. We disallow removing the
|
||||
// genesis block.
|
||||
Some(None) => return Err(MissingGenesisError),
|
||||
// If there is nothing to remove, we return an empty changeset.
|
||||
None => return Ok(ChangeSet::default()),
|
||||
};
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||
/// recover the current chain.
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.tip
|
||||
.iter()
|
||||
.map(|cp| {
|
||||
let block_id = cp.block_id();
|
||||
(block_id.height, Some(block_id.hash))
|
||||
})
|
||||
.collect()
|
||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
@@ -585,49 +444,28 @@ impl LocalChain {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the internal index mapping the height to block hash.
|
||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.index
|
||||
}
|
||||
|
||||
fn _check_index_is_consistent_with_tip(&self) -> bool {
|
||||
let tip_history = self
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
self.index == tip_history
|
||||
}
|
||||
|
||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||
let mut curr_cp = self.tip.clone();
|
||||
for (height, exp_hash) in changeset.iter().rev() {
|
||||
match curr_cp.get(*height) {
|
||||
Some(query_cp) => {
|
||||
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
|
||||
return false;
|
||||
}
|
||||
curr_cp = query_cp;
|
||||
}
|
||||
None => {
|
||||
if exp_hash.is_some() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (height, exp_hash) in changeset {
|
||||
if self.index.get(height) != exp_hash.as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get checkpoint at given `height` (if it exists).
|
||||
///
|
||||
/// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
|
||||
///
|
||||
/// [`tip`]: LocalChain::tip
|
||||
pub fn get(&self, height: u32) -> Option<CheckPoint> {
|
||||
self.tip.get(height)
|
||||
}
|
||||
|
||||
/// Iterate checkpoints over a height range.
|
||||
///
|
||||
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||
/// height).
|
||||
///
|
||||
/// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
|
||||
///
|
||||
/// [`tip`]: LocalChain::tip
|
||||
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
self.tip.range(range)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||
@@ -697,41 +535,14 @@ impl core::fmt::Display for CannotConnectError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CannotConnectError {}
|
||||
|
||||
/// The error type for [`LocalChain::apply_header_connected_to`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ApplyHeaderError {
|
||||
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
|
||||
InconsistentBlocks,
|
||||
/// Occurs when the update cannot connect with the original chain.
|
||||
CannotConnect(CannotConnectError),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ApplyHeaderError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ApplyHeaderError::InconsistentBlocks => write!(
|
||||
f,
|
||||
"the `connected_to` block conflicts with either the current or previous block"
|
||||
),
|
||||
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ApplyHeaderError {}
|
||||
|
||||
/// Applies `update_tip` onto `original_tip`.
|
||||
///
|
||||
/// On success, a tuple is returned `(changeset, can_replace)`. If `can_replace` is true, then the
|
||||
/// `update_tip` can replace the `original_tip`.
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
|
||||
introduce_older_blocks: bool,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut orig = original_tip.iter();
|
||||
let mut update = update_tip.iter();
|
||||
let mut orig = original_tip.into_iter();
|
||||
let mut update = update_tip.into_iter();
|
||||
let mut curr_orig = None;
|
||||
let mut curr_update = None;
|
||||
let mut prev_orig: Option<CheckPoint> = None;
|
||||
@@ -740,12 +551,6 @@ fn merge_chains(
|
||||
let mut prev_orig_was_invalidated = false;
|
||||
let mut potentially_invalidated_heights = vec![];
|
||||
|
||||
// If we can, we want to return the update tip as the new tip because this allows checkpoints
|
||||
// in multiple locations to keep the same `Arc` pointers when they are being updated from each
|
||||
// other using this function. We can do this as long as long as the update contains every
|
||||
// block's height of the original chain.
|
||||
let mut is_update_height_superset_of_original = true;
|
||||
|
||||
// To find the difference between the new chain and the original we iterate over both of them
|
||||
// from the tip backwards in tandem. We always dealing with the highest one from either chain
|
||||
// first and move to the next highest. The crucial logic is applied when they have blocks at the
|
||||
@@ -771,8 +576,6 @@ fn merge_chains(
|
||||
prev_orig_was_invalidated = false;
|
||||
prev_orig = curr_orig.take();
|
||||
|
||||
is_update_height_superset_of_original = false;
|
||||
|
||||
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
|
||||
// iterating because there's no possibility of adding anything to changeset.
|
||||
if u.is_none() {
|
||||
@@ -795,20 +598,12 @@ fn merge_chains(
|
||||
}
|
||||
point_of_agreement_found = true;
|
||||
prev_orig_was_invalidated = false;
|
||||
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
|
||||
// invalidation, we can break after finding the point of agreement.
|
||||
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
|
||||
// can guarantee that no older blocks are introduced.
|
||||
if Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
if is_update_height_superset_of_original {
|
||||
return Ok((update_tip, changeset));
|
||||
} else {
|
||||
let new_tip =
|
||||
original_tip.apply_changeset(&changeset).map_err(|_| {
|
||||
CannotConnectError {
|
||||
try_include_height: 0,
|
||||
}
|
||||
})?;
|
||||
return Ok((new_tip, changeset));
|
||||
}
|
||||
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
return Ok(changeset);
|
||||
}
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
@@ -842,10 +637,5 @@ fn merge_chains(
|
||||
}
|
||||
}
|
||||
|
||||
let new_tip = original_tip
|
||||
.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
Ok((new_tip, changeset))
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
97
crates/chain/src/persist.rs
Normal file
97
crates/chain/src/persist.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use crate::Append;
|
||||
|
||||
/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
|
||||
/// before they are persisted.
|
||||
///
|
||||
/// Not all changes to the in-memory representation needs to be written to disk right away, so
|
||||
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
|
||||
/// to write changes to disk.
|
||||
#[derive(Debug)]
|
||||
pub struct Persist<B, C> {
|
||||
backend: B,
|
||||
stage: C,
|
||||
}
|
||||
|
||||
impl<B, C> Persist<B, C>
|
||||
where
|
||||
B: PersistBackend<C>,
|
||||
C: Default + Append,
|
||||
{
|
||||
/// Create a new [`Persist`] from [`PersistBackend`].
|
||||
pub fn new(backend: B) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
stage: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stage a `changeset` to be committed later with [`commit`].
|
||||
///
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn stage(&mut self, changeset: C) {
|
||||
self.stage.append(changeset)
|
||||
}
|
||||
|
||||
/// Get the changes that have not been committed yet.
|
||||
pub fn staged(&self) -> &C {
|
||||
&self.stage
|
||||
}
|
||||
|
||||
/// Commit the staged changes to the underlying persistence backend.
|
||||
///
|
||||
/// Changes that are committed (if any) are returned.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// Returns a backend-defined error if this fails.
|
||||
pub fn commit(&mut self) -> Result<Option<C>, B::WriteError> {
|
||||
if self.stage.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
self.backend
|
||||
.write_changes(&self.stage)
|
||||
// if written successfully, take and return `self.stage`
|
||||
.map(|_| Some(core::mem::take(&mut self.stage)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A persistence backend for [`Persist`].
|
||||
///
|
||||
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
|
||||
/// that are to be persisted, or retrieved from persistence.
|
||||
pub trait PersistBackend<C> {
|
||||
/// The error the backend returns when it fails to write.
|
||||
type WriteError: core::fmt::Debug;
|
||||
|
||||
/// The error the backend returns when it fails to load changesets `C`.
|
||||
type LoadError: core::fmt::Debug;
|
||||
|
||||
/// Writes a changeset to the persistence backend.
|
||||
///
|
||||
/// It is up to the backend what it does with this. It could store every changeset in a list or
|
||||
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
|
||||
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
|
||||
/// changesets had been applied sequentially.
|
||||
///
|
||||
/// [`load_from_persistence`]: Self::load_from_persistence
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
||||
|
||||
/// Return the aggregate changeset `C` from persistence.
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
|
||||
}
|
||||
|
||||
impl<C> PersistBackend<C> for () {
|
||||
type WriteError = Infallible;
|
||||
|
||||
type LoadError = Infallible;
|
||||
|
||||
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
//! Helper types for spk-based blockchain clients.
|
||||
|
||||
use crate::{
|
||||
collections::BTreeMap, keychain::Indexed, local_chain::CheckPoint,
|
||||
ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Data required to perform a spk-based blockchain client sync.
|
||||
///
|
||||
/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and
|
||||
/// outpoints. The sync process also updates the chain from the given [`CheckPoint`].
|
||||
pub struct SyncRequest {
|
||||
/// A checkpoint for the current chain [`LocalChain::tip`].
|
||||
/// The sync process will return a new chain update that extends this tip.
|
||||
///
|
||||
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
|
||||
pub chain_tip: CheckPoint,
|
||||
/// Transactions that spend from or to these indexed script pubkeys.
|
||||
pub spks: Box<dyn ExactSizeIterator<Item = ScriptBuf> + Send>,
|
||||
/// Transactions with these txids.
|
||||
pub txids: Box<dyn ExactSizeIterator<Item = Txid> + Send>,
|
||||
/// Transactions with these outpoints or spent from these outpoints.
|
||||
pub outpoints: Box<dyn ExactSizeIterator<Item = OutPoint> + Send>,
|
||||
}
|
||||
|
||||
impl SyncRequest {
|
||||
/// Construct a new [`SyncRequest`] from a given `cp` tip.
|
||||
pub fn from_chain_tip(cp: CheckPoint) -> Self {
|
||||
Self {
|
||||
chain_tip: cp,
|
||||
spks: Box::new(core::iter::empty()),
|
||||
txids: Box::new(core::iter::empty()),
|
||||
outpoints: Box::new(core::iter::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_spks(
|
||||
mut self,
|
||||
spks: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.spks = Box::new(spks.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Txid`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_txids(
|
||||
mut self,
|
||||
txids: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.txids = Box::new(txids.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`OutPoint`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_outpoints(
|
||||
mut self,
|
||||
outpoints: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
|
||||
>,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(outpoints.into_iter());
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_spks(
|
||||
mut self,
|
||||
spks: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = ScriptBuf> + Send + 'static,
|
||||
Item = ScriptBuf,
|
||||
>,
|
||||
) -> Self {
|
||||
self.spks = Box::new(ExactSizeChain::new(self.spks, spks.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Txid`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_txids(
|
||||
mut self,
|
||||
txids: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = Txid> + Send + 'static,
|
||||
Item = Txid,
|
||||
>,
|
||||
) -> Self {
|
||||
self.txids = Box::new(ExactSizeChain::new(self.txids, txids.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`OutPoint`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_outpoints(
|
||||
mut self,
|
||||
outpoints: impl IntoIterator<
|
||||
IntoIter = impl ExactSizeIterator<Item = OutPoint> + Send + 'static,
|
||||
Item = OutPoint,
|
||||
>,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(ExactSizeChain::new(self.outpoints, outpoints.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`Script`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks(
|
||||
mut self,
|
||||
mut inspect: impl FnMut(&Script) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.spks = Box::new(self.spks.inspect(move |spk| inspect(spk)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`Txid`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_txids(mut self, mut inspect: impl FnMut(&Txid) + Send + Sync + 'static) -> Self {
|
||||
self.txids = Box::new(self.txids.inspect(move |txid| inspect(txid)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for [`OutPoint`]s previously added to this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_outpoints(
|
||||
mut self,
|
||||
mut inspect: impl FnMut(&OutPoint) + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.outpoints = Box::new(self.outpoints.inspect(move |op| inspect(op)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Populate the request with revealed script pubkeys from `index` with the given `spk_range`.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[must_use]
|
||||
pub fn populate_with_revealed_spks<K: Clone + Ord + core::fmt::Debug + Send + Sync>(
|
||||
self,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
spk_range: impl core::ops::RangeBounds<K>,
|
||||
) -> Self {
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::vec::Vec;
|
||||
self.chain_spks(
|
||||
index
|
||||
.revealed_spks(spk_range)
|
||||
.map(|(_, spk)| spk.to_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Data returned from a spk-based blockchain client sync.
|
||||
///
|
||||
/// See also [`SyncRequest`].
|
||||
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub chain_update: CheckPoint,
|
||||
}
|
||||
|
||||
/// Data required to perform a spk-based blockchain client full scan.
|
||||
///
|
||||
/// A client full scan iterates through all the scripts for the given keychains, fetching relevant
|
||||
/// data until some stop gap number of scripts is found that have no data. This operation is
|
||||
/// generally only used when importing or restoring previously used keychains in which the list of
|
||||
/// used scripts is not known. The full scan process also updates the chain from the given [`CheckPoint`].
|
||||
pub struct FullScanRequest<K> {
|
||||
/// A checkpoint for the current [`LocalChain::tip`].
|
||||
/// The full scan process will return a new chain update that extends this tip.
|
||||
///
|
||||
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
|
||||
pub chain_tip: CheckPoint,
|
||||
/// Iterators of script pubkeys indexed by the keychain index.
|
||||
pub spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = Indexed<ScriptBuf>> + Send>>,
|
||||
}
|
||||
|
||||
impl<K: Ord + Clone> FullScanRequest<K> {
|
||||
/// Construct a new [`FullScanRequest`] from a given `chain_tip`.
|
||||
#[must_use]
|
||||
pub fn from_chain_tip(chain_tip: CheckPoint) -> Self {
|
||||
Self {
|
||||
chain_tip,
|
||||
spks_by_keychain: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new [`FullScanRequest`] from a given `chain_tip` and `index`.
|
||||
///
|
||||
/// Unbounded script pubkey iterators for each keychain (`K`) are extracted using
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the
|
||||
/// [`FullScanRequest`].
|
||||
///
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[must_use]
|
||||
pub fn from_keychain_txout_index(
|
||||
chain_tip: CheckPoint,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
) -> Self
|
||||
where
|
||||
K: core::fmt::Debug,
|
||||
{
|
||||
let mut req = Self::from_chain_tip(chain_tip);
|
||||
for (keychain, spks) in index.all_unbounded_spk_iters() {
|
||||
req = req.set_spks_for_keychain(keychain, spks);
|
||||
}
|
||||
req
|
||||
}
|
||||
|
||||
/// Set the [`Script`]s for a given `keychain`.
|
||||
///
|
||||
/// This consumes the [`FullScanRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn set_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
spks: impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send + 'static>,
|
||||
) -> Self {
|
||||
self.spks_by_keychain
|
||||
.insert(keychain, Box::new(spks.into_iter()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Chain on additional [`Script`]s that will be synced against.
|
||||
///
|
||||
/// This consumes the [`FullScanRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn chain_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
spks: impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send + 'static>,
|
||||
) -> Self {
|
||||
match self.spks_by_keychain.remove(&keychain) {
|
||||
// clippy here suggests to remove `into_iter` from `spks.into_iter()`, but doing so
|
||||
// results in a compilation error
|
||||
#[allow(clippy::useless_conversion)]
|
||||
Some(keychain_spks) => self
|
||||
.spks_by_keychain
|
||||
.insert(keychain, Box::new(keychain_spks.chain(spks.into_iter()))),
|
||||
None => self
|
||||
.spks_by_keychain
|
||||
.insert(keychain, Box::new(spks.into_iter())),
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for every [`Script`] previously added to any keychain in
|
||||
/// this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks_for_all_keychains(
|
||||
mut self,
|
||||
inspect: impl FnMut(K, u32, &Script) + Send + Sync + Clone + 'static,
|
||||
) -> Self
|
||||
where
|
||||
K: Send + 'static,
|
||||
{
|
||||
for (keychain, spks) in core::mem::take(&mut self.spks_by_keychain) {
|
||||
let mut inspect = inspect.clone();
|
||||
self.spks_by_keychain.insert(
|
||||
keychain.clone(),
|
||||
Box::new(spks.inspect(move |(i, spk)| inspect(keychain.clone(), *i, spk))),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a closure that will be called for every [`Script`] previously added to a given
|
||||
/// `keychain` in this request.
|
||||
///
|
||||
/// This consumes the [`SyncRequest`] and returns the updated one.
|
||||
#[must_use]
|
||||
pub fn inspect_spks_for_keychain(
|
||||
mut self,
|
||||
keychain: K,
|
||||
mut inspect: impl FnMut(u32, &Script) + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
K: Send + 'static,
|
||||
{
|
||||
if let Some(spks) = self.spks_by_keychain.remove(&keychain) {
|
||||
self.spks_by_keychain.insert(
|
||||
keychain,
|
||||
Box::new(spks.inspect(move |(i, spk)| inspect(*i, spk))),
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Data returned from a spk-based blockchain client full scan.
|
||||
///
|
||||
/// See also [`FullScanRequest`].
|
||||
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
pub chain_update: CheckPoint,
|
||||
/// Last active indices for the corresponding keychains (`K`).
|
||||
pub last_active_indices: BTreeMap<K, u32>,
|
||||
}
|
||||
|
||||
/// A version of [`core::iter::Chain`] which can combine two [`ExactSizeIterator`]s to form a new
|
||||
/// [`ExactSizeIterator`].
|
||||
///
|
||||
/// The danger of this is explained in [the `ExactSizeIterator` docs]
|
||||
/// (https://doc.rust-lang.org/core/iter/trait.ExactSizeIterator.html#when-shouldnt-an-adapter-be-exactsizeiterator).
|
||||
/// This does not apply here since it would be impossible to scan an item count that overflows
|
||||
/// `usize` anyway.
|
||||
struct ExactSizeChain<A, B, I> {
|
||||
a: Option<A>,
|
||||
b: Option<B>,
|
||||
i: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<A, B, I> ExactSizeChain<A, B, I> {
|
||||
fn new(a: A, b: B) -> Self {
|
||||
ExactSizeChain {
|
||||
a: Some(a),
|
||||
b: Some(b),
|
||||
i: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, I> Iterator for ExactSizeChain<A, B, I>
|
||||
where
|
||||
A: Iterator<Item = I>,
|
||||
B: Iterator<Item = I>,
|
||||
{
|
||||
type Item = I;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(a) = &mut self.a {
|
||||
let item = a.next();
|
||||
if item.is_some() {
|
||||
return item;
|
||||
}
|
||||
self.a = None;
|
||||
}
|
||||
if let Some(b) = &mut self.b {
|
||||
let item = b.next();
|
||||
if item.is_some() {
|
||||
return item;
|
||||
}
|
||||
self.b = None;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, I> ExactSizeIterator for ExactSizeChain<A, B, I>
|
||||
where
|
||||
A: ExactSizeIterator<Item = I>,
|
||||
B: ExactSizeIterator<Item = I>,
|
||||
{
|
||||
fn len(&self) -> usize {
|
||||
let a_len = self.a.as_ref().map(|a| a.len()).unwrap_or(0);
|
||||
let b_len = self.b.as_ref().map(|a| a.len()).unwrap_or(0);
|
||||
a_len + b_len
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::{
|
||||
bitcoin::{secp256k1::Secp256k1, ScriptBuf},
|
||||
keychain::Indexed,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
|
||||
@@ -44,24 +43,18 @@ impl<D> SpkIterator<D>
|
||||
where
|
||||
D: Borrow<Descriptor<DescriptorPublicKey>>,
|
||||
{
|
||||
/// Create a new script pubkey iterator from `descriptor`.
|
||||
///
|
||||
/// This iterates from derivation index 0 and stops at index 0x7FFFFFFF (as specified in
|
||||
/// BIP-32). Non-wildcard descriptors will only return one script pubkey at derivation index 0.
|
||||
///
|
||||
/// Use [`new_with_range`](SpkIterator::new_with_range) to create an iterator with a specified
|
||||
/// derivation index range.
|
||||
/// Creates a new script pubkey iterator starting at 0 from a descriptor.
|
||||
pub fn new(descriptor: D) -> Self {
|
||||
SpkIterator::new_with_range(descriptor, 0..=BIP32_MAX_INDEX)
|
||||
}
|
||||
|
||||
/// Create a new script pubkey iterator from `descriptor` and a given `range`.
|
||||
///
|
||||
/// Non-wildcard descriptors will only emit a single script pubkey (at derivation index 0).
|
||||
/// Wildcard descriptors have an end-bound of 0x7FFFFFFF (inclusive).
|
||||
///
|
||||
/// Refer to [`new`](SpkIterator::new) for more.
|
||||
pub fn new_with_range<R>(descriptor: D, range: R) -> Self
|
||||
// Creates a new script pubkey iterator from a descriptor with a given range.
|
||||
// If the descriptor doesn't have a wildcard, we shorten whichever range you pass in
|
||||
// to have length <= 1. This means that if you pass in 0..0 or 0..1 the range will
|
||||
// remain the same, but if you pass in 0..10, we'll shorten it to 0..1
|
||||
// Also note that if the descriptor doesn't have a wildcard, passing in a range starting
|
||||
// from n > 0, will return an empty iterator.
|
||||
pub(crate) fn new_with_range<R>(descriptor: D, range: R) -> Self
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
@@ -80,6 +73,13 @@ where
|
||||
// Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
|
||||
end = end.min(BIP32_MAX_INDEX + 1);
|
||||
|
||||
if !descriptor.borrow().has_wildcard() {
|
||||
// The length of the range should be at most 1
|
||||
if end != start {
|
||||
end = start + 1;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
next_index: start,
|
||||
end,
|
||||
@@ -87,18 +87,13 @@ where
|
||||
secp: Secp256k1::verification_only(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the internal descriptor.
|
||||
pub fn descriptor(&self) -> &D {
|
||||
&self.descriptor
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Iterator for SpkIterator<D>
|
||||
where
|
||||
D: Borrow<Descriptor<DescriptorPublicKey>>,
|
||||
{
|
||||
type Item = Indexed<ScriptBuf>;
|
||||
type Item = (u32, ScriptBuf);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// For non-wildcard descriptors, we expect the first element to be Some((0, spk)), then None after.
|
||||
@@ -153,18 +148,14 @@ mod test {
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
) {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
|
||||
|
||||
let secp = Secp256k1::signing_only();
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::External, external_descriptor.clone())
|
||||
.unwrap();
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::Internal, internal_descriptor.clone())
|
||||
.unwrap();
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
}
|
||||
@@ -254,19 +245,18 @@ mod test {
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=2).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..11).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..=10).next(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
// The following dummy traits were created to test if SpkIterator is working properly.
|
||||
trait TestSendStatic: Send + 'static {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spk_iterator_is_send_and_static() {
|
||||
fn is_send_and_static<A: Send + 'static>() {}
|
||||
is_send_and_static::<SpkIterator<Descriptor<DescriptorPublicKey>>>()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
indexed_tx_graph::Indexer,
|
||||
};
|
||||
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
use bitcoin::{self, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
|
||||
|
||||
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||
///
|
||||
@@ -52,7 +52,7 @@ impl<I> Default for SpkTxOutIndex<I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
|
||||
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
|
||||
type ChangeSet = ();
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
|
||||
@@ -76,7 +76,7 @@ impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
||||
/// Scans a transaction's outputs for matching script pubkeys.
|
||||
///
|
||||
/// Typically, this is used in two situations:
|
||||
@@ -86,7 +86,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
|
||||
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
|
||||
let mut scanned_indices = BTreeSet::new();
|
||||
let txid = tx.compute_txid();
|
||||
let txid = tx.txid();
|
||||
for (i, txout) in tx.output.iter().enumerate() {
|
||||
let op = OutPoint::new(txid, i as u32);
|
||||
if let Some(spk_i) = self.scan_txout(op, txout) {
|
||||
@@ -168,7 +168,9 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
///
|
||||
/// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
|
||||
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
|
||||
self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
|
||||
self.txouts
|
||||
.get(&outpoint)
|
||||
.map(|(spk_i, txout)| (spk_i, txout))
|
||||
}
|
||||
|
||||
/// Returns the script that has been inserted at the `index`.
|
||||
@@ -215,7 +217,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// let unused_change_spks =
|
||||
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
|
||||
/// ```
|
||||
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
|
||||
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)>
|
||||
where
|
||||
R: RangeBounds<I>,
|
||||
{
|
||||
@@ -229,7 +231,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||
/// never scanned a transaction output with it.
|
||||
pub fn is_used(&self, index: &I) -> bool {
|
||||
!self.unused.contains(index)
|
||||
self.unused.get(index).is_none()
|
||||
}
|
||||
|
||||
/// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
|
||||
@@ -270,45 +272,37 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
self.spk_indices.get(script)
|
||||
}
|
||||
|
||||
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
|
||||
/// *sent* when a script pubkey in the `range` is on an input and *received* when it is on an
|
||||
/// output. For `sent` to be computed correctly, the output being spent must have already been
|
||||
/// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly,
|
||||
/// so it will be correct even if it has not been scanned.
|
||||
pub fn sent_and_received(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
range: impl RangeBounds<I>,
|
||||
) -> (Amount, Amount) {
|
||||
let mut sent = Amount::ZERO;
|
||||
let mut received = Amount::ZERO;
|
||||
/// Computes total input value going from script pubkeys in the index (sent) and the total output
|
||||
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
|
||||
/// correctly, the output being spent must have already been scanned by the index. Calculating
|
||||
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
|
||||
/// not been scanned.
|
||||
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||
let mut sent = 0;
|
||||
let mut received = 0;
|
||||
|
||||
for txin in &tx.input {
|
||||
if let Some((index, txout)) = self.txout(txin.previous_output) {
|
||||
if range.contains(index) {
|
||||
sent += txout.value;
|
||||
}
|
||||
if let Some((_, txout)) = self.txout(txin.previous_output) {
|
||||
sent += txout.value;
|
||||
}
|
||||
}
|
||||
for txout in &tx.output {
|
||||
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
|
||||
if range.contains(index) {
|
||||
received += txout.value;
|
||||
}
|
||||
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
||||
received += txout.value;
|
||||
}
|
||||
}
|
||||
|
||||
(sent, received)
|
||||
}
|
||||
|
||||
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
|
||||
/// for calling [`sent_and_received`] and subtracting sent from received.
|
||||
/// Computes the net value that this transaction gives to the script pubkeys in the index and
|
||||
/// *takes* from the transaction outputs in the index. Shorthand for calling
|
||||
/// [`sent_and_received`] and subtracting sent from received.
|
||||
///
|
||||
/// [`sent_and_received`]: Self::sent_and_received
|
||||
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
|
||||
let (sent, received) = self.sent_and_received(tx, range);
|
||||
received.to_signed().expect("valid `SignedAmount`")
|
||||
- sent.to_signed().expect("valid `SignedAmount`")
|
||||
pub fn net_value(&self, tx: &Transaction) -> i64 {
|
||||
let (sent, received) = self.sent_and_received(tx);
|
||||
received as i64 - sent as i64
|
||||
}
|
||||
|
||||
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output
|
||||
|
||||
@@ -5,25 +5,21 @@ use alloc::vec::Vec;
|
||||
|
||||
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
||||
///
|
||||
/// If transaction A is anchored in block B, and block B is in the best chain, we can
|
||||
/// [`Anchor`] implementations must be [`Ord`] by the anchor block's [`BlockId`] first.
|
||||
///
|
||||
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
|
||||
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
|
||||
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
|
||||
/// parent block of B.
|
||||
///
|
||||
/// Every [`Anchor`] implementation must contain a [`BlockId`] parameter, and must implement
|
||||
/// [`Ord`]. When implementing [`Ord`], the anchors' [`BlockId`]s should take precedence
|
||||
/// over other elements inside the [`Anchor`]s for comparison purposes, i.e., you should first
|
||||
/// compare the anchors' [`BlockId`]s and then care about the rest.
|
||||
///
|
||||
/// The example shows different types of anchors:
|
||||
/// ```
|
||||
/// # use bdk_chain::local_chain::LocalChain;
|
||||
/// # use bdk_chain::tx_graph::TxGraph;
|
||||
/// # use bdk_chain::BlockId;
|
||||
/// # use bdk_chain::ConfirmationHeightAnchor;
|
||||
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
|
||||
/// # use bdk_chain::example_utils::*;
|
||||
/// # use bitcoin::hashes::Hash;
|
||||
///
|
||||
/// // Initialize the local chain with two blocks.
|
||||
/// let chain = LocalChain::from_blocks(
|
||||
/// [
|
||||
@@ -43,7 +39,7 @@ use alloc::vec::Vec;
|
||||
/// let mut graph_a = TxGraph::<BlockId>::default();
|
||||
/// let _ = graph_a.insert_tx(tx.clone());
|
||||
/// graph_a.insert_anchor(
|
||||
/// tx.compute_txid(),
|
||||
/// tx.txid(),
|
||||
/// BlockId {
|
||||
/// height: 1,
|
||||
/// hash: Hash::hash("first".as_bytes()),
|
||||
@@ -51,14 +47,13 @@ use alloc::vec::Vec;
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block and the confirmation height of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
/// let _ = graph_b.insert_tx(tx.clone());
|
||||
/// graph_b.insert_anchor(
|
||||
/// tx.compute_txid(),
|
||||
/// tx.txid(),
|
||||
/// ConfirmationHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// height: 2,
|
||||
@@ -67,25 +62,6 @@ use alloc::vec::Vec;
|
||||
/// confirmation_height: 1,
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
/// let _ = graph_c.insert_tx(tx.clone());
|
||||
/// graph_c.insert_anchor(
|
||||
/// tx.compute_txid(),
|
||||
/// ConfirmationTimeHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// height: 2,
|
||||
/// hash: Hash::hash("third".as_bytes()),
|
||||
/// },
|
||||
/// confirmation_height: 1,
|
||||
/// confirmation_time: 123,
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
|
||||
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
|
||||
@@ -114,28 +90,17 @@ pub trait AnchorFromBlockPosition: Anchor {
|
||||
}
|
||||
|
||||
/// Trait that makes an object appendable.
|
||||
pub trait Append: Default {
|
||||
pub trait Append {
|
||||
/// Append another object of the same type onto `self`.
|
||||
fn append(&mut self, other: Self);
|
||||
|
||||
/// Returns whether the structure is considered empty.
|
||||
fn is_empty(&self) -> bool;
|
||||
|
||||
/// Take the value, replacing it with the default value.
|
||||
fn take(&mut self) -> Option<Self> {
|
||||
if self.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(core::mem::take(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
fn append(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeMap::extend(self, other)
|
||||
fn append(&mut self, mut other: Self) {
|
||||
BTreeMap::append(self, &mut other)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
@@ -144,10 +109,8 @@ impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
}
|
||||
|
||||
impl<T: Ord> Append for BTreeSet<T> {
|
||||
fn append(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeSet::extend(self, other)
|
||||
fn append(&mut self, mut other: Self) {
|
||||
BTreeSet::append(self, &mut other)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
|
||||
@@ -1,75 +1,39 @@
|
||||
//! Module for structures that store and traverse transactions.
|
||||
//!
|
||||
//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of
|
||||
//! those transactions. `TxGraph` is *monotone* in that you can always insert a transaction -- it
|
||||
//! does not care whether that transaction is in the current best chain or whether it conflicts with
|
||||
//! any of the existing transactions or what order you insert the transactions. This means that you
|
||||
//! can always combine two [`TxGraph`]s together, without resulting in inconsistencies. Furthermore,
|
||||
//! there is currently no way to delete a transaction.
|
||||
//!
|
||||
//! Transactions can be either whole or partial (i.e., transactions for which we only know some
|
||||
//! outputs, which we usually call "floating outputs"; these are usually inserted using the
|
||||
//! [`insert_txout`] method.).
|
||||
//!
|
||||
//! The graph contains transactions in the form of [`TxNode`]s. Each node contains the txid, the
|
||||
//! transaction (whole or partial), the blocks that it is anchored to (see the [`Anchor`]
|
||||
//! documentation for more details), and the timestamp of the last time we saw the transaction as
|
||||
//! unconfirmed.
|
||||
//! [`TxGraph`] is a monotone structure that inserts transactions and indexes the spends. The
|
||||
//! [`ChangeSet`] structure reports changes of [`TxGraph`] but can also be applied to a
|
||||
//! [`TxGraph`] as well. Lastly, [`TxDescendants`] is an [`Iterator`] that traverses descendants of
|
||||
//! a given transaction.
|
||||
//!
|
||||
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
|
||||
//! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
|
||||
//! methods only consider transactions that are "canonical" (i.e., in the best chain or in mempool).
|
||||
//! We decide which transactions are canonical based on the transaction's anchors and the
|
||||
//! `last_seen` (as unconfirmed) timestamp; see the [`try_get_chain_position`] documentation for
|
||||
//! more details.
|
||||
//!
|
||||
//! The [`ChangeSet`] reports changes made to a [`TxGraph`]; it can be used to either save to
|
||||
//! persistent storage, or to be applied to another [`TxGraph`].
|
||||
//!
|
||||
//! Lastly, you can use [`TxAncestors`]/[`TxDescendants`] to traverse ancestors and descendants of
|
||||
//! a given transaction, respectively.
|
||||
//! identifying and traversing conflicts and descendants of a given transaction.
|
||||
//!
|
||||
//! # Applying changes
|
||||
//!
|
||||
//! Methods that change the state of [`TxGraph`] will return [`ChangeSet`]s.
|
||||
//! [`ChangeSet`]s can be applied back to a [`TxGraph`] or be used to inform persistent storage
|
||||
//! Methods that apply changes to [`TxGraph`] will return [`ChangeSet`].
|
||||
//! [`ChangeSet`] can be applied back to a [`TxGraph`] or be used to inform persistent storage
|
||||
//! of the changes to [`TxGraph`].
|
||||
//!
|
||||
//! # Generics
|
||||
//!
|
||||
//! Anchors are represented as generics within `TxGraph<A>`. To make use of all functionality of the
|
||||
//! `TxGraph`, anchors (`A`) should implement [`Anchor`].
|
||||
//!
|
||||
//! Anchors are made generic so that different types of data can be stored with how a transaction is
|
||||
//! *anchored* to a given block. An example of this is storing a merkle proof of the transaction to
|
||||
//! the confirmation block - this can be done with a custom [`Anchor`] type. The minimal [`Anchor`]
|
||||
//! type would just be a [`BlockId`] which just represents the height and hash of the block which
|
||||
//! the transaction is contained in. Note that a transaction can be contained in multiple
|
||||
//! conflicting blocks (by nature of the Bitcoin network).
|
||||
//!
|
||||
//! ```
|
||||
//! # use bdk_chain::BlockId;
|
||||
//! # use bdk_chain::tx_graph::TxGraph;
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
//! # let tx_a = tx_from_hex(RAW_TX_1);
|
||||
//! let mut tx_graph: TxGraph = TxGraph::default();
|
||||
//! let mut graph: TxGraph = TxGraph::default();
|
||||
//! let mut another_graph: TxGraph = TxGraph::default();
|
||||
//!
|
||||
//! // insert a transaction
|
||||
//! let changeset = tx_graph.insert_tx(tx_a);
|
||||
//! let changeset = graph.insert_tx(tx_a);
|
||||
//!
|
||||
//! // We can restore the state of the `tx_graph` by applying all
|
||||
//! // the changesets obtained by mutating the original (the order doesn't matter).
|
||||
//! let mut restored_tx_graph: TxGraph = TxGraph::default();
|
||||
//! restored_tx_graph.apply_changeset(changeset);
|
||||
//!
|
||||
//! assert_eq!(tx_graph, restored_tx_graph);
|
||||
//! // the resulting changeset can be applied to another tx graph
|
||||
//! another_graph.apply_changeset(changeset);
|
||||
//! ```
|
||||
//!
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`].
|
||||
//!
|
||||
//! ```
|
||||
//! # use bdk_chain::{Append, BlockId};
|
||||
//! # use bdk_chain::BlockId;
|
||||
//! # use bdk_chain::tx_graph::TxGraph;
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
@@ -85,17 +49,14 @@
|
||||
//! let changeset = graph.apply_update(update);
|
||||
//! assert!(changeset.is_empty());
|
||||
//! ```
|
||||
//! [`try_get_chain_position`]: TxGraph::try_get_chain_position
|
||||
//! [`insert_txout`]: TxGraph::insert_txout
|
||||
|
||||
use crate::{
|
||||
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
|
||||
FullTxOut,
|
||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||
ChainOracle, ChainPosition, FullTxOut,
|
||||
};
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
use core::fmt::{self, Formatter};
|
||||
use core::{
|
||||
convert::Infallible,
|
||||
@@ -130,13 +91,13 @@ impl<A> Default for TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction node in the [`TxGraph`].
|
||||
/// An outward-facing view of a (transaction) node in the [`TxGraph`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TxNode<'a, T, A> {
|
||||
/// Txid of the transaction.
|
||||
pub txid: Txid,
|
||||
/// A partial or full representation of the transaction.
|
||||
pub tx: T,
|
||||
pub tx: &'a T,
|
||||
/// The blocks that the transaction is "anchored" in.
|
||||
pub anchors: &'a BTreeSet<A>,
|
||||
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
||||
@@ -147,7 +108,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.tx
|
||||
self.tx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +118,7 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
||||
/// outputs).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum TxNodeInternal {
|
||||
Whole(Arc<Transaction>),
|
||||
Whole(Transaction),
|
||||
Partial(BTreeMap<u32, TxOut>),
|
||||
}
|
||||
|
||||
@@ -167,7 +128,7 @@ impl Default for TxNodeInternal {
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction that is included in the chain, or is still in mempool.
|
||||
/// An outwards-facing view of a transaction that is part of the *best chain*'s history.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct CanonicalTx<'a, T, A> {
|
||||
/// How the transaction is observed as (confirmed or unconfirmed).
|
||||
@@ -182,7 +143,7 @@ pub enum CalculateFeeError {
|
||||
/// Missing `TxOut` for one or more of the inputs of the tx
|
||||
MissingTxOut(Vec<OutPoint>),
|
||||
/// When the transaction is invalid according to the graph it has a negative fee
|
||||
NegativeFee(SignedAmount),
|
||||
NegativeFee(i64),
|
||||
}
|
||||
|
||||
impl fmt::Display for CalculateFeeError {
|
||||
@@ -196,7 +157,7 @@ impl fmt::Display for CalculateFeeError {
|
||||
CalculateFeeError::NegativeFee(fee) => write!(
|
||||
f,
|
||||
"transaction is invalid according to the graph and has negative fee: {}",
|
||||
fee.display_dynamic()
|
||||
fee
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -212,7 +173,6 @@ impl<A> TxGraph<A> {
|
||||
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
|
||||
TxNodeInternal::Whole(tx) => tx
|
||||
.as_ref()
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -244,13 +204,13 @@ impl<A> TxGraph<A> {
|
||||
}
|
||||
|
||||
/// Iterate over all full transactions in the graph.
|
||||
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Transaction, A>> {
|
||||
self.txs
|
||||
.iter()
|
||||
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
|
||||
TxNodeInternal::Whole(tx) => Some(TxNode {
|
||||
txid,
|
||||
tx: tx.clone(),
|
||||
tx,
|
||||
anchors,
|
||||
last_seen_unconfirmed: *last_seen,
|
||||
}),
|
||||
@@ -263,16 +223,16 @@ impl<A> TxGraph<A> {
|
||||
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
||||
///
|
||||
/// [`get_txout`]: Self::get_txout
|
||||
pub fn get_tx(&self, txid: Txid) -> Option<Arc<Transaction>> {
|
||||
pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> {
|
||||
self.get_tx_node(txid).map(|n| n.tx)
|
||||
}
|
||||
|
||||
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
||||
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
|
||||
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Transaction, A>> {
|
||||
match &self.txs.get(&txid)? {
|
||||
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
|
||||
txid,
|
||||
tx: tx.clone(),
|
||||
tx,
|
||||
anchors,
|
||||
last_seen_unconfirmed: *last_seen,
|
||||
}),
|
||||
@@ -283,7 +243,7 @@ impl<A> TxGraph<A> {
|
||||
/// Obtains a single tx output (if any) at the specified outpoint.
|
||||
pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> {
|
||||
match &self.txs.get(&outpoint.txid)?.0 {
|
||||
TxNodeInternal::Whole(tx) => tx.as_ref().output.get(outpoint.vout as usize),
|
||||
TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize),
|
||||
TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout),
|
||||
}
|
||||
}
|
||||
@@ -294,7 +254,6 @@ impl<A> TxGraph<A> {
|
||||
pub fn tx_outputs(&self, txid: Txid) -> Option<BTreeMap<u32, &TxOut>> {
|
||||
Some(match &self.txs.get(&txid)?.0 {
|
||||
TxNodeInternal::Whole(tx) => tx
|
||||
.as_ref()
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -307,7 +266,7 @@ impl<A> TxGraph<A> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculates the fee of a given transaction. Returns [`Amount::ZERO`] if `tx` is a coinbase transaction.
|
||||
/// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
|
||||
/// Returns `OK(_)` if we have all the [`TxOut`]s being spent by `tx` in the graph (either as
|
||||
/// the full transactions or individual txouts).
|
||||
///
|
||||
@@ -318,20 +277,20 @@ impl<A> TxGraph<A> {
|
||||
/// Note `tx` does not have to be in the graph for this to work.
|
||||
///
|
||||
/// [`insert_txout`]: Self::insert_txout
|
||||
pub fn calculate_fee(&self, tx: &Transaction) -> Result<Amount, CalculateFeeError> {
|
||||
if tx.is_coinbase() {
|
||||
return Ok(Amount::ZERO);
|
||||
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
|
||||
if tx.is_coin_base() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let (inputs_sum, missing_outputs) = tx.input.iter().fold(
|
||||
(SignedAmount::ZERO, Vec::new()),
|
||||
(0_i64, Vec::new()),
|
||||
|(mut sum, mut missing_outpoints), txin| match self.get_txout(txin.previous_output) {
|
||||
None => {
|
||||
missing_outpoints.push(txin.previous_output);
|
||||
(sum, missing_outpoints)
|
||||
}
|
||||
Some(txout) => {
|
||||
sum += txout.value.to_signed().expect("valid `SignedAmount`");
|
||||
sum += txout.value as i64;
|
||||
(sum, missing_outpoints)
|
||||
}
|
||||
},
|
||||
@@ -343,17 +302,20 @@ impl<A> TxGraph<A> {
|
||||
let outputs_sum = tx
|
||||
.output
|
||||
.iter()
|
||||
.map(|txout| txout.value.to_signed().expect("valid `SignedAmount`"))
|
||||
.sum::<SignedAmount>();
|
||||
.map(|txout| txout.value as i64)
|
||||
.sum::<i64>();
|
||||
|
||||
let fee = inputs_sum - outputs_sum;
|
||||
fee.to_unsigned()
|
||||
.map_err(|_| CalculateFeeError::NegativeFee(fee))
|
||||
if fee < 0 {
|
||||
Err(CalculateFeeError::NegativeFee(fee))
|
||||
} else {
|
||||
Ok(fee as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// The transactions spending from this output.
|
||||
///
|
||||
/// [`TxGraph`] allows conflicting transactions within the graph. Obviously the transactions in
|
||||
/// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in
|
||||
/// the returned set will never be in the same active-chain.
|
||||
pub fn outspends(&self, outpoint: OutPoint) -> &HashSet<Txid> {
|
||||
self.spends.get(&outpoint).unwrap_or(&self.empty_outspends)
|
||||
@@ -369,15 +331,16 @@ impl<A> TxGraph<A> {
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> impl DoubleEndedIterator<Item = (u32, &HashSet<Txid>)> + '_ {
|
||||
let start = OutPoint::new(txid, 0);
|
||||
let end = OutPoint::new(txid, u32::MAX);
|
||||
let start = OutPoint { txid, vout: 0 };
|
||||
let end = OutPoint {
|
||||
txid,
|
||||
vout: u32::MAX,
|
||||
};
|
||||
self.spends
|
||||
.range(start..=end)
|
||||
.map(|(outpoint, spends)| (outpoint.vout, spends))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Creates an iterator that filters and maps ancestor transactions.
|
||||
///
|
||||
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
|
||||
@@ -391,10 +354,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
///
|
||||
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
|
||||
/// it visits and decide whether to visit ancestors.
|
||||
pub fn walk_ancestors<'g, T, F, O>(&'g self, tx: T, walk_map: F) -> TxAncestors<'g, A, F>
|
||||
pub fn walk_ancestors<'g, F, O>(
|
||||
&'g self,
|
||||
tx: &'g Transaction,
|
||||
walk_map: F,
|
||||
) -> TxAncestors<'g, A, F>
|
||||
where
|
||||
T: Into<Arc<Transaction>>,
|
||||
F: FnMut(usize, Arc<Transaction>) -> Option<O> + 'g,
|
||||
F: FnMut(usize, &'g Transaction) -> Option<O> + 'g,
|
||||
{
|
||||
TxAncestors::new_exclude_root(self, tx, walk_map)
|
||||
}
|
||||
@@ -415,9 +381,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
{
|
||||
TxDescendants::new_exclude_root(self, txid, walk_map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> TxGraph<A> {
|
||||
/// Creates an iterator that both filters and maps conflicting transactions (this includes
|
||||
/// descendants of directly-conflicting transactions, which are also considered conflicts).
|
||||
///
|
||||
@@ -430,7 +394,7 @@ impl<A> TxGraph<A> {
|
||||
where
|
||||
F: FnMut(usize, Txid) -> Option<O> + 'g,
|
||||
{
|
||||
let txids = self.direct_conflicts(tx).map(|(_, txid)| txid);
|
||||
let txids = self.direct_conflitcs(tx).map(|(_, txid)| txid);
|
||||
TxDescendants::from_multiple_include_root(self, txids, walk_map)
|
||||
}
|
||||
|
||||
@@ -441,11 +405,11 @@ impl<A> TxGraph<A> {
|
||||
/// Note that this only returns directly conflicting txids and won't include:
|
||||
/// - descendants of conflicting transactions (which are technically also conflicting)
|
||||
/// - transactions conflicting with the given transaction's ancestors
|
||||
pub fn direct_conflicts<'g>(
|
||||
pub fn direct_conflitcs<'g>(
|
||||
&'g self,
|
||||
tx: &'g Transaction,
|
||||
) -> impl Iterator<Item = (usize, Txid)> + '_ {
|
||||
let txid = tx.compute_txid();
|
||||
let txid = tx.txid();
|
||||
tx.input
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -466,19 +430,6 @@ impl<A> TxGraph<A> {
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Transform the [`TxGraph`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> TxGraph<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
let mut new_graph = TxGraph::<A2>::default();
|
||||
new_graph.apply_changeset(self.initial_changeset().map_anchors(f));
|
||||
new_graph
|
||||
}
|
||||
|
||||
/// Construct a new [`TxGraph`] from a list of transactions.
|
||||
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
|
||||
let mut new = Self::default();
|
||||
@@ -513,20 +464,18 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Inserts the given transaction into [`TxGraph`].
|
||||
///
|
||||
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
|
||||
pub fn insert_tx<T: Into<Arc<Transaction>>>(&mut self, tx: T) -> ChangeSet<A> {
|
||||
let tx = tx.into();
|
||||
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
|
||||
let mut update = Self::default();
|
||||
update.txs.insert(
|
||||
tx.compute_txid(),
|
||||
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
|
||||
);
|
||||
update
|
||||
.txs
|
||||
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
/// Batch insert unconfirmed transactions.
|
||||
///
|
||||
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
|
||||
/// *last seen* communicates when the transaction is last seen in mempool which is used for
|
||||
/// *last seen* communicates when the transaction is last seen in the mempool which is used for
|
||||
/// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details).
|
||||
pub fn batch_insert_unconfirmed(
|
||||
&mut self,
|
||||
@@ -534,7 +483,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
) -> ChangeSet<A> {
|
||||
let mut changeset = ChangeSet::<A>::default();
|
||||
for (tx, seen_at) in txs {
|
||||
changeset.append(self.insert_seen_at(tx.compute_txid(), seen_at));
|
||||
changeset.append(self.insert_seen_at(tx.txid(), seen_at));
|
||||
changeset.append(self.insert_tx(tx));
|
||||
}
|
||||
changeset
|
||||
@@ -552,11 +501,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
|
||||
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
|
||||
///
|
||||
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch
|
||||
/// update all unconfirmed transactions with the latest `seen_at`, see
|
||||
/// [`update_last_seen_unconfirmed`].
|
||||
///
|
||||
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
|
||||
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
|
||||
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
||||
let mut update = Self::default();
|
||||
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
|
||||
@@ -564,65 +509,6 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
/// Update the last seen time for all unconfirmed transactions.
|
||||
///
|
||||
/// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting
|
||||
/// the given `seen_at` for every transaction not yet anchored to a confirmed block,
|
||||
/// and returns the [`ChangeSet`] after applying all updates to `self`.
|
||||
///
|
||||
/// This is useful for keeping track of the latest time a transaction was seen
|
||||
/// unconfirmed, which is important for evaluating transaction conflicts in the same
|
||||
/// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for
|
||||
/// [`try_get_chain_position`].
|
||||
///
|
||||
/// A normal use of this method is to call it with the current system time. Although
|
||||
/// block headers contain a timestamp, using the header time would be less effective
|
||||
/// at tracking mempool transactions, because it can drift from actual clock time, plus
|
||||
/// we may want to update a transaction's last seen time repeatedly between blocks.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bdk_chain::example_utils::*;
|
||||
/// # use std::time::UNIX_EPOCH;
|
||||
/// # let tx = tx_from_hex(RAW_TX_1);
|
||||
/// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]);
|
||||
/// let now = std::time::SystemTime::now()
|
||||
/// .duration_since(UNIX_EPOCH)
|
||||
/// .expect("valid duration")
|
||||
/// .as_secs();
|
||||
/// let changeset = tx_graph.update_last_seen_unconfirmed(now);
|
||||
/// assert!(!changeset.last_seen.is_empty());
|
||||
/// ```
|
||||
///
|
||||
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must
|
||||
/// by strictly greater than what is currently stored for a transaction to have an effect.
|
||||
/// To insert a last seen time for a single txid, see [`insert_seen_at`].
|
||||
///
|
||||
/// [`insert_seen_at`]: Self::insert_seen_at
|
||||
/// [`try_get_chain_position`]: Self::try_get_chain_position
|
||||
pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet<A> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let unanchored_txs: Vec<Txid> = self
|
||||
.txs
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(&txid, (_, anchors, _))| {
|
||||
if anchors.is_empty() {
|
||||
Some(txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
for txid in unanchored_txs {
|
||||
changeset.append(self.insert_seen_at(txid, seen_at));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
/// Extends this graph with another so that `self` becomes the union of the two sets of
|
||||
/// transactions.
|
||||
///
|
||||
@@ -641,9 +527,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
|
||||
/// Applies [`ChangeSet`] to [`TxGraph`].
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet<A>) {
|
||||
for wrapped_tx in changeset.txs {
|
||||
let tx = wrapped_tx.as_ref();
|
||||
let txid = tx.compute_txid();
|
||||
for tx in changeset.txs {
|
||||
let txid = tx.txid();
|
||||
|
||||
tx.input
|
||||
.iter()
|
||||
@@ -657,26 +542,27 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
|
||||
match self.txs.get_mut(&txid) {
|
||||
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
|
||||
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
|
||||
*tx_node = TxNodeInternal::Whole(tx);
|
||||
}
|
||||
Some((TxNodeInternal::Whole(tx), _, _)) => {
|
||||
debug_assert_eq!(
|
||||
tx.as_ref().compute_txid(),
|
||||
tx.txid(),
|
||||
txid,
|
||||
"tx should produce txid that is same as key"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
self.txs.insert(
|
||||
txid,
|
||||
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
|
||||
);
|
||||
self.txs
|
||||
.insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (outpoint, txout) in changeset.txouts {
|
||||
let tx_entry = self.txs.entry(outpoint.txid).or_default();
|
||||
let tx_entry = self
|
||||
.txs
|
||||
.entry(outpoint.txid)
|
||||
.or_insert_with(Default::default);
|
||||
|
||||
match tx_entry {
|
||||
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
|
||||
@@ -689,13 +575,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
|
||||
for (anchor, txid) in changeset.anchors {
|
||||
if self.anchors.insert((anchor.clone(), txid)) {
|
||||
let (_, anchors, _) = self.txs.entry(txid).or_default();
|
||||
let (_, anchors, _) = self.txs.entry(txid).or_insert_with(Default::default);
|
||||
anchors.insert(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
for (txid, new_last_seen) in changeset.last_seen {
|
||||
let (_, _, last_seen) = self.txs.entry(txid).or_default();
|
||||
let (_, _, last_seen) = self.txs.entry(txid).or_insert_with(Default::default);
|
||||
if new_last_seen > *last_seen {
|
||||
*last_seen = new_last_seen;
|
||||
}
|
||||
@@ -707,7 +593,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that
|
||||
/// exist in `update` but not in `self`).
|
||||
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
||||
let mut changeset = ChangeSet::<A>::default();
|
||||
let mut changeset = ChangeSet::default();
|
||||
|
||||
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
|
||||
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
|
||||
@@ -757,22 +643,73 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
}
|
||||
|
||||
impl<A: Anchor> TxGraph<A> {
|
||||
/// Find missing block heights of `chain`.
|
||||
///
|
||||
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
|
||||
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
|
||||
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
|
||||
// Map of txids to skip.
|
||||
//
|
||||
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
|
||||
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
|
||||
// other missing-height anchors of this tx can be skipped.
|
||||
//
|
||||
// * Some(true) => skip all anchors of this txid
|
||||
// * Some(false) => do not skip anchors of this txid
|
||||
// * None => we do not know whether we can skip this txid
|
||||
let mut txids_to_skip = HashMap::<Txid, bool>::new();
|
||||
|
||||
// Keeps track of the last height emitted so we don't double up.
|
||||
let mut last_height_emitted = Option::<u32>::None;
|
||||
|
||||
self.anchors
|
||||
.iter()
|
||||
.filter(move |(_, txid)| {
|
||||
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
|
||||
let tx_anchors = match self.txs.get(txid) {
|
||||
Some((_, anchors, _)) => anchors,
|
||||
None => return true,
|
||||
};
|
||||
let mut has_missing_height = false;
|
||||
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
||||
match chain.blocks().get(&anchor_block.height) {
|
||||
None => {
|
||||
has_missing_height = true;
|
||||
continue;
|
||||
}
|
||||
Some(chain_hash) => {
|
||||
if chain_hash == &anchor_block.hash {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
!has_missing_height
|
||||
});
|
||||
#[cfg(feature = "std")]
|
||||
debug_assert!({
|
||||
println!("txid={} skip={}", txid, skip);
|
||||
true
|
||||
});
|
||||
!skip
|
||||
})
|
||||
.filter_map(move |(a, _)| {
|
||||
let anchor_block = a.anchor_block();
|
||||
if Some(anchor_block.height) != last_height_emitted
|
||||
&& !chain.blocks().contains_key(&anchor_block.height)
|
||||
{
|
||||
last_height_emitted = Some(anchor_block.height);
|
||||
Some(anchor_block.height)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||
///
|
||||
/// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
|
||||
///
|
||||
/// This method returns `Ok(None)` if the transaction is not found in the chain, and no longer
|
||||
/// belongs in the mempool. The following factors are used to approximate whether an
|
||||
/// unconfirmed transaction exists in the mempool (not evicted):
|
||||
///
|
||||
/// 1. Unconfirmed transactions that conflict with confirmed transactions are evicted.
|
||||
/// 2. Unconfirmed transactions that spend from transactions that are evicted, are also
|
||||
/// evicted.
|
||||
/// 3. Given two conflicting unconfirmed transactions, the transaction with the lower
|
||||
/// `last_seen_unconfirmed` parameter is evicted. A transaction's `last_seen_unconfirmed`
|
||||
/// parameter is the max of all it's descendants' `last_seen_unconfirmed` parameters. If the
|
||||
/// final `last_seen_unconfirmed`s are the same, the transaction with the lower `txid` (by
|
||||
/// lexicographical order) is evicted.
|
||||
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
|
||||
/// returned.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
@@ -798,17 +735,17 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
// The tx is not anchored to a block in the best chain, which means that it
|
||||
// The tx is not anchored to a block which is in the best chain, which means that it
|
||||
// might be in mempool, or it might have been dropped already.
|
||||
// Let's check conflicts to find out!
|
||||
let tx = match tx_node {
|
||||
TxNodeInternal::Whole(tx) => {
|
||||
// A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
|
||||
// should always be filtered out.
|
||||
if tx.is_coinbase() {
|
||||
if tx.is_coin_base() {
|
||||
return Ok(None);
|
||||
}
|
||||
tx.clone()
|
||||
tx
|
||||
}
|
||||
TxNodeInternal::Partial(_) => {
|
||||
// Partial transactions (outputs only) cannot have conflicts.
|
||||
@@ -825,8 +762,8 @@ impl<A: Anchor> TxGraph<A> {
|
||||
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
|
||||
// resulting array will also include `tx`
|
||||
let unconfirmed_ancestor_txs =
|
||||
TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc<Transaction>| {
|
||||
let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?;
|
||||
TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| {
|
||||
let tx_node = self.get_tx_node(ancestor_tx.txid())?;
|
||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||
// the best chain)
|
||||
for block in tx_node.anchors {
|
||||
@@ -842,10 +779,8 @@ impl<A: Anchor> TxGraph<A> {
|
||||
|
||||
// We determine our tx's last seen, which is the max between our last seen,
|
||||
// and our unconf descendants' last seen.
|
||||
let unconfirmed_descendants_txs = TxDescendants::new_include_root(
|
||||
self,
|
||||
tx.as_ref().compute_txid(),
|
||||
|_, descendant_txid: Txid| {
|
||||
let unconfirmed_descendants_txs =
|
||||
TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| {
|
||||
let tx_node = self.get_tx_node(descendant_txid)?;
|
||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||
// the best chain)
|
||||
@@ -857,9 +792,8 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
Some(Ok(tx_node))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, C::Error>>()?;
|
||||
})
|
||||
.collect::<Result<Vec<_>, C::Error>>()?;
|
||||
|
||||
let tx_last_seen = unconfirmed_descendants_txs
|
||||
.iter()
|
||||
@@ -870,8 +804,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
// Now we traverse our ancestors and consider all their conflicts
|
||||
for tx_node in unconfirmed_ancestor_txs {
|
||||
// We retrieve all the transactions conflicting with this specific ancestor
|
||||
let conflicting_txs =
|
||||
self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid));
|
||||
let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid));
|
||||
|
||||
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
|
||||
// this tx cannot exist in the best chain
|
||||
@@ -885,7 +818,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
return Ok(None);
|
||||
}
|
||||
if conflicting_tx.last_seen_unconfirmed == *last_seen
|
||||
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
|
||||
&& conflicting_tx.txid() > tx.txid()
|
||||
{
|
||||
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
||||
return Ok(None);
|
||||
@@ -978,7 +911,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
|
||||
) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>> {
|
||||
self.full_txs().filter_map(move |tx| {
|
||||
self.try_get_chain_position(chain, chain_tip, tx.txid)
|
||||
.map(|v| {
|
||||
@@ -1000,7 +933,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
|
||||
) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>> {
|
||||
self.try_list_chain_txs(chain, chain_tip)
|
||||
.map(|r| r.expect("oracle is infallible"))
|
||||
}
|
||||
@@ -1012,8 +945,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
/// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
|
||||
/// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
|
||||
///
|
||||
/// Floating outputs (i.e., outputs for which we don't have the full transaction in the graph)
|
||||
/// are ignored.
|
||||
/// Floating outputs are ignored.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
@@ -1039,7 +971,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let txout = match tx_node.tx.as_ref().output.get(op.vout as usize) {
|
||||
let txout = match tx_node.tx.output.get(op.vout as usize) {
|
||||
Some(txout) => txout.clone(),
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -1061,7 +993,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
txout,
|
||||
chain_position,
|
||||
spent_by,
|
||||
is_on_coinbase: tx_node.tx.is_coinbase(),
|
||||
is_on_coinbase: tx_node.tx.is_coin_base(),
|
||||
},
|
||||
)))
|
||||
},
|
||||
@@ -1153,10 +1085,10 @@ impl<A: Anchor> TxGraph<A> {
|
||||
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
|
||||
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
|
||||
) -> Result<Balance, C::Error> {
|
||||
let mut immature = Amount::ZERO;
|
||||
let mut trusted_pending = Amount::ZERO;
|
||||
let mut untrusted_pending = Amount::ZERO;
|
||||
let mut confirmed = Amount::ZERO;
|
||||
let mut immature = 0;
|
||||
let mut trusted_pending = 0;
|
||||
let mut untrusted_pending = 0;
|
||||
let mut confirmed = 0;
|
||||
|
||||
for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) {
|
||||
let (spk_i, txout) = res?;
|
||||
@@ -1204,9 +1136,9 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`ChangeSet`] represents changes to a [`TxGraph`].
|
||||
/// A structure that represents changes to a [`TxGraph`].
|
||||
///
|
||||
/// Since [`TxGraph`] is monotone, the "changeset" can only contain transactions to be added and
|
||||
/// Since [`TxGraph`] is monotone "changeset" can only contain transactions to be added and
|
||||
/// not removed.
|
||||
///
|
||||
/// Refer to [module-level documentation] for more.
|
||||
@@ -1227,7 +1159,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
#[must_use]
|
||||
pub struct ChangeSet<A = ()> {
|
||||
/// Added transactions.
|
||||
pub txs: BTreeSet<Arc<Transaction>>,
|
||||
pub txs: BTreeSet<Transaction>,
|
||||
/// Added txouts.
|
||||
pub txouts: BTreeMap<OutPoint, TxOut>,
|
||||
/// Added anchors.
|
||||
@@ -1248,6 +1180,11 @@ impl<A> Default for ChangeSet<A> {
|
||||
}
|
||||
|
||||
impl<A> ChangeSet<A> {
|
||||
/// Returns true if the [`ChangeSet`] is empty (no transactions or txouts).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.txs.is_empty() && self.txouts.is_empty()
|
||||
}
|
||||
|
||||
/// Iterates over all outpoints contained within [`ChangeSet`].
|
||||
pub fn txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs
|
||||
@@ -1256,7 +1193,7 @@ impl<A> ChangeSet<A> {
|
||||
tx.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(vout, txout)| (OutPoint::new(tx.compute_txid(), vout as _), txout))
|
||||
.map(move |(vout, txout)| (OutPoint::new(tx.txid(), vout as _), txout))
|
||||
})
|
||||
.chain(self.txouts.iter().map(|(op, txout)| (*op, txout)))
|
||||
}
|
||||
@@ -1265,6 +1202,8 @@ impl<A> ChangeSet<A> {
|
||||
///
|
||||
/// This is useful if you want to find which heights you need to fetch data about in order to
|
||||
/// confirm or exclude these anchors.
|
||||
///
|
||||
/// See also: [`TxGraph::missing_heights`]
|
||||
pub fn anchor_heights(&self) -> impl Iterator<Item = u32> + '_
|
||||
where
|
||||
A: Anchor,
|
||||
@@ -1279,15 +1218,31 @@ impl<A> ChangeSet<A> {
|
||||
!duplicate
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator for the [`anchor_heights`] in this changeset that are not included in
|
||||
/// `local_chain`. This tells you which heights you need to include in `local_chain` in order
|
||||
/// for it to conclusively act as a [`ChainOracle`] for the transaction anchors this changeset
|
||||
/// will add.
|
||||
///
|
||||
/// [`ChainOracle`]: crate::ChainOracle
|
||||
/// [`anchor_heights`]: Self::anchor_heights
|
||||
pub fn missing_heights_from<'a>(
|
||||
&'a self,
|
||||
local_chain: &'a LocalChain,
|
||||
) -> impl Iterator<Item = u32> + 'a
|
||||
where
|
||||
A: Anchor,
|
||||
{
|
||||
self.anchor_heights()
|
||||
.filter(move |height| !local_chain.blocks().contains_key(height))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> Append for ChangeSet<A> {
|
||||
fn append(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
self.txs.extend(other.txs);
|
||||
self.txouts.extend(other.txouts);
|
||||
self.anchors.extend(other.anchors);
|
||||
fn append(&mut self, mut other: Self) {
|
||||
self.txs.append(&mut other.txs);
|
||||
self.txouts.append(&mut other.txouts);
|
||||
self.anchors.append(&mut other.anchors);
|
||||
|
||||
// last_seen timestamps should only increase
|
||||
self.last_seen.extend(
|
||||
@@ -1307,26 +1262,6 @@ impl<A: Ord> Append for ChangeSet<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> ChangeSet<A> {
|
||||
/// Transform the [`ChangeSet`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Ord, F>(self, mut f: F) -> ChangeSet<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
ChangeSet {
|
||||
txs: self.txs,
|
||||
txouts: self.txouts,
|
||||
anchors: BTreeSet::<(A2, Txid)>::from_iter(
|
||||
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
|
||||
),
|
||||
last_seen: self.last_seen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
self
|
||||
@@ -1337,13 +1272,13 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
///
|
||||
/// The iterator excludes partial transactions.
|
||||
///
|
||||
/// Returned by the [`walk_ancestors`] method of [`TxGraph`].
|
||||
/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`].
|
||||
///
|
||||
/// [`walk_ancestors`]: TxGraph::walk_ancestors
|
||||
pub struct TxAncestors<'g, A, F> {
|
||||
graph: &'g TxGraph<A>,
|
||||
visited: HashSet<Txid>,
|
||||
queue: VecDeque<(usize, Arc<Transaction>)>,
|
||||
queue: VecDeque<(usize, &'g Transaction)>,
|
||||
filter_map: F,
|
||||
}
|
||||
|
||||
@@ -1351,13 +1286,13 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
|
||||
pub(crate) fn new_include_root(
|
||||
graph: &'g TxGraph<A>,
|
||||
tx: impl Into<Arc<Transaction>>,
|
||||
tx: &'g Transaction,
|
||||
filter_map: F,
|
||||
) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: [(0, tx.into())].into(),
|
||||
queue: [(0, tx)].into(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
@@ -1365,7 +1300,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
|
||||
pub(crate) fn new_exclude_root(
|
||||
graph: &'g TxGraph<A>,
|
||||
tx: impl Into<Arc<Transaction>>,
|
||||
tx: &'g Transaction,
|
||||
filter_map: F,
|
||||
) -> Self {
|
||||
let mut ancestors = Self {
|
||||
@@ -1374,7 +1309,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
queue: Default::default(),
|
||||
filter_map,
|
||||
};
|
||||
ancestors.populate_queue(1, tx.into());
|
||||
ancestors.populate_queue(1, tx);
|
||||
ancestors
|
||||
}
|
||||
|
||||
@@ -1387,13 +1322,12 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
filter_map: F,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Arc<Transaction>>,
|
||||
I: IntoIterator<Item = &'g Transaction>,
|
||||
{
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: txs.into_iter().map(|tx| (0, tx.into())).collect(),
|
||||
queue: txs.into_iter().map(|tx| (0, tx)).collect(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
@@ -1407,8 +1341,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
filter_map: F,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Arc<Transaction>>,
|
||||
I: IntoIterator<Item = &'g Transaction>,
|
||||
{
|
||||
let mut ancestors = Self {
|
||||
graph,
|
||||
@@ -1417,12 +1350,12 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
filter_map,
|
||||
};
|
||||
for tx in txs {
|
||||
ancestors.populate_queue(1, tx.into());
|
||||
ancestors.populate_queue(1, tx);
|
||||
}
|
||||
ancestors
|
||||
}
|
||||
|
||||
fn populate_queue(&mut self, depth: usize, tx: Arc<Transaction>) {
|
||||
fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) {
|
||||
let ancestors = tx
|
||||
.input
|
||||
.iter()
|
||||
@@ -1436,7 +1369,7 @@ impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
|
||||
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
|
||||
where
|
||||
F: FnMut(usize, Arc<Transaction>) -> Option<O>,
|
||||
F: FnMut(usize, &'g Transaction) -> Option<O>,
|
||||
{
|
||||
type Item = O;
|
||||
|
||||
@@ -1445,7 +1378,7 @@ where
|
||||
// we have exhausted all paths when queue is empty
|
||||
let (ancestor_depth, tx) = self.queue.pop_front()?;
|
||||
// ignore paths when user filters them out
|
||||
let item = match (self.filter_map)(ancestor_depth, tx.clone()) {
|
||||
let item = match (self.filter_map)(ancestor_depth, tx) {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
@@ -1457,7 +1390,7 @@ where
|
||||
|
||||
/// An iterator that traverses transaction descendants.
|
||||
///
|
||||
/// Returned by the [`walk_descendants`] method of [`TxGraph`].
|
||||
/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`].
|
||||
///
|
||||
/// [`walk_descendants`]: TxGraph::walk_descendants
|
||||
pub struct TxDescendants<'g, A, F> {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
mod tx_template;
|
||||
#[allow(unused_imports)]
|
||||
pub use tx_template::*;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
@@ -34,9 +31,12 @@ macro_rules! local_chain {
|
||||
macro_rules! chain_update {
|
||||
[ $(($height:expr, $hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
.tip()
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
.tip(),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -69,21 +69,9 @@ macro_rules! changeset {
|
||||
#[allow(unused)]
|
||||
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
|
||||
bitcoin::Transaction {
|
||||
version: bitcoin::transaction::Version::non_standard(0x00),
|
||||
version: 0x00,
|
||||
lock_time: bitcoin::absolute::LockTime::from_consensus(lt),
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub const DESCRIPTORS: [&str; 7] = [
|
||||
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)",
|
||||
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)",
|
||||
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0/*)",
|
||||
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)",
|
||||
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)",
|
||||
"wpkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX/1/0/*)",
|
||||
// non-wildcard
|
||||
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)",
|
||||
];
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
|
||||
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
||||
TxIn, TxOut, Txid, Witness,
|
||||
};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
@@ -51,12 +49,11 @@ impl TxOutTemplate {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), super::DESCRIPTORS[2]).unwrap();
|
||||
let mut graph = TxGraph::<A>::default();
|
||||
pub fn init_graph<'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
|
||||
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
spk_index.insert_spk(
|
||||
@@ -71,7 +68,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
|
||||
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
|
||||
let tx = Transaction {
|
||||
version: transaction::Version::non_standard(0),
|
||||
version: 0,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_tmp
|
||||
.inputs
|
||||
@@ -114,25 +111,25 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
.iter()
|
||||
.map(|output| match &output.spk_index {
|
||||
None => TxOut {
|
||||
value: Amount::from_sat(output.value),
|
||||
value: output.value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
Some(index) => TxOut {
|
||||
value: Amount::from_sat(output.value),
|
||||
value: output.value,
|
||||
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
tx_ids.insert(tx_tmp.tx_name, tx.compute_txid());
|
||||
tx_ids.insert(tx_tmp.tx_name, tx.txid());
|
||||
spk_index.scan(&tx);
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
|
||||
let _ = graph.insert_anchor(tx.txid(), *anchor);
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
|
||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||
}
|
||||
}
|
||||
(graph, spk_index, tx_ids)
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::common::DESCRIPTORS;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, Balance, KeychainTxOutIndex},
|
||||
local_chain::LocalChain,
|
||||
tx_graph, Append, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use bitcoin::{
|
||||
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||
@@ -26,27 +21,24 @@ use miniscript::Descriptor;
|
||||
/// agnostic.
|
||||
#[test]
|
||||
fn insert_relevant_txs() {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0])
|
||||
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
|
||||
.expect("must be valid");
|
||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
let _ = graph
|
||||
.index
|
||||
.insert_descriptor((), descriptor.clone())
|
||||
.unwrap();
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
|
||||
graph.index.add_keychain((), descriptor);
|
||||
graph.index.set_lookahead(&(), 10);
|
||||
|
||||
let tx_a = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: Amount::from_sat(10_000),
|
||||
value: 10_000,
|
||||
script_pubkey: spk_0,
|
||||
},
|
||||
TxOut {
|
||||
value: Amount::from_sat(20_000),
|
||||
value: 20_000,
|
||||
script_pubkey: spk_1,
|
||||
},
|
||||
],
|
||||
@@ -55,7 +47,7 @@ fn insert_relevant_txs() {
|
||||
|
||||
let tx_b = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.compute_txid(), 0),
|
||||
previous_output: OutPoint::new(tx_a.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(1)
|
||||
@@ -63,7 +55,7 @@ fn insert_relevant_txs() {
|
||||
|
||||
let tx_c = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.compute_txid(), 1),
|
||||
previous_output: OutPoint::new(tx_a.txid(), 1),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(2)
|
||||
@@ -73,13 +65,10 @@ fn insert_relevant_txs() {
|
||||
|
||||
let changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph::ChangeSet {
|
||||
txs: txs.iter().cloned().map(Arc::new).collect(),
|
||||
txs: txs.clone().into(),
|
||||
..Default::default()
|
||||
},
|
||||
indexer: keychain::ChangeSet {
|
||||
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
|
||||
keychains_added: [].into(),
|
||||
},
|
||||
indexer: keychain::ChangeSet([((), 9_u32)].into()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -87,18 +76,10 @@ fn insert_relevant_txs() {
|
||||
changeset,
|
||||
);
|
||||
|
||||
// The initial changeset will also contain info about the keychain we added
|
||||
let initial_changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: changeset.graph,
|
||||
indexer: keychain::ChangeSet {
|
||||
last_revealed: changeset.indexer.last_revealed,
|
||||
keychains_added: [((), descriptor)].into(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(graph.initial_changeset(), initial_changeset);
|
||||
assert_eq!(graph.initial_changeset(), changeset,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
|
||||
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
|
||||
///
|
||||
@@ -126,7 +107,7 @@ fn insert_relevant_txs() {
|
||||
///
|
||||
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
|
||||
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
|
||||
#[test]
|
||||
|
||||
fn test_list_owned_txouts() {
|
||||
// Create Local chains
|
||||
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
|
||||
@@ -134,25 +115,15 @@ fn test_list_owned_txouts() {
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
let (desc_1, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[2]).unwrap();
|
||||
let (desc_2, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
|
||||
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
let mut graph =
|
||||
IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::default();
|
||||
|
||||
assert!(!graph
|
||||
.index
|
||||
.insert_descriptor("keychain_1".into(), desc_1)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(!graph
|
||||
.index
|
||||
.insert_descriptor("keychain_2".into(), desc_2)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
graph.index.add_keychain("keychain_1".into(), desc_1);
|
||||
graph.index.add_keychain("keychain_2".into(), desc_2);
|
||||
graph.index.set_lookahead_for_all(10);
|
||||
|
||||
// Get trusted and untrusted addresses
|
||||
|
||||
@@ -162,20 +133,14 @@ fn test_list_owned_txouts() {
|
||||
{
|
||||
// we need to scope here to take immutanble reference of the graph
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_1".to_string())
|
||||
.unwrap();
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
|
||||
// TODO Assert indexes
|
||||
trusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
{
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_2".to_string())
|
||||
.unwrap();
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
|
||||
untrusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -189,7 +154,7 @@ fn test_list_owned_txouts() {
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(70000),
|
||||
value: 70000,
|
||||
script_pubkey: trusted_spks[0].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
@@ -198,7 +163,7 @@ fn test_list_owned_txouts() {
|
||||
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
||||
let tx2 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(30000),
|
||||
value: 30000,
|
||||
script_pubkey: untrusted_spks[0].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
@@ -207,11 +172,11 @@ fn test_list_owned_txouts() {
|
||||
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
|
||||
let tx3 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx2.compute_txid(), 0),
|
||||
previous_output: OutPoint::new(tx2.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(10000),
|
||||
value: 10000,
|
||||
script_pubkey: trusted_spks[1].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
@@ -220,7 +185,7 @@ fn test_list_owned_txouts() {
|
||||
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
||||
let tx4 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(20000),
|
||||
value: 20000,
|
||||
script_pubkey: untrusted_spks[1].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
@@ -229,7 +194,7 @@ fn test_list_owned_txouts() {
|
||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
||||
let tx5 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(15000),
|
||||
value: 15000,
|
||||
script_pubkey: trusted_spks[2].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
@@ -247,8 +212,10 @@ fn test_list_owned_txouts() {
|
||||
(
|
||||
*tx,
|
||||
local_chain
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.cloned()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: anchor_block.height,
|
||||
@@ -263,8 +230,9 @@ fn test_list_owned_txouts() {
|
||||
|height: u32,
|
||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
||||
let chain_tip = local_chain
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||
let txouts = graph
|
||||
.graph()
|
||||
@@ -359,31 +327,25 @@ fn test_list_owned_txouts() {
|
||||
balance,
|
||||
) = fetch(0, &graph);
|
||||
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[
|
||||
tx2.compute_txid(),
|
||||
tx3.compute_txid(),
|
||||
tx4.compute_txid(),
|
||||
tx5.compute_txid()
|
||||
]
|
||||
.into()
|
||||
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -399,29 +361,26 @@ fn test_list_owned_txouts() {
|
||||
) = fetch(1, &graph);
|
||||
|
||||
// tx2 gets into confirmed txout set
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.compute_txid(), tx2.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
// tx2 doesn't get into confirmed utxos set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -439,30 +398,21 @@ fn test_list_owned_txouts() {
|
||||
// tx3 now gets into the confirmed txout set
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
// tx3 also gets into confirmed utxo set
|
||||
assert_eq!(
|
||||
confirmed_utxos_txid,
|
||||
[tx1.compute_txid(), tx3.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(10000) // tx3 got confirmed
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx3 got confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -479,30 +429,21 @@ fn test_list_owned_txouts() {
|
||||
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(
|
||||
confirmed_utxos_txid,
|
||||
[tx1.compute_txid(), tx3.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
// Coinbase is still immature
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(10000) // tx1 got matured
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx1 got matured
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -515,10 +456,10 @@ fn test_list_owned_txouts() {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::ZERO, // coinbase matured
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(80000) // tx1 + tx3
|
||||
immature: 0, // coinbase matured
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 80000 // tx1 + tx3
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,43 +5,34 @@ mod common;
|
||||
use bdk_chain::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::Indexer,
|
||||
keychain::{self, ChangeSet, KeychainTxOutIndex},
|
||||
Append, DescriptorExt, DescriptorId,
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
Append,
|
||||
};
|
||||
|
||||
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
use crate::common::DESCRIPTORS;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
enum TestKeychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
fn parse_descriptor(descriptor: &str) -> Descriptor<DescriptorPublicKey> {
|
||||
fn init_txout_index() -> (
|
||||
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
) {
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::default();
|
||||
|
||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, descriptor)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
|
||||
fn init_txout_index(
|
||||
external_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
internal_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
lookahead: u32,
|
||||
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::External, external_descriptor)
|
||||
.unwrap();
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::Internal, internal_descriptor)
|
||||
.unwrap();
|
||||
|
||||
txout_index
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
}
|
||||
|
||||
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
|
||||
@@ -51,219 +42,119 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
|
||||
.script_pubkey()
|
||||
}
|
||||
|
||||
// We create two empty changesets lhs and rhs, we then insert various descriptors with various
|
||||
// last_revealed, append rhs to lhs, and check that the result is consistent with these rules:
|
||||
// - Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
// - Existing index updates if the new index in `other` is higher than `self`.
|
||||
// - Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
// - New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
#[test]
|
||||
fn append_changesets_check_last_revealed() {
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let descriptor_ids: Vec<_> = DESCRIPTORS
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|d| {
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, d)
|
||||
.unwrap()
|
||||
.0
|
||||
.descriptor_id()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut lhs_di = BTreeMap::<DescriptorId, u32>::default();
|
||||
let mut rhs_di = BTreeMap::<DescriptorId, u32>::default();
|
||||
lhs_di.insert(descriptor_ids[0], 7);
|
||||
lhs_di.insert(descriptor_ids[1], 0);
|
||||
lhs_di.insert(descriptor_ids[2], 3);
|
||||
|
||||
rhs_di.insert(descriptor_ids[0], 3); // value less than lhs desc 0
|
||||
rhs_di.insert(descriptor_ids[1], 5); // value more than lhs desc 1
|
||||
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
|
||||
|
||||
let mut lhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: lhs_di,
|
||||
};
|
||||
let rhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: rhs_di,
|
||||
};
|
||||
lhs.append(rhs);
|
||||
|
||||
// Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[0]), Some(&7));
|
||||
// Existing index updates if the new index in `other` is higher than `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[1]), Some(&5));
|
||||
// Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[2]), Some(&3));
|
||||
// New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_apply_contradictory_changesets_they_are_ignored() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_all_derivation_indices() {
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
let (mut txout_index, _, _) = init_txout_index();
|
||||
let derive_to: BTreeMap<_, _> =
|
||||
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
|
||||
let last_revealed: BTreeMap<_, _> = [
|
||||
(external_descriptor.descriptor_id(), 12),
|
||||
(internal_descriptor.descriptor_id(), 24),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to),
|
||||
ChangeSet {
|
||||
keychains_added: BTreeMap::new(),
|
||||
last_revealed: last_revealed.clone()
|
||||
}
|
||||
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
|
||||
&derive_to
|
||||
);
|
||||
assert_eq!(txout_index.last_revealed_indices(), derive_to);
|
||||
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to),
|
||||
txout_index.reveal_to_target_multi(&derive_to).1,
|
||||
keychain::ChangeSet::default(),
|
||||
"no changes if we set to the same thing"
|
||||
);
|
||||
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
|
||||
assert_eq!(txout_index.initial_changeset().as_inner(), &derive_to);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookahead() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
|
||||
let (mut txout_index, external_desc, internal_desc) = init_txout_index();
|
||||
|
||||
// ensure it does not break anything if lookahead is set multiple times
|
||||
(0..=10).for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::External, lookahead));
|
||||
(0..=20)
|
||||
.filter(|v| v % 2 == 0)
|
||||
.for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::Internal, lookahead));
|
||||
|
||||
assert_eq!(txout_index.inner().all_spks().len(), 30);
|
||||
|
||||
// given:
|
||||
// - external lookahead set to 10
|
||||
// - internal lookahead set to 20
|
||||
// when:
|
||||
// - set external derivation index to value higher than last, but within the lookahead value
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly
|
||||
// - stored scripts of external keychain should be of expected counts
|
||||
for index in (0..20).skip_while(|i| i % 2 == 1) {
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, index)
|
||||
.unwrap();
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, index);
|
||||
assert_eq!(
|
||||
revealed_spks,
|
||||
vec![(index, spk_at_index(&external_descriptor, index))],
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
vec![(index, spk_at_index(&external_desc, index))],
|
||||
);
|
||||
assert_eq!(
|
||||
&revealed_changeset.last_revealed,
|
||||
&[(external_descriptor.descriptor_id(), index)].into()
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::External, index)].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.inner().all_spks().len(),
|
||||
10 /* external lookahead */ +
|
||||
10 /* internal lookahead */ +
|
||||
20 /* internal lookahead */ +
|
||||
index as usize + 1 /* `derived` count */
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.revealed_spks_of_keychain(&TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::External)
|
||||
.unused_spks_of_keychain(&TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::Internal)
|
||||
.unused_spks_of_keychain(&TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// given:
|
||||
// - internal lookahead is 10
|
||||
// - internal lookahead is 20
|
||||
// - internal derivation index is `None`
|
||||
// when:
|
||||
// - derivation index is set ahead of current derivation index + lookahead
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::Internal, 24)
|
||||
.unwrap();
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
|
||||
assert_eq!(
|
||||
revealed_spks,
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
(0..=24)
|
||||
.map(|index| (index, spk_at_index(&internal_descriptor, index)))
|
||||
.map(|index| (index, spk_at_index(&internal_desc, index)))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
&revealed_changeset.last_revealed,
|
||||
&[(internal_descriptor.descriptor_id(), 24)].into()
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::Internal, 24)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.inner().all_spks().len(),
|
||||
10 /* external lookahead */ +
|
||||
10 /* internal lookahead */ +
|
||||
20 /* internal lookahead */ +
|
||||
20 /* external stored index count */ +
|
||||
25 /* internal stored index count */
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
||||
.count(),
|
||||
25,
|
||||
);
|
||||
@@ -288,18 +179,18 @@ fn test_lookahead() {
|
||||
let tx = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
script_pubkey: external_descriptor
|
||||
script_pubkey: external_desc
|
||||
.at_derivation_index(external_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: Amount::from_sat(10_000),
|
||||
value: 10_000,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: internal_descriptor
|
||||
script_pubkey: internal_desc
|
||||
.at_derivation_index(internal_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: Amount::from_sat(10_000),
|
||||
value: 10_000,
|
||||
},
|
||||
],
|
||||
..common::new_tx(external_index)
|
||||
@@ -315,13 +206,13 @@ fn test_lookahead() {
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.revealed_spks_of_keychain(&TestKeychain::External)
|
||||
.count(),
|
||||
last_external_index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
||||
.count(),
|
||||
last_internal_index as usize + 1,
|
||||
);
|
||||
@@ -335,17 +226,15 @@ fn test_lookahead() {
|
||||
// - last used index should change as expected
|
||||
#[test]
|
||||
fn test_scan_with_lookahead() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
|
||||
let (mut txout_index, external_desc, _) = init_txout_index();
|
||||
txout_index.set_lookahead_for_all(10);
|
||||
|
||||
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
external_descriptor
|
||||
external_desc
|
||||
.at_derivation_index(i)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
@@ -357,13 +246,13 @@ fn test_scan_with_lookahead() {
|
||||
let op = OutPoint::new(h!("fake tx"), spk_i);
|
||||
let txout = TxOut {
|
||||
script_pubkey: spk.clone(),
|
||||
value: Amount::ZERO,
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let changeset = txout_index.index_txout(op, &txout);
|
||||
assert_eq!(
|
||||
&changeset.last_revealed,
|
||||
&[(external_descriptor.descriptor_id(), spk_i)].into()
|
||||
changeset.as_inner(),
|
||||
&[(TestKeychain::External, spk_i)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
@@ -376,14 +265,14 @@ fn test_scan_with_lookahead() {
|
||||
}
|
||||
|
||||
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
|
||||
let spk_41 = external_descriptor
|
||||
let spk_41 = external_desc
|
||||
.at_derivation_index(41)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
let op = OutPoint::new(h!("fake tx"), 41);
|
||||
let txout = TxOut {
|
||||
script_pubkey: spk_41,
|
||||
value: Amount::ZERO,
|
||||
value: 0,
|
||||
};
|
||||
let changeset = txout_index.index_txout(op, &txout);
|
||||
assert!(changeset.is_empty());
|
||||
@@ -392,13 +281,11 @@ fn test_scan_with_lookahead() {
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_wildcard_derivations() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index = init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
let external_spk_0 = external_descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_descriptor.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_26 = external_descriptor.at_derivation_index(26).unwrap().script_pubkey();
|
||||
let external_spk_27 = external_descriptor.at_derivation_index(27).unwrap().script_pubkey();
|
||||
let (mut txout_index, external_desc, _) = init_txout_index();
|
||||
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
|
||||
let external_spk_27 = external_desc.at_derivation_index(27).unwrap().script_pubkey();
|
||||
|
||||
// - nothing is derived
|
||||
// - unused list is also empty
|
||||
@@ -406,13 +293,13 @@ fn test_wildcard_derivations() {
|
||||
// - next_derivation_index() == (0, true)
|
||||
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// - derived till 25
|
||||
// - used all spks till 15.
|
||||
@@ -426,45 +313,42 @@ fn test_wildcard_derivations() {
|
||||
|
||||
(0..=15)
|
||||
.chain([17, 20, 23])
|
||||
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
|
||||
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
|
||||
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (26, external_spk_26));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (26, external_spk_26.as_script()));
|
||||
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (16, external_spk_16));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (16, external_spk_16.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// - Use all the derived till 26.
|
||||
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
|
||||
(0..=26).for_each(|index| {
|
||||
txout_index.mark_used(TestKeychain::External, index);
|
||||
txout_index.mark_used(&TestKeychain::External, index);
|
||||
});
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (27, external_spk_27));
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (27, external_spk_27.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_wildcard_derivations() {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
|
||||
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (no_wildcard_descriptor, _) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[6]).unwrap();
|
||||
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||
let external_spk = no_wildcard_descriptor
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::External, no_wildcard_descriptor.clone())
|
||||
.unwrap();
|
||||
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
|
||||
|
||||
// given:
|
||||
// - `txout_index` with no stored scripts
|
||||
@@ -472,24 +356,14 @@ fn test_non_wildcard_derivations() {
|
||||
// - next derivation index should be new
|
||||
// - when we derive a new script, script @ index 0
|
||||
// - when we get the next unused script, script @ index 0
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
(0, true)
|
||||
);
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(
|
||||
&changeset.last_revealed,
|
||||
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
|
||||
);
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// given:
|
||||
// - the non-wildcard descriptor already has a stored and used script
|
||||
@@ -497,267 +371,26 @@ fn test_non_wildcard_derivations() {
|
||||
// - next derivation index should not be new
|
||||
// - derive new and next unused should return the old script
|
||||
// - store_up_to should not panic and return empty changeset
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
(0, false)
|
||||
);
|
||||
txout_index.mark_used(TestKeychain::External, 0);
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
|
||||
txout_index.mark_used(&TestKeychain::External, 0);
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, 200)
|
||||
.unwrap();
|
||||
assert_eq!(revealed_spks.len(), 0);
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, 200);
|
||||
assert_eq!(revealed_spks.count(), 0);
|
||||
assert!(revealed_changeset.is_empty());
|
||||
|
||||
// we check that spks_of_keychain returns a SpkIterator with just one element
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.spks_of_keychain(&TestKeychain::External)
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that calling `lookahead_to_target` stores the expected spks.
|
||||
#[test]
|
||||
fn lookahead_to_target() {
|
||||
#[derive(Default)]
|
||||
struct TestCase {
|
||||
/// Global lookahead value.
|
||||
lookahead: u32,
|
||||
/// Last revealed index for external keychain.
|
||||
external_last_revealed: Option<u32>,
|
||||
/// Last revealed index for internal keychain.
|
||||
internal_last_revealed: Option<u32>,
|
||||
/// Call `lookahead_to_target(External, u32)`.
|
||||
external_target: Option<u32>,
|
||||
/// Call `lookahead_to_target(Internal, u32)`.
|
||||
internal_target: Option<u32>,
|
||||
}
|
||||
|
||||
let test_cases = &[
|
||||
TestCase {
|
||||
lookahead: 0,
|
||||
external_target: Some(100),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 10,
|
||||
internal_target: Some(99),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 100,
|
||||
internal_target: Some(9),
|
||||
external_target: Some(10),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 12,
|
||||
external_last_revealed: Some(2),
|
||||
internal_last_revealed: Some(2),
|
||||
internal_target: Some(15),
|
||||
external_target: Some(13),
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 13,
|
||||
external_last_revealed: Some(100),
|
||||
internal_last_revealed: Some(21),
|
||||
internal_target: Some(120),
|
||||
external_target: Some(130),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut index = init_txout_index(
|
||||
external_descriptor.clone(),
|
||||
internal_descriptor.clone(),
|
||||
t.lookahead,
|
||||
);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||
}
|
||||
if let Some(last_revealed) = t.internal_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
|
||||
}
|
||||
|
||||
let keychain_test_cases = [
|
||||
(
|
||||
TestKeychain::External,
|
||||
t.external_last_revealed,
|
||||
t.external_target,
|
||||
),
|
||||
(
|
||||
TestKeychain::Internal,
|
||||
t.internal_last_revealed,
|
||||
t.internal_target,
|
||||
),
|
||||
];
|
||||
for (keychain, last_revealed, target) in keychain_test_cases {
|
||||
if let Some(target) = target {
|
||||
let original_last_stored_index = match last_revealed {
|
||||
Some(last_revealed) => Some(last_revealed + t.lookahead),
|
||||
None => t.lookahead.checked_sub(1),
|
||||
};
|
||||
let exp_last_stored_index = match original_last_stored_index {
|
||||
Some(original_last_stored_index) => {
|
||||
Ord::max(target, original_last_stored_index)
|
||||
}
|
||||
None => target,
|
||||
};
|
||||
index.lookahead_to_target(&keychain, target);
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let exp_keys = core::iter::repeat(keychain)
|
||||
.zip(0_u32..=exp_last_stored_index)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys, exp_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_descriptor_no_change() {
|
||||
let secp = Secp256k1::signing_only();
|
||||
let (desc, _) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
|
||||
let mut txout_index = KeychainTxOutIndex::<()>::default();
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
Ok(keychain::ChangeSet {
|
||||
keychains_added: [((), desc.clone())].into(),
|
||||
last_revealed: Default::default()
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
Ok(keychain::ChangeSet::default()),
|
||||
"inserting the same descriptor for keychain should return an empty changeset",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
|
||||
let desc = parse_descriptor(DESCRIPTORS[0]);
|
||||
let changesets: &[ChangeSet<TestKeychain>] = &[
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
},
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, desc.clone())].into(),
|
||||
last_revealed: [(desc.descriptor_id(), 12)].into(),
|
||||
},
|
||||
];
|
||||
|
||||
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
for changeset in changesets {
|
||||
indexer_a.apply_changeset(changeset.clone());
|
||||
}
|
||||
|
||||
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let aggregate_changesets = changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.reduce(|mut agg, cs| {
|
||||
agg.append(cs);
|
||||
agg
|
||||
})
|
||||
.expect("must aggregate changesets");
|
||||
indexer_b.apply_changeset(aggregate_changesets);
|
||||
|
||||
assert_eq!(
|
||||
indexer_a.keychains().collect::<Vec<_>>(),
|
||||
indexer_b.keychains().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.spk_at_index(TestKeychain::External, 0),
|
||||
indexer_b.spk_at_index(TestKeychain::External, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.spk_at_index(TestKeychain::Internal, 0),
|
||||
indexer_b.spk_at_index(TestKeychain::Internal, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
indexer_a.last_revealed_indices(),
|
||||
indexer_b.last_revealed_indices()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigning_same_descriptor_to_multiple_keychains_should_error() {
|
||||
let desc = parse_descriptor(DESCRIPTORS[0]);
|
||||
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let _ = indexer
|
||||
.insert_descriptor(TestKeychain::Internal, desc.clone())
|
||||
.unwrap();
|
||||
assert!(indexer
|
||||
.insert_descriptor(TestKeychain::External, desc)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reassigning_keychain_to_a_new_descriptor_should_error() {
|
||||
let desc1 = parse_descriptor(DESCRIPTORS[0]);
|
||||
let desc2 = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
let _ = indexer.insert_descriptor(TestKeychain::Internal, desc1);
|
||||
assert!(indexer
|
||||
.insert_descriptor(TestKeychain::Internal, desc2)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_querying_over_a_range_of_keychains_the_utxos_should_show_up() {
|
||||
let mut indexer = KeychainTxOutIndex::<usize>::new(0);
|
||||
let mut tx = common::new_tx(0);
|
||||
|
||||
for (i, descriptor) in DESCRIPTORS.iter().enumerate() {
|
||||
let descriptor = parse_descriptor(descriptor);
|
||||
let _ = indexer.insert_descriptor(i, descriptor.clone()).unwrap();
|
||||
if i != 4 {
|
||||
// skip one in the middle to see if uncovers any bugs
|
||||
indexer.reveal_next_spk(&i);
|
||||
}
|
||||
tx.output.push(TxOut {
|
||||
script_pubkey: descriptor.at_derivation_index(0).unwrap().script_pubkey(),
|
||||
value: Amount::from_sat(10_000),
|
||||
});
|
||||
}
|
||||
|
||||
let n_spks = DESCRIPTORS.len() - /*we skipped one*/ 1;
|
||||
|
||||
let _ = indexer.index_tx(&tx);
|
||||
assert_eq!(indexer.outpoints().len(), n_spks);
|
||||
|
||||
assert_eq!(indexer.revealed_spks(0..DESCRIPTORS.len()).count(), n_spks);
|
||||
assert_eq!(indexer.revealed_spks(1..4).count(), 4 - 1);
|
||||
assert_eq!(
|
||||
indexer.net_value(&tx, 0..DESCRIPTORS.len()).to_sat(),
|
||||
(10_000 * n_spks) as i64
|
||||
);
|
||||
assert_eq!(
|
||||
indexer.net_value(&tx, 3..6).to_sat(),
|
||||
(10_000 * (6 - 3 - /*the skipped one*/ 1)) as i64
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
use bdk_chain::{
|
||||
local_chain::{
|
||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||
LocalChain, MissingGenesisError,
|
||||
},
|
||||
BlockId,
|
||||
use bdk_chain::local_chain::{
|
||||
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
|
||||
};
|
||||
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||
use proptest::prelude::*;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
@@ -19,7 +10,7 @@ mod common;
|
||||
struct TestLocalChain<'a> {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
update: CheckPoint,
|
||||
update: Update,
|
||||
exp: ExpectedResult<'a>,
|
||||
}
|
||||
|
||||
@@ -297,27 +288,6 @@ fn update_local_chain() {
|
||||
],
|
||||
},
|
||||
},
|
||||
// Allow update that is shorter than original chain
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A C D E F
|
||||
// update | A C D'
|
||||
TestLocalChain {
|
||||
name: "allow update that is shorter than original chain",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D")), (4, h!("E")), (5, h!("F"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("C")), (3, h!("D'"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(3, Some(h!("D'"))),
|
||||
(4, None),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D'"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(TestLocalChain::run);
|
||||
@@ -380,469 +350,3 @@ fn local_chain_insert_block() {
|
||||
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_disconnect_from() {
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
original: LocalChain,
|
||||
disconnect_from: (u32, BlockHash),
|
||||
exp_result: Result<ChangeSet, MissingGenesisError>,
|
||||
exp_final: LocalChain,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "try_replace_genesis_should_fail",
|
||||
original: local_chain![(0, h!("_"))],
|
||||
disconnect_from: (0, h!("_")),
|
||||
exp_result: Err(MissingGenesisError),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "try_replace_genesis_should_fail_2",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
disconnect_from: (0, h!("_")),
|
||||
exp_result: Err(MissingGenesisError),
|
||||
exp_final: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "from_does_not_exist",
|
||||
original: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::default()),
|
||||
exp_final: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "from_has_different_blockhash",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
disconnect_from: (2, h!("not_B")),
|
||||
exp_result: Ok(ChangeSet::default()),
|
||||
exp_final: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "disconnect_one",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::from_iter([(2, None)])),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "disconnect_three",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::from_iter([(2, None), (3, None), (4, None)])),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
|
||||
let mut chain = t.original;
|
||||
let result = chain.disconnect_from(t.disconnect_from.into());
|
||||
assert_eq!(
|
||||
result, t.exp_result,
|
||||
"[{}:{}] unexpected changeset result",
|
||||
i, t.name
|
||||
);
|
||||
assert_eq!(
|
||||
chain, t.exp_final,
|
||||
"[{}:{}] unexpected final chain",
|
||||
i, t.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_from_block_ids() {
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
blocks: &'a [(u32, BlockHash)],
|
||||
exp_result: Result<(), Option<(u32, BlockHash)>>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "in_order",
|
||||
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
TestCase {
|
||||
name: "with_duplicates",
|
||||
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
|
||||
exp_result: Err(Some((2, h!("C")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "not_in_order",
|
||||
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
|
||||
exp_result: Err(Some((3, h!("D")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "empty",
|
||||
blocks: &[],
|
||||
exp_result: Err(None),
|
||||
},
|
||||
TestCase {
|
||||
name: "single",
|
||||
blocks: &[(21, h!("million"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let result = CheckPoint::from_block_ids(
|
||||
t.blocks
|
||||
.iter()
|
||||
.map(|&(height, hash)| BlockId { height, hash }),
|
||||
);
|
||||
match t.exp_result {
|
||||
Ok(_) => {
|
||||
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
|
||||
let result_vec = {
|
||||
let mut v = result
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<Vec<_>>();
|
||||
v.reverse();
|
||||
v
|
||||
};
|
||||
assert_eq!(
|
||||
&result_vec, t.blocks,
|
||||
"[{}:{}] not equal to original block ids",
|
||||
i, t.name
|
||||
);
|
||||
}
|
||||
Err(exp_last) => {
|
||||
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(
|
||||
err.as_ref()
|
||||
.map(|last_cp| (last_cp.height(), last_cp.hash())),
|
||||
exp_last,
|
||||
"[{}:{}] error's last cp height should be {:?}, got {:?}",
|
||||
i,
|
||||
t.name,
|
||||
exp_last,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_query() {
|
||||
struct TestCase {
|
||||
chain: LocalChain,
|
||||
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
|
||||
/// range.
|
||||
///
|
||||
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
|
||||
/// it. If not, [`CheckPoint::query`] should return `None`.
|
||||
query_range: (u32, u32),
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||
query_range: (0, 2),
|
||||
},
|
||||
TestCase {
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
query_range: (0, 3),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases.into_iter() {
|
||||
let tip = t.chain.tip();
|
||||
for h in t.query_range.0..=t.query_range.1 {
|
||||
let query_result = tip.get(h);
|
||||
|
||||
// perform an exhausitive search for the checkpoint at height `h`
|
||||
let exp_hash = t
|
||||
.chain
|
||||
.iter_checkpoints()
|
||||
.find(|cp| cp.height() == h)
|
||||
.map(|cp| cp.hash());
|
||||
|
||||
match query_result {
|
||||
Some(cp) => {
|
||||
assert_eq!(Some(cp.hash()), exp_hash);
|
||||
assert_eq!(cp.height(), h);
|
||||
}
|
||||
None => assert!(exp_hash.is_none()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_insert() {
|
||||
struct TestCase<'a> {
|
||||
/// The name of the test.
|
||||
name: &'a str,
|
||||
/// The original checkpoint chain to call [`CheckPoint::insert`] on.
|
||||
chain: &'a [(u32, BlockHash)],
|
||||
/// The `block_id` to insert.
|
||||
to_insert: (u32, BlockHash),
|
||||
/// The expected final checkpoint chain after calling [`CheckPoint::insert`].
|
||||
exp_final_chain: &'a [(u32, BlockHash)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "insert_above_tip",
|
||||
chain: &[(1, h!("a")), (2, h!("b"))],
|
||||
to_insert: (4, h!("d")),
|
||||
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (4, h!("d"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_already_exists_expect_no_change",
|
||||
chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
||||
to_insert: (2, h!("b")),
|
||||
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_in_middle",
|
||||
chain: &[(2, h!("b")), (4, h!("d")), (5, h!("e"))],
|
||||
to_insert: (3, h!("c")),
|
||||
exp_final_chain: &[(2, h!("b")), (3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "replace_one",
|
||||
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
||||
to_insert: (5, h!("E")),
|
||||
exp_final_chain: &[(3, h!("c")), (4, h!("d")), (5, h!("E"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "insert_conflict_should_evict",
|
||||
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e")), (6, h!("f"))],
|
||||
to_insert: (4, h!("D")),
|
||||
exp_final_chain: &[(3, h!("c")), (4, h!("D"))],
|
||||
},
|
||||
];
|
||||
|
||||
fn genesis_block() -> impl Iterator<Item = BlockId> {
|
||||
core::iter::once((0, h!("_"))).map(BlockId::from)
|
||||
}
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Running [{}] '{}'", i, t.name);
|
||||
|
||||
let chain = CheckPoint::from_block_ids(
|
||||
genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
|
||||
)
|
||||
.expect("test formed incorrectly, must construct checkpoint chain");
|
||||
|
||||
let exp_final_chain = CheckPoint::from_block_ids(
|
||||
genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
|
||||
)
|
||||
.expect("test formed incorrectly, must construct checkpoint chain");
|
||||
|
||||
assert_eq!(
|
||||
chain.insert(t.to_insert.into()),
|
||||
exp_final_chain,
|
||||
"unexpected final chain"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_apply_header_connected_to() {
|
||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||
Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash,
|
||||
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
|
||||
time: 0,
|
||||
bits: bitcoin::CompactTarget::default(),
|
||||
nonce: 0,
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
header: Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("_"));
|
||||
let hash = header.block_hash();
|
||||
let height = 1;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "connected_to_self_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (height, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let hash = header.block_hash();
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: prev_hash,
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_prev_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![(height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(BlockHash::all_zeros());
|
||||
let hash = header.block_hash();
|
||||
let height = 0;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "genesis_applied_to_self",
|
||||
chain: local_chain![(0, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("Z"));
|
||||
let height = 10;
|
||||
let hash = header.block_hash();
|
||||
let prev_height = height - 1;
|
||||
let prev_hash = header.prev_blockhash;
|
||||
TestCase {
|
||||
name: "connect_at_connected_to",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
header,
|
||||
height: 10,
|
||||
connected_to: BlockId {
|
||||
height: 3,
|
||||
hash: h!("C"),
|
||||
},
|
||||
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: h!("not_prev_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_prev_hash",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
|
||||
header,
|
||||
height: prev_height + 1,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height,
|
||||
hash: h!("not_current_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_current_block",
|
||||
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("B"));
|
||||
let height = 3;
|
||||
let connected_to = BlockId {
|
||||
height: 4,
|
||||
hash: h!("D"),
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_is_greater",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
|
||||
let exp_result = t
|
||||
.exp_result
|
||||
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
|
||||
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_height_range_bounds(
|
||||
height_upper_bound: u32,
|
||||
) -> impl Strategy<Value = (Bound<u32>, Bound<u32>)> {
|
||||
fn generate_height_bound(height_upper_bound: u32) -> impl Strategy<Value = Bound<u32>> {
|
||||
prop_oneof![
|
||||
(0..height_upper_bound).prop_map(Bound::Included),
|
||||
(0..height_upper_bound).prop_map(Bound::Excluded),
|
||||
Just(Bound::Unbounded),
|
||||
]
|
||||
}
|
||||
(
|
||||
generate_height_bound(height_upper_bound),
|
||||
generate_height_bound(height_upper_bound),
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
|
||||
proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
|
||||
heights.insert(0); // must have genesis
|
||||
CheckPoint::from_block_ids(heights.into_iter().map(|height| {
|
||||
let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
|
||||
BlockId { height, hash }
|
||||
}))
|
||||
.expect("blocks must be in order as it comes from btreeset")
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig {
|
||||
..Default::default()
|
||||
})]
|
||||
|
||||
/// Ensure that [`CheckPoint::range`] returns the expected checkpoint heights by comparing it
|
||||
/// against a more primitive approach.
|
||||
#[test]
|
||||
fn checkpoint_range(
|
||||
range in generate_height_range_bounds(21_000),
|
||||
cp in generate_checkpoints(21_000, 2100)
|
||||
) {
|
||||
let exp_heights = cp.iter().map(|cp| cp.height()).filter(|h| range.contains(h)).collect::<Vec<u32>>();
|
||||
let heights = cp.range(range).map(|cp| cp.height()).collect::<Vec<u32>>();
|
||||
prop_assert_eq!(heights, exp_heights);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
|
||||
};
|
||||
use bitcoin::{absolute, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
|
||||
#[test]
|
||||
fn spk_txout_sent_and_received() {
|
||||
@@ -13,70 +11,48 @@ fn spk_txout_sent_and_received() {
|
||||
index.insert_spk(1, spk2.clone());
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: transaction::Version::TWO,
|
||||
version: 0x02,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(42_000),
|
||||
value: 42_000,
|
||||
script_pubkey: spk1.clone(),
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, ..),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, ..1),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, 1..),
|
||||
(Amount::from_sat(0), Amount::from_sat(0))
|
||||
);
|
||||
assert_eq!(index.net_value(&tx1, ..), SignedAmount::from_sat(42_000));
|
||||
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
|
||||
assert_eq!(index.net_value(&tx1), 42_000);
|
||||
index.index_tx(&tx1);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1, ..),
|
||||
(Amount::from_sat(0), Amount::from_sat(42_000)),
|
||||
index.sent_and_received(&tx1),
|
||||
(0, 42_000),
|
||||
"shouldn't change after scanning"
|
||||
);
|
||||
|
||||
let tx2 = Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
version: 0x1,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx1.compute_txid(),
|
||||
txid: tx1.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: Amount::from_sat(20_000),
|
||||
value: 20_000,
|
||||
script_pubkey: spk2,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: spk1,
|
||||
value: Amount::from_sat(30_000),
|
||||
value: 30_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, ..),
|
||||
(Amount::from_sat(42_000), Amount::from_sat(50_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, ..1),
|
||||
(Amount::from_sat(42_000), Amount::from_sat(30_000))
|
||||
);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx2, 1..),
|
||||
(Amount::from_sat(0), Amount::from_sat(20_000))
|
||||
);
|
||||
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
|
||||
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
|
||||
assert_eq!(index.net_value(&tx2), 8_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -97,11 +73,11 @@ fn mark_used() {
|
||||
assert!(spk_index.is_used(&1));
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: transaction::Version::TWO,
|
||||
version: 0x02,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(42_000),
|
||||
value: 42_000,
|
||||
script_pubkey: spk1,
|
||||
}],
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use bdk_chain::{keychain::Balance, BlockId};
|
||||
use bitcoin::{Amount, OutPoint, Script};
|
||||
use bitcoin::{OutPoint, Script};
|
||||
use common::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -81,10 +79,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(20000),
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -112,15 +110,14 @@ fn test_tx_conflict_handling() {
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// the txgraph is going to pick tx_conflict_2 because of higher lexicographical txid
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -152,10 +149,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -194,10 +191,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(40000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 40000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -229,10 +226,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -264,10 +261,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(20000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 20000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -313,10 +310,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -358,10 +355,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -399,10 +396,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(20000),
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -444,10 +441,10 @@ fn test_tx_conflict_handling() {
|
||||
]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -489,10 +486,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::from_sat(30000),
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::ZERO,
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -534,10 +531,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
@@ -585,10 +582,10 @@ fn test_tx_conflict_handling() {
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: Amount::ZERO,
|
||||
trusted_pending: Amount::ZERO,
|
||||
untrusted_pending: Amount::ZERO,
|
||||
confirmed: Amount::from_sat(50000),
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.15.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,9 +12,6 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.16.0" }
|
||||
electrum-client = { version = "0.20" }
|
||||
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
|
||||
electrum-client = { version = "0.18" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# BDK Electrum
|
||||
|
||||
BDK Electrum extends [`electrum-client`] to update [`bdk_chain`] structures
|
||||
from an Electrum server.
|
||||
|
||||
[`electrum-client`]: https://docs.rs/electrum-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
BDK Electrum client library for updating the keychain tracker.
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
local_chain::CheckPoint,
|
||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||
tx_graph::TxGraph,
|
||||
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
use core::str::FromStr;
|
||||
use electrum_client::{ElectrumApi, Error, HeaderNotification};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// We include a chain suffix of a certain length for the purpose of robustness.
|
||||
const CHAIN_SUFFIX_LENGTH: u32 = 8;
|
||||
|
||||
/// Wrapper around an [`electrum_client::ElectrumApi`] which includes an internal in-memory
|
||||
/// transaction cache to avoid re-fetching already downloaded transactions.
|
||||
#[derive(Debug)]
|
||||
pub struct BdkElectrumClient<E> {
|
||||
/// The internal [`electrum_client::ElectrumApi`]
|
||||
pub inner: E,
|
||||
/// The transaction cache
|
||||
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
|
||||
}
|
||||
|
||||
impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
/// Creates a new bdk client from a [`electrum_client::ElectrumApi`]
|
||||
pub fn new(client: E) -> Self {
|
||||
Self {
|
||||
inner: client,
|
||||
tx_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts transactions into the transaction cache so that the client will not fetch these
|
||||
/// transactions.
|
||||
pub fn populate_tx_cache<A>(&self, tx_graph: impl AsRef<TxGraph<A>>) {
|
||||
let txs = tx_graph
|
||||
.as_ref()
|
||||
.full_txs()
|
||||
.map(|tx_node| (tx_node.txid, tx_node.tx));
|
||||
|
||||
let mut tx_cache = self.tx_cache.lock().unwrap();
|
||||
for (txid, tx) in txs {
|
||||
tx_cache.insert(txid, tx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch transaction of given `txid`.
|
||||
///
|
||||
/// If it hits the cache it will return the cached version and avoid making the request.
|
||||
pub fn fetch_tx(&self, txid: Txid) -> Result<Arc<Transaction>, Error> {
|
||||
let tx_cache = self.tx_cache.lock().unwrap();
|
||||
|
||||
if let Some(tx) = tx_cache.get(&txid) {
|
||||
return Ok(Arc::clone(tx));
|
||||
}
|
||||
|
||||
drop(tx_cache);
|
||||
|
||||
let tx = Arc::new(self.inner.transaction_get(&txid)?);
|
||||
|
||||
self.tx_cache.lock().unwrap().insert(txid, Arc::clone(&tx));
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// Broadcasts a transaction to the network.
|
||||
///
|
||||
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
|
||||
pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
|
||||
self.inner.transaction_broadcast(tx)
|
||||
}
|
||||
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
|
||||
/// returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
|
||||
/// see [`FullScanRequest`]
|
||||
/// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
|
||||
/// associated transactions
|
||||
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request
|
||||
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
|
||||
pub fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumFullScanResult<K>, Error> {
|
||||
let mut request_spks = request.spks_by_keychain;
|
||||
|
||||
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
|
||||
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
|
||||
// cannot be collected. In addition, we keep track of whether an spk has an active tx
|
||||
// history for determining the `last_active_index`.
|
||||
// * key: (keychain, spk_index) that identifies the spk.
|
||||
// * val: (script_pubkey, has_tx_history).
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
let update = loop {
|
||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
|
||||
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
if !request_spks.is_empty() {
|
||||
if !scanned_spks.is_empty() {
|
||||
scanned_spks.append(
|
||||
&mut self.populate_with_spks(
|
||||
&cps,
|
||||
&mut graph_update,
|
||||
&mut scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?,
|
||||
);
|
||||
}
|
||||
for (keychain, keychain_spks) in &mut request_spks {
|
||||
scanned_spks.extend(
|
||||
self.populate_with_spks(
|
||||
&cps,
|
||||
&mut graph_update,
|
||||
keychain_spks,
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// check for reorgs during scan process
|
||||
let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
|
||||
if tip.hash() != server_blockhash {
|
||||
continue; // reorg
|
||||
}
|
||||
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
self.fetch_prev_txout(&mut graph_update)?;
|
||||
}
|
||||
|
||||
let chain_update = tip;
|
||||
|
||||
let keychain_update = request_spks
|
||||
.into_keys()
|
||||
.filter_map(|k| {
|
||||
scanned_spks
|
||||
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
|
||||
.rev()
|
||||
.find(|(_, (_, active))| *active)
|
||||
.map(|((_, i), _)| (k, *i))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
break FullScanResult {
|
||||
graph_update,
|
||||
chain_update,
|
||||
last_active_indices: keychain_update,
|
||||
};
|
||||
};
|
||||
|
||||
Ok(ElectrumFullScanResult(update))
|
||||
}
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
|
||||
/// and returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client sync,
|
||||
/// see [`SyncRequest`]
|
||||
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request
|
||||
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
|
||||
/// calculation
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: Self::full_scan
|
||||
pub fn sync(
|
||||
&self,
|
||||
request: SyncRequest,
|
||||
batch_size: usize,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumSyncResult, Error> {
|
||||
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
|
||||
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
|
||||
let mut full_scan_res = self
|
||||
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
|
||||
.with_confirmation_height_anchor();
|
||||
|
||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
|
||||
self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
|
||||
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
|
||||
}
|
||||
|
||||
Ok(ElectrumSyncResult(SyncResult {
|
||||
chain_update: full_scan_res.chain_update,
|
||||
graph_update: full_scan_res.graph_update,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
|
||||
///
|
||||
/// Transactions that contains an output with requested spk, or spends form an output with
|
||||
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
|
||||
/// also included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
|
||||
let mut unused_spk_count = 0_usize;
|
||||
let mut scanned_spks = BTreeMap::new();
|
||||
|
||||
loop {
|
||||
let spks = (0..batch_size)
|
||||
.map_while(|_| spks.next())
|
||||
.collect::<Vec<_>>();
|
||||
if spks.is_empty() {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
|
||||
let spk_histories = self
|
||||
.inner
|
||||
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
||||
|
||||
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||
if spk_history.is_empty() {
|
||||
scanned_spks.insert(spk_index, (spk, false));
|
||||
unused_spk_count += 1;
|
||||
if unused_spk_count > stop_gap {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
scanned_spks.insert(spk_index, (spk, true));
|
||||
unused_spk_count = 0;
|
||||
}
|
||||
|
||||
for tx_res in spk_history {
|
||||
let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
||||
// which we do not have by default. This data is needed to calculate the transaction fee.
|
||||
fn fetch_prev_txout(
|
||||
&self,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
) -> Result<(), Error> {
|
||||
let full_txs: Vec<Arc<Transaction>> =
|
||||
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
||||
for tx in full_txs {
|
||||
for vin in &tx.input {
|
||||
let outpoint = vin.previous_output;
|
||||
let vout = outpoint.vout;
|
||||
let prev_tx = self.fetch_tx(outpoint.txid)?;
|
||||
let txout = prev_tx.output[vout as usize].clone();
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
|
||||
///
|
||||
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
|
||||
/// included. Anchors of the aforementioned transactions are included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_outpoints(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
) -> Result<(), Error> {
|
||||
for outpoint in outpoints {
|
||||
let op_txid = outpoint.txid;
|
||||
let op_tx = self.fetch_tx(op_txid)?;
|
||||
let op_txout = match op_tx.output.get(outpoint.vout as usize) {
|
||||
Some(txout) => txout,
|
||||
None => continue,
|
||||
};
|
||||
debug_assert_eq!(op_tx.compute_txid(), op_txid);
|
||||
|
||||
// attempt to find the following transactions (alongside their chain positions), and
|
||||
// add to our sparsechain `update`:
|
||||
let mut has_residing = false; // tx in which the outpoint resides
|
||||
let mut has_spending = false; // tx that spends the outpoint
|
||||
for res in self.inner.script_get_history(&op_txout.script_pubkey)? {
|
||||
if has_residing && has_spending {
|
||||
break;
|
||||
}
|
||||
|
||||
if !has_residing && res.tx_hash == op_txid {
|
||||
has_residing = true;
|
||||
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if !has_spending && res.tx_hash != op_txid {
|
||||
let res_tx = self.fetch_tx(res.tx_hash)?;
|
||||
// we exclude txs/anchors that do not spend our specified outpoint(s)
|
||||
has_spending = res_tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|txin| txin.previous_output == outpoint);
|
||||
if !has_spending {
|
||||
continue;
|
||||
}
|
||||
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
|
||||
fn populate_with_txids(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
) -> Result<(), Error> {
|
||||
for txid in txids {
|
||||
let tx = match self.fetch_tx(txid) {
|
||||
Ok(tx) => tx,
|
||||
Err(electrum_client::Error::Protocol(_)) => continue,
|
||||
Err(other_err) => return Err(other_err),
|
||||
};
|
||||
|
||||
let spk = tx
|
||||
.output
|
||||
.first()
|
||||
.map(|txo| &txo.script_pubkey)
|
||||
.expect("tx must have an output");
|
||||
|
||||
// because of restrictions of the Electrum API, we have to use the `script_get_history`
|
||||
// call to get confirmation status of our transaction
|
||||
let anchor = match self
|
||||
.inner
|
||||
.script_get_history(spk)?
|
||||
.into_iter()
|
||||
.find(|r| r.tx_hash == txid)
|
||||
{
|
||||
Some(r) => determine_tx_anchor(cps, r.height, txid),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let _ = graph_update.insert_tx(tx);
|
||||
if let Some(anchor) = anchor {
|
||||
let _ = graph_update.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`BdkElectrumClient::full_scan`].
|
||||
///
|
||||
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
|
||||
|
||||
impl<K> ElectrumFullScanResult<K> {
|
||||
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
||||
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(FullScanResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
||||
chain_update: res.chain_update,
|
||||
last_active_indices: res.last_active_indices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`BdkElectrumClient::sync`].
|
||||
///
|
||||
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
|
||||
|
||||
impl ElectrumSyncResult {
|
||||
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
||||
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(SyncResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
||||
chain_update: res.chain_update,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into_confirmation_time_result(
|
||||
graph_update: TxGraph<ConfirmationHeightAnchor>,
|
||||
client: &impl ElectrumApi,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let relevant_heights = graph_update
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
|
||||
anchor_block: a.anchor_block,
|
||||
confirmation_height: a.confirmation_height,
|
||||
confirmation_time: height_to_time[&a.confirmation_height],
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &impl ElectrumApi,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
let new_tip_height = height as u32;
|
||||
|
||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
}
|
||||
|
||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||
// to construct our checkpoint update.
|
||||
let mut new_blocks = {
|
||||
let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
|
||||
let hashes = client
|
||||
.block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|h| h.block_hash());
|
||||
(start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
|
||||
};
|
||||
|
||||
// Find the "point of agreement" (if any).
|
||||
let agreement_cp = {
|
||||
let mut agreement_cp = Option::<CheckPoint>::None;
|
||||
for cp in prev_tip.iter() {
|
||||
let cp_block = cp.block_id();
|
||||
let hash = match new_blocks.get(&cp_block.height) {
|
||||
Some(&hash) => hash,
|
||||
None => {
|
||||
assert!(
|
||||
new_tip_height >= cp_block.height,
|
||||
"already checked that electrum's tip cannot be smaller"
|
||||
);
|
||||
let hash = client.block_header(cp_block.height as _)?.block_hash();
|
||||
new_blocks.insert(cp_block.height, hash);
|
||||
hash
|
||||
}
|
||||
};
|
||||
if hash == cp_block.hash {
|
||||
agreement_cp = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
agreement_cp
|
||||
};
|
||||
|
||||
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
||||
|
||||
let new_tip = new_blocks
|
||||
.into_iter()
|
||||
// Prune `new_blocks` to only include blocks that are actually new.
|
||||
.filter(|(height, _)| Some(*height) > agreement_height)
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(agreement_cp, |prev_cp, block| {
|
||||
Some(match prev_cp {
|
||||
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
||||
None => CheckPoint::new(block),
|
||||
})
|
||||
})
|
||||
.expect("must have at least one checkpoint");
|
||||
|
||||
Ok((new_tip, agreement_height))
|
||||
}
|
||||
|
||||
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
|
||||
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
|
||||
///
|
||||
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
|
||||
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
|
||||
///
|
||||
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
|
||||
fn determine_tx_anchor(
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
raw_height: i32,
|
||||
txid: Txid,
|
||||
) -> Option<ConfirmationHeightAnchor> {
|
||||
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
|
||||
// height of 0. To avoid invalid representation in our data structures, we manually set
|
||||
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
|
||||
// unconfirmed for all other transactions.
|
||||
if txid
|
||||
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
|
||||
.expect("must deserialize genesis coinbase txid")
|
||||
{
|
||||
let anchor_block = cps.values().next()?.block_id();
|
||||
return Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: 0,
|
||||
});
|
||||
}
|
||||
match raw_height {
|
||||
h if h <= 0 => {
|
||||
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
|
||||
None
|
||||
}
|
||||
h => {
|
||||
let h = h as u32;
|
||||
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
|
||||
if h > anchor_block.height {
|
||||
None
|
||||
} else {
|
||||
Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: h,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
544
crates/electrum/src/electrum_ext.rs
Normal file
544
crates/electrum/src/electrum_ext.rs
Normal file
@@ -0,0 +1,544 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// We include a chain suffix of a certain length for the purpose of robustness.
|
||||
const CHAIN_SUFFIX_LENGTH: u32 = 8;
|
||||
|
||||
/// Represents updates fetched from an Electrum server, but excludes full transactions.
|
||||
///
|
||||
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
|
||||
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
|
||||
/// fetch the full transactions from Electrum and finalize the update.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
|
||||
|
||||
impl RelevantTxids {
|
||||
/// Determine the full transactions that are missing from `graph`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn into_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
seen_at: Option<u64>,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
|
||||
let new_txs = client.batch_transaction_get(&missing)?;
|
||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
|
||||
for (txid, anchors) in self.0 {
|
||||
if let Some(seen_at) = seen_at {
|
||||
let _ = graph.insert_seen_at(txid, seen_at);
|
||||
}
|
||||
for anchor in anchors {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
/// Finalizes [`RelevantTxids`] with `new_txs` and anchors of type
|
||||
/// [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
|
||||
/// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
|
||||
/// use it.
|
||||
pub fn into_confirmation_time_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
seen_at: Option<u64>,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let graph = self.into_tx_graph(client, seen_at, missing)?;
|
||||
|
||||
let relevant_heights = {
|
||||
let mut visited_heights = HashSet::new();
|
||||
graph
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height_upper_bound())
|
||||
.filter(move |&h| visited_heights.insert(h))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
let graph_changeset = {
|
||||
let old_changeset = TxGraph::default().apply_update(graph);
|
||||
tx_graph::ChangeSet {
|
||||
txs: old_changeset.txs,
|
||||
txouts: old_changeset.txouts,
|
||||
last_seen: old_changeset.last_seen,
|
||||
anchors: old_changeset
|
||||
.anchors
|
||||
.into_iter()
|
||||
.map(|(height_anchor, txid)| {
|
||||
let confirmation_height = height_anchor.confirmation_height;
|
||||
let confirmation_time = height_to_time[&confirmation_height];
|
||||
let time_anchor = ConfirmationTimeHeightAnchor {
|
||||
anchor_block: height_anchor.anchor_block,
|
||||
confirmation_height,
|
||||
confirmation_time,
|
||||
};
|
||||
(time_anchor, txid)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_graph = TxGraph::default();
|
||||
new_graph.apply_changeset(graph_changeset);
|
||||
Ok(new_graph)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combination of chain and transactions updates from electrum
|
||||
///
|
||||
/// We have to update the chain and the txids at the same time since we anchor the txids to
|
||||
/// the same chain tip that we check before and after we gather the txids.
|
||||
#[derive(Debug)]
|
||||
pub struct ElectrumUpdate {
|
||||
/// Chain update
|
||||
pub chain_update: local_chain::Update,
|
||||
/// Transaction updates from electrum
|
||||
pub relevant_txids: RelevantTxids,
|
||||
}
|
||||
|
||||
/// Trait to extend [`Client`] functionality.
|
||||
pub trait ElectrumExt {
|
||||
/// Scan the blockchain (via electrum) for the data specified and returns updates for
|
||||
/// [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||
/// - `keychain_spks`: keychains that we want to scan transactions for
|
||||
/// - `txids`: transactions for which we want updated [`Anchor`]s
|
||||
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to included in the update
|
||||
///
|
||||
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
|
||||
/// single batch request.
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
|
||||
|
||||
/// Convenience method to call [`scan`] without requiring a keychain.
|
||||
///
|
||||
/// [`scan`]: ElectrumExt::scan
|
||||
fn scan_without_keychain(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
batch_size: usize,
|
||||
) -> Result<ElectrumUpdate, Error> {
|
||||
let spk_iter = misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk));
|
||||
|
||||
let (electrum_update, _) = self.scan(
|
||||
prev_tip,
|
||||
[((), spk_iter)].into(),
|
||||
txids,
|
||||
outpoints,
|
||||
usize::MAX,
|
||||
batch_size,
|
||||
)?;
|
||||
|
||||
Ok(electrum_update)
|
||||
}
|
||||
}
|
||||
|
||||
impl ElectrumExt for Client {
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||
let mut request_spks = keychain_spks
|
||||
.into_iter()
|
||||
.map(|(k, s)| (k, s.into_iter()))
|
||||
.collect::<BTreeMap<K, _>>();
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
let txids = txids.into_iter().collect::<Vec<_>>();
|
||||
let outpoints = outpoints.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let (electrum_update, keychain_update) = loop {
|
||||
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
|
||||
let mut relevant_txids = RelevantTxids::default();
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
if !request_spks.is_empty() {
|
||||
if !scanned_spks.is_empty() {
|
||||
scanned_spks.append(&mut populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
&mut scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?);
|
||||
}
|
||||
for (keychain, keychain_spks) in &mut request_spks {
|
||||
scanned_spks.extend(
|
||||
populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
keychain_spks,
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
populate_with_txids(self, &cps, &mut relevant_txids, &mut txids.iter().cloned())?;
|
||||
|
||||
let _txs = populate_with_outpoints(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
&mut outpoints.iter().cloned(),
|
||||
)?;
|
||||
|
||||
// check for reorgs during scan process
|
||||
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
|
||||
if tip.hash() != server_blockhash {
|
||||
continue; // reorg
|
||||
}
|
||||
|
||||
let chain_update = local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: true,
|
||||
};
|
||||
|
||||
let keychain_update = request_spks
|
||||
.into_keys()
|
||||
.filter_map(|k| {
|
||||
scanned_spks
|
||||
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
|
||||
.rev()
|
||||
.find(|(_, (_, active))| *active)
|
||||
.map(|((_, i), _)| (k, *i))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
break (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
);
|
||||
};
|
||||
|
||||
Ok((electrum_update, keychain_update))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &Client,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
let new_tip_height = height as u32;
|
||||
|
||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
}
|
||||
|
||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||
// to construct our checkpoint update.
|
||||
let mut new_blocks = {
|
||||
let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
|
||||
let hashes = client
|
||||
.block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|h| h.block_hash());
|
||||
(start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
|
||||
};
|
||||
|
||||
// Find the "point of agreement" (if any).
|
||||
let agreement_cp = {
|
||||
let mut agreement_cp = Option::<CheckPoint>::None;
|
||||
for cp in prev_tip.iter() {
|
||||
let cp_block = cp.block_id();
|
||||
let hash = match new_blocks.get(&cp_block.height) {
|
||||
Some(&hash) => hash,
|
||||
None => {
|
||||
assert!(
|
||||
new_tip_height >= cp_block.height,
|
||||
"already checked that electrum's tip cannot be smaller"
|
||||
);
|
||||
let hash = client.block_header(cp_block.height as _)?.block_hash();
|
||||
new_blocks.insert(cp_block.height, hash);
|
||||
hash
|
||||
}
|
||||
};
|
||||
if hash == cp_block.hash {
|
||||
agreement_cp = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
agreement_cp
|
||||
};
|
||||
|
||||
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
||||
|
||||
let new_tip = new_blocks
|
||||
.into_iter()
|
||||
// Prune `new_blocks` to only include blocks that are actually new.
|
||||
.filter(|(height, _)| Some(*height) > agreement_height)
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(agreement_cp, |prev_cp, block| {
|
||||
Some(match prev_cp {
|
||||
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
||||
None => CheckPoint::new(block),
|
||||
})
|
||||
})
|
||||
.expect("must have at least one checkpoint");
|
||||
|
||||
Ok((new_tip, agreement_height))
|
||||
}
|
||||
|
||||
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
|
||||
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
|
||||
///
|
||||
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
|
||||
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
|
||||
///
|
||||
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
|
||||
fn determine_tx_anchor(
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
raw_height: i32,
|
||||
txid: Txid,
|
||||
) -> Option<ConfirmationHeightAnchor> {
|
||||
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
|
||||
// height of 0. To avoid invalid representation in our data structures, we manually set
|
||||
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
|
||||
// unconfirmed for all other transactions.
|
||||
if txid
|
||||
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
|
||||
.expect("must deserialize genesis coinbase txid")
|
||||
{
|
||||
let anchor_block = cps.values().next()?.block_id();
|
||||
return Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: 0,
|
||||
});
|
||||
}
|
||||
match raw_height {
|
||||
h if h <= 0 => {
|
||||
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
|
||||
None
|
||||
}
|
||||
h => {
|
||||
let h = h as u32;
|
||||
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
|
||||
if h > anchor_block.height {
|
||||
None
|
||||
} else {
|
||||
Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: h,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_with_outpoints(
|
||||
client: &Client,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
outpoints: &mut impl Iterator<Item = OutPoint>,
|
||||
) -> Result<HashMap<Txid, Transaction>, Error> {
|
||||
let mut full_txs = HashMap::new();
|
||||
for outpoint in outpoints {
|
||||
let txid = outpoint.txid;
|
||||
let tx = client.transaction_get(&txid)?;
|
||||
debug_assert_eq!(tx.txid(), txid);
|
||||
let txout = match tx.output.get(outpoint.vout as usize) {
|
||||
Some(txout) => txout,
|
||||
None => continue,
|
||||
};
|
||||
// attempt to find the following transactions (alongside their chain positions), and
|
||||
// add to our sparsechain `update`:
|
||||
let mut has_residing = false; // tx in which the outpoint resides
|
||||
let mut has_spending = false; // tx that spends the outpoint
|
||||
for res in client.script_get_history(&txout.script_pubkey)? {
|
||||
if has_residing && has_spending {
|
||||
break;
|
||||
}
|
||||
|
||||
if res.tx_hash == txid {
|
||||
if has_residing {
|
||||
continue;
|
||||
}
|
||||
has_residing = true;
|
||||
full_txs.insert(res.tx_hash, tx.clone());
|
||||
} else {
|
||||
if has_spending {
|
||||
continue;
|
||||
}
|
||||
let res_tx = match full_txs.get(&res.tx_hash) {
|
||||
Some(tx) => tx,
|
||||
None => {
|
||||
let res_tx = client.transaction_get(&res.tx_hash)?;
|
||||
full_txs.insert(res.tx_hash, res_tx);
|
||||
full_txs.get(&res.tx_hash).expect("just inserted")
|
||||
}
|
||||
};
|
||||
has_spending = res_tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|txin| txin.previous_output == outpoint);
|
||||
if !has_spending {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
|
||||
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(full_txs)
|
||||
}
|
||||
|
||||
fn populate_with_txids(
|
||||
client: &Client,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
txids: &mut impl Iterator<Item = Txid>,
|
||||
) -> Result<(), Error> {
|
||||
for txid in txids {
|
||||
let tx = match client.transaction_get(&txid) {
|
||||
Ok(tx) => tx,
|
||||
Err(electrum_client::Error::Protocol(_)) => continue,
|
||||
Err(other_err) => return Err(other_err),
|
||||
};
|
||||
|
||||
let spk = tx
|
||||
.output
|
||||
.get(0)
|
||||
.map(|txo| &txo.script_pubkey)
|
||||
.expect("tx must have an output");
|
||||
|
||||
let anchor = match client
|
||||
.script_get_history(spk)?
|
||||
.into_iter()
|
||||
.find(|r| r.tx_hash == txid)
|
||||
{
|
||||
Some(r) => determine_tx_anchor(cps, r.height, txid),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let tx_entry = relevant_txids.0.entry(txid).or_default();
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
client: &Client,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
|
||||
let mut unused_spk_count = 0_usize;
|
||||
let mut scanned_spks = BTreeMap::new();
|
||||
|
||||
loop {
|
||||
let spks = (0..batch_size)
|
||||
.map_while(|_| spks.next())
|
||||
.collect::<Vec<_>>();
|
||||
if spks.is_empty() {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
|
||||
let spk_histories =
|
||||
client.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
||||
|
||||
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||
if spk_history.is_empty() {
|
||||
scanned_spks.insert(spk_index, (spk, false));
|
||||
unused_spk_count += 1;
|
||||
if unused_spk_count > stop_gap {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
scanned_spks.insert(spk_index, (spk, true));
|
||||
unused_spk_count = 0;
|
||||
}
|
||||
|
||||
for tx in spk_history {
|
||||
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,30 @@
|
||||
//! This crate is used for updating structures of [`bdk_chain`] with data from an Electrum server.
|
||||
//! This crate is used for updating structures of the [`bdk_chain`] crate with data from electrum.
|
||||
//!
|
||||
//! The two primary methods are [`BdkElectrumClient::sync`] and [`BdkElectrumClient::full_scan`]. In most cases
|
||||
//! [`BdkElectrumClient::sync`] is used to sync the transaction histories of scripts that the application
|
||||
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
|
||||
//! has shown a user. [`BdkElectrumClient::full_scan`] is meant to be used when importing or restoring a
|
||||
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
|
||||
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
|
||||
//! sync or full scan the user receives relevant blockchain data and output updates for
|
||||
//! [`bdk_chain`].
|
||||
//! The star of the show is the [`ElectrumExt::scan`] method, which scans for relevant blockchain
|
||||
//! data (via electrum) and outputs updates for [`bdk_chain`] structures as a tuple of form:
|
||||
//!
|
||||
//! Refer to [`example_electrum`] for a complete example.
|
||||
//! ([`bdk_chain::local_chain::Update`], [`RelevantTxids`], `keychain_update`)
|
||||
//!
|
||||
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
|
||||
//! An [`RelevantTxids`] only includes `txid`s and no full transactions. The caller is
|
||||
//! responsible for obtaining full transactions before applying. This can be done with
|
||||
//! these steps:
|
||||
//!
|
||||
//! 1. Determine which full transactions are missing. The method [`missing_full_txs`] of
|
||||
//! [`RelevantTxids`] can be used.
|
||||
//!
|
||||
//! 2. Obtaining the full transactions. To do this via electrum, the method
|
||||
//! [`batch_transaction_get`] can be used.
|
||||
//!
|
||||
//! Refer to [`bdk_electrum_example`] for a complete example.
|
||||
//!
|
||||
//! [`ElectrumClient::scan`]: electrum_client::ElectrumClient::scan
|
||||
//! [`missing_full_txs`]: RelevantTxids::missing_full_txs
|
||||
//! [`batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
|
||||
//! [`bdk_electrum_example`]: https://github.com/LLFourn/bdk_core_staging/tree/master/bdk_electrum_example
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod bdk_electrum_client;
|
||||
pub use bdk_electrum_client::*;
|
||||
|
||||
mod electrum_ext;
|
||||
pub use bdk_chain;
|
||||
pub use electrum_client;
|
||||
pub use electrum_ext::*;
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||
keychain::Balance,
|
||||
local_chain::LocalChain,
|
||||
spk_client::SyncRequest,
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_electrum::BdkElectrumClient;
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||
) -> anyhow::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() -> anyhow::Result<()> {
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
let client = BdkElectrumClient::new(electrum_client);
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_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 update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip())
|
||||
.chain_spks(core::iter::once(spk_to_track)),
|
||||
5,
|
||||
true,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(update.graph_update);
|
||||
|
||||
// Check to see if tx is confirmed.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT,
|
||||
..Balance::default()
|
||||
},
|
||||
);
|
||||
|
||||
for tx in recv_graph.graph().full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transaction's previous outputs.
|
||||
let fee = recv_graph
|
||||
.graph()
|
||||
.calculate_fee(&tx.tx)
|
||||
.expect("fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("valid `Amount`");
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
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() -> anyhow::Result<()> {
|
||||
const REORG_COUNT: usize = 8;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
let client = BdkElectrumClient::new(electrum_client);
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_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 update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(update.graph_update.clone());
|
||||
|
||||
// Retain a snapshot of all anchors before reorg process.
|
||||
let initial_anchors = update.graph_update.all_anchors();
|
||||
|
||||
// Check if initial balance is correct.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * 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 update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(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(update.graph_update.all_anchors()) {
|
||||
println!("New anchor added at reorg depth {}", depth);
|
||||
}
|
||||
let _ = recv_graph.apply_update(update.graph_update);
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT * depth as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
depth,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.15.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,23 +12,23 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", default-features = false }
|
||||
esplora-client = { version = "0.8.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
|
||||
esplora-client = { version = "0.6.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
|
||||
bitcoin = { version = "0.32.0", optional = true, default-features = false }
|
||||
miniscript = { version = "12.0.0", optional = true, default-features = false }
|
||||
# use these dependencies if you need to enable their /no-std features
|
||||
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["std", "async-https", "blocking-https-rustls"]
|
||||
std = ["bdk_chain/std", "miniscript?/std"]
|
||||
default = ["std", "async-https", "blocking"]
|
||||
std = ["bdk_chain/std"]
|
||||
async = ["async-trait", "futures", "esplora-client/async"]
|
||||
async-https = ["async", "esplora-client/async-https"]
|
||||
async-https-rustls = ["async", "esplora-client/async-https-rustls"]
|
||||
blocking = ["esplora-client/blocking"]
|
||||
blocking-https-rustls = ["esplora-client/blocking-https-rustls"]
|
||||
|
||||
@@ -30,7 +30,7 @@ use bdk_esplora::EsploraExt;
|
||||
// use bdk_esplora::EsploraAsyncExt;
|
||||
```
|
||||
|
||||
For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
|
||||
For full examples, refer to [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora) (blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
|
||||
|
||||
[`esplora-client`]: https://docs.rs/esplora-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::CheckPoint,
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use bdk_chain::{Anchor, Indexed};
|
||||
use esplora_client::{Amount, TxStatus};
|
||||
use esplora_client::{Error, TxStatus};
|
||||
use futures::{stream::FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::AsyncClient`].
|
||||
///
|
||||
@@ -25,251 +19,271 @@ type Error = Box<esplora_client::Error>;
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EsploraAsyncExt {
|
||||
/// Scan keychain scripts for transactions against Esplora, returning an update that can be
|
||||
/// applied to the receiving structures.
|
||||
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
|
||||
///
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
|
||||
/// see [`FullScanRequest`]
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no
|
||||
/// associated transactions. `parallel_requests` specifies the max number of HTTP requests to
|
||||
/// make in parallel.
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## 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>(
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
request: FullScanRequest<K>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
/// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
|
||||
/// indices.
|
||||
///
|
||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
||||
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
|
||||
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
///
|
||||
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<FullScanResult<K>, Error>;
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||
/// specified and return a [`TxGraph`].
|
||||
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
|
||||
///
|
||||
/// - `request`: struct with data required to perform a spk-based blockchain client sync, see
|
||||
/// [`SyncRequest`]
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraAsyncExt::full_scan
|
||||
async fn sync(
|
||||
/// [`scan_txs_with_keychains`]: EsploraAsyncExt::scan_txs_with_keychains
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn scan_txs(
|
||||
&self,
|
||||
request: SyncRequest,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<SyncResult, Error>;
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
self.scan_txs_with_keychains(
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
txids,
|
||||
outpoints,
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.await
|
||||
.map(|(g, _)| g)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
request: FullScanRequest<K>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
|
||||
let new_tip_height = self.get_height().await?;
|
||||
|
||||
// atomically fetch blocks from esplora
|
||||
let mut fetched_blocks = {
|
||||
let heights = (0..=new_tip_height).rev();
|
||||
let hashes = self
|
||||
.get_blocks(Some(new_tip_height))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| b.id);
|
||||
heights.zip(hashes).collect::<BTreeMap<u32, BlockHash>>()
|
||||
};
|
||||
|
||||
// fetch heights that the caller is interested in
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
let hash = self.get_block_hash(height).await?;
|
||||
entry.insert(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// find the earliest point of agreement between local chain and fetched chain
|
||||
let earliest_agreement_cp = {
|
||||
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
|
||||
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height).await?
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
earliest_agreement_cp
|
||||
};
|
||||
|
||||
let tip = {
|
||||
// first checkpoint to use for the update chain
|
||||
let first_cp = match earliest_agreement_cp {
|
||||
Some(cp) => cp,
|
||||
None => {
|
||||
let (&height, &hash) = fetched_blocks
|
||||
.iter()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
CheckPoint::new(BlockId { height, hash })
|
||||
}
|
||||
};
|
||||
// transform fetched chain into the update chain
|
||||
fetched_blocks
|
||||
// we exclude anything at or below the first cp of the update chain otherwise
|
||||
// building the chain will fail
|
||||
.split_off(&(first_cp.height() + 1))
|
||||
.into_iter()
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(first_cp, |prev_cp, block| {
|
||||
prev_cp.push(block).expect("must extend checkpoint")
|
||||
})
|
||||
};
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<FullScanResult<K>, Error> {
|
||||
let latest_blocks = fetch_latest_blocks(self).await?;
|
||||
let (graph_update, last_active_indices) = full_scan_for_index_and_graph(
|
||||
self,
|
||||
request.spks_by_keychain,
|
||||
stop_gap,
|
||||
parallel_requests,
|
||||
)
|
||||
.await?;
|
||||
let chain_update = chain_update(
|
||||
self,
|
||||
&latest_blocks,
|
||||
&request.chain_tip,
|
||||
graph_update.all_anchors(),
|
||||
)
|
||||
.await?;
|
||||
Ok(FullScanResult {
|
||||
chain_update,
|
||||
graph_update,
|
||||
last_active_indices,
|
||||
})
|
||||
}
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
async fn sync(
|
||||
&self,
|
||||
request: SyncRequest,
|
||||
parallel_requests: usize,
|
||||
) -> Result<SyncResult, Error> {
|
||||
let latest_blocks = fetch_latest_blocks(self).await?;
|
||||
let graph_update = sync_for_index_and_graph(
|
||||
self,
|
||||
request.spks,
|
||||
request.txids,
|
||||
request.outpoints,
|
||||
parallel_requests,
|
||||
)
|
||||
.await?;
|
||||
let chain_update = chain_update(
|
||||
self,
|
||||
&latest_blocks,
|
||||
&request.chain_tip,
|
||||
graph_update.all_anchors(),
|
||||
)
|
||||
.await?;
|
||||
Ok(SyncResult {
|
||||
chain_update,
|
||||
graph_update,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
/// Fetch latest blocks from Esplora in an atomic call.
|
||||
///
|
||||
/// We want to do this before fetching transactions and anchors as we cannot fetch latest blocks AND
|
||||
/// transactions atomically, and the checkpoint tip is used to determine last-scanned block (for
|
||||
/// block-based chain-sources). Therefore it's better to be conservative when setting the tip (use
|
||||
/// an earlier tip rather than a later tip) otherwise the caller may accidentally skip blocks when
|
||||
/// alternating between chain-sources.
|
||||
async fn fetch_latest_blocks(
|
||||
client: &esplora_client::AsyncClient,
|
||||
) -> Result<BTreeMap<u32, BlockHash>, Error> {
|
||||
Ok(client
|
||||
.get_blocks(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Used instead of [`esplora_client::BlockingClient::get_block_hash`].
|
||||
///
|
||||
/// This first checks the previously fetched `latest_blocks` before fetching from Esplora again.
|
||||
async fn fetch_block(
|
||||
client: &esplora_client::AsyncClient,
|
||||
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||
height: u32,
|
||||
) -> Result<Option<BlockHash>, Error> {
|
||||
if let Some(&hash) = latest_blocks.get(&height) {
|
||||
return Ok(Some(hash));
|
||||
}
|
||||
|
||||
// We avoid fetching blocks higher than previously fetched `latest_blocks` as the local chain
|
||||
// tip is used to signal for the last-synced-up-to-height.
|
||||
let &tip_height = latest_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.expect("must have atleast one entry");
|
||||
if height > tip_height {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(client.get_block_hash(height).await?))
|
||||
}
|
||||
|
||||
/// Create the [`local_chain::Update`].
|
||||
///
|
||||
/// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched
|
||||
/// should not surpass `latest_blocks`.
|
||||
async fn chain_update<A: Anchor>(
|
||||
client: &esplora_client::AsyncClient,
|
||||
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||
local_tip: &CheckPoint,
|
||||
anchors: &BTreeSet<(A, Txid)>,
|
||||
) -> Result<CheckPoint, Error> {
|
||||
let mut point_of_agreement = None;
|
||||
let mut conflicts = vec![];
|
||||
for local_cp in local_tip.iter() {
|
||||
let remote_hash = match fetch_block(client, latest_blocks, local_cp.height()).await? {
|
||||
Some(hash) => hash,
|
||||
None => continue,
|
||||
};
|
||||
if remote_hash == local_cp.hash() {
|
||||
point_of_agreement = Some(local_cp.clone());
|
||||
break;
|
||||
} else {
|
||||
// it is not strictly necessary to include all the conflicted heights (we do need the
|
||||
// first one) but it seems prudent to make sure the updated chain's heights are a
|
||||
// superset of the existing chain after update.
|
||||
conflicts.push(BlockId {
|
||||
height: local_cp.height(),
|
||||
hash: remote_hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut tip = point_of_agreement.expect("remote esplora should have same genesis block");
|
||||
|
||||
tip = tip
|
||||
.extend(conflicts.into_iter().rev())
|
||||
.expect("evicted are in order");
|
||||
|
||||
for anchor in anchors {
|
||||
let height = anchor.0.anchor_block().height;
|
||||
if tip.get(height).is_none() {
|
||||
let hash = match fetch_block(client, latest_blocks, height).await? {
|
||||
Some(hash) => hash,
|
||||
None => continue,
|
||||
};
|
||||
tip = tip.insert(BlockId { height, hash });
|
||||
}
|
||||
}
|
||||
|
||||
// insert the most recent blocks at the tip to make sure we update the tip and make the update
|
||||
// robust.
|
||||
for (&height, &hash) in latest_blocks.iter() {
|
||||
tip = tip.insert(BlockId { height, hash });
|
||||
}
|
||||
|
||||
Ok(tip)
|
||||
}
|
||||
|
||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||
client: &esplora_client::AsyncClient,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = Indexed<ScriptBuf>> + Send> + Send,
|
||||
>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
loop {
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.map(|(spk_index, spk)| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen).await?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Result::<_, Error>::Ok((spk_index, spk_txs));
|
||||
loop {
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.map(|(spk_index, spk)| {
|
||||
let client = self.clone();
|
||||
async move {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen).await?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Result::<_, Error>::Ok((spk_index, spk_txs));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let past_gap_limit = if let Some(i) = last_active_index {
|
||||
last_index > i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index >= stop_gap as u32
|
||||
};
|
||||
if past_gap_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
let client = self.clone();
|
||||
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
@@ -277,314 +291,39 @@ async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||
break;
|
||||
}
|
||||
|
||||
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: Amount::from_sat(prevout.value),
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
for op in outpoints.into_iter() {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&op.txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&op.txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if gap_limit_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((graph, last_active_indexes))
|
||||
}
|
||||
|
||||
async fn sync_for_index_and_graph(
|
||||
client: &esplora_client::AsyncClient,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let mut graph = full_scan_for_index_and_graph(
|
||||
client,
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.await
|
||||
.map(|(g, _)| g)?;
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
let client = client.clone();
|
||||
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints.into_iter() {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = client.get_tx(&op.txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = client.get_tx_status(&op.txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op_status) = client.get_output_status(&op.txid, op.vout as _).await? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = client.get_tx(&txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = client.get_tx_status(&txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{collections::BTreeSet, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Txid},
|
||||
local_chain::LocalChain,
|
||||
BlockId,
|
||||
};
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
use esplora_client::Builder;
|
||||
|
||||
use crate::async_ext::{chain_update, fetch_latest_blocks};
|
||||
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
/// Ensure that update does not remove heights (from original), and all anchor heights are included.
|
||||
#[tokio::test]
|
||||
pub async fn test_finalize_chain_update() -> anyhow::Result<()> {
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
/// Initial blockchain height to start the env with.
|
||||
initial_env_height: u32,
|
||||
/// Initial checkpoint heights to start with.
|
||||
initial_cps: &'a [u32],
|
||||
/// The final blockchain height of the env.
|
||||
final_env_height: u32,
|
||||
/// The anchors to test with: `(height, txid)`. Only the height is provided as we can fetch
|
||||
/// the blockhash from the env.
|
||||
anchors: &'a [(u32, Txid)],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "chain_extends",
|
||||
initial_env_height: 60,
|
||||
initial_cps: &[59, 60],
|
||||
final_env_height: 90,
|
||||
anchors: &[],
|
||||
},
|
||||
TestCase {
|
||||
name: "introduce_older_heights",
|
||||
initial_env_height: 50,
|
||||
initial_cps: &[10, 15],
|
||||
final_env_height: 50,
|
||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "introduce_older_heights_after_chain_extends",
|
||||
initial_env_height: 50,
|
||||
initial_cps: &[10, 15],
|
||||
final_env_height: 100,
|
||||
anchors: &[(11, h!("A")), (14, h!("B"))],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("[{}] running test case: {}", i, t.name);
|
||||
|
||||
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()?;
|
||||
|
||||
// set env to `initial_env_height`
|
||||
if let Some(to_mine) = t
|
||||
.initial_env_height
|
||||
.checked_sub(env.make_checkpoint_tip().height())
|
||||
{
|
||||
env.mine_blocks(to_mine as _, None)?;
|
||||
}
|
||||
while client.get_height().await? < t.initial_env_height {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// craft initial `local_chain`
|
||||
let local_chain = {
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.genesis_hash()?);
|
||||
// force `chain_update_blocking` to add all checkpoints in `t.initial_cps`
|
||||
let anchors = t
|
||||
.initial_cps
|
||||
.iter()
|
||||
.map(|&height| -> anyhow::Result<_> {
|
||||
Ok((
|
||||
BlockId {
|
||||
height,
|
||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||
},
|
||||
Txid::all_zeros(),
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
||||
let update = chain_update(
|
||||
&client,
|
||||
&fetch_latest_blocks(&client).await?,
|
||||
&chain.tip(),
|
||||
&anchors,
|
||||
)
|
||||
.await?;
|
||||
chain.apply_update(update)?;
|
||||
chain
|
||||
};
|
||||
println!("local chain height: {}", local_chain.tip().height());
|
||||
|
||||
// extend env chain
|
||||
if let Some(to_mine) = t
|
||||
.final_env_height
|
||||
.checked_sub(env.make_checkpoint_tip().height())
|
||||
{
|
||||
env.mine_blocks(to_mine as _, None)?;
|
||||
}
|
||||
while client.get_height().await? < t.final_env_height {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// craft update
|
||||
let update = {
|
||||
let anchors = t
|
||||
.anchors
|
||||
.iter()
|
||||
.map(|&(height, txid)| -> anyhow::Result<_> {
|
||||
Ok((
|
||||
BlockId {
|
||||
height,
|
||||
hash: env.bitcoind.client.get_block_hash(height as _)?,
|
||||
},
|
||||
txid,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
chain_update(
|
||||
&client,
|
||||
&fetch_latest_blocks(&client).await?,
|
||||
&local_chain.tip(),
|
||||
&anchors,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
// apply update
|
||||
let mut updated_local_chain = local_chain.clone();
|
||||
updated_local_chain.apply_update(update)?;
|
||||
println!(
|
||||
"updated local chain height: {}",
|
||||
updated_local_chain.tip().height()
|
||||
);
|
||||
|
||||
assert!(
|
||||
{
|
||||
let initial_heights = local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let updated_heights = updated_local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
updated_heights.is_superset(&initial_heights)
|
||||
},
|
||||
"heights from the initial chain must all be in the updated chain",
|
||||
);
|
||||
|
||||
assert!(
|
||||
{
|
||||
let exp_anchor_heights = t
|
||||
.anchors
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.chain(t.initial_cps.iter().copied())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let anchor_heights = updated_local_chain
|
||||
.iter_checkpoints()
|
||||
.map(|cp| cp.height())
|
||||
.collect::<BTreeSet<_>>();
|
||||
anchor_heights.is_superset(&exp_anchor_heights)
|
||||
},
|
||||
"anchor heights must all be in updated chain",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok((graph, last_active_indexes))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,4 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! This crate is used for updating structures of [`bdk_chain`] with data from an Esplora server.
|
||||
//!
|
||||
//! The two primary methods are [`EsploraExt::sync`] and [`EsploraExt::full_scan`]. In most cases
|
||||
//! [`EsploraExt::sync`] is used to sync the transaction histories of scripts that the application
|
||||
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
|
||||
//! has shown a user. [`EsploraExt::full_scan`] is meant to be used when importing or restoring a
|
||||
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
|
||||
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
|
||||
//! sync or full scan the user receives relevant blockchain data and output updates for [`bdk_chain`]
|
||||
//! via a new [`TxGraph`] to be appended to any existing [`TxGraph`] data.
|
||||
//!
|
||||
//! Refer to [`example_esplora`] for a complete example.
|
||||
//!
|
||||
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
|
||||
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
|
||||
|
||||
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
@@ -31,6 +14,8 @@ mod async_ext;
|
||||
#[cfg(feature = "async")]
|
||||
pub use async_ext::*;
|
||||
|
||||
const ASSUME_FINAL_DEPTH: u32 = 15;
|
||||
|
||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
|
||||
if let TxStatus {
|
||||
block_height: Some(height),
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
|
||||
use bdk_esplora::EsploraAsyncExt;
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, AsyncClient, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: AsyncClient,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Result<Self, anyhow::Error> {
|
||||
let bitcoind_exe =
|
||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
Ok(Self {
|
||||
bitcoind,
|
||||
electrsd,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
@@ -47,57 +95,19 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
let sync_update = {
|
||||
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||
client.sync(request, 1).await?
|
||||
};
|
||||
|
||||
assert!(
|
||||
{
|
||||
let update_cps = sync_update
|
||||
.chain_update
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let superset_cps = cp_tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
superset_cps.is_superset(&update_cps)
|
||||
},
|
||||
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||
);
|
||||
|
||||
let graph_update = sync_update.graph_update;
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("valid `Amount`");
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
let graph_update = env
|
||||
.client
|
||||
.scan_txs(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
@@ -107,12 +117,10 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
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]
|
||||
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 base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
@@ -137,6 +145,8 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -150,37 +160,36 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
// 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.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 3, 1).await?
|
||||
};
|
||||
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||
assert!(full_scan_update.last_active_indices.is_empty());
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 4, 1).await?
|
||||
};
|
||||
assert_eq!(
|
||||
full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.txid,
|
||||
txid_4th_addr
|
||||
);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env
|
||||
.client
|
||||
.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
2,
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env
|
||||
.client
|
||||
.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
3,
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -194,38 +203,34 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 5, 1).await?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
let (graph_update, active_indices) = env
|
||||
.client
|
||||
.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
4,
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 6, 1).await?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env
|
||||
.client
|
||||
.scan_txs_with_keychains(keychains, vec![].into_iter(), vec![].into_iter(), 5, 1)
|
||||
.await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
|
||||
use bdk_esplora::EsploraExt;
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, BlockingClient, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
|
||||
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 mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
@@ -47,57 +95,16 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
let sync_update = {
|
||||
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||
client.sync(request, 1)?
|
||||
};
|
||||
|
||||
assert!(
|
||||
{
|
||||
let update_cps = sync_update
|
||||
.chain_update
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let superset_cps = cp_tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
superset_cps.is_superset(&update_cps)
|
||||
},
|
||||
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||
);
|
||||
|
||||
let graph_update = sync_update.graph_update;
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("valid `Amount`");
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
let graph_update = env.client.scan_txs(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)?;
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
@@ -108,12 +115,10 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
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]
|
||||
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 base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
@@ -138,6 +143,8 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -151,37 +158,30 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
// 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.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 3, 1)?
|
||||
};
|
||||
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||
assert!(full_scan_update.last_active_indices.is_empty());
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 4, 1)?
|
||||
};
|
||||
assert_eq!(
|
||||
full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.txid,
|
||||
txid_4th_addr
|
||||
);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
2,
|
||||
1,
|
||||
)?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
3,
|
||||
1,
|
||||
)?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
@@ -195,38 +195,34 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 5, 1)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
||||
keychains.clone(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
4,
|
||||
1,
|
||||
)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 6, 1)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
||||
keychains,
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
5,
|
||||
1,
|
||||
)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.13.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_file_store"
|
||||
description = "A simple append-only flat file database for persisting bdk_chain data."
|
||||
description = "A simple append-only flat file implementation of Persist for Bitcoin Dev Kit."
|
||||
keywords = ["bitcoin", "persist", "persistence", "bdk", "file"]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.6.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# BDK File Store
|
||||
|
||||
This is a simple append-only flat file database for persisting [`bdk_chain`] changesets.
|
||||
This is a simple append-only flat file implementation of
|
||||
[`Persist`](`bdk_chain::Persist`).
|
||||
|
||||
The main structure is [`Store`] which works with any [`bdk_chain`] based changesets to persist data into a flat file.
|
||||
The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
|
||||
`Wallet` to persist wallet data into a flat file.
|
||||
|
||||
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
|
||||
[`bdk`]: https://docs.rs/bdk/latest
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, Seek},
|
||||
io::{self, Seek},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
@@ -14,9 +14,8 @@ use crate::bincode_options;
|
||||
///
|
||||
/// [`next`]: Self::next
|
||||
pub struct EntryIter<'t, T> {
|
||||
/// Buffered reader around the file
|
||||
db_file: BufReader<&'t mut File>,
|
||||
finished: bool,
|
||||
db_file: Option<&'t mut File>,
|
||||
|
||||
/// The file position for the first read of `db_file`.
|
||||
start_pos: Option<u64>,
|
||||
types: PhantomData<T>,
|
||||
@@ -25,9 +24,8 @@ pub struct EntryIter<'t, T> {
|
||||
impl<'t, T> EntryIter<'t, T> {
|
||||
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
|
||||
Self {
|
||||
db_file: BufReader::new(db_file),
|
||||
db_file: Some(db_file),
|
||||
start_pos: Some(start_pos),
|
||||
finished: false,
|
||||
types: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -40,44 +38,44 @@ where
|
||||
type Item = Result<T, IterError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.finished {
|
||||
return None;
|
||||
}
|
||||
(|| {
|
||||
if let Some(start) = self.start_pos.take() {
|
||||
self.db_file.seek(io::SeekFrom::Start(start))?;
|
||||
}
|
||||
// closure which reads a single entry starting from `self.pos`
|
||||
let read_one = |f: &mut File, start_pos: Option<u64>| -> Result<Option<T>, IterError> {
|
||||
let pos = match start_pos {
|
||||
Some(pos) => f.seek(io::SeekFrom::Start(pos))?,
|
||||
None => f.stream_position()?,
|
||||
};
|
||||
|
||||
let pos_before_read = self.db_file.stream_position()?;
|
||||
match bincode_options().deserialize_from(&mut self.db_file) {
|
||||
Ok(changeset) => Ok(Some(changeset)),
|
||||
match bincode_options().deserialize_from(&*f) {
|
||||
Ok(changeset) => {
|
||||
f.stream_position()?;
|
||||
Ok(Some(changeset))
|
||||
}
|
||||
Err(e) => {
|
||||
self.finished = true;
|
||||
let pos_after_read = self.db_file.stream_position()?;
|
||||
// allow unexpected EOF if 0 bytes were read
|
||||
if let bincode::ErrorKind::Io(inner) = &*e {
|
||||
if inner.kind() == io::ErrorKind::UnexpectedEof
|
||||
&& pos_after_read == pos_before_read
|
||||
{
|
||||
return Ok(None);
|
||||
if inner.kind() == io::ErrorKind::UnexpectedEof {
|
||||
let eof = f.seek(io::SeekFrom::End(0))?;
|
||||
if pos == eof {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.db_file.seek(io::SeekFrom::Start(pos_before_read))?;
|
||||
f.seek(io::SeekFrom::Start(pos))?;
|
||||
Err(IterError::Bincode(*e))
|
||||
}
|
||||
}
|
||||
})()
|
||||
.transpose()
|
||||
};
|
||||
|
||||
let result = read_one(self.db_file.as_mut()?, self.start_pos.take());
|
||||
if result.is_err() {
|
||||
self.db_file = None;
|
||||
}
|
||||
result.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T> Drop for EntryIter<'t, T> {
|
||||
fn drop(&mut self) {
|
||||
// This syncs the underlying file's offset with the buffer's position. This way, we
|
||||
// maintain the correct position to start the next read/write.
|
||||
if let Ok(pos) = self.db_file.stream_position() {
|
||||
let _ = self.db_file.get_mut().seek(io::SeekFrom::Start(pos));
|
||||
}
|
||||
impl From<io::Error> for IterError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
IterError::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +97,4 @@ impl core::fmt::Display for IterError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for IterError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
IterError::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IterError {}
|
||||
|
||||
@@ -13,14 +13,14 @@ pub(crate) fn bincode_options() -> impl bincode::Options {
|
||||
|
||||
/// Error that occurs due to problems encountered with the file.
|
||||
#[derive(Debug)]
|
||||
pub enum FileError {
|
||||
pub enum FileError<'a> {
|
||||
/// IO error, this may mean that the file is too short.
|
||||
Io(io::Error),
|
||||
/// Magic bytes do not match what is expected.
|
||||
InvalidMagicBytes { got: Vec<u8>, expected: Vec<u8> },
|
||||
InvalidMagicBytes { got: Vec<u8>, expected: &'a [u8] },
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FileError {
|
||||
impl<'a> core::fmt::Display for FileError<'a> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "io error trying to read file: {}", e),
|
||||
@@ -33,10 +33,10 @@ impl core::fmt::Display for FileError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for FileError {
|
||||
impl<'a> From<io::Error> for FileError<'a> {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FileError {}
|
||||
impl<'a> std::error::Error for FileError<'a> {}
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
use bdk_chain::Append;
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
fmt::Debug,
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Read, Seek, Write},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use bdk_chain::{Append, PersistBackend};
|
||||
use bincode::Options;
|
||||
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
|
||||
/// Persists an append-only list of changesets (`C`) to a single file.
|
||||
///
|
||||
/// The changesets are the results of altering a tracker implementation (`T`).
|
||||
#[derive(Debug)]
|
||||
pub struct Store<C>
|
||||
where
|
||||
C: Sync + Send,
|
||||
{
|
||||
magic_len: usize,
|
||||
pub struct Store<'a, C> {
|
||||
magic: &'a [u8],
|
||||
db_file: File,
|
||||
marker: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C> Store<C>
|
||||
impl<'a, C> PersistBackend<C> for Store<'a, C>
|
||||
where
|
||||
C: Append
|
||||
+ serde::Serialize
|
||||
+ serde::de::DeserializeOwned
|
||||
+ core::marker::Send
|
||||
+ core::marker::Sync,
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
type WriteError = std::io::Error;
|
||||
|
||||
type LoadError = IterError;
|
||||
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
|
||||
self.append_changeset(changeset)
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
self.aggregate_changesets().map_err(|e| e.iter_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C> Store<'a, C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
||||
///
|
||||
@@ -34,7 +48,7 @@ where
|
||||
/// the `Store` in the future with [`open`].
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
pub fn create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -50,11 +64,10 @@ where
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?;
|
||||
f.write_all(magic)?;
|
||||
Ok(Self {
|
||||
magic_len: magic.len(),
|
||||
magic,
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
@@ -70,7 +83,7 @@ where
|
||||
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
|
||||
///
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn open<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -81,24 +94,24 @@ where
|
||||
if magic_buf != magic {
|
||||
return Err(FileError::InvalidMagicBytes {
|
||||
got: magic_buf,
|
||||
expected: magic.to_vec(),
|
||||
expected: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic_len: magic.len(),
|
||||
magic,
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to open existing [`Store`] file; create it if the file is non-existent.
|
||||
/// Attempt to open existing [`Store`] file; create it if the file is non-existant.
|
||||
///
|
||||
/// Internally, this calls either [`open`] or [`create_new`].
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open_or_create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
pub fn open_or_create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
@@ -119,18 +132,18 @@ where
|
||||
/// always iterate over all entries until `None` is returned if you want your next write to go
|
||||
/// at the end; otherwise, you will write over existing entries.
|
||||
pub fn iter_changesets(&mut self) -> EntryIter<C> {
|
||||
EntryIter::new(self.magic_len as u64, &mut self.db_file)
|
||||
EntryIter::new(self.magic.len() as u64, &mut self.db_file)
|
||||
}
|
||||
|
||||
/// Loads all the changesets that have been stored as one giant changeset.
|
||||
///
|
||||
/// This function returns the aggregate changeset, or `None` if nothing was persisted.
|
||||
/// If reading or deserializing any of the entries fails, an error is returned that
|
||||
/// consists of all those it was able to read.
|
||||
/// This function returns a tuple of the aggregate changeset and a result that indicates
|
||||
/// whether an error occurred while reading or deserializing one of the entries. If so the
|
||||
/// changeset will consist of all of those it was able to read.
|
||||
///
|
||||
/// You should usually check the error. In many applications, it may make sense to do a full
|
||||
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
|
||||
/// changesets was unable to read changes of the derivation indices of a keychain.
|
||||
/// changesets it was unable to read changed the derivation indices of the tracker.
|
||||
///
|
||||
/// **WARNING**: This method changes the write position of the underlying file. The next
|
||||
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
||||
@@ -168,7 +181,7 @@ where
|
||||
bincode_options()
|
||||
.serialize_into(&mut self.db_file, changeset)
|
||||
.map_err(|e| match *e {
|
||||
bincode::ErrorKind::Io(error) => error,
|
||||
bincode::ErrorKind::Io(inner) => inner,
|
||||
unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
|
||||
})?;
|
||||
|
||||
@@ -198,7 +211,7 @@ impl<C> std::fmt::Display for AggregateChangesetsError<C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
@@ -206,7 +219,6 @@ mod test {
|
||||
|
||||
use bincode::DefaultOptions;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
io::{Read, Write},
|
||||
vec::Vec,
|
||||
};
|
||||
@@ -216,7 +228,10 @@ mod test {
|
||||
const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] =
|
||||
[98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49];
|
||||
|
||||
type TestChangeSet = BTreeSet<String>;
|
||||
type TestChangeSet = Vec<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestTracker;
|
||||
|
||||
/// Check behavior of [`Store::create_new`] and [`Store::open`].
|
||||
#[test]
|
||||
@@ -238,7 +253,7 @@ mod test {
|
||||
fn open_or_create_new() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let changeset = BTreeSet::from(["hello".to_string(), "world".to_string()]);
|
||||
let changeset = vec!["hello".to_string(), "world".to_string()];
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
@@ -289,7 +304,7 @@ mod test {
|
||||
let mut data = [255_u8; 2000];
|
||||
data[..TEST_MAGIC_BYTES_LEN].copy_from_slice(&TEST_MAGIC_BYTES);
|
||||
|
||||
let changeset = TestChangeSet::from(["one".into(), "two".into(), "three!".into()]);
|
||||
let changeset = vec!["one".into(), "two".into(), "three!".into()];
|
||||
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&data).expect("should write");
|
||||
@@ -325,119 +340,4 @@ mod test {
|
||||
|
||||
assert_eq!(got_bytes, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_write_is_short() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = [
|
||||
TestChangeSet::from(["1".into()]),
|
||||
TestChangeSet::from(["2".into(), "3".into()]),
|
||||
TestChangeSet::from(["4".into(), "5".into(), "6".into()]),
|
||||
];
|
||||
let last_changeset = TestChangeSet::from(["7".into(), "8".into(), "9".into()]);
|
||||
let last_changeset_bytes = bincode_options().serialize(&last_changeset).unwrap();
|
||||
|
||||
for short_write_len in 1..last_changeset_bytes.len() - 1 {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", short_write_len));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// simulate creating a file, writing data where the last write is incomplete
|
||||
{
|
||||
let mut db =
|
||||
Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
// this is the incomplete write
|
||||
db.db_file
|
||||
.write_all(&last_changeset_bytes[..short_write_len])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// load file again and aggregate changesets
|
||||
// write the last changeset again (this time it succeeds)
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let err = db
|
||||
.aggregate_changesets()
|
||||
.expect_err("should return error as last read is short");
|
||||
assert_eq!(
|
||||
err.changeset,
|
||||
changesets.iter().cloned().reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets that are written in full",
|
||||
);
|
||||
db.db_file.write_all(&last_changeset_bytes).unwrap();
|
||||
}
|
||||
|
||||
// load file again - this time we should successfully aggregate all changesets
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let aggregated_changesets = db
|
||||
.aggregate_changesets()
|
||||
.expect("aggregating all changesets should succeed");
|
||||
assert_eq!(
|
||||
aggregated_changesets,
|
||||
changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(core::iter::once(last_changeset.clone()))
|
||||
.reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_after_short_read() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = (0..20)
|
||||
.map(|n| TestChangeSet::from([format!("{}", n)]))
|
||||
.collect::<Vec<_>>();
|
||||
let last_changeset = TestChangeSet::from(["last".into()]);
|
||||
|
||||
for read_count in 0..changesets.len() {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", read_count));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// First, we create the file with all the changesets!
|
||||
let mut db = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
drop(db);
|
||||
|
||||
// We re-open the file and read `read_count` number of changesets.
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let mut exp_aggregation = db
|
||||
.iter_changesets()
|
||||
.take(read_count)
|
||||
.map(|r| r.expect("must read valid changeset"))
|
||||
.fold(TestChangeSet::default(), |mut acc, v| {
|
||||
Append::append(&mut acc, v);
|
||||
acc
|
||||
});
|
||||
// We write after a short read.
|
||||
db.append_changeset(&last_changeset)
|
||||
.expect("last write must succeed");
|
||||
Append::append(&mut exp_aggregation, last_changeset.clone());
|
||||
drop(db);
|
||||
|
||||
// We open the file again and check whether aggregate changeset is expected.
|
||||
let aggregation = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.unwrap()
|
||||
.aggregate_changesets()
|
||||
.expect("must aggregate changesets")
|
||||
.unwrap_or_default();
|
||||
assert_eq!(aggregation, exp_aggregation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "bdk_hwi"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
description = "Utilities to use bdk with hardware wallets"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13" }
|
||||
hwi = { version = "0.9.0", features = [ "miniscript"] }
|
||||
@@ -1,3 +0,0 @@
|
||||
# BDK HWI Signer
|
||||
|
||||
This crate contains `HWISigner`, an implementation of a `TransactionSigner` to be used with hardware wallets.
|
||||
@@ -1,41 +0,0 @@
|
||||
//! HWI Signer
|
||||
//!
|
||||
//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut devices = HWIClient::enumerate()?;
|
||||
//! if devices.is_empty() {
|
||||
//! panic!("No devices found!");
|
||||
//! }
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new(
|
||||
//! # "",
|
||||
//! # "",
|
||||
//! # Network::Testnet,
|
||||
//! # )?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(custom_signer),
|
||||
//! );
|
||||
//!
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
|
||||
|
||||
mod signer;
|
||||
pub use signer::*;
|
||||
@@ -1,94 +0,0 @@
|
||||
use bdk_wallet::bitcoin::bip32::Fingerprint;
|
||||
use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bdk_wallet::bitcoin::Psbt;
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
use hwi::HWIClient;
|
||||
|
||||
use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Custom signer for Hardware Wallets
|
||||
///
|
||||
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
|
||||
pub struct HWISigner {
|
||||
fingerprint: Fingerprint,
|
||||
client: HWIClient,
|
||||
}
|
||||
|
||||
impl HWISigner {
|
||||
/// Create a instance from the specified device and chain
|
||||
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
|
||||
let client = HWIClient::get_client(device, false, chain)?;
|
||||
Ok(HWISigner {
|
||||
fingerprint: device.fingerprint,
|
||||
client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for HWISigner {
|
||||
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||
SignerId::Fingerprint(self.fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
_sign_options: &bdk_wallet::SignOptions,
|
||||
_secp: &Secp256k1<All>,
|
||||
) -> Result<(), SignerError> {
|
||||
psbt.combine(
|
||||
self.client
|
||||
.sign_tx(psbt)
|
||||
.map_err(|e| {
|
||||
SignerError::External(format!("While signing with hardware wallet: {}", e))
|
||||
})?
|
||||
.psbt,
|
||||
)
|
||||
.expect("Failed to combine HW signed psbt with passed PSBT");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: re-enable this once we have the `get_funded_wallet` test util
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// #[test]
|
||||
// fn test_hardware_signer() {
|
||||
// use std::sync::Arc;
|
||||
//
|
||||
// use bdk_wallet::tests::get_funded_wallet;
|
||||
// use bdk_wallet::signer::SignerOrdering;
|
||||
// use bdk_wallet::bitcoin::Network;
|
||||
// use crate::HWISigner;
|
||||
// use hwi::HWIClient;
|
||||
//
|
||||
// let mut devices = HWIClient::enumerate().unwrap();
|
||||
// if devices.is_empty() {
|
||||
// panic!("No devices found!");
|
||||
// }
|
||||
// let device = devices.remove(0).unwrap();
|
||||
// let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap();
|
||||
// let descriptors = client.get_descriptors::<String>(None).unwrap();
|
||||
// let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap();
|
||||
//
|
||||
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
|
||||
// wallet.add_signer(
|
||||
// bdk_wallet::KeychainKind::External,
|
||||
// SignerOrdering(200),
|
||||
// Arc::new(custom_signer),
|
||||
// );
|
||||
//
|
||||
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
|
||||
// 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, Default::default()).unwrap();
|
||||
// assert!(finalized);
|
||||
// }
|
||||
// }
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "bdk_sqlite"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_sqlite"
|
||||
description = "A simple SQLite relational database client for persisting bdk_chain data."
|
||||
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
|
||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -1,8 +0,0 @@
|
||||
# BDK SQLite
|
||||
|
||||
This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
|
||||
|
||||
The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
|
||||
|
||||
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
|
||||
[SQLite]: https://www.sqlite.org/index.html
|
||||
@@ -1,69 +0,0 @@
|
||||
-- schema version control
|
||||
CREATE TABLE version
|
||||
(
|
||||
version INTEGER
|
||||
) STRICT;
|
||||
INSERT INTO version
|
||||
VALUES (1);
|
||||
|
||||
-- network is the valid network for all other table data
|
||||
CREATE TABLE network
|
||||
(
|
||||
name TEXT UNIQUE NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- keychain is the json serialized keychain structure as JSONB,
|
||||
-- descriptor is the complete descriptor string,
|
||||
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
|
||||
-- last revealed index is a u32
|
||||
CREATE TABLE keychain
|
||||
(
|
||||
keychain BLOB PRIMARY KEY NOT NULL,
|
||||
descriptor TEXT NOT NULL,
|
||||
descriptor_id BLOB NOT NULL,
|
||||
last_revealed INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- hash is block hash hex string,
|
||||
-- block height is a u32,
|
||||
CREATE TABLE block
|
||||
(
|
||||
hash TEXT PRIMARY KEY NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
-- whole_tx is a consensus encoded transaction,
|
||||
-- last seen is a u64 unix epoch seconds
|
||||
CREATE TABLE tx
|
||||
(
|
||||
txid TEXT PRIMARY KEY NOT NULL,
|
||||
whole_tx BLOB,
|
||||
last_seen INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- Outpoint txid hash hex string (reversed)
|
||||
-- Outpoint vout
|
||||
-- TxOut value as SATs
|
||||
-- TxOut script consensus encoded
|
||||
CREATE TABLE txout
|
||||
(
|
||||
txid TEXT NOT NULL,
|
||||
vout INTEGER NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
script BLOB NOT NULL,
|
||||
PRIMARY KEY (txid, vout)
|
||||
) STRICT;
|
||||
|
||||
-- join table between anchor and tx
|
||||
-- block hash hex string
|
||||
-- anchor is a json serialized Anchor structure as JSONB,
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
CREATE TABLE anchor_tx
|
||||
(
|
||||
block_hash TEXT NOT NULL,
|
||||
anchor BLOB NOT NULL,
|
||||
txid TEXT NOT NULL REFERENCES tx (txid),
|
||||
UNIQUE (anchor, txid),
|
||||
FOREIGN KEY (block_hash) REFERENCES block(hash)
|
||||
) STRICT;
|
||||
@@ -1,34 +0,0 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod schema;
|
||||
mod store;
|
||||
|
||||
use bdk_chain::bitcoin::Network;
|
||||
pub use rusqlite;
|
||||
pub use store::Store;
|
||||
|
||||
/// Error that occurs while reading or writing change sets with the SQLite database.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid network, cannot change the one already stored in the database.
|
||||
Network { expected: Network, given: Network },
|
||||
/// SQLite error.
|
||||
Sqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Network { expected, given } => write!(
|
||||
f,
|
||||
"network error trying to read or write change set, expected {}, given {}",
|
||||
expected, given
|
||||
),
|
||||
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
@@ -1,96 +0,0 @@
|
||||
use crate::Store;
|
||||
use rusqlite::{named_params, Connection, Error};
|
||||
|
||||
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
|
||||
const MIGRATIONS: &[&str] = &[SCHEMA_0];
|
||||
|
||||
/// Schema migration related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Migrate sqlite db schema to latest version.
|
||||
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
|
||||
let stmts = &MIGRATIONS
|
||||
.iter()
|
||||
.flat_map(|stmt| {
|
||||
// remove comment lines
|
||||
let s = stmt
|
||||
.split('\n')
|
||||
.filter(|l| !l.starts_with("--") && !l.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
// split into statements
|
||||
s.split(';')
|
||||
// remove extra spaces
|
||||
.map(|s| {
|
||||
s.trim()
|
||||
.split(' ')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
// remove empty statements
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let version = Self::get_schema_version(conn)?;
|
||||
let stmts = &stmts[(version as usize)..];
|
||||
|
||||
// begin transaction, all migration statements and new schema version commit or rollback
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// execute every statement and return `Some` new schema version
|
||||
// if execution fails, return `Error::Rusqlite`
|
||||
// if no statements executed returns `None`
|
||||
let new_version = stmts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|version_stmt| {
|
||||
tx.execute(version_stmt.1.as_str(), [])
|
||||
// map result value to next migration version
|
||||
.map(|_| version_stmt.0 as i32 + version + 1)
|
||||
})
|
||||
.last()
|
||||
.transpose()?;
|
||||
|
||||
// if `Some` new statement version, set new schema version
|
||||
if let Some(version) = new_version {
|
||||
Self::set_schema_version(&tx, version)?;
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
|
||||
let statement = conn.prepare_cached("SELECT version FROM version");
|
||||
match statement {
|
||||
Err(Error::SqliteFailure(e, Some(msg))) => {
|
||||
if msg == "no such table: version" {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(Error::SqliteFailure(e, Some(msg)))
|
||||
}
|
||||
}
|
||||
Ok(mut stmt) => {
|
||||
let mut rows = stmt.query([])?;
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let version: i32 = row.get(0)?;
|
||||
Ok(version)
|
||||
}
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
_ => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
|
||||
conn.execute(
|
||||
"UPDATE version SET version=:version",
|
||||
named_params! {":version": version},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,758 +0,0 @@
|
||||
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
|
||||
use bdk_chain::bitcoin::hashes::Hash;
|
||||
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use bdk_chain::bitcoin::{BlockHash, Txid};
|
||||
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
use rusqlite::{named_params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::Error;
|
||||
use bdk_chain::CombinedChangeSet;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
|
||||
};
|
||||
|
||||
/// Persists data in to a relational schema based [SQLite] database file.
|
||||
///
|
||||
/// The changesets loaded or stored represent changes to keychain and blockchain data.
|
||||
///
|
||||
/// [SQLite]: https://www.sqlite.org/index.html
|
||||
pub struct Store<K, A> {
|
||||
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
|
||||
conn: Mutex<Connection>,
|
||||
keychain_marker: PhantomData<K>,
|
||||
anchor_marker: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<K, A> Debug for Store<K, A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(&self.conn, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Creates a new store from a [`Connection`].
|
||||
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
|
||||
Self::migrate(&mut conn)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
keychain_marker: Default::default(),
|
||||
anchor_marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
|
||||
let connection = self.conn.get_mut().expect("unlocked connection mutex");
|
||||
connection.transaction().map_err(Error::Sqlite)
|
||||
}
|
||||
}
|
||||
|
||||
/// Network table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert [`Network`] for which all other tables data is valid.
|
||||
///
|
||||
/// Error if trying to insert different network value.
|
||||
fn insert_network(
|
||||
current_network: &Option<Network>,
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
network_changeset: &Option<Network>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(network) = network_changeset {
|
||||
match current_network {
|
||||
// if no network change do nothing
|
||||
Some(current_network) if current_network == network => Ok(()),
|
||||
// if new network not the same as current, error
|
||||
Some(current_network) => Err(Error::Network {
|
||||
expected: *current_network,
|
||||
given: *network,
|
||||
}),
|
||||
// insert network if none exists
|
||||
None => {
|
||||
let insert_network_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
|
||||
.expect("insert network statement");
|
||||
let name = network.to_string();
|
||||
insert_network_stmt
|
||||
.execute(named_params! {":name": name })
|
||||
.map_err(Error::Sqlite)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the valid [`Network`] for this database, or `None` if not set.
|
||||
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
|
||||
let mut select_network_stmt = db_transaction
|
||||
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
|
||||
.expect("select network statement");
|
||||
|
||||
let network = select_network_stmt
|
||||
.query_row([], |row| {
|
||||
let network = row.get_unwrap::<usize, String>(0);
|
||||
let network = Network::from_str(network.as_str()).expect("valid network");
|
||||
Ok(network)
|
||||
})
|
||||
.map_err(Error::Sqlite);
|
||||
match network {
|
||||
Ok(network) => Ok(Some(network)),
|
||||
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert or delete local chain blocks.
|
||||
///
|
||||
/// Error if trying to insert existing block hash.
|
||||
fn insert_or_delete_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
chain_changeset: &local_chain::ChangeSet,
|
||||
) -> Result<(), Error> {
|
||||
for (height, hash) in chain_changeset.iter() {
|
||||
match hash {
|
||||
// add new hash at height
|
||||
Some(hash) => {
|
||||
let insert_block_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
|
||||
.expect("insert block statement");
|
||||
let hash = hash.to_string();
|
||||
insert_block_stmt
|
||||
.execute(named_params! {":hash": hash, ":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
// delete block at height
|
||||
None => {
|
||||
let delete_block_stmt = &mut db_transaction
|
||||
.prepare_cached("DELETE FROM block WHERE height IS :height")
|
||||
.expect("delete block statement");
|
||||
delete_block_stmt
|
||||
.execute(named_params! {":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all blocks.
|
||||
fn select_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
|
||||
let mut select_blocks_stmt = db_transaction
|
||||
.prepare_cached("SELECT height, hash FROM block")
|
||||
.expect("select blocks statement");
|
||||
|
||||
let blocks = select_blocks_stmt
|
||||
.query_map([], |row| {
|
||||
let height = row.get_unwrap::<usize, u32>(0);
|
||||
let hash = row.get_unwrap::<usize, String>(1);
|
||||
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
|
||||
Ok((height, hash))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Keychain table related functions.
|
||||
///
|
||||
/// The keychain objects are stored as [`JSONB`] data.
|
||||
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + Send,
|
||||
{
|
||||
/// Insert keychain with descriptor and last active index.
|
||||
///
|
||||
/// If keychain exists only update last active index.
|
||||
fn insert_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
|
||||
let insert_keychain_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
|
||||
.expect("insert keychain statement");
|
||||
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
|
||||
let descriptor_id = descriptor.descriptor_id().to_byte_array();
|
||||
let descriptor = descriptor.to_string();
|
||||
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update descriptor last revealed index.
|
||||
fn update_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
|
||||
let update_last_revealed_stmt = &mut db_transaction
|
||||
.prepare_cached(
|
||||
"UPDATE keychain SET last_revealed = :last_revealed
|
||||
WHERE descriptor_id = :descriptor_id",
|
||||
)
|
||||
.expect("update last revealed statement");
|
||||
let descriptor_id = descriptor_id.to_byte_array();
|
||||
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select keychains added.
|
||||
fn select_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
|
||||
let mut select_keychains_added_stmt = db_transaction
|
||||
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
|
||||
.expect("select keychains statement");
|
||||
|
||||
let keychains = select_keychains_added_stmt
|
||||
.query_map([], |row| {
|
||||
let keychain = row.get_unwrap::<usize, String>(0);
|
||||
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
|
||||
let descriptor = row.get_unwrap::<usize, String>(1);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
Ok((keychain, descriptor))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
keychains
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select descriptor last revealed indexes.
|
||||
fn select_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
|
||||
let mut select_last_revealed_stmt = db_transaction
|
||||
.prepare_cached(
|
||||
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
|
||||
)
|
||||
.expect("select last revealed statement");
|
||||
|
||||
let last_revealed = select_last_revealed_stmt
|
||||
.query_map([], |row| {
|
||||
let descriptor = row.get_unwrap::<usize, String>(0);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
let descriptor_id = descriptor.descriptor_id();
|
||||
let last_revealed = row.get_unwrap::<usize, u32>(1);
|
||||
Ok((descriptor_id, last_revealed))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_revealed
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tx (transaction) and txout (transaction output) table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert transactions.
|
||||
///
|
||||
/// Error if trying to insert existing txid.
|
||||
fn insert_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx in tx_graph_changeset.graph.txs.iter() {
|
||||
let insert_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
|
||||
.expect("insert or update tx whole_tx statement");
|
||||
let txid = tx.compute_txid().to_string();
|
||||
let whole_tx = serialize(&tx);
|
||||
insert_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transactions.
|
||||
fn select_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
|
||||
let mut select_tx_stmt = db_transaction
|
||||
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
|
||||
.expect("select tx statement");
|
||||
|
||||
let txs = select_tx_stmt
|
||||
.query_map([], |row| {
|
||||
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
|
||||
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
|
||||
Ok(Arc::new(whole_tx))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
|
||||
txs.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select all transactions with last_seen values.
|
||||
fn select_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<Txid, u64>, Error> {
|
||||
// load tx last_seen
|
||||
let mut select_last_seen_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
|
||||
.expect("select tx last seen statement");
|
||||
|
||||
let last_seen = select_last_seen_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let last_seen = row.get_unwrap::<usize, u64>(1);
|
||||
Ok((txid, last_seen))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_seen
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Insert txouts.
|
||||
///
|
||||
/// Error if trying to insert existing outpoint.
|
||||
fn insert_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for txout in tx_graph_changeset.graph.txouts.iter() {
|
||||
let insert_txout_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
|
||||
.expect("insert txout statement");
|
||||
let txid = txout.0.txid.to_string();
|
||||
let vout = txout.0.vout;
|
||||
let value = txout.1.value.to_sat();
|
||||
let script = txout.1.script_pubkey.as_bytes();
|
||||
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transaction outputs.
|
||||
fn select_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
|
||||
// load tx outs
|
||||
let mut select_txout_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, vout, value, script FROM txout")
|
||||
.expect("select txout statement");
|
||||
|
||||
let txouts = select_txout_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let vout = row.get_unwrap::<usize, u32>(1);
|
||||
let outpoint = OutPoint::new(txid, vout);
|
||||
let value = row.get_unwrap::<usize, u64>(2);
|
||||
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
|
||||
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
|
||||
let txout = TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
script_pubkey,
|
||||
};
|
||||
Ok((outpoint, txout))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
txouts
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update transaction last seen times.
|
||||
fn update_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
|
||||
let insert_or_update_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
|
||||
.expect("insert or update tx last_seen statement");
|
||||
let txid = tx_last_seen.0.to_string();
|
||||
let last_seen = *tx_last_seen.1;
|
||||
insert_or_update_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Anchor table related functions.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Insert anchors.
|
||||
fn insert_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
// serde_json::to_string
|
||||
for anchor in tx_graph_changeset.graph.anchors.iter() {
|
||||
let insert_anchor_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
|
||||
.expect("insert anchor statement");
|
||||
let block_hash = anchor.0.anchor_block().hash.to_string();
|
||||
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
|
||||
let txid = anchor.1.to_string();
|
||||
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all anchors.
|
||||
fn select_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<(A, Txid)>, Error> {
|
||||
// serde_json::from_str
|
||||
let mut select_anchor_stmt = db_transaction
|
||||
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
|
||||
.expect("select anchor statement");
|
||||
let anchors = select_anchor_stmt
|
||||
.query_map([], |row| {
|
||||
let hash = row.get_unwrap::<usize, String>(0);
|
||||
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
|
||||
let anchor = row.get_unwrap::<usize, String>(1);
|
||||
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
|
||||
// double check anchor blob block hash matches
|
||||
assert_eq!(hash, anchor.anchor_block().hash);
|
||||
let txid = row.get_unwrap::<usize, String>(2);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
Ok((anchor, txid))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
anchors
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to read and write all [`CombinedChangeSet`] data.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Write the given `changeset` atomically.
|
||||
pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
|
||||
// no need to write anything if changeset is empty
|
||||
if changeset.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network_changeset = &changeset.network;
|
||||
let current_network = Self::select_network(&db_transaction)?;
|
||||
Self::insert_network(¤t_network, &db_transaction, network_changeset)?;
|
||||
|
||||
let chain_changeset = &changeset.chain;
|
||||
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
|
||||
|
||||
let tx_graph_changeset = &changeset.indexed_tx_graph;
|
||||
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
|
||||
db_transaction.commit().map_err(Error::Sqlite)
|
||||
}
|
||||
|
||||
/// Read the entire database and return the aggregate [`CombinedChangeSet`].
|
||||
pub fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network = Self::select_network(&db_transaction)?;
|
||||
let chain = Self::select_blocks(&db_transaction)?;
|
||||
let keychains_added = Self::select_keychains(&db_transaction)?;
|
||||
let last_revealed = Self::select_last_revealed(&db_transaction)?;
|
||||
let txs = Self::select_txs(&db_transaction)?;
|
||||
let last_seen = Self::select_last_seen(&db_transaction)?;
|
||||
let txouts = Self::select_txouts(&db_transaction)?;
|
||||
let anchors = Self::select_anchors(&db_transaction)?;
|
||||
|
||||
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
|
||||
txs,
|
||||
txouts,
|
||||
anchors,
|
||||
last_seen,
|
||||
};
|
||||
|
||||
let indexer: keychain::ChangeSet<K> = keychain::ChangeSet {
|
||||
keychains_added,
|
||||
last_revealed,
|
||||
};
|
||||
|
||||
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>> =
|
||||
indexed_tx_graph::ChangeSet { graph, indexer };
|
||||
|
||||
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(CombinedChangeSet {
|
||||
chain,
|
||||
indexed_tx_graph,
|
||||
network,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::store::Append;
|
||||
use bdk_chain::bitcoin::consensus::encode::deserialize;
|
||||
use bdk_chain::bitcoin::constants::genesis_block;
|
||||
use bdk_chain::bitcoin::hashes::hex::FromHex;
|
||||
use bdk_chain::bitcoin::transaction::Transaction;
|
||||
use bdk_chain::bitcoin::Network::Testnet;
|
||||
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
|
||||
use bdk_chain::miniscript::Descriptor;
|
||||
use bdk_chain::CombinedChangeSet;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
|
||||
ConfirmationTimeHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
|
||||
enum Keychain {
|
||||
External { account: u32, name: String },
|
||||
Internal { account: u32, name: String },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
|
||||
confirmation_height: height,
|
||||
confirmation_time: time,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
|
||||
confirmation_height: height,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
fn create_test_changesets<A: Anchor + Copy>(
|
||||
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
|
||||
) -> (
|
||||
Vec<CombinedChangeSet<Keychain, A>>,
|
||||
CombinedChangeSet<Keychain, A>,
|
||||
) {
|
||||
let secp = &secp256k1::Secp256k1::signing_only();
|
||||
|
||||
let network_changeset = Some(Testnet);
|
||||
|
||||
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
|
||||
let block_hash_1 =
|
||||
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
|
||||
.unwrap();
|
||||
let block_hash_2 =
|
||||
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
|
||||
.unwrap();
|
||||
|
||||
let block_changeset = [
|
||||
(0, Some(block_hash_0)),
|
||||
(1, Some(block_hash_1)),
|
||||
(2, Some(block_hash_2)),
|
||||
]
|
||||
.into();
|
||||
|
||||
let ext_keychain = Keychain::External {
|
||||
account: 0,
|
||||
name: "ext test".to_string(),
|
||||
};
|
||||
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
|
||||
let ext_desc_id = ext_desc.descriptor_id();
|
||||
let int_keychain = Keychain::Internal {
|
||||
account: 0,
|
||||
name: "int test".to_string(),
|
||||
};
|
||||
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
|
||||
let int_desc_id = int_desc.descriptor_id();
|
||||
|
||||
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
|
||||
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
|
||||
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
|
||||
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
|
||||
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
|
||||
|
||||
let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
|
||||
let txout0_0 = tx0.output.first().unwrap().clone();
|
||||
let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
|
||||
let txout1_0 = tx1.output.first().unwrap().clone();
|
||||
|
||||
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
|
||||
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
|
||||
|
||||
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx0.clone(), tx1.clone()].into(),
|
||||
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
|
||||
anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
|
||||
last_seen: [
|
||||
(tx0.compute_txid(), 1598918400),
|
||||
(tx1.compute_txid(), 1598919121),
|
||||
(tx2.compute_txid(), 1608919121),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
|
||||
let keychain_changeset = keychain::ChangeSet {
|
||||
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
|
||||
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset,
|
||||
indexer: keychain_changeset,
|
||||
};
|
||||
|
||||
// test changesets to write to db
|
||||
let mut changesets = Vec::new();
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: block_changeset,
|
||||
indexed_tx_graph: graph_changeset,
|
||||
network: network_changeset,
|
||||
});
|
||||
|
||||
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
|
||||
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx2.clone()].into(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: BTreeSet::default(),
|
||||
last_seen: [(tx2.compute_txid(), 1708919121)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset2,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset2,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// create changeset that adds a new anchor2 for tx0 and tx1
|
||||
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
|
||||
txs: BTreeSet::default(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
|
||||
last_seen: BTreeMap::default(),
|
||||
};
|
||||
|
||||
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset3,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset3,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// aggregated test changesets
|
||||
let agg_test_changesets =
|
||||
changesets
|
||||
.iter()
|
||||
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
|
||||
i.append(cs.clone());
|
||||
i
|
||||
});
|
||||
|
||||
(changesets, agg_test_changesets)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.6.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]
|
||||
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
|
||||
electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
|
||||
[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,304 +0,0 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
|
||||
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
|
||||
},
|
||||
local_chain::CheckPoint,
|
||||
BlockId,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
pub use electrsd;
|
||||
pub use electrsd::bitcoind;
|
||||
pub use electrsd::bitcoind::anyhow;
|
||||
pub use electrsd::bitcoind::bitcoincore_rpc;
|
||||
pub use electrsd::electrum_client;
|
||||
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: transaction::Version::ONE,
|
||||
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: Amount::ZERO,
|
||||
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)
|
||||
}
|
||||
|
||||
/// Create a checkpoint linked list of all the blocks in the chain.
|
||||
pub fn make_checkpoint_tip(&self) -> CheckPoint {
|
||||
CheckPoint::from_block_ids((0_u32..).map_while(|height| {
|
||||
self.bitcoind
|
||||
.client
|
||||
.get_block_hash(height as u64)
|
||||
.ok()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
}))
|
||||
.expect("must craft tip")
|
||||
}
|
||||
|
||||
/// Get the genesis hash of the blockchain.
|
||||
pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
|
||||
let hash = self.bitcoind.client.get_block_hash(0)?;
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::TestEnv;
|
||||
use electrsd::bitcoind::{anyhow::Result, 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(())
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk_wallet;
|
||||
extern crate bitcoin;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk_wallet::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
|
||||
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
|
||||
/// can be derived from the policy.
|
||||
///
|
||||
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// We start with a miniscript policy string
|
||||
let policy_str = "or(
|
||||
10@thresh(4,
|
||||
pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)
|
||||
),1@and(
|
||||
older(4209713),
|
||||
thresh(2,
|
||||
pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068)
|
||||
)
|
||||
)
|
||||
)"
|
||||
.replace(&[' ', '\n', '\t'][..], "");
|
||||
|
||||
println!("Compiling policy: \n{}", policy_str);
|
||||
|
||||
// Parse the string as a [`Concrete`] type miniscript policy.
|
||||
let policy = Concrete::<String>::from_str(&policy_str)?;
|
||||
|
||||
// Create a `wsh` type descriptor from the policy.
|
||||
// `policy.compile()` returns the resulting miniscript from the policy.
|
||||
let descriptor = Descriptor::new_wsh(policy.compile()?)?.to_string();
|
||||
|
||||
println!("Compiled into Descriptor: \n{}", descriptor);
|
||||
|
||||
// Do the same for another (internal) keychain
|
||||
let policy_str = "or(
|
||||
10@thresh(2,
|
||||
pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec)
|
||||
),1@and(
|
||||
pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),
|
||||
older(12960)
|
||||
)
|
||||
)"
|
||||
.replace(&[' ', '\n', '\t'][..], "");
|
||||
|
||||
println!("Compiling internal policy: \n{}", policy_str);
|
||||
|
||||
let policy = Concrete::<String>::from_str(&policy_str)?;
|
||||
let internal_descriptor = Descriptor::new_wsh(policy.compile()?)?.to_string();
|
||||
println!(
|
||||
"Compiled into internal Descriptor: \n{}",
|
||||
internal_descriptor
|
||||
);
|
||||
|
||||
// Create a new wallet from descriptors
|
||||
let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
|
||||
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.next_unused_address(KeychainKind::External),
|
||||
);
|
||||
|
||||
// BDK also has it's own `Policy` structure to represent the spending condition in a more
|
||||
// human readable json format.
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
println!(
|
||||
"The BDK spending policy: \n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::psbt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of keychains
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub enum KeychainKind {
|
||||
/// External keychain, used for deriving recipient addresses.
|
||||
External = 0,
|
||||
/// Internal keychain, used for deriving change addresses.
|
||||
Internal = 1,
|
||||
}
|
||||
|
||||
impl KeychainKind {
|
||||
/// Return [`KeychainKind`] as a byte
|
||||
pub fn as_byte(&self) -> u8 {
|
||||
match self {
|
||||
KeychainKind::External => b'e',
|
||||
KeychainKind::Internal => b'i',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for KeychainKind {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
KeychainKind::External => b"e",
|
||||
KeychainKind::Internal => b"i",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unspent output owned by a [`Wallet`].
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LocalOutput {
|
||||
/// Reference to a transaction output
|
||||
pub outpoint: OutPoint,
|
||||
/// Transaction output
|
||||
pub txout: TxOut,
|
||||
/// Type of keychain
|
||||
pub keychain: KeychainKind,
|
||||
/// Whether this UTXO is spent or not
|
||||
pub is_spent: bool,
|
||||
/// The derivation index for the script pubkey in the wallet
|
||||
pub derivation_index: u32,
|
||||
/// The confirmation time for transaction containing this utxo
|
||||
pub confirmation_time: ConfirmationTime,
|
||||
}
|
||||
|
||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WeightedUtxo {
|
||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
///
|
||||
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
|
||||
pub satisfaction_weight: usize,
|
||||
/// The UTXO
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
Local(LocalOutput),
|
||||
/// A UTXO owned by another wallet.
|
||||
Foreign {
|
||||
/// The location of the output.
|
||||
outpoint: OutPoint,
|
||||
/// The nSequence value to set for this input.
|
||||
sequence: Option<Sequence>,
|
||||
/// The information about the input we require to add it to a PSBT.
|
||||
// Box it to stop the type being too big.
|
||||
psbt_input: Box<psbt::Input>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Utxo {
|
||||
/// Get the location of the UTXO
|
||||
pub fn outpoint(&self) -> OutPoint {
|
||||
match &self {
|
||||
Utxo::Local(local) => local.outpoint,
|
||||
Utxo::Foreign { outpoint, .. } => *outpoint,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `TxOut` of the UTXO
|
||||
pub fn txout(&self) -> &TxOut {
|
||||
match &self {
|
||||
Utxo::Local(local) => &local.txout,
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
..
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
}
|
||||
|
||||
if let Some(txout) = &psbt_input.witness_utxo {
|
||||
return txout;
|
||||
}
|
||||
|
||||
unreachable!("Foreign UTXOs will always have one of these set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sequence number if an explicit sequence number has to be set for this input.
|
||||
pub fn sequence(&self) -> Option<Sequence> {
|
||||
match self {
|
||||
Utxo::Local(_) => None,
|
||||
Utxo::Foreign { sequence, .. } => *sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
# Example RPC CLI
|
||||
|
||||
### Simple Regtest Test
|
||||
|
||||
1. Start local regtest bitcoind.
|
||||
```
|
||||
mkdir -p /tmp/regtest/bitcoind
|
||||
bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
|
||||
```
|
||||
2. Create a test bitcoind wallet and set bitcoind env.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
|
||||
export RPC_URL=127.0.0.1:18443
|
||||
export RPC_USER=<your-rpc-username>
|
||||
export RPC_PASS=<your-rpc-password>
|
||||
```
|
||||
3. Get test bitcoind wallet info.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
|
||||
```
|
||||
4. Get new test bitcoind wallet address.
|
||||
```
|
||||
BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
|
||||
echo $BITCOIND_ADDRESS
|
||||
```
|
||||
5. Generate 101 blocks with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
|
||||
```
|
||||
6. Verify test bitcoind wallet balance.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
|
||||
```
|
||||
7. Set descriptor env and get address from RPC CLI wallet.
|
||||
```
|
||||
export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
|
||||
cargo run -- --network regtest address next
|
||||
```
|
||||
8. Send 5 test bitcoin to RPC CLI wallet.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
|
||||
```
|
||||
9. Sync blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
10. Get RPC CLI wallet unconfirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
11. Generate 1 block with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
|
||||
```
|
||||
12. Sync the blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
13. Get RPC CLI wallet confirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
14. Get RPC CLI wallet transactions.
|
||||
```
|
||||
cargo run -- --network regtest txout list
|
||||
```
|
||||
@@ -12,10 +12,10 @@ use bdk_bitcoind_rpc::{
|
||||
Emitter,
|
||||
};
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Block, Transaction},
|
||||
bitcoin::{Block, Transaction},
|
||||
indexed_tx_graph, keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
Append, ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow,
|
||||
@@ -42,7 +42,7 @@ type ChangeSet = (
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Emission {
|
||||
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
|
||||
Block { height: u32, block: Block },
|
||||
Mempool(Vec<(Transaction, u64)>),
|
||||
Tip(u32),
|
||||
}
|
||||
@@ -64,6 +64,9 @@ struct RpcArgs {
|
||||
/// Starting block height to fallback to if no point of agreement if found
|
||||
#[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
|
||||
fallback_height: u32,
|
||||
/// The unused-scripts lookahead will be kept at this size
|
||||
#[clap(long, default_value = "10")]
|
||||
lookahead: u32,
|
||||
}
|
||||
|
||||
impl From<RpcArgs> for Auth {
|
||||
@@ -110,22 +113,17 @@ enum RpcCommands {
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
println!(
|
||||
"[{:>10}s] loaded initial changeset from db",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
let (init_chain_changeset, init_graph_changeset) = init_changeset;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
graph.apply_changeset(init_graph_changeset);
|
||||
graph.apply_changeset(init_changeset.1);
|
||||
graph
|
||||
});
|
||||
println!(
|
||||
@@ -133,15 +131,7 @@ fn main() -> anyhow::Result<()> {
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
let chain = Mutex::new(if init_chain_changeset.is_empty() {
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
let mut db = db.lock().unwrap();
|
||||
db.append_changeset(&(chain_changeset, Default::default()))?;
|
||||
chain
|
||||
} else {
|
||||
LocalChain::from_changeset(init_chain_changeset)?
|
||||
});
|
||||
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
|
||||
println!(
|
||||
"[{:>10}s] loaded local chain from changeset",
|
||||
start.elapsed().as_secs_f32()
|
||||
@@ -150,7 +140,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let rpc_cmd = match args.command {
|
||||
example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
let res = example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -163,42 +153,45 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd,
|
||||
);
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
match rpc_cmd {
|
||||
RpcCommands::Sync { rpc_args } => {
|
||||
let RpcArgs {
|
||||
fallback_height, ..
|
||||
fallback_height,
|
||||
lookahead,
|
||||
..
|
||||
} = rpc_args;
|
||||
|
||||
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
|
||||
|
||||
let chain_tip = chain.lock().unwrap().tip();
|
||||
let rpc_client = rpc_args.new_client()?;
|
||||
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
|
||||
let mut db_stage = ChangeSet::default();
|
||||
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Instant::now();
|
||||
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
let mut db = db.lock().unwrap();
|
||||
|
||||
let chain_update =
|
||||
CheckPoint::from_header(&block.header, height).into_update(false);
|
||||
let chain_changeset = chain
|
||||
.apply_update(emission.checkpoint)
|
||||
.apply_update(chain_update)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
|
||||
db_stage.append((chain_changeset, graph_changeset));
|
||||
let graph_changeset = graph.apply_block_relevant(block, height);
|
||||
db.stage((chain_changeset, graph_changeset));
|
||||
|
||||
// commit staged db changes in intervals
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
let db = &mut *db.lock().unwrap();
|
||||
last_db_commit = Instant::now();
|
||||
if let Some(changeset) = db_stage.take() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
db.commit()?;
|
||||
println!(
|
||||
"[{:>10}s] committed to db (took {}s)",
|
||||
start.elapsed().as_secs_f32(),
|
||||
@@ -233,19 +226,20 @@ fn main() -> anyhow::Result<()> {
|
||||
mempool_txs.iter().map(|(tx, time)| (tx, *time)),
|
||||
);
|
||||
{
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db_stage.append((local_chain::ChangeSet::default(), graph_changeset));
|
||||
if let Some(changeset) = db_stage.take() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage((local_chain::ChangeSet::default(), graph_changeset));
|
||||
db.commit()?; // commit one last time
|
||||
}
|
||||
}
|
||||
RpcCommands::Live { rpc_args } => {
|
||||
let RpcArgs {
|
||||
fallback_height, ..
|
||||
fallback_height,
|
||||
lookahead,
|
||||
..
|
||||
} = rpc_args;
|
||||
let sigterm_flag = start_ctrlc_handler();
|
||||
|
||||
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
|
||||
let last_cp = chain.lock().unwrap().tip();
|
||||
|
||||
println!(
|
||||
@@ -262,8 +256,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
loop {
|
||||
match emitter.next_block()? {
|
||||
Some(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
Some((height, block)) => {
|
||||
if sigterm_flag.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
@@ -271,7 +264,7 @@ fn main() -> anyhow::Result<()> {
|
||||
block_count = rpc_client.get_block_count()? as u32;
|
||||
tx.send(Emission::Tip(block_count))?;
|
||||
}
|
||||
tx.send(Emission::Block(block_emission))?;
|
||||
tx.send(Emission::Block { height, block })?;
|
||||
}
|
||||
None => {
|
||||
if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
|
||||
@@ -293,20 +286,20 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut tip_height = 0_u32;
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Option::<Instant>::None;
|
||||
let mut db_stage = ChangeSet::default();
|
||||
|
||||
for emission in rx {
|
||||
let mut db = db.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
let mut chain = chain.lock().unwrap();
|
||||
|
||||
let changeset = match emission {
|
||||
Emission::Block(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
Emission::Block { height, block } => {
|
||||
let chain_update =
|
||||
CheckPoint::from_header(&block.header, height).into_update(false);
|
||||
let chain_changeset = chain
|
||||
.apply_update(block_emission.checkpoint)
|
||||
.apply_update(chain_update)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset =
|
||||
graph.apply_block_relevant(&block_emission.block, height);
|
||||
let graph_changeset = graph.apply_block_relevant(block, height);
|
||||
(chain_changeset, graph_changeset)
|
||||
}
|
||||
Emission::Mempool(mempool_txs) => {
|
||||
@@ -320,14 +313,12 @@ fn main() -> anyhow::Result<()> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
db_stage.append(changeset);
|
||||
|
||||
db.stage(changeset);
|
||||
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
let db = &mut *db.lock().unwrap();
|
||||
last_db_commit = Instant::now();
|
||||
if let Some(changeset) = db_stage.take() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
db.commit()?;
|
||||
println!(
|
||||
"[{:>10}s] committed to db (took {}s)",
|
||||
start.elapsed().as_secs_f32(),
|
||||
|
||||
@@ -3,15 +3,12 @@ use anyhow::Context;
|
||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
use bdk_file_store::Store;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::{cmp::Reverse, collections::BTreeMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
absolute, address,
|
||||
secp256k1::Secp256k1,
|
||||
sighash::{Prevouts, SighashCache},
|
||||
transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
|
||||
absolute, address, psbt::Prevouts, secp256k1::Secp256k1, sighash::SighashCache, Address,
|
||||
Network, Sequence, Transaction, TxIn, TxOut,
|
||||
},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
@@ -20,7 +17,7 @@ use bdk_chain::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut,
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
|
||||
};
|
||||
pub use bdk_file_store;
|
||||
pub use clap;
|
||||
@@ -32,6 +29,7 @@ pub type KeychainChangeSet<A> = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
pub type Database<'m, C> = Persist<Store<'m, C>, C>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
@@ -55,6 +53,7 @@ pub struct Args<CS: clap::Subcommand, S: clap::Args> {
|
||||
pub command: Commands<CS, S>,
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
#[clap(flatten)]
|
||||
@@ -74,9 +73,7 @@ pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
},
|
||||
/// Send coins to an address.
|
||||
Send {
|
||||
/// Amount to send in satoshis
|
||||
value: u64,
|
||||
/// Destination address
|
||||
address: Address<address::NetworkUnchecked>,
|
||||
#[clap(short, default_value = "bnb")]
|
||||
coin_select: CoinSelectionAlgo,
|
||||
@@ -138,6 +135,7 @@ impl core::fmt::Display for CoinSelectionAlgo {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address.
|
||||
@@ -146,17 +144,14 @@ pub enum AddressCmd {
|
||||
New,
|
||||
/// List all addresses
|
||||
List {
|
||||
/// List change addresses
|
||||
#[clap(long)]
|
||||
change: bool,
|
||||
},
|
||||
/// Get last revealed address index for each keychain.
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum TxOutCmd {
|
||||
/// List transaction outputs.
|
||||
List {
|
||||
/// Return only spent outputs.
|
||||
#[clap(short, long)]
|
||||
@@ -190,20 +185,18 @@ impl core::fmt::Display for Keychain {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateTxChange {
|
||||
pub index_changeset: keychain::ChangeSet<Keychain>,
|
||||
pub change_keychain: Keychain,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
|
||||
) -> anyhow::Result<(
|
||||
Transaction,
|
||||
Option<(keychain::ChangeSet<Keychain>, (Keychain, u32))>,
|
||||
)>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
@@ -237,7 +230,7 @@ where
|
||||
.iter()
|
||||
.map(|(plan, utxo)| {
|
||||
WeightedValue::new(
|
||||
utxo.txout.value.to_sat(),
|
||||
utxo.txout.value,
|
||||
plan.expected_weight() as _,
|
||||
plan.witness_version().is_some(),
|
||||
)
|
||||
@@ -245,33 +238,29 @@ where
|
||||
.collect();
|
||||
|
||||
let mut outputs = vec![TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
value,
|
||||
script_pubkey: address.script_pubkey(),
|
||||
}];
|
||||
|
||||
let internal_keychain = if graph
|
||||
.index
|
||||
.keychains()
|
||||
.any(|(k, _)| *k == Keychain::Internal)
|
||||
{
|
||||
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
|
||||
Keychain::Internal
|
||||
} else {
|
||||
Keychain::External
|
||||
};
|
||||
|
||||
let ((change_index, change_script), change_changeset) = graph
|
||||
.index
|
||||
.next_unused_spk(&internal_keychain)
|
||||
.expect("Must exist");
|
||||
let ((change_index, change_script), change_changeset) =
|
||||
graph.index.next_unused_spk(&internal_keychain);
|
||||
changeset.append(change_changeset);
|
||||
|
||||
// Clone to drop the immutable reference.
|
||||
let change_script = change_script.into();
|
||||
|
||||
let change_plan = bdk_tmp_plan::plan_satisfaction(
|
||||
&graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.at_derivation_index(change_index)
|
||||
.expect("change_index can't be hardened"),
|
||||
&assets,
|
||||
@@ -279,7 +268,7 @@ where
|
||||
.expect("failed to obtain change plan");
|
||||
|
||||
let mut change_output = TxOut {
|
||||
value: Amount::ZERO,
|
||||
value: 0,
|
||||
script_pubkey: change_script,
|
||||
};
|
||||
|
||||
@@ -288,9 +277,8 @@ where
|
||||
min_drain_value: graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.dust_value(),
|
||||
..CoinSelectorOpt::fund_outputs(
|
||||
&outputs,
|
||||
@@ -318,13 +306,13 @@ where
|
||||
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
||||
|
||||
if let Some(drain_value) = selection_meta.drain_value {
|
||||
change_output.value = Amount::from_sat(drain_value);
|
||||
change_output.value = drain_value;
|
||||
// if the selection tells us to use change and the change value is sufficient, we add it as an output
|
||||
outputs.push(change_output)
|
||||
}
|
||||
|
||||
let mut transaction = Transaction {
|
||||
version: transaction::Version::TWO,
|
||||
version: 0x02,
|
||||
// because the temporary planning module does not support timelocks, we can use the chain
|
||||
// tip as the `lock_time` for anti-fee-sniping purposes
|
||||
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
|
||||
@@ -400,11 +388,7 @@ where
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some(CreateTxChange {
|
||||
index_changeset: changeset,
|
||||
change_keychain: internal_keychain,
|
||||
index: change_index,
|
||||
})
|
||||
Some((changeset, (internal_keychain, change_index)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -412,57 +396,50 @@ where
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
// Alias the elements of `Result` of `planned_utxos`
|
||||
pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
|
||||
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints();
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())
|
||||
.filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(keychain, _)| *keychain == &k)
|
||||
.expect("keychain must exist")
|
||||
.1
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
})
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
.filter_map(
|
||||
#[allow(clippy::type_complexity)]
|
||||
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
|
||||
graph: &Mutex<KeychainTxGraph<A>>,
|
||||
db: &Mutex<Store<C>>,
|
||||
db: &Mutex<Database<C>>,
|
||||
chain: &Mutex<O>,
|
||||
keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
network: Network,
|
||||
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
|
||||
cmd: Commands<CS, S>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
C: Default
|
||||
+ Append
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ From<KeychainChangeSet<A>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Debug,
|
||||
C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
|
||||
{
|
||||
match cmd {
|
||||
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
|
||||
@@ -478,15 +455,15 @@ where
|
||||
_ => unreachable!("only these two variants exist in match arm"),
|
||||
};
|
||||
|
||||
let ((spk_i, spk), index_changeset) =
|
||||
spk_chooser(index, &Keychain::External).expect("Must exist");
|
||||
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.append_changeset(&C::from((
|
||||
db.stage(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)))?;
|
||||
let addr = Address::from_script(spk.as_script(), network)
|
||||
.context("failed to derive address")?;
|
||||
)));
|
||||
db.commit()?;
|
||||
let addr =
|
||||
Address::from_script(spk, network).context("failed to derive address")?;
|
||||
println!("[address @ {}] {}", spk_i, addr);
|
||||
Ok(())
|
||||
}
|
||||
@@ -501,14 +478,14 @@ where
|
||||
true => Keychain::Internal,
|
||||
false => Keychain::External,
|
||||
};
|
||||
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
|
||||
for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
|
||||
let address = Address::from_script(spk, network)
|
||||
.expect("should always be able to derive address");
|
||||
println!(
|
||||
"{:?} {} used:{}",
|
||||
spk_i,
|
||||
address,
|
||||
index.is_used(target_keychain, spk_i)
|
||||
index.is_used(&(target_keychain, spk_i))
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -520,11 +497,11 @@ where
|
||||
let chain = &*chain.lock().unwrap();
|
||||
fn print_balances<'a>(
|
||||
title_str: &'a str,
|
||||
items: impl IntoIterator<Item = (&'a str, Amount)>,
|
||||
items: impl IntoIterator<Item = (&'a str, u64)>,
|
||||
) {
|
||||
println!("{}:", title_str);
|
||||
for (name, amount) in items.into_iter() {
|
||||
println!(" {:<10} {:>12} sats", name, amount.to_sat())
|
||||
println!(" {:<10} {:>12} sats", name, amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,7 +538,7 @@ where
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints();
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
match txout_cmd {
|
||||
TxOutCmd::List {
|
||||
@@ -572,7 +549,7 @@ where
|
||||
} => {
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())
|
||||
.try_filter_chain_txouts(chain, chain_tip, outpoints)
|
||||
.filter(|r| match r {
|
||||
Ok((_, full_txo)) => match (spent, unspent) {
|
||||
(true, false) => full_txo.spent_by.is_some(),
|
||||
@@ -618,27 +595,23 @@ where
|
||||
let (tx, change_info) =
|
||||
create_tx(graph, chain, keymap, coin_select, address, value)?;
|
||||
|
||||
if let Some(CreateTxChange {
|
||||
index_changeset,
|
||||
change_keychain,
|
||||
index,
|
||||
}) = change_info
|
||||
{
|
||||
if let Some((index_changeset, (change_keychain, index))) = change_info {
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this, then we don't want to broadcast.
|
||||
{
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.append_changeset(&C::from((
|
||||
db.stage(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)))?;
|
||||
)));
|
||||
db.commit()?;
|
||||
}
|
||||
|
||||
// We don't want other callers/threads to use this address while we're using it
|
||||
// but we also don't want to scan the tx we just created because it's not
|
||||
// technically in the blockchain yet.
|
||||
graph.index.mark_used(change_keychain, index);
|
||||
graph.index.mark_used(&change_keychain, index);
|
||||
(tx, Some((change_keychain, index)))
|
||||
} else {
|
||||
(tx, None)
|
||||
@@ -647,23 +620,23 @@ where
|
||||
|
||||
match (broadcast)(chain_specific, &transaction) {
|
||||
Ok(_) => {
|
||||
println!("Broadcasted Tx : {}", transaction.compute_txid());
|
||||
println!("Broadcasted Tx : {}", transaction.txid());
|
||||
|
||||
let keychain_changeset = graph.lock().unwrap().insert_tx(transaction);
|
||||
|
||||
// We know the tx is at least unconfirmed now. Note if persisting here fails,
|
||||
// it's not a big deal since we can always find it again form
|
||||
// blockchain.
|
||||
db.lock().unwrap().append_changeset(&C::from((
|
||||
db.lock().unwrap().stage(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
keychain_changeset,
|
||||
)))?;
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some((keychain, index)) = change_index {
|
||||
// We failed to broadcast, so allow our change address to be used in the future
|
||||
graph.lock().unwrap().index.unmark_used(keychain, index);
|
||||
graph.lock().unwrap().index.unmark_used(&keychain, index);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
@@ -672,38 +645,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// The initial state returned by [`init`].
|
||||
pub struct Init<CS: clap::Subcommand, S: clap::Args, C>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
|
||||
{
|
||||
/// Arguments parsed by the cli.
|
||||
pub args: Args<CS, S>,
|
||||
/// Descriptor keymap.
|
||||
pub keymap: KeyMap,
|
||||
/// Keychain-txout index.
|
||||
pub index: KeychainTxOutIndex<Keychain>,
|
||||
/// Persistence backend.
|
||||
pub db: Mutex<Store<C>>,
|
||||
/// Initial changeset.
|
||||
pub init_changeset: C,
|
||||
}
|
||||
|
||||
/// Parses command line arguments and initializes all components, creating
|
||||
/// a file store with the given parameters, or loading one if it exists.
|
||||
pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &[u8],
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn init<'m, CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &'m [u8],
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
) -> anyhow::Result<(
|
||||
Args<CS, S>,
|
||||
KeyMap,
|
||||
KeychainTxOutIndex<Keychain>,
|
||||
Mutex<Database<'m, C>>,
|
||||
C,
|
||||
)>
|
||||
where
|
||||
C: Default
|
||||
+ Append
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Debug
|
||||
+ core::marker::Send
|
||||
+ core::marker::Sync
|
||||
+ 'static,
|
||||
C: Default + Append + Serialize + DeserializeOwned,
|
||||
{
|
||||
if std::env::var("BDK_DB_PATH").is_err() {
|
||||
std::env::set_var("BDK_DB_PATH", db_default_path);
|
||||
@@ -713,11 +667,9 @@ where
|
||||
|
||||
let mut index = KeychainTxOutIndex::<Keychain>::default();
|
||||
|
||||
// TODO: descriptors are already stored in the db, so we shouldn't re-insert
|
||||
// them in the index here. However, the keymap is not stored in the database.
|
||||
let (descriptor, mut keymap) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
|
||||
let _ = index.insert_descriptor(Keychain::External, descriptor)?;
|
||||
index.add_keychain(Keychain::External, descriptor);
|
||||
|
||||
if let Some((internal_descriptor, internal_keymap)) = args
|
||||
.change_descriptor
|
||||
@@ -726,22 +678,22 @@ where
|
||||
.transpose()?
|
||||
{
|
||||
keymap.extend(internal_keymap);
|
||||
let _ = index.insert_descriptor(Keychain::Internal, internal_descriptor)?;
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
let mut db_backend = match Store::<'m, C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
Ok(db_backend) => db_backend,
|
||||
// we cannot return `err` directly as it has lifetime `'m`
|
||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||
};
|
||||
|
||||
let init_changeset = db_backend.aggregate_changesets()?.unwrap_or_default();
|
||||
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||
|
||||
Ok(Init {
|
||||
Ok((
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db: Mutex::new(db_backend),
|
||||
Mutex::new(Database::new(db_backend)),
|
||||
init_changeset,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
collections::BTreeSet,
|
||||
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, Client, ElectrumApi},
|
||||
BdkElectrumClient,
|
||||
ElectrumExt, ElectrumUpdate,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow::{self, Context},
|
||||
@@ -104,15 +103,8 @@ type ChangeSet = (
|
||||
);
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let (disk_local_chain, disk_tx_graph) = init_changeset;
|
||||
let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
|
||||
example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
@@ -120,17 +112,12 @@ fn main() -> anyhow::Result<()> {
|
||||
graph
|
||||
});
|
||||
|
||||
let chain = Mutex::new({
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&disk_local_chain)?;
|
||||
chain
|
||||
});
|
||||
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
|
||||
|
||||
let electrum_cmd = match &args.command {
|
||||
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
let res = example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -143,63 +130,57 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(args.network)?);
|
||||
let client = electrum_cmd.electrum_args().client(args.network)?;
|
||||
|
||||
// Tell the electrum client about the txs we've already got locally so it doesn't re-download them
|
||||
client.populate_tx_cache(&*graph.lock().unwrap());
|
||||
|
||||
let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
|
||||
let response = match electrum_cmd.clone() {
|
||||
ElectrumCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let request = {
|
||||
let (keychain_spks, tip) = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
|
||||
FullScanRequest::from_chain_tip(chain.tip())
|
||||
.set_spks_for_keychain(
|
||||
Keychain::External,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::External)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.set_spks_for_keychain(
|
||||
Keychain::Internal,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::Internal)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::new();
|
||||
move |k, spk_i, _| {
|
||||
if once.insert(k) {
|
||||
eprint!("\nScanning {}: {} ", k, spk_i);
|
||||
} else {
|
||||
eprint!("{} ", spk_i);
|
||||
let keychain_spks = graph
|
||||
.index
|
||||
.spks_of_all_keychains()
|
||||
.into_iter()
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
io::stdout().flush().expect("must flush");
|
||||
}
|
||||
|
||||
eprint!("{} ", i);
|
||||
let _ = io::stdout().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let tip = chain.tip();
|
||||
(keychain_spks, tip)
|
||||
};
|
||||
|
||||
let res = client
|
||||
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
|
||||
client
|
||||
.scan(
|
||||
tip,
|
||||
keychain_spks,
|
||||
core::iter::empty(),
|
||||
core::iter::empty(),
|
||||
stop_gap,
|
||||
scan_options.batch_size,
|
||||
)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
(
|
||||
res.chain_update,
|
||||
res.graph_update,
|
||||
Some(res.last_active_indices),
|
||||
)
|
||||
}
|
||||
ElectrumCommands::Sync {
|
||||
mut unused_spks,
|
||||
@@ -212,6 +193,7 @@ fn main() -> anyhow::Result<()> {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
@@ -221,136 +203,131 @@ fn main() -> anyhow::Result<()> {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let chain_tip = chain.tip();
|
||||
let mut request = SyncRequest::from_chain_tip(chain_tip.clone());
|
||||
|
||||
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
|
||||
Box::new(core::iter::empty());
|
||||
if all_spks {
|
||||
let all_spks = graph
|
||||
.index
|
||||
.revealed_spks(..)
|
||||
.map(|(index, spk)| (index, spk.to_owned()))
|
||||
.all_spks()
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
request = request.chain_spks(all_spks.into_iter().map(|((k, spk_i), spk)| {
|
||||
eprint!("Scanning {}: {}", k, spk_i);
|
||||
spk
|
||||
}));
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!("scanning {:?}", index);
|
||||
script
|
||||
})));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = graph
|
||||
.index
|
||||
.unused_spks()
|
||||
.map(|(index, spk)| (index, spk.to_owned()))
|
||||
.unused_spks(..)
|
||||
.map(|(k, v)| (*k, ScriptBuf::from(v)))
|
||||
.collect::<Vec<_>>();
|
||||
request =
|
||||
request.chain_spks(unused_spks.into_iter().map(move |((k, spk_i), spk)| {
|
||||
eprint!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
spk_i,
|
||||
);
|
||||
spk
|
||||
}));
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {:?} has been used",
|
||||
Address::from_script(&script, args.network).unwrap(),
|
||||
index
|
||||
);
|
||||
|
||||
script
|
||||
})));
|
||||
}
|
||||
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
|
||||
if utxos {
|
||||
let init_outpoints = graph.index.outpoints();
|
||||
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&*chain,
|
||||
chain_tip.block_id(),
|
||||
init_outpoints.iter().cloned(),
|
||||
)
|
||||
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
|
||||
eprint!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
utxo.outpoint
|
||||
}));
|
||||
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
};
|
||||
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, chain_tip.block_id())
|
||||
.list_chain_txs(&*chain, chain_tip)
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
|
||||
request = request.chain_txids(
|
||||
unconfirmed_txids
|
||||
.into_iter()
|
||||
.inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)),
|
||||
);
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
}));
|
||||
}
|
||||
|
||||
let total_spks = request.spks.len();
|
||||
let total_txids = request.txids.len();
|
||||
let total_ops = request.outpoints.len();
|
||||
request = request
|
||||
.inspect_spks({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
|
||||
}
|
||||
})
|
||||
.inspect_txids({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
|
||||
}
|
||||
})
|
||||
.inspect_outpoints({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
|
||||
}
|
||||
});
|
||||
|
||||
let res = client
|
||||
.sync(request, scan_options.batch_size, false)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
let tip = chain.tip();
|
||||
|
||||
// drop lock on graph and chain
|
||||
drop((graph, chain));
|
||||
|
||||
(res.chain_update, res.graph_update, None)
|
||||
let electrum_update = client
|
||||
.scan_without_keychain(tip, spks, txids, outpoints, scan_options.batch_size)
|
||||
.context("scanning the blockchain")?;
|
||||
(electrum_update, BTreeMap::new())
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
) = response;
|
||||
|
||||
let missing_txids = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
relevant_txids.missing_full_txs(graph.graph())
|
||||
};
|
||||
|
||||
let now = std::time::UNIX_EPOCH
|
||||
.elapsed()
|
||||
.expect("must get time")
|
||||
.as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?;
|
||||
|
||||
let db_changeset = {
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
|
||||
let chain_changeset = chain.apply_update(chain_update)?;
|
||||
let chain = chain.apply_update(chain_update)?;
|
||||
|
||||
let mut indexed_tx_graph_changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
if let Some(keychain_update) = keychain_update {
|
||||
let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
indexed_tx_graph_changeset.append(keychain_changeset.into());
|
||||
}
|
||||
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
|
||||
let indexed_tx_graph = {
|
||||
let mut changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
changeset.append(indexed_tx_graph::ChangeSet {
|
||||
indexer,
|
||||
..Default::default()
|
||||
});
|
||||
changeset.append(graph.apply_update(graph_update));
|
||||
changeset
|
||||
};
|
||||
|
||||
(chain_changeset, indexed_tx_graph_changeset)
|
||||
(chain, indexed_tx_graph)
|
||||
};
|
||||
|
||||
let mut db = db.lock().unwrap();
|
||||
db.append_changeset(&db_changeset)?;
|
||||
db.stage(db_changeset);
|
||||
db.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
|
||||
@@ -61,7 +60,6 @@ enum EsploraCommands {
|
||||
esplora_args: EsploraArgs,
|
||||
},
|
||||
}
|
||||
|
||||
impl EsploraCommands {
|
||||
fn esplora_args(&self) -> EsploraArgs {
|
||||
match self {
|
||||
@@ -88,7 +86,7 @@ impl EsploraArgs {
|
||||
_ => panic!("unsupported network"),
|
||||
});
|
||||
|
||||
let client = esplora_client::Builder::new(esplora_url).build_blocking();
|
||||
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
@@ -101,13 +99,8 @@ pub struct ScanOptions {
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
|
||||
@@ -132,7 +125,7 @@ fn main() -> anyhow::Result<()> {
|
||||
example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
|
||||
// These are general commands handled by example_cli. Execute the cmd and return.
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
let res = example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
@@ -147,70 +140,72 @@ fn main() -> anyhow::Result<()> {
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
|
||||
db.lock().unwrap().commit()?;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
let client = esplora_cmd.esplora_args().client(args.network)?;
|
||||
// Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
|
||||
// syncing.
|
||||
//
|
||||
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
|
||||
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
|
||||
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
|
||||
// number of consecutive spks have no transaction history. A Scan is done in situations of
|
||||
// wallet restoration. It is a special case. Applications should use "sync" style updates
|
||||
// after an initial scan.
|
||||
//
|
||||
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
|
||||
// status or fetch missing transactions.
|
||||
let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd {
|
||||
let indexed_tx_graph_changeset = match &esplora_cmd {
|
||||
EsploraCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let request = {
|
||||
let chain_tip = chain.lock().expect("mutex must not be poisoned").tip();
|
||||
let indexed_graph = &*graph.lock().expect("mutex must not be poisoned");
|
||||
FullScanRequest::from_keychain_txout_index(chain_tip, &indexed_graph.index)
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<Keychain>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
if once.insert(keychain) {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
}
|
||||
eprint!("{} ", spk_i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
let keychain_spks = graph
|
||||
.lock()
|
||||
.expect("mutex must not be poisoned")
|
||||
.index
|
||||
.spks_of_all_keychains()
|
||||
.into_iter()
|
||||
// This `map` is purely for logging.
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
})
|
||||
};
|
||||
eprint!("{} ", i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
|
||||
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
|
||||
// represents the last active spk derivation indices of keychains
|
||||
// (`keychain_indices_update`).
|
||||
let mut update = client
|
||||
.full_scan(request, *stop_gap, scan_options.parallel_requests)
|
||||
let (graph_update, last_active_indices) = client
|
||||
.scan_txs_with_keychains(
|
||||
keychain_spks,
|
||||
core::iter::empty(),
|
||||
core::iter::empty(),
|
||||
*stop_gap,
|
||||
scan_options.parallel_requests,
|
||||
)
|
||||
.context("scanning for transactions")?;
|
||||
|
||||
// We want to keep track of the latest time a transaction was seen unconfirmed.
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
||||
let mut chain = chain.lock().expect("mutex must not be poisoned");
|
||||
// Because we did a stop gap based scan we are likely to have some updates to our
|
||||
// deriviation indices. Usually before a scan you are on a fresh wallet with no
|
||||
// addresses derived so we need to derive up to last active addresses the scan found
|
||||
// before adding the transactions.
|
||||
(chain.apply_update(update.chain_update)?, {
|
||||
let index_changeset = graph
|
||||
.index
|
||||
.reveal_to_target_multi(&update.last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
})
|
||||
let (_, index_changeset) = graph.index.reveal_to_target_multi(&last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
}
|
||||
EsploraCommands::Sync {
|
||||
mut unused_spks,
|
||||
@@ -231,67 +226,64 @@ fn main() -> anyhow::Result<()> {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let local_tip = chain.lock().expect("mutex must not be poisoned").tip();
|
||||
// Spks, outpoints and txids we want updates on will be accumulated here.
|
||||
let mut request = SyncRequest::from_chain_tip(local_tip.clone());
|
||||
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
|
||||
// in.
|
||||
{
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if *all_spks {
|
||||
let all_spks = graph
|
||||
.index
|
||||
.revealed_spks(..)
|
||||
.map(|((k, i), spk)| (k, i, spk.to_owned()))
|
||||
.all_spks()
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
request = request.chain_spks(all_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprint!("scanning {}:{}", k, i);
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!("scanning {:?}", index);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
}));
|
||||
script
|
||||
})));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = graph
|
||||
.index
|
||||
.unused_spks()
|
||||
.map(|(index, spk)| (index, spk.to_owned()))
|
||||
.unused_spks(..)
|
||||
.map(|(k, v)| (*k, v.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
request =
|
||||
request.chain_spks(unused_spks.into_iter().map(move |((k, i), spk)| {
|
||||
eprint!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
i,
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
}));
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {:?} has been used",
|
||||
Address::from_script(&script, args.network).unwrap(),
|
||||
index
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
script
|
||||
})));
|
||||
}
|
||||
if utxos {
|
||||
// We want to search for whether the UTXO is spent, and spent by which
|
||||
// transaction. We provide the outpoint of the UTXO to
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
let init_outpoints = graph.index.outpoints();
|
||||
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&*chain,
|
||||
local_tip.block_id(),
|
||||
init_outpoints.iter().cloned(),
|
||||
)
|
||||
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
request = request.chain_outpoints(
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprint!(
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
@@ -307,60 +299,56 @@ fn main() -> anyhow::Result<()> {
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, local_tip.block_id())
|
||||
.list_chain_txs(&*chain, chain_tip)
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
request = request.chain_txids(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprint!("Checking if {} is confirmed yet", txid);
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let total_spks = request.spks.len();
|
||||
let total_txids = request.txids.len();
|
||||
let total_ops = request.outpoints.len();
|
||||
request = request
|
||||
.inspect_spks({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
|
||||
}
|
||||
})
|
||||
.inspect_txids({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
|
||||
}
|
||||
})
|
||||
.inspect_outpoints({
|
||||
let mut visited = 0;
|
||||
move |_| {
|
||||
visited += 1;
|
||||
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
|
||||
}
|
||||
});
|
||||
let mut update = client.sync(request, scan_options.parallel_requests)?;
|
||||
let graph_update =
|
||||
client.scan_txs(spks, txids, outpoints, scan_options.parallel_requests)?;
|
||||
|
||||
// Update last seen unconfirmed
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
(
|
||||
chain.lock().unwrap().apply_update(update.chain_update)?,
|
||||
graph.lock().unwrap().apply_update(update.graph_update),
|
||||
)
|
||||
graph.lock().unwrap().apply_update(graph_update)
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
|
||||
// Now that we're done updating the `IndexedTxGraph`, it's time to update the `LocalChain`! We
|
||||
// want the `LocalChain` to have data about all the anchors in the `TxGraph` - for this reason,
|
||||
// we want retrieve the blocks at the heights of the newly added anchors that are missing from
|
||||
// our view of the chain.
|
||||
let (missing_block_heights, tip) = {
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let missing_block_heights = indexed_tx_graph_changeset
|
||||
.graph
|
||||
.missing_heights_from(chain)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let tip = chain.tip();
|
||||
(missing_block_heights, tip)
|
||||
};
|
||||
|
||||
println!("prev tip: {}", tip.height());
|
||||
println!("missing block heights: {:?}", missing_block_heights);
|
||||
|
||||
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
|
||||
let chain_changeset = {
|
||||
let chain_update = client
|
||||
.update_local_chain(tip, missing_block_heights)
|
||||
.context("scanning for blocks")?;
|
||||
println!("new tip: {}", chain_update.tip.height());
|
||||
chain.lock().unwrap().apply_update(chain_update)?
|
||||
};
|
||||
|
||||
// We persist the changes
|
||||
let mut db = db.lock().unwrap();
|
||||
db.append_changeset(&(local_chain_changeset, indexed_tx_graph_changeset))?;
|
||||
db.stage((chain_changeset, indexed_tx_graph_changeset));
|
||||
db.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_electrum = { path = "../../crates/electrum" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user