Compare commits
51 Commits
dependabot
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2a57060b | ||
|
|
d33acc1466 | ||
|
|
d1ea0ef3d1 | ||
|
|
60abd87a32 | ||
|
|
71fff1613d | ||
|
|
d494f63d08 | ||
|
|
83e7b7ec40 | ||
|
|
9294e30943 | ||
|
|
b74c2e2622 | ||
|
|
81aeaba48a | ||
|
|
c7b47af72f | ||
|
|
40f0765d30 | ||
|
|
bf67519768 | ||
|
|
b6422f7ffc | ||
|
|
eb1714aee0 | ||
|
|
705690ee8f | ||
|
|
cd602430ee | ||
|
|
264bb85efc | ||
|
|
761189ab2b | ||
|
|
5b77942993 | ||
|
|
f9dad51ae1 | ||
|
|
8f6dad76ef | ||
|
|
887e112e8f | ||
|
|
21d8875826 | ||
|
|
6e6bad9223 | ||
|
|
105d70e974 | ||
|
|
9efaead8f1 | ||
|
|
1ff9d5ce8f | ||
|
|
8694624bd5 | ||
|
|
003271117c | ||
|
|
f6418ba911 | ||
|
|
028caa9f8c | ||
|
|
d71829914a | ||
|
|
a1d34afa24 | ||
|
|
9cc03324f4 | ||
|
|
de54e710ed | ||
|
|
95d34854f4 | ||
|
|
7eff024213 | ||
|
|
1def76f1f1 | ||
|
|
c9467dcbb2 | ||
|
|
bc796f412a | ||
|
|
4fd539b647 | ||
|
|
01698ae5ec | ||
|
|
f4863c6314 | ||
|
|
b5612f269a | ||
|
|
e7fbc8bcf3 | ||
|
|
2251b8d416 | ||
|
|
b13505c1c3 | ||
|
|
0adff9c35f | ||
|
|
908b0f9f5e | ||
|
|
169385bb5b |
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Run simulator image
|
- name: Run simulator image
|
||||||
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install python dependencies
|
- name: Install python dependencies
|
||||||
|
|||||||
28
.github/workflows/cont_integration.yml
vendored
28
.github/workflows/cont_integration.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
rust:
|
rust:
|
||||||
- version: stable
|
- version: stable
|
||||||
clippy: true
|
clippy: true
|
||||||
- version: 1.57.0 # MSRV
|
- version: 1.63.0 # MSRV
|
||||||
features:
|
features:
|
||||||
- --no-default-features
|
- --no-default-features
|
||||||
- --all-features
|
- --all-features
|
||||||
@@ -28,28 +28,12 @@ jobs:
|
|||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.2.1
|
uses: Swatinem/rust-cache@v2.2.1
|
||||||
- name: Pin dependencies for MSRV
|
- name: Pin dependencies for MSRV
|
||||||
if: matrix.rust.version == '1.57.0'
|
if: matrix.rust.version == '1.63.0'
|
||||||
run: |
|
run: |
|
||||||
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 zip --precise "0.6.2"
|
||||||
cargo update -p time --precise "0.3.13"
|
cargo update -p time --precise "0.3.20"
|
||||||
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"
|
cargo update -p jobserver --precise "0.1.26"
|
||||||
|
cargo update -p home --precise "0.5.5"
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build ${{ matrix.features }}
|
run: cargo build ${{ matrix.features }}
|
||||||
- name: Test
|
- name: Test
|
||||||
@@ -134,9 +118,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
# we pin clippy instead of using "stable" so that our CI doesn't break
|
toolchain: stable
|
||||||
# at each new cargo release
|
|
||||||
toolchain: "1.67.0"
|
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
override: true
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"crates/electrum",
|
"crates/electrum",
|
||||||
"crates/esplora",
|
"crates/esplora",
|
||||||
"crates/bitcoind_rpc",
|
"crates/bitcoind_rpc",
|
||||||
|
"crates/hwi",
|
||||||
"example-crates/example_cli",
|
"example-crates/example_cli",
|
||||||
"example-crates/example_electrum",
|
"example-crates/example_electrum",
|
||||||
"example-crates/example_esplora",
|
"example-crates/example_esplora",
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -15,7 +15,7 @@
|
|||||||
<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://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://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"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></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://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://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -60,51 +60,19 @@ Fully working examples of how to use these components are in `/example-crates`:
|
|||||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||||
|
|
||||||
## Minimum Supported Rust Version (MSRV)
|
## Minimum Supported Rust Version (MSRV)
|
||||||
This library should compile with any combination of features with Rust 1.57.0.
|
This library should compile with any combination of features with Rust 1.63.0.
|
||||||
|
|
||||||
To build with the MSRV you will need to pin dependencies as follows:
|
To build with the MSRV you will need to pin dependencies as follows:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# log 0.4.19 has MSRV 1.60.0+
|
# zip 0.6.3 has MSRV 1.64.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"
|
cargo update -p zip --precise "0.6.2"
|
||||||
# time 0.3.14 has MSRV 1.59.0+
|
# time 0.3.21 has MSRV 1.65.0
|
||||||
cargo update -p time --precise "0.3.13"
|
cargo update -p time --precise "0.3.20"
|
||||||
# byteorder 1.5.0 has MSRV 1.60.0+
|
# jobserver 0.1.27 has MSRV 1.66.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"
|
cargo update -p jobserver --precise "0.1.26"
|
||||||
|
# home 0.5.9 has MSRV 1.70.0
|
||||||
|
cargo update -p home --precise "0.5.5"
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
msrv="1.57.0"
|
msrv="1.63.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
version = "1.0.0-alpha.2"
|
version = "1.0.0-alpha.4"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
documentation = "https://docs.rs/bdk"
|
documentation = "https://docs.rs/bdk"
|
||||||
description = "A modern, lightweight, descriptor-based wallet library"
|
description = "A modern, lightweight, descriptor-based wallet library"
|
||||||
@@ -10,7 +10,7 @@ readme = "README.md"
|
|||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
authors = ["Bitcoin Dev Kit Developers"]
|
authors = ["Bitcoin Dev Kit Developers"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.63"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "^0.8"
|
rand = "^0.8"
|
||||||
@@ -18,11 +18,10 @@ miniscript = { version = "10.0.0", features = ["serde"], default-features = fals
|
|||||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
bdk_chain = { path = "../chain", version = "0.6.0", features = ["miniscript", "serde"], default-features = false }
|
bdk_chain = { path = "../chain", version = "0.8.0", features = ["miniscript", "serde"], default-features = false }
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
hwi = { version = "0.7.0", optional = true, features = [ "miniscript"] }
|
bip39 = { version = "2.0", optional = true }
|
||||||
bip39 = { version = "1.0.1", optional = true }
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
@@ -34,8 +33,6 @@ std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
|
|||||||
compiler = ["miniscript/compiler"]
|
compiler = ["miniscript/compiler"]
|
||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
keys-bip39 = ["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
|
# 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
|
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<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://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://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"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></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://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://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
let mnemonic_with_passphrase = (mnemonic, None);
|
let mnemonic_with_passphrase = (mnemonic, None);
|
||||||
|
|
||||||
// define external and internal derivation key path
|
// define external and internal derivation key path
|
||||||
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
|
let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
|
||||||
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
|
let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
|
||||||
|
|
||||||
// generate external and internal descriptor from mnemonic
|
// generate external and internal descriptor from mnemonic
|
||||||
let (external_descriptor, ext_keymap) =
|
let (external_descriptor, ext_keymap) =
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ mod test {
|
|||||||
|
|
||||||
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||||
let purpose = path.get(0).unwrap();
|
let purpose = path.first().unwrap();
|
||||||
assert_matches!(purpose, Hardened { index: 44 });
|
assert_matches!(purpose, Hardened { index: 44 });
|
||||||
let coin_type = path.get(1).unwrap();
|
let coin_type = path.get(1).unwrap();
|
||||||
assert_matches!(coin_type, Hardened { index: 0 });
|
assert_matches!(coin_type, Hardened { index: 0 });
|
||||||
@@ -589,7 +589,7 @@ mod test {
|
|||||||
|
|
||||||
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||||
let purpose = path.get(0).unwrap();
|
let purpose = path.first().unwrap();
|
||||||
assert_matches!(purpose, Hardened { index: 44 });
|
assert_matches!(purpose, Hardened { index: 44 });
|
||||||
let coin_type = path.get(1).unwrap();
|
let coin_type = path.get(1).unwrap();
|
||||||
assert_matches!(coin_type, Hardened { index: 1 });
|
assert_matches!(coin_type, Hardened { index: 1 });
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ extern crate std;
|
|||||||
pub extern crate alloc;
|
pub extern crate alloc;
|
||||||
|
|
||||||
pub extern crate bitcoin;
|
pub extern crate bitcoin;
|
||||||
#[cfg(feature = "hardware-signer")]
|
|
||||||
pub extern crate hwi;
|
|
||||||
pub extern crate miniscript;
|
pub extern crate miniscript;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
//! Wallet
|
//! Wallet
|
||||||
//!
|
//!
|
||||||
//! This module defines the [`Wallet`] structure.
|
//! This module defines the [`Wallet`].
|
||||||
use crate::collections::{BTreeMap, HashMap, HashSet};
|
use crate::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use alloc::{
|
use alloc::{
|
||||||
boxed::Box,
|
boxed::Box,
|
||||||
@@ -50,10 +50,6 @@ pub mod tx_builder;
|
|||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(feature = "hardware-signer")]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))]
|
|
||||||
pub mod hardwaresigner;
|
|
||||||
|
|
||||||
pub use utils::IsDust;
|
pub use utils::IsDust;
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
@@ -77,7 +73,7 @@ const COINBASE_MATURITY: u32 = 100;
|
|||||||
|
|
||||||
/// A Bitcoin wallet
|
/// A Bitcoin wallet
|
||||||
///
|
///
|
||||||
/// The `Wallet` struct acts as a way of coherently interfacing with output descriptors and related transactions.
|
/// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions.
|
||||||
/// Its main components are:
|
/// Its main components are:
|
||||||
///
|
///
|
||||||
/// 1. output *descriptors* from which it can derive addresses.
|
/// 1. output *descriptors* from which it can derive addresses.
|
||||||
@@ -237,6 +233,7 @@ impl Wallet {
|
|||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<Self, DescriptorError> {
|
) -> Result<Self, DescriptorError> {
|
||||||
Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
|
Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
|
||||||
|
NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
|
||||||
NewError::Descriptor(e) => e,
|
NewError::Descriptor(e) => e,
|
||||||
NewError::Write(_) => unreachable!("mock-write must always succeed"),
|
NewError::Write(_) => unreachable!("mock-write must always succeed"),
|
||||||
})
|
})
|
||||||
@@ -251,6 +248,7 @@ impl Wallet {
|
|||||||
) -> Result<Self, crate::descriptor::DescriptorError> {
|
) -> Result<Self, crate::descriptor::DescriptorError> {
|
||||||
Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
|
Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
|
NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
|
||||||
NewError::Descriptor(e) => e,
|
NewError::Descriptor(e) => e,
|
||||||
NewError::Write(_) => unreachable!("mock-write must always succeed"),
|
NewError::Write(_) => unreachable!("mock-write must always succeed"),
|
||||||
})
|
})
|
||||||
@@ -264,6 +262,11 @@ where
|
|||||||
/// Infallibly return a derived address using the external descriptor, see [`AddressIndex`] for
|
/// Infallibly return a derived address using the external descriptor, see [`AddressIndex`] for
|
||||||
/// available address index selection strategies. If none of the keys in the descriptor are derivable
|
/// available address index selection strategies. If none of the keys in the descriptor are derivable
|
||||||
/// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
/// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This panics when the caller requests for an address of derivation index greater than the
|
||||||
|
/// BIP32 max index.
|
||||||
pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo {
|
pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo {
|
||||||
self.try_get_address(address_index).unwrap()
|
self.try_get_address(address_index).unwrap()
|
||||||
}
|
}
|
||||||
@@ -275,6 +278,11 @@ where
|
|||||||
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
||||||
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
|
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
|
||||||
/// be returned for any [`AddressIndex`].
|
/// be returned for any [`AddressIndex`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This panics when the caller requests for an address of derivation index greater than the
|
||||||
|
/// BIP32 max index.
|
||||||
pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo {
|
pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo {
|
||||||
self.try_get_internal_address(address_index).unwrap()
|
self.try_get_internal_address(address_index).unwrap()
|
||||||
}
|
}
|
||||||
@@ -288,6 +296,8 @@ where
|
|||||||
/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
|
/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum NewError<W> {
|
pub enum NewError<W> {
|
||||||
|
/// Database already has data.
|
||||||
|
NonEmptyDatabase,
|
||||||
/// There was problem with the passed-in descriptor(s).
|
/// There was problem with the passed-in descriptor(s).
|
||||||
Descriptor(crate::descriptor::DescriptorError),
|
Descriptor(crate::descriptor::DescriptorError),
|
||||||
/// We were unable to write the wallet's data to the persistence backend.
|
/// We were unable to write the wallet's data to the persistence backend.
|
||||||
@@ -300,6 +310,10 @@ where
|
|||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
NewError::NonEmptyDatabase => write!(
|
||||||
|
f,
|
||||||
|
"database already has data - use `load` or `new_or_load` methods instead"
|
||||||
|
),
|
||||||
NewError::Descriptor(e) => e.fmt(f),
|
NewError::Descriptor(e) => e.fmt(f),
|
||||||
NewError::Write(e) => e.fmt(f),
|
NewError::Write(e) => e.fmt(f),
|
||||||
}
|
}
|
||||||
@@ -348,7 +362,7 @@ where
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
impl<L> std::error::Error for LoadError<L> where L: core::fmt::Display + core::fmt::Debug {}
|
impl<L> std::error::Error for LoadError<L> where L: core::fmt::Display + core::fmt::Debug {}
|
||||||
|
|
||||||
/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existant.
|
/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
|
||||||
///
|
///
|
||||||
/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
|
/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
|
||||||
///
|
///
|
||||||
@@ -446,13 +460,18 @@ impl<D> Wallet<D> {
|
|||||||
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
|
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
|
||||||
descriptor: E,
|
descriptor: E,
|
||||||
change_descriptor: Option<E>,
|
change_descriptor: Option<E>,
|
||||||
db: D,
|
mut db: D,
|
||||||
network: Network,
|
network: Network,
|
||||||
genesis_hash: BlockHash,
|
genesis_hash: BlockHash,
|
||||||
) -> Result<Self, NewError<D::WriteError>>
|
) -> Result<Self, NewError<D::WriteError>>
|
||||||
where
|
where
|
||||||
D: PersistBackend<ChangeSet>,
|
D: PersistBackend<ChangeSet>,
|
||||||
{
|
{
|
||||||
|
if let Ok(changeset) = db.load_from_persistence() {
|
||||||
|
if changeset.is_some() {
|
||||||
|
return Err(NewError::NonEmptyDatabase);
|
||||||
|
}
|
||||||
|
}
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
|
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
|
||||||
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
|
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
|
||||||
@@ -517,7 +536,9 @@ impl<D> Wallet<D> {
|
|||||||
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
|
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
|
||||||
.map_err(LoadError::Descriptor)?;
|
.map_err(LoadError::Descriptor)?;
|
||||||
|
|
||||||
let indexed_graph = IndexedTxGraph::new(index);
|
let mut indexed_graph = IndexedTxGraph::new(index);
|
||||||
|
indexed_graph.apply_changeset(changeset.indexed_tx_graph);
|
||||||
|
|
||||||
let persist = Persist::new(db);
|
let persist = Persist::new(db);
|
||||||
|
|
||||||
Ok(Wallet {
|
Ok(Wallet {
|
||||||
@@ -613,6 +634,9 @@ impl<D> Wallet<D> {
|
|||||||
genesis_hash,
|
genesis_hash,
|
||||||
)
|
)
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
|
NewError::NonEmptyDatabase => {
|
||||||
|
unreachable!("database is already checked to have no data")
|
||||||
|
}
|
||||||
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
||||||
NewError::Write(e) => NewOrLoadError::Write(e),
|
NewError::Write(e) => NewOrLoadError::Write(e),
|
||||||
}),
|
}),
|
||||||
@@ -635,6 +659,11 @@ impl<D> Wallet<D> {
|
|||||||
///
|
///
|
||||||
/// A `PersistBackend<ChangeSet>::WriteError` will result if unable to persist the new address
|
/// A `PersistBackend<ChangeSet>::WriteError` will result if unable to persist the new address
|
||||||
/// to the `PersistBackend`.
|
/// to the `PersistBackend`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This panics when the caller requests for an address of derivation index greater than the
|
||||||
|
/// BIP32 max index.
|
||||||
pub fn try_get_address(
|
pub fn try_get_address(
|
||||||
&mut self,
|
&mut self,
|
||||||
address_index: AddressIndex,
|
address_index: AddressIndex,
|
||||||
@@ -655,6 +684,11 @@ impl<D> Wallet<D> {
|
|||||||
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
||||||
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
|
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
|
||||||
/// be returned for any [`AddressIndex`].
|
/// be returned for any [`AddressIndex`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This panics when the caller requests for an address of derivation index greater than the
|
||||||
|
/// BIP32 max index.
|
||||||
pub fn try_get_internal_address(
|
pub fn try_get_internal_address(
|
||||||
&mut self,
|
&mut self,
|
||||||
address_index: AddressIndex,
|
address_index: AddressIndex,
|
||||||
@@ -677,6 +711,11 @@ impl<D> Wallet<D> {
|
|||||||
/// See [`AddressIndex`] for available address index selection strategies. If none of the keys
|
/// See [`AddressIndex`] for available address index selection strategies. If none of the keys
|
||||||
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will
|
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will
|
||||||
/// always be returned for any [`AddressIndex`].
|
/// always be returned for any [`AddressIndex`].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This panics when the caller requests for an address of derivation index greater than the
|
||||||
|
/// BIP32 max index.
|
||||||
fn _get_address(
|
fn _get_address(
|
||||||
&mut self,
|
&mut self,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
@@ -696,12 +735,14 @@ impl<D> Wallet<D> {
|
|||||||
let ((index, spk), index_changeset) = txout_index.next_unused_spk(&keychain);
|
let ((index, spk), index_changeset) = txout_index.next_unused_spk(&keychain);
|
||||||
(index, spk.into(), Some(index_changeset))
|
(index, spk.into(), Some(index_changeset))
|
||||||
}
|
}
|
||||||
AddressIndex::Peek(index) => {
|
AddressIndex::Peek(mut peek_index) => {
|
||||||
let (index, spk) = txout_index
|
let mut spk_iter = txout_index.unbounded_spk_iter(&keychain);
|
||||||
.spks_of_keychain(&keychain)
|
if !spk_iter.descriptor().has_wildcard() {
|
||||||
.take(index as usize + 1)
|
peek_index = 0;
|
||||||
.last()
|
}
|
||||||
.unwrap();
|
let (index, spk) = spk_iter
|
||||||
|
.nth(peek_index as usize)
|
||||||
|
.expect("derivation index is out of bounds");
|
||||||
(index, spk, None)
|
(index, spk, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -731,7 +772,7 @@ impl<D> Wallet<D> {
|
|||||||
///
|
///
|
||||||
/// Will only return `Some(_)` if the wallet has given out the spk.
|
/// Will only return `Some(_)` if the wallet has given out the spk.
|
||||||
pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> {
|
pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> {
|
||||||
self.indexed_graph.index.index_of_spk(spk).copied()
|
self.indexed_graph.index.index_of_spk(spk)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the list of unspent outputs of this wallet
|
/// Return the list of unspent outputs of this wallet
|
||||||
@@ -770,7 +811,7 @@ impl<D> Wallet<D> {
|
|||||||
self.chain.tip()
|
self.chain.tip()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`.
|
/// Get unbounded script pubkey iterators for both `Internal` and `External` keychains.
|
||||||
///
|
///
|
||||||
/// This is intended to be used when doing a full scan of your addresses (e.g. after restoring
|
/// This is intended to be used when doing a full scan of your addresses (e.g. after restoring
|
||||||
/// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g.
|
/// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g.
|
||||||
@@ -778,36 +819,36 @@ impl<D> Wallet<D> {
|
|||||||
///
|
///
|
||||||
/// Note carefully that iterators go over **all** script pubkeys on the keychains (not what
|
/// Note carefully that iterators go over **all** script pubkeys on the keychains (not what
|
||||||
/// script pubkeys the wallet is storing internally).
|
/// script pubkeys the wallet is storing internally).
|
||||||
pub fn spks_of_all_keychains(
|
pub fn all_unbounded_spk_iters(
|
||||||
&self,
|
&self,
|
||||||
) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, ScriptBuf)> + Clone> {
|
) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, ScriptBuf)> + Clone> {
|
||||||
self.indexed_graph.index.spks_of_all_keychains()
|
self.indexed_graph.index.all_unbounded_spk_iters()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets an iterator over all the script pubkeys in a single keychain.
|
/// Get an unbounded script pubkey iterator for the given `keychain`.
|
||||||
///
|
///
|
||||||
/// See [`spks_of_all_keychains`] for more documentation
|
/// See [`all_unbounded_spk_iters`] for more documentation
|
||||||
///
|
///
|
||||||
/// [`spks_of_all_keychains`]: Self::spks_of_all_keychains
|
/// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters
|
||||||
pub fn spks_of_keychain(
|
pub fn unbounded_spk_iter(
|
||||||
&self,
|
&self,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
) -> impl Iterator<Item = (u32, ScriptBuf)> + Clone {
|
) -> impl Iterator<Item = (u32, ScriptBuf)> + Clone {
|
||||||
self.indexed_graph.index.spks_of_keychain(&keychain)
|
self.indexed_graph.index.unbounded_spk_iter(&keychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
|
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
|
||||||
/// wallet's database.
|
/// wallet's database.
|
||||||
pub fn get_utxo(&self, op: OutPoint) -> Option<LocalOutput> {
|
pub fn get_utxo(&self, op: OutPoint) -> Option<LocalOutput> {
|
||||||
let (&spk_i, _) = self.indexed_graph.index.txout(op)?;
|
let (keychain, index, _) = self.indexed_graph.index.txout(op)?;
|
||||||
self.indexed_graph
|
self.indexed_graph
|
||||||
.graph()
|
.graph()
|
||||||
.filter_chain_unspents(
|
.filter_chain_unspents(
|
||||||
&self.chain,
|
&self.chain,
|
||||||
self.chain.tip().block_id(),
|
self.chain.tip().block_id(),
|
||||||
core::iter::once((spk_i, op)),
|
core::iter::once(((), op)),
|
||||||
)
|
)
|
||||||
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
|
.map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo))
|
||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1445,7 +1486,7 @@ impl<D> Wallet<D> {
|
|||||||
let ((index, spk), index_changeset) =
|
let ((index, spk), index_changeset) =
|
||||||
self.indexed_graph.index.next_unused_spk(&change_keychain);
|
self.indexed_graph.index.next_unused_spk(&change_keychain);
|
||||||
let spk = spk.into();
|
let spk = spk.into();
|
||||||
self.indexed_graph.index.mark_used(&change_keychain, index);
|
self.indexed_graph.index.mark_used(change_keychain, index);
|
||||||
self.persist
|
self.persist
|
||||||
.stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
|
.stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
|
||||||
index_changeset,
|
index_changeset,
|
||||||
@@ -1626,7 +1667,7 @@ impl<D> Wallet<D> {
|
|||||||
.into();
|
.into();
|
||||||
|
|
||||||
let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
|
let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
|
||||||
Some(&(keychain, derivation_index)) => {
|
Some((keychain, derivation_index)) => {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let satisfaction_weight = self
|
let satisfaction_weight = self
|
||||||
.get_descriptor_for_keychain(keychain)
|
.get_descriptor_for_keychain(keychain)
|
||||||
@@ -1670,7 +1711,7 @@ impl<D> Wallet<D> {
|
|||||||
for (index, txout) in tx.output.iter().enumerate() {
|
for (index, txout) in tx.output.iter().enumerate() {
|
||||||
let change_type = self.map_keychain(KeychainKind::Internal);
|
let change_type = self.map_keychain(KeychainKind::Internal);
|
||||||
match txout_index.index_of_spk(&txout.script_pubkey) {
|
match txout_index.index_of_spk(&txout.script_pubkey) {
|
||||||
Some(&(keychain, _)) if keychain == change_type => change_index = Some(index),
|
Some((keychain, _)) if keychain == change_type => change_index = Some(index),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1925,10 +1966,10 @@ impl<D> Wallet<D> {
|
|||||||
pub fn cancel_tx(&mut self, tx: &Transaction) {
|
pub fn cancel_tx(&mut self, tx: &Transaction) {
|
||||||
let txout_index = &mut self.indexed_graph.index;
|
let txout_index = &mut self.indexed_graph.index;
|
||||||
for txout in &tx.output {
|
for txout in &tx.output {
|
||||||
if let Some(&(keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) {
|
if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) {
|
||||||
// NOTE: unmark_used will **not** make something unused if it has actually been used
|
// NOTE: unmark_used will **not** make something unused if it has actually been used
|
||||||
// by a tx in the tracker. It only removes the superficial marking.
|
// by a tx in the tracker. It only removes the superficial marking.
|
||||||
txout_index.unmark_used(&keychain, index);
|
txout_index.unmark_used(keychain, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1944,7 +1985,7 @@ impl<D> Wallet<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
|
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
|
||||||
let &(keychain, child) = self
|
let (keychain, child) = self
|
||||||
.indexed_graph
|
.indexed_graph
|
||||||
.index
|
.index
|
||||||
.index_of_spk(&txout.script_pubkey)?;
|
.index_of_spk(&txout.script_pubkey)?;
|
||||||
@@ -2158,7 +2199,7 @@ impl<D> Wallet<D> {
|
|||||||
{
|
{
|
||||||
// Try to find the prev_script in our db to figure out if this is internal or external,
|
// Try to find the prev_script in our db to figure out if this is internal or external,
|
||||||
// and the derivation index
|
// and the derivation index
|
||||||
let &(keychain, child) = self
|
let (keychain, child) = self
|
||||||
.indexed_graph
|
.indexed_graph
|
||||||
.index
|
.index
|
||||||
.index_of_spk(&utxo.txout.script_pubkey)
|
.index_of_spk(&utxo.txout.script_pubkey)
|
||||||
@@ -2212,7 +2253,7 @@ impl<D> Wallet<D> {
|
|||||||
|
|
||||||
// Try to figure out the keychain and derivation for every input and output
|
// Try to figure out the keychain and derivation for every input and output
|
||||||
for (is_input, index, out) in utxos.into_iter() {
|
for (is_input, index, out) in utxos.into_iter() {
|
||||||
if let Some(&(keychain, child)) =
|
if let Some((keychain, child)) =
|
||||||
self.indexed_graph.index.index_of_spk(&out.script_pubkey)
|
self.indexed_graph.index.index_of_spk(&out.script_pubkey)
|
||||||
{
|
{
|
||||||
let desc = self.get_descriptor_for_keychain(keychain);
|
let desc = self.get_descriptor_for_keychain(keychain);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::collections::BTreeMap;
|
use crate::collections::BTreeMap;
|
||||||
|
use alloc::string::String;
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
@@ -162,16 +163,10 @@ pub enum SignerError {
|
|||||||
SighashError(sighash::Error),
|
SighashError(sighash::Error),
|
||||||
/// Miniscript PSBT error
|
/// Miniscript PSBT error
|
||||||
MiniscriptPsbt(MiniscriptPsbtError),
|
MiniscriptPsbt(MiniscriptPsbtError),
|
||||||
/// Error while signing using hardware wallets
|
/// To be used only by external libraries implementing [`InputSigner`] or
|
||||||
#[cfg(feature = "hardware-signer")]
|
/// [`TransactionSigner`], so that they can return their own custom errors, without having to
|
||||||
HWIError(hwi::error::Error),
|
/// modify [`SignerError`] in BDK.
|
||||||
}
|
External(String),
|
||||||
|
|
||||||
#[cfg(feature = "hardware-signer")]
|
|
||||||
impl From<hwi::error::Error> for SignerError {
|
|
||||||
fn from(e: hwi::error::Error) -> Self {
|
|
||||||
SignerError::HWIError(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sighash::Error> for SignerError {
|
impl From<sighash::Error> for SignerError {
|
||||||
@@ -196,8 +191,7 @@ impl fmt::Display for SignerError {
|
|||||||
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
|
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
|
||||||
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
|
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
|
||||||
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
|
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
|
||||||
#[cfg(feature = "hardware-signer")]
|
Self::External(err) => write!(f, "{}", err),
|
||||||
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +215,7 @@ pub enum SignerContext {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper structure to pair a signer with its context
|
/// Wrapper to pair a signer with its context
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
|
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
|
||||||
signer: S,
|
signer: S,
|
||||||
@@ -812,9 +806,10 @@ pub struct SignOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Customize which taproot script-path leaves the signer should sign.
|
/// Customize which taproot script-path leaves the signer should sign.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum TapLeavesOptions {
|
pub enum TapLeavesOptions {
|
||||||
/// The signer will sign all the leaves it has a key for.
|
/// The signer will sign all the leaves it has a key for.
|
||||||
|
#[default]
|
||||||
All,
|
All,
|
||||||
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
/// 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.
|
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||||
@@ -825,12 +820,6 @@ pub enum TapLeavesOptions {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TapLeavesOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
TapLeavesOptions::All
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
#[allow(clippy::derivable_impls)]
|
||||||
impl Default for SignOptions {
|
impl Default for SignOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|||||||
@@ -811,9 +811,10 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Ordering of the transaction's inputs and outputs
|
/// Ordering of the transaction's inputs and outputs
|
||||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
pub enum TxOrdering {
|
pub enum TxOrdering {
|
||||||
/// Randomized (default)
|
/// Randomized (default)
|
||||||
|
#[default]
|
||||||
Shuffle,
|
Shuffle,
|
||||||
/// Unchanged
|
/// Unchanged
|
||||||
Untouched,
|
Untouched,
|
||||||
@@ -821,12 +822,6 @@ pub enum TxOrdering {
|
|||||||
Bip69Lexicographic,
|
Bip69Lexicographic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TxOrdering {
|
|
||||||
fn default() -> Self {
|
|
||||||
TxOrdering::Shuffle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TxOrdering {
|
impl TxOrdering {
|
||||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
|
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
|
||||||
pub fn sort_tx(&self, tx: &mut Transaction) {
|
pub fn sort_tx(&self, tx: &mut Transaction) {
|
||||||
@@ -880,9 +875,10 @@ impl RbfValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Policy regarding the use of change outputs when creating a transaction
|
/// Policy regarding the use of change outputs when creating a transaction
|
||||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
pub enum ChangeSpendPolicy {
|
pub enum ChangeSpendPolicy {
|
||||||
/// Use both change and non-change outputs (default)
|
/// Use both change and non-change outputs (default)
|
||||||
|
#[default]
|
||||||
ChangeAllowed,
|
ChangeAllowed,
|
||||||
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
|
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
|
||||||
OnlyChange,
|
OnlyChange,
|
||||||
@@ -890,12 +886,6 @@ pub enum ChangeSpendPolicy {
|
|||||||
ChangeForbidden,
|
ChangeForbidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ChangeSpendPolicy {
|
|
||||||
fn default() -> Self {
|
|
||||||
ChangeSpendPolicy::ChangeAllowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChangeSpendPolicy {
|
impl ChangeSpendPolicy {
|
||||||
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
|
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use bdk::signer::{SignOptions, SignerError};
|
|||||||
use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection};
|
use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection};
|
||||||
use bdk::wallet::error::CreateTxError;
|
use bdk::wallet::error::CreateTxError;
|
||||||
use bdk::wallet::tx_builder::AddForeignUtxoError;
|
use bdk::wallet::tx_builder::AddForeignUtxoError;
|
||||||
use bdk::wallet::AddressIndex::*;
|
|
||||||
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
|
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
|
||||||
|
use bdk::wallet::{AddressIndex::*, NewError};
|
||||||
use bdk::{FeeRate, KeychainKind};
|
use bdk::{FeeRate, KeychainKind};
|
||||||
use bdk_chain::COINBASE_MATURITY;
|
use bdk_chain::COINBASE_MATURITY;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime};
|
||||||
@@ -71,19 +71,33 @@ fn load_recovers_wallet() {
|
|||||||
let file_path = temp_dir.path().join("store.db");
|
let file_path = temp_dir.path().join("store.db");
|
||||||
|
|
||||||
// create new wallet
|
// create new wallet
|
||||||
let wallet_keychains = {
|
let wallet_spk_index = {
|
||||||
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
|
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
|
||||||
let wallet =
|
let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet)
|
||||||
Wallet::new(get_test_wpkh(), None, db, Network::Testnet).expect("must init wallet");
|
.expect("must init wallet");
|
||||||
wallet.keychains().clone()
|
|
||||||
|
wallet.try_get_address(New).unwrap();
|
||||||
|
wallet.spk_index().clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// recover wallet
|
// recover wallet
|
||||||
{
|
{
|
||||||
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
|
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
|
||||||
let wallet = Wallet::load(get_test_wpkh(), None, db).expect("must recover wallet");
|
let wallet =
|
||||||
|
Wallet::load(get_test_tr_single_sig_xprv(), None, db).expect("must recover wallet");
|
||||||
assert_eq!(wallet.network(), Network::Testnet);
|
assert_eq!(wallet.network(), Network::Testnet);
|
||||||
assert_eq!(wallet.spk_index().keychains(), &wallet_keychains);
|
assert_eq!(wallet.spk_index().keychains(), wallet_spk_index.keychains());
|
||||||
|
assert_eq!(
|
||||||
|
wallet.spk_index().last_revealed_indices(),
|
||||||
|
wallet_spk_index.last_revealed_indices()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `new` can only be called on empty db
|
||||||
|
{
|
||||||
|
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
|
||||||
|
let result = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet);
|
||||||
|
assert!(matches!(result, Err(NewError::NonEmptyDatabase)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +106,7 @@ fn new_or_load() {
|
|||||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||||
let file_path = temp_dir.path().join("store.db");
|
let file_path = temp_dir.path().join("store.db");
|
||||||
|
|
||||||
// init wallet when non-existant
|
// init wallet when non-existent
|
||||||
let wallet_keychains = {
|
let wallet_keychains = {
|
||||||
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
|
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
|
||||||
.expect("must create db");
|
.expect("must create db");
|
||||||
@@ -3577,41 +3591,6 @@ fn test_fee_rate_sign_grinding_low_r() {
|
|||||||
assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
|
assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(feature = "test-hardware-signer")]
|
|
||||||
// #[test]
|
|
||||||
// fn test_hardware_signer() {
|
|
||||||
// use std::sync::Arc;
|
|
||||||
//
|
|
||||||
// use bdk::signer::SignerOrdering;
|
|
||||||
// use bdk::wallet::hardwaresigner::HWISigner;
|
|
||||||
// use hwi::types::HWIChain;
|
|
||||||
// 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, HWIChain::Regtest).unwrap();
|
|
||||||
// let descriptors = client.get_descriptors::<String>(None).unwrap();
|
|
||||||
// let custom_signer = HWISigner::from_device(&device, HWIChain::Regtest).unwrap();
|
|
||||||
//
|
|
||||||
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
|
|
||||||
// wallet.add_signer(
|
|
||||||
// KeychainKind::External,
|
|
||||||
// SignerOrdering(200),
|
|
||||||
// Arc::new(custom_signer),
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// let addr = wallet.get_address(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);
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_taproot_load_descriptor_duplicated_keys() {
|
fn test_taproot_load_descriptor_duplicated_keys() {
|
||||||
// Added after issue https://github.com/bitcoindevkit/bdk/issues/760
|
// Added after issue https://github.com/bitcoindevkit/bdk/issues/760
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_bitcoind_rpc"
|
name = "bdk_bitcoind_rpc"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.63"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
documentation = "https://docs.rs/bdk_bitcoind_rpc"
|
documentation = "https://docs.rs/bdk_bitcoind_rpc"
|
||||||
@@ -16,7 +16,7 @@ readme = "README.md"
|
|||||||
# For no-std, remember to enable the bitcoin/no-std feature
|
# For no-std, remember to enable the bitcoin/no-std feature
|
||||||
bitcoin = { version = "0.30", default-features = false }
|
bitcoin = { version = "0.30", default-features = false }
|
||||||
bitcoincore-rpc = { version = "0.17" }
|
bitcoincore-rpc = { version = "0.17" }
|
||||||
bdk_chain = { path = "../chain", version = "0.6", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.8", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use bitcoin::{block::Header, Block, BlockHash, Transaction};
|
|||||||
pub use bitcoincore_rpc;
|
pub use bitcoincore_rpc;
|
||||||
use bitcoincore_rpc::bitcoincore_rpc_json;
|
use bitcoincore_rpc::bitcoincore_rpc_json;
|
||||||
|
|
||||||
/// A structure that emits data sourced from [`bitcoincore_rpc::Client`].
|
/// The [`Emitter`] is used to emit data sourced from [`bitcoincore_rpc::Client`].
|
||||||
///
|
///
|
||||||
/// Refer to [module-level documentation] for more.
|
/// Refer to [module-level documentation] for more.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_chain"
|
name = "bdk_chain"
|
||||||
version = "0.6.0"
|
version = "0.8.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.63"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
documentation = "https://docs.rs/bdk_chain"
|
documentation = "https://docs.rs/bdk_chain"
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
|||||||
|
|
||||||
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
|
/// 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.
|
/// Refer to [`Anchor`] for more details.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -186,6 +188,8 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
|||||||
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
||||||
/// transaction.
|
/// transaction.
|
||||||
///
|
///
|
||||||
|
/// Note that the confirmation block and the anchor block can be different here.
|
||||||
|
///
|
||||||
/// Refer to [`Anchor`] for more details.
|
/// Refer to [`Anchor`] for more details.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::BlockId;
|
|||||||
/// Represents a service that tracks the blockchain.
|
/// Represents a service that tracks the blockchain.
|
||||||
///
|
///
|
||||||
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
|
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
|
||||||
/// is an ancestor of another "static block".
|
/// is an ancestor of the `chain_tip`.
|
||||||
///
|
///
|
||||||
/// [`is_block_in_chain`]: Self::is_block_in_chain
|
/// [`is_block_in_chain`]: Self::is_block_in_chain
|
||||||
pub trait ChainOracle {
|
pub trait ChainOracle {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
//! Contains the [`IndexedTxGraph`] structure and associated types.
|
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
|
||||||
//!
|
//! [`IndexedTxGraph`] documentation for more.
|
||||||
//! This is essentially a [`TxGraph`] combined with an indexer.
|
|
||||||
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
||||||
|
|
||||||
@@ -11,9 +9,9 @@ use crate::{
|
|||||||
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
|
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
|
||||||
///
|
///
|
||||||
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct IndexedTxGraph<A, I> {
|
pub struct IndexedTxGraph<A, I> {
|
||||||
/// Transaction index.
|
/// Transaction index.
|
||||||
@@ -266,7 +264,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure that represents changes to an [`IndexedTxGraph`].
|
/// Represents changes to an [`IndexedTxGraph`].
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ impl<K: Ord> Append for ChangeSet<K> {
|
|||||||
*index = other_index.max(*index);
|
*index = other_index.max(*index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||||
self.0.append(&mut other.0);
|
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||||
|
self.0.extend(other.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the changeset are empty.
|
/// Returns whether the changeset are empty.
|
||||||
|
|||||||
@@ -5,23 +5,56 @@ use crate::{
|
|||||||
spk_iter::BIP32_MAX_INDEX,
|
spk_iter::BIP32_MAX_INDEX,
|
||||||
SpkIterator, SpkTxOutIndex,
|
SpkIterator, SpkTxOutIndex,
|
||||||
};
|
};
|
||||||
use alloc::vec::Vec;
|
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||||
use bitcoin::{OutPoint, Script, TxOut};
|
use core::{
|
||||||
use core::{fmt::Debug, ops::Deref};
|
fmt::Debug,
|
||||||
|
ops::{Bound, RangeBounds},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::Append;
|
use crate::Append;
|
||||||
|
|
||||||
/// A convenient wrapper around [`SpkTxOutIndex`] that relates script pubkeys to miniscript public
|
const DEFAULT_LOOKAHEAD: u32 = 1_000;
|
||||||
/// [`Descriptor`]s.
|
|
||||||
|
/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and
|
||||||
|
/// indexes [`TxOut`]s with them.
|
||||||
///
|
///
|
||||||
/// Descriptors are referenced by the provided keychain generic (`K`).
|
/// A single keychain is a chain of script pubkeys derived from a single [`Descriptor`]. Keychains
|
||||||
|
/// are identified using the `K` generic. Script pubkeys are identified by the keychain that they
|
||||||
|
/// are derived from `K`, as well as the derivation index `u32`.
|
||||||
///
|
///
|
||||||
/// Script pubkeys for a descriptor are revealed chronologically from index 0. I.e., If the last
|
/// # Revealed script pubkeys
|
||||||
/// revealed index of a descriptor is 5; scripts of indices 0 to 4 are guaranteed to be already
|
|
||||||
/// revealed. In addition to revealed scripts, we have a `lookahead` parameter for each keychain,
|
|
||||||
/// which defines the number of script pubkeys to store ahead of the last revealed index.
|
|
||||||
///
|
///
|
||||||
/// Methods that could update the last revealed index will return [`super::ChangeSet`] to report
|
/// Tracking how script pubkeys are revealed is useful for collecting chain data. For example, if
|
||||||
|
/// the user has requested 5 script pubkeys (to receive money with), we only need to use those
|
||||||
|
/// script pubkeys to scan for chain data.
|
||||||
|
///
|
||||||
|
/// Call [`reveal_to_target`] or [`reveal_next_spk`] to reveal more script pubkeys.
|
||||||
|
/// Call [`revealed_keychain_spks`] or [`revealed_spks`] to iterate through revealed script pubkeys.
|
||||||
|
///
|
||||||
|
/// # Lookahead script pubkeys
|
||||||
|
///
|
||||||
|
/// When an user first recovers a wallet (i.e. from a recovery phrase and/or descriptor), we will
|
||||||
|
/// NOT have knowledge of which script pubkeys are revealed. So when we index a transaction or
|
||||||
|
/// txout (using [`index_tx`]/[`index_txout`]) we scan the txouts against script pubkeys derived
|
||||||
|
/// above the last revealed index. These additionally-derived script pubkeys are called the
|
||||||
|
/// lookahead.
|
||||||
|
///
|
||||||
|
/// The [`KeychainTxOutIndex`] is constructed with the `lookahead` and cannot be altered. The
|
||||||
|
/// default `lookahead` count is 1000. Use [`new`] to set a custom `lookahead`.
|
||||||
|
///
|
||||||
|
/// # Unbounded script pubkey iterator
|
||||||
|
///
|
||||||
|
/// For script-pubkey-based chain sources (such as Electrum/Esplora), an initial scan is best done
|
||||||
|
/// by iterating though derived script pubkeys one by one and requesting transaction histories for
|
||||||
|
/// each script pubkey. We will stop after x-number of script pubkeys have empty histories. An
|
||||||
|
/// unbounded script pubkey iterator is useful to pass to such a chain source.
|
||||||
|
///
|
||||||
|
/// Call [`unbounded_spk_iter`] to get an unbounded script pubkey iterator for a given keychain.
|
||||||
|
/// Call [`all_unbounded_spk_iters`] to get unbounded script pubkey iterators for all keychains.
|
||||||
|
///
|
||||||
|
/// # Change sets
|
||||||
|
///
|
||||||
|
/// Methods that can update the last revealed index will return [`super::ChangeSet`] to report
|
||||||
/// these changes. This can be persisted for future recovery.
|
/// these changes. This can be persisted for future recovery.
|
||||||
///
|
///
|
||||||
/// ## Synopsis
|
/// ## Synopsis
|
||||||
@@ -46,7 +79,7 @@ use crate::Append;
|
|||||||
/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||||
/// # let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
/// # 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 (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||||
/// # let descriptor_for_user_42 = external_descriptor.clone();
|
/// # let (descriptor_for_user_42, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap();
|
||||||
/// txout_index.add_keychain(MyKeychain::External, external_descriptor);
|
/// txout_index.add_keychain(MyKeychain::External, external_descriptor);
|
||||||
/// txout_index.add_keychain(MyKeychain::Internal, internal_descriptor);
|
/// txout_index.add_keychain(MyKeychain::Internal, internal_descriptor);
|
||||||
/// txout_index.add_keychain(MyKeychain::MyAppUser { user_id: 42 }, descriptor_for_user_42);
|
/// txout_index.add_keychain(MyKeychain::MyAppUser { user_id: 42 }, descriptor_for_user_42);
|
||||||
@@ -57,6 +90,15 @@ use crate::Append;
|
|||||||
/// [`Ord`]: core::cmp::Ord
|
/// [`Ord`]: core::cmp::Ord
|
||||||
/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex
|
/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex
|
||||||
/// [`Descriptor`]: crate::miniscript::Descriptor
|
/// [`Descriptor`]: crate::miniscript::Descriptor
|
||||||
|
/// [`reveal_to_target`]: KeychainTxOutIndex::reveal_to_target
|
||||||
|
/// [`reveal_next_spk`]: KeychainTxOutIndex::reveal_next_spk
|
||||||
|
/// [`revealed_keychain_spks`]: KeychainTxOutIndex::revealed_keychain_spks
|
||||||
|
/// [`revealed_spks`]: KeychainTxOutIndex::revealed_spks
|
||||||
|
/// [`index_tx`]: KeychainTxOutIndex::index_tx
|
||||||
|
/// [`index_txout`]: KeychainTxOutIndex::index_txout
|
||||||
|
/// [`new`]: KeychainTxOutIndex::new
|
||||||
|
/// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter
|
||||||
|
/// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KeychainTxOutIndex<K> {
|
pub struct KeychainTxOutIndex<K> {
|
||||||
inner: SpkTxOutIndex<(K, u32)>,
|
inner: SpkTxOutIndex<(K, u32)>,
|
||||||
@@ -65,25 +107,12 @@ pub struct KeychainTxOutIndex<K> {
|
|||||||
// last revealed indexes
|
// last revealed indexes
|
||||||
last_revealed: BTreeMap<K, u32>,
|
last_revealed: BTreeMap<K, u32>,
|
||||||
// lookahead settings for each keychain
|
// lookahead settings for each keychain
|
||||||
lookahead: BTreeMap<K, u32>,
|
lookahead: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K> Default for KeychainTxOutIndex<K> {
|
impl<K> Default for KeychainTxOutIndex<K> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self::new(DEFAULT_LOOKAHEAD)
|
||||||
inner: SpkTxOutIndex::default(),
|
|
||||||
keychains: BTreeMap::default(),
|
|
||||||
last_revealed: BTreeMap::default(),
|
|
||||||
lookahead: BTreeMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K> Deref for KeychainTxOutIndex<K> {
|
|
||||||
type Target = SpkTxOutIndex<(K, u32)>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +143,37 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
|
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
|
||||||
self.is_relevant(tx)
|
self.inner.is_relevant(tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K> KeychainTxOutIndex<K> {
|
||||||
|
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead`.
|
||||||
|
///
|
||||||
|
/// The `lookahead` is the number of script pubkeys to derive and cache from the internal
|
||||||
|
/// descriptors over and above the last revealed script index. Without a lookahead the index
|
||||||
|
/// will miss outputs you own when processing transactions whose output script pubkeys lie
|
||||||
|
/// beyond the last revealed index. In certain situations, such as when performing an initial
|
||||||
|
/// scan of the blockchain during wallet import, it may be uncertain or unknown what the index
|
||||||
|
/// of the last revealed script pubkey actually is.
|
||||||
|
///
|
||||||
|
/// Refer to [struct-level docs](KeychainTxOutIndex) for more about `lookahead`.
|
||||||
|
pub fn new(lookahead: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: SpkTxOutIndex::default(),
|
||||||
|
keychains: BTreeMap::new(),
|
||||||
|
last_revealed: BTreeMap::new(),
|
||||||
|
lookahead,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].
|
||||||
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||||
/// Return a reference to the internal [`SpkTxOutIndex`].
|
/// Return a reference to the internal [`SpkTxOutIndex`].
|
||||||
|
///
|
||||||
|
/// **WARNING:** The internal index will contain lookahead spks. Refer to
|
||||||
|
/// [struct-level docs](KeychainTxOutIndex) for more about `lookahead`.
|
||||||
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
|
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
|
||||||
&self.inner
|
&self.inner
|
||||||
}
|
}
|
||||||
@@ -129,7 +183,116 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
self.inner.outpoints()
|
self.inner.outpoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a reference to the internal map of the keychain to descriptors.
|
/// Iterate over known txouts that spend to tracked script pubkeys.
|
||||||
|
pub fn txouts(
|
||||||
|
&self,
|
||||||
|
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> + ExactSizeIterator {
|
||||||
|
self.inner
|
||||||
|
.txouts()
|
||||||
|
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds all txouts on a transaction that has previously been scanned and indexed.
|
||||||
|
pub fn txouts_in_tx(
|
||||||
|
&self,
|
||||||
|
txid: Txid,
|
||||||
|
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> {
|
||||||
|
self.inner
|
||||||
|
.txouts_in_tx(txid)
|
||||||
|
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the [`TxOut`] of `outpoint` if it has been indexed.
|
||||||
|
///
|
||||||
|
/// The associated keychain and keychain index of the txout's spk is also returned.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::txout`] internally.
|
||||||
|
pub fn txout(&self, outpoint: OutPoint) -> Option<(K, u32, &TxOut)> {
|
||||||
|
self.inner
|
||||||
|
.txout(outpoint)
|
||||||
|
.map(|((k, i), txo)| (k.clone(), *i, txo))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the script that exists under the given `keychain`'s `index`.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::spk_at_index`] internally.
|
||||||
|
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> {
|
||||||
|
self.inner.spk_at_index(&(keychain, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the keychain and keychain index associated with the spk.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::index_of_spk`] internally.
|
||||||
|
pub fn index_of_spk(&self, script: &Script) -> Option<(K, u32)> {
|
||||||
|
self.inner.index_of_spk(script).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the spk under the `keychain`'s `index` has been used.
|
||||||
|
///
|
||||||
|
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||||
|
/// never scanned a transaction output with it.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::is_used`] internally.
|
||||||
|
pub fn is_used(&self, keychain: K, index: u32) -> bool {
|
||||||
|
self.inner.is_used(&(keychain, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output
|
||||||
|
/// with it.
|
||||||
|
///
|
||||||
|
/// This only has an effect when the `index` had been added to `self` already and was unused.
|
||||||
|
///
|
||||||
|
/// Returns whether the `index` was initially present as `unused`.
|
||||||
|
///
|
||||||
|
/// This is useful when you want to reserve a script pubkey for something but don't want to add
|
||||||
|
/// the transaction output using it to the index yet. Other callers will consider `index` on
|
||||||
|
/// `keychain` used until you call [`unmark_used`].
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::mark_used`] internally.
|
||||||
|
///
|
||||||
|
/// [`unmark_used`]: Self::unmark_used
|
||||||
|
pub fn mark_used(&mut self, keychain: K, index: u32) -> bool {
|
||||||
|
self.inner.mark_used(&(keychain, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
|
||||||
|
/// `unused`.
|
||||||
|
///
|
||||||
|
/// Note that if `self` has scanned an output with this script pubkey, then this will have no
|
||||||
|
/// effect.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::unmark_used`] internally.
|
||||||
|
///
|
||||||
|
/// [`mark_used`]: Self::mark_used
|
||||||
|
pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool {
|
||||||
|
self.inner.unmark_used(&(keychain, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::sent_and_received`] internally.
|
||||||
|
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||||
|
self.inner.sent_and_received(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// This calls [`SpkTxOutIndex::net_value`] internally.
|
||||||
|
///
|
||||||
|
/// [`sent_and_received`]: Self::sent_and_received
|
||||||
|
pub fn net_value(&self, tx: &Transaction) -> i64 {
|
||||||
|
self.inner.net_value(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||||
|
/// Return a reference to the internal map of keychain to descriptors.
|
||||||
pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
|
pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
|
||||||
&self.keychains
|
&self.keychains
|
||||||
}
|
}
|
||||||
@@ -145,54 +308,22 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
|
pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
|
||||||
let old_descriptor = &*self
|
let old_descriptor = &*self
|
||||||
.keychains
|
.keychains
|
||||||
.entry(keychain)
|
.entry(keychain.clone())
|
||||||
.or_insert_with(|| descriptor.clone());
|
.or_insert_with(|| descriptor.clone());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&descriptor, old_descriptor,
|
&descriptor, old_descriptor,
|
||||||
"keychain already contains a different descriptor"
|
"keychain already contains a different descriptor"
|
||||||
);
|
);
|
||||||
|
self.replenish_lookahead(&keychain, self.lookahead);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the lookahead setting for each keychain.
|
/// Get the lookahead setting.
|
||||||
///
|
///
|
||||||
/// Refer to [`set_lookahead`] for a deeper explanation of the `lookahead`.
|
/// Refer to [`new`] for more information on the `lookahead`.
|
||||||
///
|
///
|
||||||
/// [`set_lookahead`]: Self::set_lookahead
|
/// [`new`]: Self::new
|
||||||
pub fn lookaheads(&self) -> &BTreeMap<K, u32> {
|
pub fn lookahead(&self) -> u32 {
|
||||||
&self.lookahead
|
self.lookahead
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method to call [`set_lookahead`] for all keychains.
|
|
||||||
///
|
|
||||||
/// [`set_lookahead`]: Self::set_lookahead
|
|
||||||
pub fn set_lookahead_for_all(&mut self, lookahead: u32) {
|
|
||||||
for keychain in &self.keychains.keys().cloned().collect::<Vec<_>>() {
|
|
||||||
self.set_lookahead(keychain, lookahead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the lookahead count for `keychain`.
|
|
||||||
///
|
|
||||||
/// The lookahead is the number of scripts to cache ahead of the last revealed script index. This
|
|
||||||
/// is useful to find outputs you own when processing block data that lie beyond the last revealed
|
|
||||||
/// index. In certain situations, such as when performing an initial scan of the blockchain during
|
|
||||||
/// wallet import, it may be uncertain or unknown what the last revealed index is.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// This will panic if the `keychain` does not exist.
|
|
||||||
pub fn set_lookahead(&mut self, keychain: &K, lookahead: u32) {
|
|
||||||
self.lookahead.insert(keychain.clone(), lookahead);
|
|
||||||
self.replenish_lookahead(keychain);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method to call [`lookahead_to_target`] for multiple keychains.
|
|
||||||
///
|
|
||||||
/// [`lookahead_to_target`]: Self::lookahead_to_target
|
|
||||||
pub fn lookahead_to_target_multi(&mut self, target_indexes: BTreeMap<K, u32>) {
|
|
||||||
for (keychain, target_index) in target_indexes {
|
|
||||||
self.lookahead_to_target(&keychain, target_index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store lookahead scripts until `target_index`.
|
/// Store lookahead scripts until `target_index`.
|
||||||
@@ -201,22 +332,14 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||||
let next_index = self.next_store_index(keychain);
|
let next_index = self.next_store_index(keychain);
|
||||||
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
|
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
|
||||||
let old_lookahead = self.lookahead.insert(keychain.clone(), temp_lookahead);
|
self.replenish_lookahead(keychain, temp_lookahead);
|
||||||
self.replenish_lookahead(keychain);
|
|
||||||
|
|
||||||
// revert
|
|
||||||
match old_lookahead {
|
|
||||||
Some(lookahead) => self.lookahead.insert(keychain.clone(), lookahead),
|
|
||||||
None => self.lookahead.remove(keychain),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replenish_lookahead(&mut self, keychain: &K) {
|
fn replenish_lookahead(&mut self, keychain: &K, lookahead: u32) {
|
||||||
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
|
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
|
||||||
let next_store_index = self.next_store_index(keychain);
|
let next_store_index = self.next_store_index(keychain);
|
||||||
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
|
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
|
||||||
let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v);
|
|
||||||
|
|
||||||
for (new_index, new_spk) in
|
for (new_index, new_spk) in
|
||||||
SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead)
|
SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead)
|
||||||
@@ -231,64 +354,74 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
fn next_store_index(&self, keychain: &K) -> u32 {
|
fn next_store_index(&self, keychain: &K) -> u32 {
|
||||||
self.inner()
|
self.inner()
|
||||||
.all_spks()
|
.all_spks()
|
||||||
|
// This range is filtering out the spks with a keychain different than
|
||||||
|
// `keychain`. We don't use filter here as range is more optimized.
|
||||||
.range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
|
.range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
|
||||||
.last()
|
.last()
|
||||||
.map_or(0, |((_, v), _)| *v + 1)
|
.map_or(0, |((_, index), _)| *index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates script pubkey iterators for every `keychain`. The iterators iterate over all
|
/// Get an unbounded spk iterator over a given `keychain`.
|
||||||
/// derivable script pubkeys.
|
///
|
||||||
pub fn spks_of_all_keychains(
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This will panic if the given `keychain`'s descriptor does not exist.
|
||||||
|
pub fn unbounded_spk_iter(&self, keychain: &K) -> SpkIterator<Descriptor<DescriptorPublicKey>> {
|
||||||
|
SpkIterator::new(
|
||||||
|
self.keychains
|
||||||
|
.get(keychain)
|
||||||
|
.expect("keychain does not exist")
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unbounded spk iterators for all keychains.
|
||||||
|
pub fn all_unbounded_spk_iters(
|
||||||
&self,
|
&self,
|
||||||
) -> BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> {
|
) -> BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> {
|
||||||
self.keychains
|
self.keychains
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(keychain, descriptor)| {
|
.map(|(k, descriptor)| (k.clone(), SpkIterator::new(descriptor.clone())))
|
||||||
(
|
|
||||||
keychain.clone(),
|
|
||||||
SpkIterator::new_with_range(descriptor.clone(), 0..),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a script pubkey iterator for the given `keychain`'s descriptor (if it exists). The
|
/// Iterate over revealed spks of all keychains.
|
||||||
/// iterator iterates over all derivable scripts of the keychain's descriptor.
|
pub fn revealed_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
|
||||||
///
|
self.keychains.keys().flat_map(|keychain| {
|
||||||
/// # Panics
|
self.revealed_keychain_spks(keychain)
|
||||||
///
|
.map(|(i, spk)| (keychain.clone(), i, spk))
|
||||||
/// This will panic if the `keychain` does not exist.
|
})
|
||||||
pub fn spks_of_keychain(&self, keychain: &K) -> SpkIterator<Descriptor<DescriptorPublicKey>> {
|
|
||||||
let descriptor = self
|
|
||||||
.keychains
|
|
||||||
.get(keychain)
|
|
||||||
.expect("keychain must exist")
|
|
||||||
.clone();
|
|
||||||
SpkIterator::new_with_range(descriptor, 0..)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience method to get [`revealed_spks_of_keychain`] of all keychains.
|
/// Iterate over revealed spks of the given `keychain`.
|
||||||
///
|
pub fn revealed_keychain_spks(
|
||||||
/// [`revealed_spks_of_keychain`]: Self::revealed_spks_of_keychain
|
|
||||||
pub fn revealed_spks_of_all_keychains(
|
|
||||||
&self,
|
|
||||||
) -> BTreeMap<K, impl Iterator<Item = (u32, &Script)> + Clone> {
|
|
||||||
self.keychains
|
|
||||||
.keys()
|
|
||||||
.map(|keychain| (keychain.clone(), self.revealed_spks_of_keychain(keychain)))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over the script pubkeys revealed by this index under `keychain`.
|
|
||||||
pub fn revealed_spks_of_keychain(
|
|
||||||
&self,
|
&self,
|
||||||
keychain: &K,
|
keychain: &K,
|
||||||
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
|
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
|
||||||
let next_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
|
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
|
||||||
self.inner
|
self.inner
|
||||||
.all_spks()
|
.all_spks()
|
||||||
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_index))
|
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
|
||||||
.map(|((_, derivation_index), spk)| (*derivation_index, spk.as_script()))
|
.map(|((_, i), spk)| (*i, spk.as_script()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over revealed, but unused, spks of all keychains.
|
||||||
|
pub fn unused_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
|
||||||
|
self.keychains.keys().flat_map(|keychain| {
|
||||||
|
self.unused_keychain_spks(keychain)
|
||||||
|
.map(|(i, spk)| (keychain.clone(), i, spk))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over revealed, but unused, spks of the given `keychain`.
|
||||||
|
pub fn unused_keychain_spks(
|
||||||
|
&self,
|
||||||
|
keychain: &K,
|
||||||
|
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
|
||||||
|
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
|
||||||
|
self.inner
|
||||||
|
.unused_spks((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
|
||||||
|
.map(|((_, i), spk)| (*i, spk))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next derivation index for `keychain`. The next index is the index after the last revealed
|
/// Get the next derivation index for `keychain`. The next index is the index after the last revealed
|
||||||
@@ -387,55 +520,45 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
let has_wildcard = descriptor.has_wildcard();
|
let has_wildcard = descriptor.has_wildcard();
|
||||||
|
|
||||||
let target_index = if has_wildcard { target_index } else { 0 };
|
let target_index = if has_wildcard { target_index } else { 0 };
|
||||||
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
|
let next_reveal_index = self
|
||||||
let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v);
|
.last_revealed
|
||||||
|
.get(keychain)
|
||||||
|
.map_or(0, |index| *index + 1);
|
||||||
|
|
||||||
debug_assert_eq!(
|
debug_assert!(next_reveal_index + self.lookahead >= self.next_store_index(keychain));
|
||||||
next_reveal_index + lookahead,
|
|
||||||
self.next_store_index(keychain)
|
|
||||||
);
|
|
||||||
|
|
||||||
// if we need to reveal new indices, the latest revealed index goes here
|
// If the target_index is already revealed, we are done
|
||||||
let mut reveal_to_index = None;
|
if next_reveal_index > target_index {
|
||||||
|
return (
|
||||||
// if the target is not yet revealed, but is already stored (due to lookahead), we need to
|
|
||||||
// set the `reveal_to_index` as target here (as the `for` loop below only updates
|
|
||||||
// `reveal_to_index` for indexes that are NOT stored)
|
|
||||||
if next_reveal_index <= target_index && target_index < next_reveal_index + lookahead {
|
|
||||||
reveal_to_index = Some(target_index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we range over indexes that are not stored
|
|
||||||
let range = next_reveal_index + lookahead..=target_index + lookahead;
|
|
||||||
for (new_index, new_spk) in SpkIterator::new_with_range(descriptor, range) {
|
|
||||||
let _inserted = self
|
|
||||||
.inner
|
|
||||||
.insert_spk((keychain.clone(), new_index), new_spk);
|
|
||||||
debug_assert!(_inserted, "must not have existing spk",);
|
|
||||||
|
|
||||||
// everything after `target_index` is stored for lookahead only
|
|
||||||
if new_index <= target_index {
|
|
||||||
reveal_to_index = Some(new_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match reveal_to_index {
|
|
||||||
Some(index) => {
|
|
||||||
let _old_index = self.last_revealed.insert(keychain.clone(), index);
|
|
||||||
debug_assert!(_old_index < Some(index));
|
|
||||||
(
|
|
||||||
SpkIterator::new_with_range(descriptor.clone(), next_reveal_index..index + 1),
|
|
||||||
super::ChangeSet(core::iter::once((keychain.clone(), index)).collect()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => (
|
|
||||||
SpkIterator::new_with_range(
|
SpkIterator::new_with_range(
|
||||||
descriptor.clone(),
|
descriptor.clone(),
|
||||||
next_reveal_index..next_reveal_index,
|
next_reveal_index..next_reveal_index,
|
||||||
),
|
),
|
||||||
super::ChangeSet::default(),
|
super::ChangeSet::default(),
|
||||||
),
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We range over the indexes that are not stored and insert their spks in the index.
|
||||||
|
// Indexes from next_reveal_index to next_reveal_index + lookahead are already stored (due
|
||||||
|
// to lookahead), so we only range from next_reveal_index + lookahead to target + lookahead
|
||||||
|
let range = next_reveal_index + self.lookahead..=target_index + self.lookahead;
|
||||||
|
for (new_index, new_spk) in SpkIterator::new_with_range(descriptor, range) {
|
||||||
|
let _inserted = self
|
||||||
|
.inner
|
||||||
|
.insert_spk((keychain.clone(), new_index), new_spk);
|
||||||
|
debug_assert!(_inserted, "must not have existing spk");
|
||||||
|
debug_assert!(
|
||||||
|
has_wildcard || new_index == 0,
|
||||||
|
"non-wildcard descriptors must not iterate past index 0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _old_index = self.last_revealed.insert(keychain.clone(), target_index);
|
||||||
|
debug_assert!(_old_index < Some(target_index));
|
||||||
|
(
|
||||||
|
SpkIterator::new_with_range(descriptor.clone(), next_reveal_index..target_index + 1),
|
||||||
|
super::ChangeSet(core::iter::once((keychain.clone(), target_index)).collect()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to reveal the next script pubkey for `keychain`.
|
/// Attempts to reveal the next script pubkey for `keychain`.
|
||||||
@@ -475,13 +598,13 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
///
|
///
|
||||||
/// Panics if `keychain` has never been added to the index
|
/// Panics if `keychain` has never been added to the index
|
||||||
pub fn next_unused_spk(&mut self, keychain: &K) -> ((u32, &Script), super::ChangeSet<K>) {
|
pub fn next_unused_spk(&mut self, keychain: &K) -> ((u32, &Script), super::ChangeSet<K>) {
|
||||||
let need_new = self.unused_spks_of_keychain(keychain).next().is_none();
|
let need_new = self.unused_keychain_spks(keychain).next().is_none();
|
||||||
// this rather strange branch is needed because of some lifetime issues
|
// this rather strange branch is needed because of some lifetime issues
|
||||||
if need_new {
|
if need_new {
|
||||||
self.reveal_next_spk(keychain)
|
self.reveal_next_spk(keychain)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.unused_spks_of_keychain(keychain)
|
self.unused_keychain_spks(keychain)
|
||||||
.next()
|
.next()
|
||||||
.expect("we already know next exists"),
|
.expect("we already know next exists"),
|
||||||
super::ChangeSet::default(),
|
super::ChangeSet::default(),
|
||||||
@@ -489,58 +612,44 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output with it.
|
/// Iterate over all [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
|
||||||
/// This only has an effect when the `index` had been added to `self` already and was unused.
|
|
||||||
///
|
|
||||||
/// Returns whether the `index` was initially present as `unused`.
|
|
||||||
///
|
|
||||||
/// This is useful when you want to reserve a script pubkey for something but don't want to add
|
|
||||||
/// the transaction output using it to the index yet. Other callers will consider `index` on
|
|
||||||
/// `keychain` used until you call [`unmark_used`].
|
|
||||||
///
|
|
||||||
/// [`unmark_used`]: Self::unmark_used
|
|
||||||
pub fn mark_used(&mut self, keychain: &K, index: u32) -> bool {
|
|
||||||
self.inner.mark_used(&(keychain.clone(), index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
|
|
||||||
/// `unused`.
|
|
||||||
///
|
|
||||||
/// Note that if `self` has scanned an output with this script pubkey, then this will have no
|
|
||||||
/// effect.
|
|
||||||
///
|
|
||||||
/// [`mark_used`]: Self::mark_used
|
|
||||||
pub fn unmark_used(&mut self, keychain: &K, index: u32) -> bool {
|
|
||||||
self.inner.unmark_used(&(keychain.clone(), index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over all unused script pubkeys for a `keychain` stored in the index.
|
|
||||||
pub fn unused_spks_of_keychain(
|
|
||||||
&self,
|
|
||||||
keychain: &K,
|
|
||||||
) -> impl DoubleEndedIterator<Item = (u32, &Script)> {
|
|
||||||
let next_index = self.last_revealed.get(keychain).map_or(0, |&v| v + 1);
|
|
||||||
let range = (keychain.clone(), u32::MIN)..(keychain.clone(), next_index);
|
|
||||||
self.inner
|
|
||||||
.unused_spks(range)
|
|
||||||
.map(|((_, i), script)| (*i, script))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over all the [`OutPoint`] that have a `TxOut` with a script pubkey derived from
|
|
||||||
/// `keychain`.
|
/// `keychain`.
|
||||||
pub fn txouts_of_keychain(
|
///
|
||||||
|
/// Use [`keychain_outpoints_in_range`](KeychainTxOutIndex::keychain_outpoints_in_range) to
|
||||||
|
/// iterate over a specific derivation range.
|
||||||
|
pub fn keychain_outpoints(
|
||||||
&self,
|
&self,
|
||||||
keychain: &K,
|
keychain: &K,
|
||||||
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
|
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
|
||||||
|
self.keychain_outpoints_in_range(keychain, ..)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
|
||||||
|
/// `keychain` in a given derivation `range`.
|
||||||
|
pub fn keychain_outpoints_in_range(
|
||||||
|
&self,
|
||||||
|
keychain: &K,
|
||||||
|
range: impl RangeBounds<u32>,
|
||||||
|
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
|
||||||
|
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
|
||||||
|
Bound::Unbounded => Bound::Unbounded,
|
||||||
|
};
|
||||||
|
let end = match range.end_bound() {
|
||||||
|
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
|
||||||
|
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
|
||||||
|
Bound::Unbounded => Bound::Unbounded,
|
||||||
|
};
|
||||||
self.inner
|
self.inner
|
||||||
.outputs_in_range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
|
.outputs_in_range((start, end))
|
||||||
.map(|((_, i), op)| (*i, op))
|
.map(|((_, i), op)| (*i, op))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
|
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
|
||||||
/// found a [`TxOut`] with it's script pubkey.
|
/// found a [`TxOut`] with it's script pubkey.
|
||||||
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
|
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
|
||||||
self.txouts_of_keychain(keychain).last().map(|(i, _)| i)
|
self.keychain_outpoints(keychain).last().map(|(i, _)| i)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found
|
/// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release).
|
//! This crate is a collection of core structures for [Bitcoin Dev Kit].
|
||||||
//!
|
//!
|
||||||
//! The goal of this crate is to give wallets the mechanisms needed to:
|
//! The goal of this crate is to give wallets the mechanisms needed to:
|
||||||
//!
|
//!
|
||||||
@@ -12,9 +12,8 @@
|
|||||||
//! you do it synchronously or asynchronously. If you know a fact about the blockchain, you can just
|
//! 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
|
//! tell `bdk_chain`'s APIs about it, and that information will be integrated, if it can be done
|
||||||
//! consistently.
|
//! consistently.
|
||||||
//! 2. Error-free APIs.
|
//! 2. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
|
||||||
//! 3. 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.
|
||||||
//! cache or how you fetch it.
|
|
||||||
//!
|
//!
|
||||||
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
|
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{BlockId, ChainOracle};
|
|||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
use bitcoin::BlockHash;
|
use bitcoin::BlockHash;
|
||||||
|
|
||||||
/// A structure that represents changes to [`LocalChain`].
|
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||||
///
|
///
|
||||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||||
@@ -127,7 +127,7 @@ impl CheckPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure that iterates over checkpoints backwards.
|
/// Iterates over checkpoints backwards.
|
||||||
pub struct CheckPointIter {
|
pub struct CheckPointIter {
|
||||||
current: Option<Arc<CPInner>>,
|
current: Option<Arc<CPInner>>,
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ impl IntoIterator for CheckPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A struct to update [`LocalChain`].
|
/// Used to update [`LocalChain`].
|
||||||
///
|
///
|
||||||
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
|
/// 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
|
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
|
||||||
@@ -420,6 +420,28 @@ impl LocalChain {
|
|||||||
Ok(changeset)
|
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> {
|
||||||
|
if self.index.get(&block_id.height) != Some(&block_id.hash) {
|
||||||
|
return Ok(ChangeSet::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeset = self
|
||||||
|
.index
|
||||||
|
.range(block_id.height..)
|
||||||
|
.map(|(&height, _)| (height, None))
|
||||||
|
.collect::<ChangeSet>();
|
||||||
|
self.apply_changeset(&changeset).map(|_| changeset)
|
||||||
|
}
|
||||||
|
|
||||||
/// Reindex the heights in the chain from (and including) `from` height
|
/// Reindex the heights in the chain from (and including) `from` height
|
||||||
fn reindex(&mut self, from: u32) {
|
fn reindex(&mut self, from: u32) {
|
||||||
let _ = self.index.split_off(&from);
|
let _ = self.index.split_off(&from);
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ where
|
|||||||
secp: Secp256k1::verification_only(),
|
secp: Secp256k1::verification_only(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the internal descriptor.
|
||||||
|
pub fn descriptor(&self) -> &D {
|
||||||
|
&self.descriptor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D> Iterator for SpkIterator<D>
|
impl<D> Iterator for SpkIterator<D>
|
||||||
@@ -148,7 +153,7 @@ mod test {
|
|||||||
Descriptor<DescriptorPublicKey>,
|
Descriptor<DescriptorPublicKey>,
|
||||||
Descriptor<DescriptorPublicKey>,
|
Descriptor<DescriptorPublicKey>,
|
||||||
) {
|
) {
|
||||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
|
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||||
|
|
||||||
let secp = Secp256k1::signing_only();
|
let secp = Secp256k1::signing_only();
|
||||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||||
|
|||||||
@@ -168,9 +168,7 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
|||||||
///
|
///
|
||||||
/// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
|
/// 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)> {
|
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
|
||||||
self.txouts
|
self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
|
||||||
.get(&outpoint)
|
|
||||||
.map(|(spk_i, txout)| (spk_i, txout))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the script that has been inserted at the `index`.
|
/// Returns the script that has been inserted at the `index`.
|
||||||
@@ -217,7 +215,7 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
|||||||
/// let unused_change_spks =
|
/// let unused_change_spks =
|
||||||
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
|
/// 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)>
|
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
|
||||||
where
|
where
|
||||||
R: RangeBounds<I>,
|
R: RangeBounds<I>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,21 +5,25 @@ use alloc::vec::Vec;
|
|||||||
|
|
||||||
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
||||||
///
|
///
|
||||||
/// [`Anchor`] implementations must be [`Ord`] by the anchor block's [`BlockId`] first.
|
/// If transaction A is anchored in block B, and block B is in the best chain, we can
|
||||||
///
|
|
||||||
/// 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
|
/// 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
|
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
|
||||||
/// parent block of B.
|
/// 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::local_chain::LocalChain;
|
||||||
/// # use bdk_chain::tx_graph::TxGraph;
|
/// # use bdk_chain::tx_graph::TxGraph;
|
||||||
/// # use bdk_chain::BlockId;
|
/// # use bdk_chain::BlockId;
|
||||||
/// # use bdk_chain::ConfirmationHeightAnchor;
|
/// # use bdk_chain::ConfirmationHeightAnchor;
|
||||||
|
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
|
||||||
/// # use bdk_chain::example_utils::*;
|
/// # use bdk_chain::example_utils::*;
|
||||||
/// # use bitcoin::hashes::Hash;
|
/// # use bitcoin::hashes::Hash;
|
||||||
///
|
|
||||||
/// // Initialize the local chain with two blocks.
|
/// // Initialize the local chain with two blocks.
|
||||||
/// let chain = LocalChain::from_blocks(
|
/// let chain = LocalChain::from_blocks(
|
||||||
/// [
|
/// [
|
||||||
@@ -47,6 +51,7 @@ use alloc::vec::Vec;
|
|||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
|
/// // 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
|
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
|
||||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
/// // 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.
|
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||||
@@ -62,6 +67,25 @@ use alloc::vec::Vec;
|
|||||||
/// confirmation_height: 1,
|
/// 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.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 {
|
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
|
||||||
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
|
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
|
||||||
@@ -99,8 +123,10 @@ pub trait Append {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||||
fn append(&mut self, mut other: Self) {
|
fn append(&mut self, other: Self) {
|
||||||
BTreeMap::append(self, &mut other)
|
// 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 is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
@@ -109,8 +135,10 @@ impl<K: Ord, V> Append for BTreeMap<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Ord> Append for BTreeSet<T> {
|
impl<T: Ord> Append for BTreeSet<T> {
|
||||||
fn append(&mut self, mut other: Self) {
|
fn append(&mut self, other: Self) {
|
||||||
BTreeSet::append(self, &mut other)
|
// 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 is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
//! Module for structures that store and traverse transactions.
|
//! Module for structures that store and traverse transactions.
|
||||||
//!
|
//!
|
||||||
//! [`TxGraph`] is a monotone structure that inserts transactions and indexes the spends. The
|
//! [`TxGraph`] contains transactions and indexes them so you can easily traverse the graph of those transactions.
|
||||||
//! [`ChangeSet`] structure reports changes of [`TxGraph`] but can also be applied to a
|
//! `TxGraph` is *monotone* in that you can always insert a transaction -- it doesn't care whether that
|
||||||
//! [`TxGraph`] as well. Lastly, [`TxDescendants`] is an [`Iterator`] that traverses descendants of
|
//! transaction is in the current best chain or whether it conflicts with any of the
|
||||||
//! a given transaction.
|
//! 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 it's anchored in (see the [`Anchor`]
|
||||||
|
//! documentation for more details), and the timestamp of the last time we saw
|
||||||
|
//! the transaction as unconfirmed.
|
||||||
//!
|
//!
|
||||||
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
|
//! Conflicting transactions are allowed to coexist within a [`TxGraph`]. This is useful for
|
||||||
//! identifying and traversing conflicts and descendants of a given transaction.
|
//! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
|
||||||
|
//! methods only consider "canonical" (i.e., in the best chain or in mempool) transactions,
|
||||||
|
//! we decide which transactions are canonical based on anchors `last_seen_unconfirmed`;
|
||||||
|
//! 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.
|
||||||
//!
|
//!
|
||||||
//! # Applying changes
|
//! # Applying changes
|
||||||
//!
|
//!
|
||||||
@@ -49,6 +69,8 @@
|
|||||||
//! let changeset = graph.apply_update(update);
|
//! let changeset = graph.apply_update(update);
|
||||||
//! assert!(changeset.is_empty());
|
//! assert!(changeset.is_empty());
|
||||||
//! ```
|
//! ```
|
||||||
|
//! [`try_get_chain_position`]: TxGraph::try_get_chain_position
|
||||||
|
//! [`insert_txout`]: TxGraph::insert_txout
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||||
@@ -91,7 +113,7 @@ impl<A> Default for TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An outward-facing view of a (transaction) node in the [`TxGraph`].
|
/// A transaction node in the [`TxGraph`].
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct TxNode<'a, T, A> {
|
pub struct TxNode<'a, T, A> {
|
||||||
/// Txid of the transaction.
|
/// Txid of the transaction.
|
||||||
@@ -128,7 +150,7 @@ impl Default for TxNodeInternal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An outwards-facing view of a transaction that is part of the *best chain*'s history.
|
/// A transaction that is included in the chain, or is still in mempool.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct CanonicalTx<'a, T, A> {
|
pub struct CanonicalTx<'a, T, A> {
|
||||||
/// How the transaction is observed as (confirmed or unconfirmed).
|
/// How the transaction is observed as (confirmed or unconfirmed).
|
||||||
@@ -475,7 +497,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
/// Batch insert unconfirmed transactions.
|
/// Batch insert unconfirmed transactions.
|
||||||
///
|
///
|
||||||
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
|
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
|
||||||
/// *last seen* communicates when the transaction is last seen in the mempool which is used for
|
/// *last seen* communicates when the transaction is last seen in mempool which is used for
|
||||||
/// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details).
|
/// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details).
|
||||||
pub fn batch_insert_unconfirmed(
|
pub fn batch_insert_unconfirmed(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -559,10 +581,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (outpoint, txout) in changeset.txouts {
|
for (outpoint, txout) in changeset.txouts {
|
||||||
let tx_entry = self
|
let tx_entry = self.txs.entry(outpoint.txid).or_default();
|
||||||
.txs
|
|
||||||
.entry(outpoint.txid)
|
|
||||||
.or_insert_with(Default::default);
|
|
||||||
|
|
||||||
match tx_entry {
|
match tx_entry {
|
||||||
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
|
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
|
||||||
@@ -575,13 +594,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
|
|
||||||
for (anchor, txid) in changeset.anchors {
|
for (anchor, txid) in changeset.anchors {
|
||||||
if self.anchors.insert((anchor.clone(), txid)) {
|
if self.anchors.insert((anchor.clone(), txid)) {
|
||||||
let (_, anchors, _) = self.txs.entry(txid).or_insert_with(Default::default);
|
let (_, anchors, _) = self.txs.entry(txid).or_default();
|
||||||
anchors.insert(anchor);
|
anchors.insert(anchor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (txid, new_last_seen) in changeset.last_seen {
|
for (txid, new_last_seen) in changeset.last_seen {
|
||||||
let (_, _, last_seen) = self.txs.entry(txid).or_insert_with(Default::default);
|
let (_, _, last_seen) = self.txs.entry(txid).or_default();
|
||||||
if new_last_seen > *last_seen {
|
if new_last_seen > *last_seen {
|
||||||
*last_seen = new_last_seen;
|
*last_seen = new_last_seen;
|
||||||
}
|
}
|
||||||
@@ -708,8 +727,20 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
|
|
||||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||||
///
|
///
|
||||||
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
|
/// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
|
||||||
/// returned.
|
///
|
||||||
|
/// 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.
|
||||||
///
|
///
|
||||||
/// # Error
|
/// # Error
|
||||||
///
|
///
|
||||||
@@ -735,7 +766,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tx is not anchored to a block which is in the best chain, which means that it
|
// The tx is not anchored to a block in the best chain, which means that it
|
||||||
// might be in mempool, or it might have been dropped already.
|
// might be in mempool, or it might have been dropped already.
|
||||||
// Let's check conflicts to find out!
|
// Let's check conflicts to find out!
|
||||||
let tx = match tx_node {
|
let tx = match tx_node {
|
||||||
@@ -945,7 +976,8 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
/// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
|
/// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
|
||||||
/// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
|
/// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
|
||||||
///
|
///
|
||||||
/// Floating outputs are ignored.
|
/// Floating outputs (i.e., outputs for which we don't have the full transaction in the graph)
|
||||||
|
/// are ignored.
|
||||||
///
|
///
|
||||||
/// # Error
|
/// # Error
|
||||||
///
|
///
|
||||||
@@ -1136,9 +1168,9 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure that represents changes to a [`TxGraph`].
|
/// The [`ChangeSet`] represents changes to a [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// Since [`TxGraph`] is monotone "changeset" can only contain transactions to be added and
|
/// Since [`TxGraph`] is monotone, the "changeset" can only contain transactions to be added and
|
||||||
/// not removed.
|
/// not removed.
|
||||||
///
|
///
|
||||||
/// Refer to [module-level documentation] for more.
|
/// Refer to [module-level documentation] for more.
|
||||||
@@ -1239,10 +1271,12 @@ impl<A> ChangeSet<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Ord> Append for ChangeSet<A> {
|
impl<A: Ord> Append for ChangeSet<A> {
|
||||||
fn append(&mut self, mut other: Self) {
|
fn append(&mut self, other: Self) {
|
||||||
self.txs.append(&mut other.txs);
|
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||||
self.txouts.append(&mut other.txouts);
|
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||||
self.anchors.append(&mut other.anchors);
|
self.txs.extend(other.txs);
|
||||||
|
self.txouts.extend(other.txouts);
|
||||||
|
self.anchors.extend(other.anchors);
|
||||||
|
|
||||||
// last_seen timestamps should only increase
|
// last_seen timestamps should only increase
|
||||||
self.last_seen.extend(
|
self.last_seen.extend(
|
||||||
@@ -1272,7 +1306,7 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
|||||||
///
|
///
|
||||||
/// The iterator excludes partial transactions.
|
/// The iterator excludes partial transactions.
|
||||||
///
|
///
|
||||||
/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`].
|
/// Returned by the [`walk_ancestors`] method of [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// [`walk_ancestors`]: TxGraph::walk_ancestors
|
/// [`walk_ancestors`]: TxGraph::walk_ancestors
|
||||||
pub struct TxAncestors<'g, A, F> {
|
pub struct TxAncestors<'g, A, F> {
|
||||||
@@ -1390,7 +1424,7 @@ where
|
|||||||
|
|
||||||
/// An iterator that traverses transaction descendants.
|
/// An iterator that traverses transaction descendants.
|
||||||
///
|
///
|
||||||
/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`].
|
/// Returned by the [`walk_descendants`] method of [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// [`walk_descendants`]: TxGraph::walk_descendants
|
/// [`walk_descendants`]: TxGraph::walk_descendants
|
||||||
pub struct TxDescendants<'g, A, F> {
|
pub struct TxDescendants<'g, A, F> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod tx_template;
|
mod tx_template;
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub use tx_template::*;
|
pub use tx_template::*;
|
||||||
|
|
||||||
#[allow(unused_macros)]
|
#[allow(unused_macros)]
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ fn insert_relevant_txs() {
|
|||||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||||
|
|
||||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
|
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
||||||
|
KeychainTxOutIndex::new(10),
|
||||||
|
);
|
||||||
graph.index.add_keychain((), descriptor);
|
graph.index.add_keychain((), descriptor);
|
||||||
graph.index.set_lookahead(&(), 10);
|
|
||||||
|
|
||||||
let tx_a = Transaction {
|
let tx_a = Transaction {
|
||||||
output: vec![
|
output: vec![
|
||||||
@@ -118,12 +119,12 @@ fn test_list_owned_txouts() {
|
|||||||
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").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 (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
|
||||||
|
|
||||||
let mut graph =
|
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
||||||
IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::default();
|
KeychainTxOutIndex::new(10),
|
||||||
|
);
|
||||||
|
|
||||||
graph.index.add_keychain("keychain_1".into(), desc_1);
|
graph.index.add_keychain("keychain_1".into(), desc_1);
|
||||||
graph.index.add_keychain("keychain_2".into(), desc_2);
|
graph.index.add_keychain("keychain_2".into(), desc_2);
|
||||||
graph.index.set_lookahead_for_all(10);
|
|
||||||
|
|
||||||
// Get trusted and untrusted addresses
|
// Get trusted and untrusted addresses
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ enum TestKeychain {
|
|||||||
Internal,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_txout_index() -> (
|
fn init_txout_index(
|
||||||
|
lookahead: u32,
|
||||||
|
) -> (
|
||||||
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
|
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
|
||||||
Descriptor<DescriptorPublicKey>,
|
Descriptor<DescriptorPublicKey>,
|
||||||
Descriptor<DescriptorPublicKey>,
|
Descriptor<DescriptorPublicKey>,
|
||||||
) {
|
) {
|
||||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::default();
|
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||||
|
|
||||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||||
@@ -46,7 +48,7 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
|
|||||||
fn test_set_all_derivation_indices() {
|
fn test_set_all_derivation_indices() {
|
||||||
use bdk_chain::indexed_tx_graph::Indexer;
|
use bdk_chain::indexed_tx_graph::Indexer;
|
||||||
|
|
||||||
let (mut txout_index, _, _) = init_txout_index();
|
let (mut txout_index, _, _) = init_txout_index(0);
|
||||||
let derive_to: BTreeMap<_, _> =
|
let derive_to: BTreeMap<_, _> =
|
||||||
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
|
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -64,19 +66,10 @@ fn test_set_all_derivation_indices() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lookahead() {
|
fn test_lookahead() {
|
||||||
let (mut txout_index, external_desc, internal_desc) = init_txout_index();
|
let (mut txout_index, external_desc, internal_desc) = init_txout_index(10);
|
||||||
|
|
||||||
// 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:
|
// given:
|
||||||
// - external lookahead set to 10
|
// - external lookahead set to 10
|
||||||
// - internal lookahead set to 20
|
|
||||||
// when:
|
// when:
|
||||||
// - set external derivation index to value higher than last, but within the lookahead value
|
// - set external derivation index to value higher than last, but within the lookahead value
|
||||||
// expect:
|
// expect:
|
||||||
@@ -97,37 +90,37 @@ fn test_lookahead() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index.inner().all_spks().len(),
|
txout_index.inner().all_spks().len(),
|
||||||
10 /* external lookahead */ +
|
10 /* external lookahead */ +
|
||||||
20 /* internal lookahead */ +
|
10 /* internal lookahead */ +
|
||||||
index as usize + 1 /* `derived` count */
|
index as usize + 1 /* `derived` count */
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.revealed_spks_of_keychain(&TestKeychain::External)
|
.revealed_keychain_spks(&TestKeychain::External)
|
||||||
.count(),
|
.count(),
|
||||||
index as usize + 1,
|
index as usize + 1,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||||
.count(),
|
.count(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.unused_spks_of_keychain(&TestKeychain::External)
|
.unused_keychain_spks(&TestKeychain::External)
|
||||||
.count(),
|
.count(),
|
||||||
index as usize + 1,
|
index as usize + 1,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.unused_spks_of_keychain(&TestKeychain::Internal)
|
.unused_keychain_spks(&TestKeychain::Internal)
|
||||||
.count(),
|
.count(),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// given:
|
// given:
|
||||||
// - internal lookahead is 20
|
// - internal lookahead is 10
|
||||||
// - internal derivation index is `None`
|
// - internal derivation index is `None`
|
||||||
// when:
|
// when:
|
||||||
// - derivation index is set ahead of current derivation index + lookahead
|
// - derivation index is set ahead of current derivation index + lookahead
|
||||||
@@ -148,13 +141,13 @@ fn test_lookahead() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index.inner().all_spks().len(),
|
txout_index.inner().all_spks().len(),
|
||||||
10 /* external lookahead */ +
|
10 /* external lookahead */ +
|
||||||
20 /* internal lookahead */ +
|
10 /* internal lookahead */ +
|
||||||
20 /* external stored index count */ +
|
20 /* external stored index count */ +
|
||||||
25 /* internal stored index count */
|
25 /* internal stored index count */
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||||
.count(),
|
.count(),
|
||||||
25,
|
25,
|
||||||
);
|
);
|
||||||
@@ -206,13 +199,13 @@ fn test_lookahead() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.revealed_spks_of_keychain(&TestKeychain::External)
|
.revealed_keychain_spks(&TestKeychain::External)
|
||||||
.count(),
|
.count(),
|
||||||
last_external_index as usize + 1,
|
last_external_index as usize + 1,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.revealed_spks_of_keychain(&TestKeychain::Internal)
|
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||||
.count(),
|
.count(),
|
||||||
last_internal_index as usize + 1,
|
last_internal_index as usize + 1,
|
||||||
);
|
);
|
||||||
@@ -226,8 +219,7 @@ fn test_lookahead() {
|
|||||||
// - last used index should change as expected
|
// - last used index should change as expected
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scan_with_lookahead() {
|
fn test_scan_with_lookahead() {
|
||||||
let (mut txout_index, external_desc, _) = init_txout_index();
|
let (mut txout_index, external_desc, _) = init_txout_index(10);
|
||||||
txout_index.set_lookahead_for_all(10);
|
|
||||||
|
|
||||||
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
|
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -281,7 +273,7 @@ fn test_scan_with_lookahead() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn test_wildcard_derivations() {
|
fn test_wildcard_derivations() {
|
||||||
let (mut txout_index, external_desc, _) = init_txout_index();
|
let (mut txout_index, external_desc, _) = init_txout_index(0);
|
||||||
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
|
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_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_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
|
||||||
@@ -313,7 +305,7 @@ fn test_wildcard_derivations() {
|
|||||||
|
|
||||||
(0..=15)
|
(0..=15)
|
||||||
.chain([17, 20, 23])
|
.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), (26, true));
|
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
|
||||||
|
|
||||||
@@ -329,7 +321,7 @@ fn test_wildcard_derivations() {
|
|||||||
// - Use all the derived till 26.
|
// - Use all the derived till 26.
|
||||||
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
|
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
|
||||||
(0..=26).for_each(|index| {
|
(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);
|
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||||
@@ -339,7 +331,7 @@ fn test_wildcard_derivations() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_non_wildcard_derivations() {
|
fn test_non_wildcard_derivations() {
|
||||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
|
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||||
|
|
||||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||||
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||||
@@ -372,7 +364,7 @@ fn test_non_wildcard_derivations() {
|
|||||||
// - derive new and next unused should return the old script
|
// - derive new and next unused should return the old script
|
||||||
// - store_up_to should not panic and return empty changeset
|
// - store_up_to should not panic and return empty changeset
|
||||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
|
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
|
||||||
txout_index.mark_used(&TestKeychain::External, 0);
|
txout_index.mark_used(TestKeychain::External, 0);
|
||||||
|
|
||||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||||
assert_eq!(spk, (0, external_spk.as_script()));
|
assert_eq!(spk, (0, external_spk.as_script()));
|
||||||
@@ -389,7 +381,7 @@ fn test_non_wildcard_derivations() {
|
|||||||
// we check that spks_of_keychain returns a SpkIterator with just one element
|
// we check that spks_of_keychain returns a SpkIterator with just one element
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txout_index
|
txout_index
|
||||||
.spks_of_keychain(&TestKeychain::External)
|
.revealed_keychain_spks(&TestKeychain::External)
|
||||||
.count(),
|
.count(),
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use bdk_chain::local_chain::{
|
use bdk_chain::local_chain::{
|
||||||
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
|
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, MissingGenesisError, Update,
|
||||||
};
|
};
|
||||||
use bitcoin::BlockHash;
|
use bitcoin::BlockHash;
|
||||||
|
|
||||||
@@ -350,3 +350,76 @@ fn local_chain_insert_block() {
|
|||||||
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ fn test_tx_conflict_handling() {
|
|||||||
..Default::default()
|
..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_txs: HashSet::from(["tx1", "tx_conflict_2"]),
|
||||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
|
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
|
||||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_electrum"
|
name = "bdk_electrum"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -12,6 +12,6 @@ readme = "README.md"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.8.0", default-features = false }
|
||||||
electrum-client = { version = "0.18" }
|
electrum-client = { version = "0.18" }
|
||||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
# BDK Electrum
|
# BDK Electrum
|
||||||
|
|
||||||
BDK Electrum client library for updating the keychain tracker.
|
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/
|
||||||
|
|||||||
@@ -134,76 +134,63 @@ pub struct ElectrumUpdate {
|
|||||||
|
|
||||||
/// Trait to extend [`Client`] functionality.
|
/// Trait to extend [`Client`] functionality.
|
||||||
pub trait ElectrumExt {
|
pub trait ElectrumExt {
|
||||||
/// Scan the blockchain (via electrum) for the data specified and returns updates for
|
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
|
||||||
/// [`bdk_chain`] data structures.
|
/// returns updates for [`bdk_chain`] data structures.
|
||||||
///
|
///
|
||||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||||
/// - `keychain_spks`: keychains that we want to scan transactions for
|
/// - `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
|
/// The full 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
|
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
|
||||||
/// single batch request.
|
/// single batch request.
|
||||||
fn scan<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
prev_tip: CheckPoint,
|
prev_tip: CheckPoint,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
|
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
|
||||||
|
|
||||||
/// Convenience method to call [`scan`] without requiring a keychain.
|
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
|
||||||
|
/// and returns updates for [`bdk_chain`] data structures.
|
||||||
///
|
///
|
||||||
/// [`scan`]: ElectrumExt::scan
|
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||||
fn scan_without_keychain(
|
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
|
||||||
|
/// - `txids`: transactions for which we want updated [`Anchor`]s
|
||||||
|
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||||
|
/// want to include in the update
|
||||||
|
///
|
||||||
|
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
|
||||||
|
/// request.
|
||||||
|
///
|
||||||
|
/// 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`]: ElectrumExt::full_scan
|
||||||
|
fn sync(
|
||||||
&self,
|
&self,
|
||||||
prev_tip: CheckPoint,
|
prev_tip: CheckPoint,
|
||||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
) -> Result<ElectrumUpdate, Error> {
|
) -> 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 {
|
impl ElectrumExt for Client {
|
||||||
fn scan<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
prev_tip: CheckPoint,
|
prev_tip: CheckPoint,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||||
let mut request_spks = keychain_spks
|
let mut request_spks = keychain_spks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, s)| (k, s.into_iter()))
|
.map(|(k, s)| (k.clone(), s.into_iter()))
|
||||||
.collect::<BTreeMap<K, _>>();
|
.collect::<BTreeMap<K, _>>();
|
||||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||||
|
|
||||||
let txids = txids.into_iter().collect::<Vec<_>>();
|
|
||||||
let outpoints = outpoints.into_iter().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let (electrum_update, keychain_update) = loop {
|
let (electrum_update, keychain_update) = loop {
|
||||||
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
|
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
|
||||||
let mut relevant_txids = RelevantTxids::default();
|
let mut relevant_txids = RelevantTxids::default();
|
||||||
@@ -242,15 +229,6 @@ impl ElectrumExt for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// check for reorgs during scan process
|
||||||
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
|
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
|
||||||
if tip.hash() != server_blockhash {
|
if tip.hash() != server_blockhash {
|
||||||
@@ -284,6 +262,41 @@ impl ElectrumExt for Client {
|
|||||||
|
|
||||||
Ok((electrum_update, keychain_update))
|
Ok((electrum_update, keychain_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync(
|
||||||
|
&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 (mut electrum_update, _) = self.full_scan(
|
||||||
|
prev_tip.clone(),
|
||||||
|
[((), spk_iter)].into(),
|
||||||
|
usize::MAX,
|
||||||
|
batch_size,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (tip, _) = construct_update_tip(self, prev_tip)?;
|
||||||
|
let cps = tip
|
||||||
|
.iter()
|
||||||
|
.take(10)
|
||||||
|
.map(|cp| (cp.height(), cp))
|
||||||
|
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||||
|
|
||||||
|
populate_with_txids(self, &cps, &mut electrum_update.relevant_txids, txids)?;
|
||||||
|
|
||||||
|
let _txs =
|
||||||
|
populate_with_outpoints(self, &cps, &mut electrum_update.relevant_txids, outpoints)?;
|
||||||
|
|
||||||
|
Ok(electrum_update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||||
@@ -405,7 +418,7 @@ fn populate_with_outpoints(
|
|||||||
client: &Client,
|
client: &Client,
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
cps: &BTreeMap<u32, CheckPoint>,
|
||||||
relevant_txids: &mut RelevantTxids,
|
relevant_txids: &mut RelevantTxids,
|
||||||
outpoints: &mut impl Iterator<Item = OutPoint>,
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
) -> Result<HashMap<Txid, Transaction>, Error> {
|
) -> Result<HashMap<Txid, Transaction>, Error> {
|
||||||
let mut full_txs = HashMap::new();
|
let mut full_txs = HashMap::new();
|
||||||
for outpoint in outpoints {
|
for outpoint in outpoints {
|
||||||
@@ -466,7 +479,7 @@ fn populate_with_txids(
|
|||||||
client: &Client,
|
client: &Client,
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
cps: &BTreeMap<u32, CheckPoint>,
|
||||||
relevant_txids: &mut RelevantTxids,
|
relevant_txids: &mut RelevantTxids,
|
||||||
txids: &mut impl Iterator<Item = Txid>,
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
for txid in txids {
|
for txid in txids {
|
||||||
let tx = match client.transaction_get(&txid) {
|
let tx = match client.transaction_get(&txid) {
|
||||||
@@ -477,7 +490,7 @@ fn populate_with_txids(
|
|||||||
|
|
||||||
let spk = tx
|
let spk = tx
|
||||||
.output
|
.output
|
||||||
.get(0)
|
.first()
|
||||||
.map(|txo| &txo.script_pubkey)
|
.map(|txo| &txo.script_pubkey)
|
||||||
.expect("tx must have an output");
|
.expect("tx must have an output");
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
//! This crate is used for updating structures of the [`bdk_chain`] crate with data from electrum.
|
//! This crate is used for updating structures of [`bdk_chain`] with data from an Electrum server.
|
||||||
//!
|
//!
|
||||||
//! The star of the show is the [`ElectrumExt::scan`] method, which scans for relevant blockchain
|
//! The two primary methods are [`ElectrumExt::sync`] and [`ElectrumExt::full_scan`]. In most cases
|
||||||
//! data (via electrum) and outputs updates for [`bdk_chain`] structures as a tuple of form:
|
//! [`ElectrumExt::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. [`ElectrumExt::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`] including [`RelevantTxids`].
|
||||||
//!
|
//!
|
||||||
//! ([`bdk_chain::local_chain::Update`], [`RelevantTxids`], `keychain_update`)
|
//! The [`RelevantTxids`] only includes `txid`s and not full transactions. The caller is responsible
|
||||||
|
//! for obtaining full transactions before applying new data to their [`bdk_chain`]. This can be
|
||||||
|
//! done with these steps:
|
||||||
//!
|
//!
|
||||||
//! An [`RelevantTxids`] only includes `txid`s and no full transactions. The caller is
|
//! 1. Determine which full transactions are missing. Use [`RelevantTxids::missing_full_txs`].
|
||||||
//! 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
|
//! 2. Obtaining the full transactions. To do this via electrum use [`ElectrumApi::batch_transaction_get`].
|
||||||
//! [`RelevantTxids`] can be used.
|
|
||||||
//!
|
//!
|
||||||
//! 2. Obtaining the full transactions. To do this via electrum, the method
|
//! Refer to [`example_electrum`] for a complete example.
|
||||||
//! [`batch_transaction_get`] can be used.
|
|
||||||
//!
|
//!
|
||||||
//! Refer to [`bdk_electrum_example`] for a complete example.
|
//! [`ElectrumApi::batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
|
||||||
//!
|
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
|
||||||
//! [`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)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_esplora"
|
name = "bdk_esplora"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.6.0", default-features = false }
|
bdk_chain = { path = "../chain", version = "0.8.0", default-features = false }
|
||||||
esplora-client = { version = "0.6.0", default-features = false }
|
esplora-client = { version = "0.6.0", default-features = false }
|
||||||
async-trait = { version = "0.1.66", optional = true }
|
async-trait = { version = "0.1.66", optional = true }
|
||||||
futures = { version = "0.3.26", optional = true }
|
futures = { version = "0.3.26", optional = true }
|
||||||
|
|||||||
@@ -36,58 +36,45 @@ pub trait EsploraAsyncExt {
|
|||||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||||
) -> Result<local_chain::Update, Error>;
|
) -> Result<local_chain::Update, Error>;
|
||||||
|
|
||||||
/// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
|
/// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
|
||||||
/// indices.
|
/// returns a [`TxGraph`] and a map of last active indices.
|
||||||
///
|
///
|
||||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
/// * `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
|
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
|
async fn full_scan<K: Ord + Clone + Send>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<
|
keychain_spks: BTreeMap<
|
||||||
K,
|
K,
|
||||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
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,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||||
|
|
||||||
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
|
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||||
|
/// specified and return a [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// [`scan_txs_with_keychains`]: EsploraAsyncExt::scan_txs_with_keychains
|
/// * `misc_spks`: scripts that we want to sync 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
|
||||||
|
///
|
||||||
|
/// 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
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
async fn scan_txs(
|
async fn sync(
|
||||||
&self,
|
&self,
|
||||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, 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(target_arch = "wasm32", async_trait(?Send))]
|
||||||
@@ -199,14 +186,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
|
async fn full_scan<K: Ord + Clone + Send>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<
|
keychain_spks: BTreeMap<
|
||||||
K,
|
K,
|
||||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
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,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||||
@@ -275,6 +260,32 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok((graph, last_active_indexes))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync(
|
||||||
|
&self,
|
||||||
|
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 = self
|
||||||
|
.full_scan(
|
||||||
|
[(
|
||||||
|
(),
|
||||||
|
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();
|
let mut txids = txids.into_iter();
|
||||||
loop {
|
loop {
|
||||||
let handles = txids
|
let handles = txids
|
||||||
@@ -323,7 +334,6 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(graph)
|
||||||
Ok((graph, last_active_indexes))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ use crate::{anchor_from_status, ASSUME_FINAL_DEPTH};
|
|||||||
pub trait EsploraExt {
|
pub trait EsploraExt {
|
||||||
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
|
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
|
||||||
///
|
///
|
||||||
/// * `prev_tip` is the previous tip of [`LocalChain::tip`].
|
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||||
/// * `get_heights` is the block heights that we are interested in fetching from Esplora.
|
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||||
///
|
///
|
||||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||||
///
|
///
|
||||||
@@ -34,54 +34,42 @@ pub trait EsploraExt {
|
|||||||
request_heights: impl IntoIterator<Item = u32>,
|
request_heights: impl IntoIterator<Item = u32>,
|
||||||
) -> Result<local_chain::Update, Error>;
|
) -> Result<local_chain::Update, Error>;
|
||||||
|
|
||||||
/// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
|
/// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
|
||||||
/// indices.
|
/// returns a [`TxGraph`] and a map of last active indices.
|
||||||
///
|
///
|
||||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
/// * `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
|
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||||
/// parallel.
|
/// parallel.
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
fn scan_txs_with_keychains<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||||
|
|
||||||
/// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
|
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||||
|
/// specified and return a [`TxGraph`].
|
||||||
///
|
///
|
||||||
/// [`scan_txs_with_keychains`]: EsploraExt::scan_txs_with_keychains
|
/// * `misc_spks`: scripts that we want to sync 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
|
||||||
|
///
|
||||||
|
/// 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`]: EsploraExt::full_scan
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
fn scan_txs(
|
fn sync(
|
||||||
&self,
|
&self,
|
||||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, 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,
|
|
||||||
)
|
|
||||||
.map(|(g, _)| g)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EsploraExt for esplora_client::BlockingClient {
|
impl EsploraExt for esplora_client::BlockingClient {
|
||||||
@@ -190,11 +178,9 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_txs_with_keychains<K: Ord + Clone>(
|
fn full_scan<K: Ord + Clone>(
|
||||||
&self,
|
&self,
|
||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||||
txids: impl IntoIterator<Item = Txid>,
|
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||||
@@ -266,6 +252,31 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok((graph, last_active_indexes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync(
|
||||||
|
&self,
|
||||||
|
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||||
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
|
parallel_requests: usize,
|
||||||
|
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||||
|
let mut graph = self
|
||||||
|
.full_scan(
|
||||||
|
[(
|
||||||
|
(),
|
||||||
|
misc_spks
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, spk)| (i as u32, spk)),
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
|
usize::MAX,
|
||||||
|
parallel_requests,
|
||||||
|
)
|
||||||
|
.map(|(g, _)| g)?;
|
||||||
|
|
||||||
let mut txids = txids.into_iter();
|
let mut txids = txids.into_iter();
|
||||||
loop {
|
loop {
|
||||||
let handles = txids
|
let handles = txids
|
||||||
@@ -292,7 +303,7 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for op in outpoints.into_iter() {
|
for op in outpoints {
|
||||||
if graph.get_tx(op.txid).is_none() {
|
if graph.get_tx(op.txid).is_none() {
|
||||||
if let Some(tx) = self.get_tx(&op.txid)? {
|
if let Some(tx) = self.get_tx(&op.txid)? {
|
||||||
let _ = graph.insert_tx(tx);
|
let _ = graph.insert_tx(tx);
|
||||||
@@ -317,7 +328,6 @@ impl EsploraExt for esplora_client::BlockingClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(graph)
|
||||||
Ok((graph, last_active_indexes))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![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 bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
||||||
use esplora_client::TxStatus;
|
use esplora_client::TxStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let graph_update = env
|
let graph_update = env
|
||||||
.client
|
.client
|
||||||
.scan_txs(
|
.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -166,28 +166,10 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = env
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
|
||||||
.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!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = env
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
|
||||||
.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!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -209,24 +191,12 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||||
// The last active indice won't be updated in the first case but will in the second one.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = env
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
|
||||||
.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();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = env
|
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
|
||||||
.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();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
|||||||
sleep(Duration::from_millis(10))
|
sleep(Duration::from_millis(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph_update = env.client.scan_txs(
|
let graph_update = env.client.sync(
|
||||||
misc_spks.into_iter(),
|
misc_spks.into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
vec![].into_iter(),
|
vec![].into_iter(),
|
||||||
@@ -164,22 +164,10 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||||
// will.
|
// will.
|
||||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
|
||||||
keychains.clone(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
2,
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
assert!(graph_update.full_txs().next().is_none());
|
assert!(graph_update.full_txs().next().is_none());
|
||||||
assert!(active_indices.is_empty());
|
assert!(active_indices.is_empty());
|
||||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
|
||||||
keychains.clone(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
3,
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
|
|
||||||
@@ -201,24 +189,12 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||||
// The last active indice won't be updated in the first case but will in the second one.
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
|
||||||
keychains.clone(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
4,
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 1);
|
assert_eq!(txs.len(), 1);
|
||||||
assert!(txs.contains(&txid_4th_addr));
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
assert_eq!(active_indices[&0], 3);
|
assert_eq!(active_indices[&0], 3);
|
||||||
let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
|
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
|
||||||
keychains,
|
|
||||||
vec![].into_iter(),
|
|
||||||
vec![].into_iter(),
|
|
||||||
5,
|
|
||||||
1,
|
|
||||||
)?;
|
|
||||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
assert_eq!(txs.len(), 2);
|
assert_eq!(txs.len(), 2);
|
||||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk_file_store"
|
name = "bdk_file_store"
|
||||||
version = "0.2.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
repository = "https://github.com/bitcoindevkit/bdk"
|
||||||
@@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk_chain = { path = "../chain", version = "0.6.0", features = [ "serde", "miniscript" ] }
|
bdk_chain = { path = "../chain", version = "0.8.0", features = [ "serde", "miniscript" ] }
|
||||||
bincode = { version = "1" }
|
bincode = { version = "1" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to open existing [`Store`] file; create it if the file is non-existant.
|
/// Attempt to open existing [`Store`] file; create it if the file is non-existent.
|
||||||
///
|
///
|
||||||
/// Internally, this calls either [`open`] or [`create_new`].
|
/// Internally, this calls either [`open`] or [`create_new`].
|
||||||
///
|
///
|
||||||
|
|||||||
13
crates/hwi/Cargo.toml
Normal file
13
crates/hwi/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "bdk_hwi"
|
||||||
|
version = "0.1.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 = { path = "../bdk" }
|
||||||
|
hwi = { version = "0.7.0", features = [ "miniscript"] }
|
||||||
42
crates/hwi/src/lib.rs
Normal file
42
crates/hwi/src/lib.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//! HWI Signer
|
||||||
|
//!
|
||||||
|
//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be
|
||||||
|
//! used with hardware wallets.
|
||||||
|
//! ```no_run
|
||||||
|
//! # use bdk::bitcoin::Network;
|
||||||
|
//! # use bdk::signer::SignerOrdering;
|
||||||
|
//! # use bdk_hwi::HWISigner;
|
||||||
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
|
//! # use bdk::{FeeRate, 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_no_persist(
|
||||||
|
//! # "",
|
||||||
|
//! # None,
|
||||||
|
//! # 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::signer::TransactionSigner
|
||||||
|
|
||||||
|
mod signer;
|
||||||
|
pub use signer::*;
|
||||||
94
crates/hwi/src/signer.rs
Normal file
94
crates/hwi/src/signer.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use bdk::bitcoin::bip32::Fingerprint;
|
||||||
|
use bdk::bitcoin::psbt::PartiallySignedTransaction;
|
||||||
|
use bdk::bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
|
||||||
|
use hwi::error::Error;
|
||||||
|
use hwi::types::{HWIChain, HWIDevice};
|
||||||
|
use hwi::HWIClient;
|
||||||
|
|
||||||
|
use bdk::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 PartiallySignedTransaction,
|
||||||
|
_sign_options: &bdk::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::tests::get_funded_wallet;
|
||||||
|
// use bdk::signer::SignerOrdering;
|
||||||
|
// use bdk::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::KeychainKind::External,
|
||||||
|
// SignerOrdering(200),
|
||||||
|
// Arc::new(custom_signer),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// let addr = wallet.get_address(bdk::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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -12,7 +12,7 @@ use bdk_bitcoind_rpc::{
|
|||||||
Emitter,
|
Emitter,
|
||||||
};
|
};
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{Block, Transaction},
|
bitcoin::{constants::genesis_block, Block, Transaction},
|
||||||
indexed_tx_graph, keychain,
|
indexed_tx_graph, keychain,
|
||||||
local_chain::{self, CheckPoint, LocalChain},
|
local_chain::{self, CheckPoint, LocalChain},
|
||||||
ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||||
@@ -64,9 +64,6 @@ struct RpcArgs {
|
|||||||
/// Starting block height to fallback to if no point of agreement if found
|
/// Starting block height to fallback to if no point of agreement if found
|
||||||
#[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
|
#[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
|
||||||
fallback_height: u32,
|
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 {
|
impl From<RpcArgs> for Auth {
|
||||||
@@ -120,10 +117,11 @@ fn main() -> anyhow::Result<()> {
|
|||||||
"[{:>10}s] loaded initial changeset from db",
|
"[{:>10}s] loaded initial changeset from db",
|
||||||
start.elapsed().as_secs_f32()
|
start.elapsed().as_secs_f32()
|
||||||
);
|
);
|
||||||
|
let (init_chain_changeset, init_graph_changeset) = init_changeset;
|
||||||
|
|
||||||
let graph = Mutex::new({
|
let graph = Mutex::new({
|
||||||
let mut graph = IndexedTxGraph::new(index);
|
let mut graph = IndexedTxGraph::new(index);
|
||||||
graph.apply_changeset(init_changeset.1);
|
graph.apply_changeset(init_graph_changeset);
|
||||||
graph
|
graph
|
||||||
});
|
});
|
||||||
println!(
|
println!(
|
||||||
@@ -131,7 +129,16 @@ fn main() -> anyhow::Result<()> {
|
|||||||
start.elapsed().as_secs_f32()
|
start.elapsed().as_secs_f32()
|
||||||
);
|
);
|
||||||
|
|
||||||
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
|
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.stage((chain_changeset, Default::default()));
|
||||||
|
db.commit()?;
|
||||||
|
chain
|
||||||
|
} else {
|
||||||
|
LocalChain::from_changeset(init_chain_changeset)?
|
||||||
|
});
|
||||||
println!(
|
println!(
|
||||||
"[{:>10}s] loaded local chain from changeset",
|
"[{:>10}s] loaded local chain from changeset",
|
||||||
start.elapsed().as_secs_f32()
|
start.elapsed().as_secs_f32()
|
||||||
@@ -161,13 +168,9 @@ fn main() -> anyhow::Result<()> {
|
|||||||
match rpc_cmd {
|
match rpc_cmd {
|
||||||
RpcCommands::Sync { rpc_args } => {
|
RpcCommands::Sync { rpc_args } => {
|
||||||
let RpcArgs {
|
let RpcArgs {
|
||||||
fallback_height,
|
fallback_height, ..
|
||||||
lookahead,
|
|
||||||
..
|
|
||||||
} = rpc_args;
|
} = rpc_args;
|
||||||
|
|
||||||
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
|
|
||||||
|
|
||||||
let chain_tip = chain.lock().unwrap().tip();
|
let chain_tip = chain.lock().unwrap().tip();
|
||||||
let rpc_client = rpc_args.new_client()?;
|
let rpc_client = rpc_args.new_client()?;
|
||||||
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
|
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
|
||||||
@@ -233,13 +236,10 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
RpcCommands::Live { rpc_args } => {
|
RpcCommands::Live { rpc_args } => {
|
||||||
let RpcArgs {
|
let RpcArgs {
|
||||||
fallback_height,
|
fallback_height, ..
|
||||||
lookahead,
|
|
||||||
..
|
|
||||||
} = rpc_args;
|
} = rpc_args;
|
||||||
let sigterm_flag = start_ctrlc_handler();
|
let sigterm_flag = start_ctrlc_handler();
|
||||||
|
|
||||||
graph.lock().unwrap().index.set_lookahead_for_all(lookahead);
|
|
||||||
let last_cp = chain.lock().unwrap().tip();
|
let last_cp = chain.lock().unwrap().tip();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -478,14 +478,14 @@ where
|
|||||||
true => Keychain::Internal,
|
true => Keychain::Internal,
|
||||||
false => Keychain::External,
|
false => Keychain::External,
|
||||||
};
|
};
|
||||||
for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
|
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
|
||||||
let address = Address::from_script(spk, network)
|
let address = Address::from_script(spk, network)
|
||||||
.expect("should always be able to derive address");
|
.expect("should always be able to derive address");
|
||||||
println!(
|
println!(
|
||||||
"{:?} {} used:{}",
|
"{:?} {} used:{}",
|
||||||
spk_i,
|
spk_i,
|
||||||
address,
|
address,
|
||||||
index.is_used(&(target_keychain, spk_i))
|
index.is_used(target_keychain, spk_i)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -611,7 +611,7 @@ where
|
|||||||
// We don't want other callers/threads to use this address while we're using it
|
// 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
|
// but we also don't want to scan the tx we just created because it's not
|
||||||
// technically in the blockchain yet.
|
// technically in the blockchain yet.
|
||||||
graph.index.mark_used(&change_keychain, index);
|
graph.index.mark_used(change_keychain, index);
|
||||||
(tx, Some((change_keychain, index)))
|
(tx, Some((change_keychain, index)))
|
||||||
} else {
|
} else {
|
||||||
(tx, None)
|
(tx, None)
|
||||||
@@ -636,7 +636,7 @@ where
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Some((keychain, index)) = change_index {
|
if let Some((keychain, index)) = change_index {
|
||||||
// We failed to broadcast, so allow our change address to be used in the future
|
// 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)
|
Err(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
|
bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid},
|
||||||
indexed_tx_graph::{self, IndexedTxGraph},
|
indexed_tx_graph::{self, IndexedTxGraph},
|
||||||
keychain,
|
keychain,
|
||||||
local_chain::{self, LocalChain},
|
local_chain::{self, LocalChain},
|
||||||
@@ -112,7 +112,12 @@ fn main() -> anyhow::Result<()> {
|
|||||||
graph
|
graph
|
||||||
});
|
});
|
||||||
|
|
||||||
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
|
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 electrum_cmd = match &args.command {
|
let electrum_cmd = match &args.command {
|
||||||
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||||
@@ -150,7 +155,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let keychain_spks = graph
|
let keychain_spks = graph
|
||||||
.index
|
.index
|
||||||
.spks_of_all_keychains()
|
.all_unbounded_spk_iters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(keychain, iter)| {
|
.map(|(keychain, iter)| {
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
@@ -172,14 +177,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
.scan(
|
.full_scan(tip, keychain_spks, stop_gap, scan_options.batch_size)
|
||||||
tip,
|
|
||||||
keychain_spks,
|
|
||||||
core::iter::empty(),
|
|
||||||
core::iter::empty(),
|
|
||||||
stop_gap,
|
|
||||||
scan_options.batch_size,
|
|
||||||
)
|
|
||||||
.context("scanning the blockchain")?
|
.context("scanning the blockchain")?
|
||||||
}
|
}
|
||||||
ElectrumCommands::Sync {
|
ElectrumCommands::Sync {
|
||||||
@@ -208,29 +206,28 @@ fn main() -> anyhow::Result<()> {
|
|||||||
if all_spks {
|
if all_spks {
|
||||||
let all_spks = graph
|
let all_spks = graph
|
||||||
.index
|
.index
|
||||||
.all_spks()
|
.revealed_spks()
|
||||||
.iter()
|
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||||
.map(|(k, v)| (*k, v.clone()))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
|
||||||
eprintln!("scanning {:?}", index);
|
eprintln!("scanning {}:{}", k, i);
|
||||||
script
|
spk
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
if unused_spks {
|
if unused_spks {
|
||||||
let unused_spks = graph
|
let unused_spks = graph
|
||||||
.index
|
.index
|
||||||
.unused_spks(..)
|
.unused_spks()
|
||||||
.map(|(k, v)| (*k, ScriptBuf::from(v)))
|
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Checking if address {} {:?} has been used",
|
"Checking if address {} {}:{} has been used",
|
||||||
Address::from_script(&script, args.network).unwrap(),
|
Address::from_script(&spk, args.network).unwrap(),
|
||||||
index
|
k,
|
||||||
|
i,
|
||||||
);
|
);
|
||||||
|
spk
|
||||||
script
|
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +276,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
drop((graph, chain));
|
drop((graph, chain));
|
||||||
|
|
||||||
let electrum_update = client
|
let electrum_update = client
|
||||||
.scan_without_keychain(tip, spks, txids, outpoints, scan_options.batch_size)
|
.sync(tip, spks, txids, outpoints, scan_options.batch_size)
|
||||||
.context("scanning the blockchain")?;
|
.context("scanning the blockchain")?;
|
||||||
(electrum_update, BTreeMap::new())
|
(electrum_update, BTreeMap::new())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.lock()
|
.lock()
|
||||||
.expect("mutex must not be poisoned")
|
.expect("mutex must not be poisoned")
|
||||||
.index
|
.index
|
||||||
.spks_of_all_keychains()
|
.all_unbounded_spk_iters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// This `map` is purely for logging.
|
// This `map` is purely for logging.
|
||||||
.map(|(keychain, iter)| {
|
.map(|(keychain, iter)| {
|
||||||
@@ -188,13 +188,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// represents the last active spk derivation indices of keychains
|
// represents the last active spk derivation indices of keychains
|
||||||
// (`keychain_indices_update`).
|
// (`keychain_indices_update`).
|
||||||
let (graph_update, last_active_indices) = client
|
let (graph_update, last_active_indices) = client
|
||||||
.scan_txs_with_keychains(
|
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
|
||||||
keychain_spks,
|
|
||||||
core::iter::empty(),
|
|
||||||
core::iter::empty(),
|
|
||||||
*stop_gap,
|
|
||||||
scan_options.parallel_requests,
|
|
||||||
)
|
|
||||||
.context("scanning for transactions")?;
|
.context("scanning for transactions")?;
|
||||||
|
|
||||||
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
||||||
@@ -241,32 +235,32 @@ fn main() -> anyhow::Result<()> {
|
|||||||
if *all_spks {
|
if *all_spks {
|
||||||
let all_spks = graph
|
let all_spks = graph
|
||||||
.index
|
.index
|
||||||
.all_spks()
|
.revealed_spks()
|
||||||
.iter()
|
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||||
.map(|(k, v)| (*k, v.clone()))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
|
||||||
eprintln!("scanning {:?}", index);
|
eprintln!("scanning {}:{}", k, i);
|
||||||
// Flush early to ensure we print at every iteration.
|
// Flush early to ensure we print at every iteration.
|
||||||
let _ = io::stderr().flush();
|
let _ = io::stderr().flush();
|
||||||
script
|
spk
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
if unused_spks {
|
if unused_spks {
|
||||||
let unused_spks = graph
|
let unused_spks = graph
|
||||||
.index
|
.index
|
||||||
.unused_spks(..)
|
.unused_spks()
|
||||||
.map(|(k, v)| (*k, v.to_owned()))
|
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Checking if address {} {:?} has been used",
|
"Checking if address {} {}:{} has been used",
|
||||||
Address::from_script(&script, args.network).unwrap(),
|
Address::from_script(&spk, args.network).unwrap(),
|
||||||
index
|
k,
|
||||||
|
i,
|
||||||
);
|
);
|
||||||
// Flush early to ensure we print at every iteration.
|
// Flush early to ensure we print at every iteration.
|
||||||
let _ = io::stderr().flush();
|
let _ = io::stderr().flush();
|
||||||
script
|
spk
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
if utxos {
|
if utxos {
|
||||||
@@ -312,7 +306,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let graph_update =
|
let graph_update =
|
||||||
client.scan_txs(spks, txids, outpoints, scan_options.parallel_requests)?;
|
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
|
||||||
|
|
||||||
graph.lock().unwrap().apply_update(graph_update)
|
graph.lock().unwrap().apply_update(graph_update)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
let prev_tip = wallet.latest_checkpoint();
|
let prev_tip = wallet.latest_checkpoint();
|
||||||
let keychain_spks = wallet
|
let keychain_spks = wallet
|
||||||
.spks_of_all_keychains()
|
.all_unbounded_spk_iters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, k_spks)| {
|
.map(|(k, k_spks)| {
|
||||||
let mut once = Some(());
|
let mut once = Some(());
|
||||||
@@ -61,7 +61,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
relevant_txids,
|
relevant_txids,
|
||||||
},
|
},
|
||||||
keychain_update,
|
keychain_update,
|
||||||
) = client.scan(prev_tip, keychain_spks, None, None, STOP_GAP, BATCH_SIZE)?;
|
) = client.full_scan(prev_tip, keychain_spks, STOP_GAP, BATCH_SIZE)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
let prev_tip = wallet.latest_checkpoint();
|
let prev_tip = wallet.latest_checkpoint();
|
||||||
let keychain_spks = wallet
|
let keychain_spks = wallet
|
||||||
.spks_of_all_keychains()
|
.all_unbounded_spk_iters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, k_spks)| {
|
.map(|(k, k_spks)| {
|
||||||
let mut once = Some(());
|
let mut once = Some(());
|
||||||
@@ -54,7 +54,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let (update_graph, last_active_indices) = client
|
let (update_graph, last_active_indices) = client
|
||||||
.scan_txs_with_keychains(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)
|
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
|
||||||
.await?;
|
.await?;
|
||||||
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||||
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
|
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
let prev_tip = wallet.latest_checkpoint();
|
let prev_tip = wallet.latest_checkpoint();
|
||||||
let keychain_spks = wallet
|
let keychain_spks = wallet
|
||||||
.spks_of_all_keychains()
|
.all_unbounded_spk_iters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, k_spks)| {
|
.map(|(k, k_spks)| {
|
||||||
let mut once = Some(());
|
let mut once = Some(());
|
||||||
@@ -54,7 +54,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (update_graph, last_active_indices) =
|
let (update_graph, last_active_indices) =
|
||||||
client.scan_txs_with_keychains(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)?;
|
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
|
||||||
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
let missing_heights = update_graph.missing_heights(wallet.local_chain());
|
||||||
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
|
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
|
||||||
let update = Update {
|
let update = Update {
|
||||||
|
|||||||
Reference in New Issue
Block a user