Compare commits
16 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8452f9b8 | ||
|
|
108061dddb | ||
|
|
a2d940132d | ||
|
|
2a055de555 | ||
|
|
096b8ef781 | ||
|
|
2eea0f4e90 | ||
|
|
475c5024ec | ||
|
|
b8aa76cd05 | ||
|
|
0958ff56b2 | ||
|
|
54942a902d | ||
|
|
d975a48e7c | ||
|
|
2f059a1588 | ||
|
|
af15ebba94 | ||
|
|
1b7c6df569 | ||
|
|
7607b49283 | ||
|
|
f6781652b7 |
8
.github/workflows/cont_integration.yml
vendored
8
.github/workflows/cont_integration.yml
vendored
@@ -58,8 +58,8 @@ jobs:
|
||||
working-directory: ./crates/chain
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
- name: Check bdk wallet
|
||||
working-directory: ./crates/wallet
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
|
||||
- name: Check esplora
|
||||
@@ -89,8 +89,8 @@ jobs:
|
||||
target: "wasm32-unknown-unknown"
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
- name: Check bdk wallet
|
||||
working-directory: ./crates/wallet
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
|
||||
4
.github/workflows/nightly_docs.yml
vendored
4
.github/workflows/nightly_docs.yml
vendored
@@ -10,15 +10,13 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2022-12-14
|
||||
run: rustup default nightly-2024-05-12
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Pin dependencies for MSRV
|
||||
run: cargo update -p home --precise "0.5.5"
|
||||
- name: Build docs
|
||||
run: cargo doc --no-deps
|
||||
env:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ Cargo.lock
|
||||
|
||||
# Example persisted files.
|
||||
*.db
|
||||
*.sqlite*
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/bdk",
|
||||
"crates/wallet",
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/sqlite",
|
||||
"crates/electrum",
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
|
||||
24
README.md
24
README.md
@@ -10,11 +10,11 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://docs.rs/bdk_wallet"><img alt="Wallet API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
@@ -22,7 +22,7 @@
|
||||
<h4>
|
||||
<a href="https://bitcoindevkit.org">Project Homepage</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.rs/bdk">Documentation</a>
|
||||
<a href="https://docs.rs/bdk_wallet">Documentation</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
|
||||
|
||||
The project is split up into several crates in the `/crates` directory:
|
||||
|
||||
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`wallet`](./crates/wallet): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`chain`](./crates/chain): Tools for storing and indexing chain data
|
||||
- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet
|
||||
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
|
||||
@@ -47,10 +47,10 @@ The project is split up into several crates in the `/crates` directory:
|
||||
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
|
||||
Fully working examples of how to use these components are in `/example-crates`:
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
|
||||
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk` library.
|
||||
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk` library.
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
|
||||
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
|
||||
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
|
||||
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
|
||||
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
|
||||
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
|
||||
@@ -68,15 +68,9 @@ 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:
|
||||
|
||||
```shell
|
||||
# zip 0.6.3 has MSRV 1.64.0
|
||||
cargo update -p zip --precise "0.6.2"
|
||||
# time 0.3.21 has MSRV 1.65.0
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
# jobserver 0.1.27 has MSRV 1.66.0
|
||||
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"
|
||||
# proptest 1.4.0 has MSRV 1.65.0
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -16,10 +16,10 @@ readme = "README.md"
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.31", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bdk_chain = { path = "../chain", version = "0.14", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.15", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
|
||||
@@ -58,9 +58,6 @@ extern crate alloc;
|
||||
#[cfg(feature = "serde")]
|
||||
pub extern crate serde_crate as serde;
|
||||
|
||||
#[cfg(feature = "bincode")]
|
||||
extern crate bincode;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[macro_use]
|
||||
extern crate std;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.14.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.15.0" }
|
||||
electrum-client = { version = "0.19" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
|
||||
@@ -523,10 +523,10 @@ fn fetch_prev_txout<C: ElectrumApi>(
|
||||
for tx in full_txs {
|
||||
for vin in &tx.input {
|
||||
let outpoint = vin.previous_output;
|
||||
let vout = outpoint.vout;
|
||||
let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?;
|
||||
for txout in prev_tx.output.clone() {
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
let txout = prev_tx.output[vout as usize].clone();
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.15.0", default-features = false }
|
||||
esplora-client = { version = "0.7.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
@@ -22,7 +22,7 @@ bitcoin = { version = "0.31.0", optional = true, default-features = false }
|
||||
miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,8 +12,8 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_persist = { path = "../persist", version = "0.2.0"}
|
||||
bdk_chain = { path = "../chain", version = "0.15.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_persist = { path = "../persist", version = "0.3.0"}
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# BDK File Store
|
||||
|
||||
This is a simple append-only flat file implementation of
|
||||
[`PersistBackend`](bdk_persist::PersistBackend).
|
||||
This is a simple append-only flat file implementation of [`PersistBackend`](bdk_persist::PersistBackend).
|
||||
|
||||
The main structure is [`Store`](crate::Store), which can be used with [`bdk`]'s
|
||||
`Wallet` to persist wallet data into a flat file.
|
||||
The main structure is [`Store`] which works with any [`bdk_chain`] based changesets to persist data into a flat file.
|
||||
|
||||
[`bdk`]: https://docs.rs/bdk/latest
|
||||
[`bdk_persist`]: https://docs.rs/bdk_persist/latest
|
||||
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
|
||||
|
||||
@@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../bdk" }
|
||||
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.12" }
|
||||
hwi = { version = "0.8.0", features = [ "miniscript"] }
|
||||
|
||||
3
crates/hwi/README.md
Normal file
3
crates/hwi/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# BDK HWI Signer
|
||||
|
||||
This crate contains `HWISigner`, an implementation of a `TransactionSigner` to be used with hardware wallets.
|
||||
@@ -3,10 +3,10 @@
|
||||
//! 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_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
@@ -35,7 +35,7 @@
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`TransactionSigner`]: bdk::wallet::signer::TransactionSigner
|
||||
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
|
||||
|
||||
mod signer;
|
||||
pub use signer::*;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use bdk::bitcoin::bip32::Fingerprint;
|
||||
use bdk::bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bdk::bitcoin::Psbt;
|
||||
use bdk_wallet::bitcoin::bip32::Fingerprint;
|
||||
use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bdk_wallet::bitcoin::Psbt;
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
use hwi::HWIClient;
|
||||
|
||||
use bdk::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||
use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Custom signer for Hardware Wallets
|
||||
@@ -38,7 +38,7 @@ impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
_sign_options: &bdk::SignOptions,
|
||||
_sign_options: &bdk_wallet::SignOptions,
|
||||
_secp: &Secp256k1<All>,
|
||||
) -> Result<(), SignerError> {
|
||||
psbt.combine(
|
||||
@@ -61,9 +61,9 @@ impl TransactionSigner for HWISigner {
|
||||
// fn test_hardware_signer() {
|
||||
// use std::sync::Arc;
|
||||
//
|
||||
// use bdk::tests::get_funded_wallet;
|
||||
// use bdk::signer::SignerOrdering;
|
||||
// use bdk::bitcoin::Network;
|
||||
// use bdk_wallet::tests::get_funded_wallet;
|
||||
// use bdk_wallet::signer::SignerOrdering;
|
||||
// use bdk_wallet::bitcoin::Network;
|
||||
// use crate::HWISigner;
|
||||
// use hwi::HWIClient;
|
||||
//
|
||||
@@ -78,12 +78,12 @@ impl TransactionSigner for HWISigner {
|
||||
//
|
||||
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
|
||||
// wallet.add_signer(
|
||||
// bdk::KeychainKind::External,
|
||||
// bdk_wallet::KeychainKind::External,
|
||||
// SignerOrdering(200),
|
||||
// Arc::new(custom_signer),
|
||||
// );
|
||||
//
|
||||
// let addr = wallet.get_address(bdk::wallet::AddressIndex::LastUnused);
|
||||
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
|
||||
// let mut builder = wallet.build_tx();
|
||||
// builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
// let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk_persist"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_persist"
|
||||
description = "Types that define data persistence of a BDK wallet"
|
||||
@@ -14,9 +14,9 @@ rust-version = "1.63"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.15.0", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["bdk_chain/std"]
|
||||
|
||||
|
||||
default = ["bdk_chain/std", "miniscript"]
|
||||
serde = ["bdk_chain/serde"]
|
||||
miniscript = ["bdk_chain/miniscript"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# BDK Persist
|
||||
|
||||
This crate is home to the [`PersistBackend`](crate::PersistBackend) trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures. The [`Persist`](crate::Persist) type provides a convenient wrapper around a `PersistBackend` that allows staging changes before committing them.
|
||||
This crate is home to the [`PersistBackend`] trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures.
|
||||
|
||||
The [`Persist`] type provides a convenient wrapper around a [`PersistBackend`] that allows staging changes before committing them.
|
||||
|
||||
73
crates/persist/src/changeset.rs
Normal file
73
crates/persist/src/changeset.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
use bdk_chain::{bitcoin::Network, indexed_tx_graph, keychain, local_chain, Anchor, Append};
|
||||
|
||||
/// Changes from a combination of [`bdk_chain`] structures.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(bdk_chain::serde::Deserialize, bdk_chain::serde::Serialize),
|
||||
serde(
|
||||
crate = "bdk_chain::serde",
|
||||
bound(
|
||||
deserialize = "A: Ord + bdk_chain::serde::Deserialize<'de>, K: Ord + bdk_chain::serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + bdk_chain::serde::Serialize, K: Ord + bdk_chain::serde::Serialize",
|
||||
),
|
||||
)
|
||||
)]
|
||||
pub struct CombinedChangeSet<K, A> {
|
||||
/// Changes to the [`LocalChain`](local_chain::LocalChain).
|
||||
pub chain: local_chain::ChangeSet,
|
||||
/// Changes to [`IndexedTxGraph`](indexed_tx_graph::IndexedTxGraph).
|
||||
pub indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
/// Stores the network type of the transaction data.
|
||||
pub network: Option<Network>,
|
||||
}
|
||||
|
||||
impl<K, A> Default for CombinedChangeSet<K, A> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chain: Default::default(),
|
||||
indexed_tx_graph: Default::default(),
|
||||
network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord, A: Anchor> Append for CombinedChangeSet<K, A> {
|
||||
fn append(&mut self, other: Self) {
|
||||
Append::append(&mut self.chain, other.chain);
|
||||
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network type must either be just introduced or remain the same"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> From<local_chain::ChangeSet> for CombinedChangeSet<K, A> {
|
||||
fn from(chain: local_chain::ChangeSet) -> Self {
|
||||
Self {
|
||||
chain,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> From<indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>>
|
||||
for CombinedChangeSet<K, A>
|
||||
{
|
||||
fn from(indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![no_std]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod changeset;
|
||||
mod persist;
|
||||
pub use changeset::*;
|
||||
pub use persist::*;
|
||||
|
||||
19
crates/sqlite/Cargo.toml
Normal file
19
crates/sqlite/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "bdk_sqlite"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_sqlite"
|
||||
description = "A simple SQLite based implementation of Persist for Bitcoin Dev Kit."
|
||||
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.15.0", features = ["serde", "miniscript"] }
|
||||
bdk_persist = { path = "../persist", version = "0.3.0", features = ["serde"] }
|
||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
8
crates/sqlite/README.md
Normal file
8
crates/sqlite/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# BDK SQLite
|
||||
|
||||
This is a simple [SQLite] relational database schema backed implementation of [`PersistBackend`](bdk_persist::PersistBackend).
|
||||
|
||||
The main structure is `Store` which persists [`bdk_persist`] `CombinedChangeSet` data into a SQLite database file.
|
||||
|
||||
[`bdk_persist`]:https://docs.rs/bdk_persist/latest/bdk_persist/
|
||||
[SQLite]: https://www.sqlite.org/index.html
|
||||
69
crates/sqlite/schema/schema_0.sql
Normal file
69
crates/sqlite/schema/schema_0.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- schema version control
|
||||
CREATE TABLE version
|
||||
(
|
||||
version INTEGER
|
||||
) STRICT;
|
||||
INSERT INTO version
|
||||
VALUES (1);
|
||||
|
||||
-- network is the valid network for all other table data
|
||||
CREATE TABLE network
|
||||
(
|
||||
name TEXT UNIQUE NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- keychain is the json serialized keychain structure as JSONB,
|
||||
-- descriptor is the complete descriptor string,
|
||||
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
|
||||
-- last revealed index is a u32
|
||||
CREATE TABLE keychain
|
||||
(
|
||||
keychain BLOB PRIMARY KEY NOT NULL,
|
||||
descriptor TEXT NOT NULL,
|
||||
descriptor_id BLOB NOT NULL,
|
||||
last_revealed INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- hash is block hash hex string,
|
||||
-- block height is a u32,
|
||||
CREATE TABLE block
|
||||
(
|
||||
hash TEXT PRIMARY KEY NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
-- whole_tx is a consensus encoded transaction,
|
||||
-- last seen is a u64 unix epoch seconds
|
||||
CREATE TABLE tx
|
||||
(
|
||||
txid TEXT PRIMARY KEY NOT NULL,
|
||||
whole_tx BLOB,
|
||||
last_seen INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- Outpoint txid hash hex string (reversed)
|
||||
-- Outpoint vout
|
||||
-- TxOut value as SATs
|
||||
-- TxOut script consensus encoded
|
||||
CREATE TABLE txout
|
||||
(
|
||||
txid TEXT NOT NULL,
|
||||
vout INTEGER NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
script BLOB NOT NULL,
|
||||
PRIMARY KEY (txid, vout)
|
||||
) STRICT;
|
||||
|
||||
-- join table between anchor and tx
|
||||
-- block hash hex string
|
||||
-- anchor is a json serialized Anchor structure as JSONB,
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
CREATE TABLE anchor_tx
|
||||
(
|
||||
block_hash TEXT NOT NULL,
|
||||
anchor BLOB NOT NULL,
|
||||
txid TEXT NOT NULL REFERENCES tx (txid),
|
||||
UNIQUE (anchor, txid),
|
||||
FOREIGN KEY (block_hash) REFERENCES block(hash)
|
||||
) STRICT;
|
||||
34
crates/sqlite/src/lib.rs
Normal file
34
crates/sqlite/src/lib.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod schema;
|
||||
mod store;
|
||||
|
||||
use bdk_chain::bitcoin::Network;
|
||||
pub use rusqlite;
|
||||
pub use store::Store;
|
||||
|
||||
/// Error that occurs while reading or writing change sets with the SQLite database.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid network, cannot change the one already stored in the database.
|
||||
Network { expected: Network, given: Network },
|
||||
/// SQLite error.
|
||||
Sqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Network { expected, given } => write!(
|
||||
f,
|
||||
"network error trying to read or write change set, expected {}, given {}",
|
||||
expected, given
|
||||
),
|
||||
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
96
crates/sqlite/src/schema.rs
Normal file
96
crates/sqlite/src/schema.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::Store;
|
||||
use rusqlite::{named_params, Connection, Error};
|
||||
|
||||
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
|
||||
const MIGRATIONS: &[&str] = &[SCHEMA_0];
|
||||
|
||||
/// Schema migration related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Migrate sqlite db schema to latest version.
|
||||
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
|
||||
let stmts = &MIGRATIONS
|
||||
.iter()
|
||||
.flat_map(|stmt| {
|
||||
// remove comment lines
|
||||
let s = stmt
|
||||
.split('\n')
|
||||
.filter(|l| !l.starts_with("--") && !l.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
// split into statements
|
||||
s.split(';')
|
||||
// remove extra spaces
|
||||
.map(|s| {
|
||||
s.trim()
|
||||
.split(' ')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
// remove empty statements
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let version = Self::get_schema_version(conn)?;
|
||||
let stmts = &stmts[(version as usize)..];
|
||||
|
||||
// begin transaction, all migration statements and new schema version commit or rollback
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// execute every statement and return `Some` new schema version
|
||||
// if execution fails, return `Error::Rusqlite`
|
||||
// if no statements executed returns `None`
|
||||
let new_version = stmts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|version_stmt| {
|
||||
tx.execute(version_stmt.1.as_str(), [])
|
||||
// map result value to next migration version
|
||||
.map(|_| version_stmt.0 as i32 + version + 1)
|
||||
})
|
||||
.last()
|
||||
.transpose()?;
|
||||
|
||||
// if `Some` new statement version, set new schema version
|
||||
if let Some(version) = new_version {
|
||||
Self::set_schema_version(&tx, version)?;
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
|
||||
let statement = conn.prepare_cached("SELECT version FROM version");
|
||||
match statement {
|
||||
Err(Error::SqliteFailure(e, Some(msg))) => {
|
||||
if msg == "no such table: version" {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(Error::SqliteFailure(e, Some(msg)))
|
||||
}
|
||||
}
|
||||
Ok(mut stmt) => {
|
||||
let mut rows = stmt.query([])?;
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let version: i32 = row.get(0)?;
|
||||
Ok(version)
|
||||
}
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
_ => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
|
||||
conn.execute(
|
||||
"UPDATE version SET version=:version",
|
||||
named_params! {":version": version},
|
||||
)
|
||||
}
|
||||
}
|
||||
779
crates/sqlite/src/store.rs
Normal file
779
crates/sqlite/src/store.rs
Normal file
@@ -0,0 +1,779 @@
|
||||
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
|
||||
use bdk_chain::bitcoin::hashes::Hash;
|
||||
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use bdk_chain::bitcoin::{BlockHash, Txid};
|
||||
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
use rusqlite::{named_params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::Error;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
|
||||
};
|
||||
use bdk_persist::CombinedChangeSet;
|
||||
|
||||
/// Persists data in to a relational schema based [SQLite] database file.
|
||||
///
|
||||
/// The changesets loaded or stored represent changes to keychain and blockchain data.
|
||||
///
|
||||
/// [SQLite]: https://www.sqlite.org/index.html
|
||||
pub struct Store<K, A> {
|
||||
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
|
||||
conn: Mutex<Connection>,
|
||||
keychain_marker: PhantomData<K>,
|
||||
anchor_marker: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<K, A> Debug for Store<K, A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(&self.conn, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Creates a new store from a [`Connection`].
|
||||
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
|
||||
Self::migrate(&mut conn)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
keychain_marker: Default::default(),
|
||||
anchor_marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
|
||||
let connection = self.conn.get_mut().expect("unlocked connection mutex");
|
||||
connection.transaction().map_err(Error::Sqlite)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A, C> bdk_persist::PersistBackend<C> for Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
C: Clone + From<CombinedChangeSet<K, A>> + Into<CombinedChangeSet<K, A>>,
|
||||
{
|
||||
fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()> {
|
||||
self.write(&changeset.clone().into())
|
||||
.map_err(|e| anyhow::anyhow!(e).context("unable to write changes to sqlite database"))
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> anyhow::Result<Option<C>> {
|
||||
self.read()
|
||||
.map(|c| c.map(Into::into))
|
||||
.map_err(|e| anyhow::anyhow!(e).context("unable to read changes from sqlite database"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Network table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert [`Network`] for which all other tables data is valid.
|
||||
///
|
||||
/// Error if trying to insert different network value.
|
||||
fn insert_network(
|
||||
current_network: &Option<Network>,
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
network_changeset: &Option<Network>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(network) = network_changeset {
|
||||
match current_network {
|
||||
// if no network change do nothing
|
||||
Some(current_network) if current_network == network => Ok(()),
|
||||
// if new network not the same as current, error
|
||||
Some(current_network) => Err(Error::Network {
|
||||
expected: *current_network,
|
||||
given: *network,
|
||||
}),
|
||||
// insert network if none exists
|
||||
None => {
|
||||
let insert_network_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
|
||||
.expect("insert network statement");
|
||||
let name = network.to_string();
|
||||
insert_network_stmt
|
||||
.execute(named_params! {":name": name })
|
||||
.map_err(Error::Sqlite)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the valid [`Network`] for this database, or `None` if not set.
|
||||
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
|
||||
let mut select_network_stmt = db_transaction
|
||||
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
|
||||
.expect("select network statement");
|
||||
|
||||
let network = select_network_stmt
|
||||
.query_row([], |row| {
|
||||
let network = row.get_unwrap::<usize, String>(0);
|
||||
let network = Network::from_str(network.as_str()).expect("valid network");
|
||||
Ok(network)
|
||||
})
|
||||
.map_err(Error::Sqlite);
|
||||
match network {
|
||||
Ok(network) => Ok(Some(network)),
|
||||
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert or delete local chain blocks.
|
||||
///
|
||||
/// Error if trying to insert existing block hash.
|
||||
fn insert_or_delete_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
chain_changeset: &local_chain::ChangeSet,
|
||||
) -> Result<(), Error> {
|
||||
for (height, hash) in chain_changeset.iter() {
|
||||
match hash {
|
||||
// add new hash at height
|
||||
Some(hash) => {
|
||||
let insert_block_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
|
||||
.expect("insert block statement");
|
||||
let hash = hash.to_string();
|
||||
insert_block_stmt
|
||||
.execute(named_params! {":hash": hash, ":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
// delete block at height
|
||||
None => {
|
||||
let delete_block_stmt = &mut db_transaction
|
||||
.prepare_cached("DELETE FROM block WHERE height IS :height")
|
||||
.expect("delete block statement");
|
||||
delete_block_stmt
|
||||
.execute(named_params! {":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all blocks.
|
||||
fn select_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
|
||||
let mut select_blocks_stmt = db_transaction
|
||||
.prepare_cached("SELECT height, hash FROM block")
|
||||
.expect("select blocks statement");
|
||||
|
||||
let blocks = select_blocks_stmt
|
||||
.query_map([], |row| {
|
||||
let height = row.get_unwrap::<usize, u32>(0);
|
||||
let hash = row.get_unwrap::<usize, String>(1);
|
||||
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
|
||||
Ok((height, hash))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Keychain table related functions.
|
||||
///
|
||||
/// The keychain objects are stored as [`JSONB`] data.
|
||||
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + Send,
|
||||
{
|
||||
/// Insert keychain with descriptor and last active index.
|
||||
///
|
||||
/// If keychain exists only update last active index.
|
||||
fn insert_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
|
||||
let insert_keychain_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
|
||||
.expect("insert keychain statement");
|
||||
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
|
||||
let descriptor_id = descriptor.descriptor_id().to_byte_array();
|
||||
let descriptor = descriptor.to_string();
|
||||
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update descriptor last revealed index.
|
||||
fn update_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
|
||||
let update_last_revealed_stmt = &mut db_transaction
|
||||
.prepare_cached(
|
||||
"UPDATE keychain SET last_revealed = :last_revealed
|
||||
WHERE descriptor_id = :descriptor_id",
|
||||
)
|
||||
.expect("update last revealed statement");
|
||||
let descriptor_id = descriptor_id.to_byte_array();
|
||||
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select keychains added.
|
||||
fn select_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
|
||||
let mut select_keychains_added_stmt = db_transaction
|
||||
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
|
||||
.expect("select keychains statement");
|
||||
|
||||
let keychains = select_keychains_added_stmt
|
||||
.query_map([], |row| {
|
||||
let keychain = row.get_unwrap::<usize, String>(0);
|
||||
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
|
||||
let descriptor = row.get_unwrap::<usize, String>(1);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
Ok((keychain, descriptor))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
keychains
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select descriptor last revealed indexes.
|
||||
fn select_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
|
||||
let mut select_last_revealed_stmt = db_transaction
|
||||
.prepare_cached(
|
||||
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
|
||||
)
|
||||
.expect("select last revealed statement");
|
||||
|
||||
let last_revealed = select_last_revealed_stmt
|
||||
.query_map([], |row| {
|
||||
let descriptor = row.get_unwrap::<usize, String>(0);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
let descriptor_id = descriptor.descriptor_id();
|
||||
let last_revealed = row.get_unwrap::<usize, u32>(1);
|
||||
Ok((descriptor_id, last_revealed))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_revealed
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tx (transaction) and txout (transaction output) table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert transactions.
|
||||
///
|
||||
/// Error if trying to insert existing txid.
|
||||
fn insert_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx in tx_graph_changeset.graph.txs.iter() {
|
||||
let insert_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
|
||||
.expect("insert or update tx whole_tx statement");
|
||||
let txid = tx.txid().to_string();
|
||||
let whole_tx = serialize(&tx);
|
||||
insert_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transactions.
|
||||
fn select_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
|
||||
let mut select_tx_stmt = db_transaction
|
||||
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
|
||||
.expect("select tx statement");
|
||||
|
||||
let txs = select_tx_stmt
|
||||
.query_map([], |row| {
|
||||
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
|
||||
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
|
||||
Ok(Arc::new(whole_tx))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
|
||||
txs.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select all transactions with last_seen values.
|
||||
fn select_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<Txid, u64>, Error> {
|
||||
// load tx last_seen
|
||||
let mut select_last_seen_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
|
||||
.expect("select tx last seen statement");
|
||||
|
||||
let last_seen = select_last_seen_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let last_seen = row.get_unwrap::<usize, u64>(1);
|
||||
Ok((txid, last_seen))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_seen
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Insert txouts.
|
||||
///
|
||||
/// Error if trying to insert existing outpoint.
|
||||
fn insert_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for txout in tx_graph_changeset.graph.txouts.iter() {
|
||||
let insert_txout_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
|
||||
.expect("insert txout statement");
|
||||
let txid = txout.0.txid.to_string();
|
||||
let vout = txout.0.vout;
|
||||
let value = txout.1.value.to_sat();
|
||||
let script = txout.1.script_pubkey.as_bytes();
|
||||
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transaction outputs.
|
||||
fn select_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
|
||||
// load tx outs
|
||||
let mut select_txout_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, vout, value, script FROM txout")
|
||||
.expect("select txout statement");
|
||||
|
||||
let txouts = select_txout_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let vout = row.get_unwrap::<usize, u32>(1);
|
||||
let outpoint = OutPoint::new(txid, vout);
|
||||
let value = row.get_unwrap::<usize, u64>(2);
|
||||
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
|
||||
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
|
||||
let txout = TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
script_pubkey,
|
||||
};
|
||||
Ok((outpoint, txout))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
txouts
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update transaction last seen times.
|
||||
fn update_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
|
||||
let insert_or_update_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
|
||||
.expect("insert or update tx last_seen statement");
|
||||
let txid = tx_last_seen.0.to_string();
|
||||
let last_seen = *tx_last_seen.1;
|
||||
insert_or_update_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Anchor table related functions.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Insert anchors.
|
||||
fn insert_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
// serde_json::to_string
|
||||
for anchor in tx_graph_changeset.graph.anchors.iter() {
|
||||
let insert_anchor_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
|
||||
.expect("insert anchor statement");
|
||||
let block_hash = anchor.0.anchor_block().hash.to_string();
|
||||
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
|
||||
let txid = anchor.1.to_string();
|
||||
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all anchors.
|
||||
fn select_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<(A, Txid)>, Error> {
|
||||
// serde_json::from_str
|
||||
let mut select_anchor_stmt = db_transaction
|
||||
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
|
||||
.expect("select anchor statement");
|
||||
let anchors = select_anchor_stmt
|
||||
.query_map([], |row| {
|
||||
let hash = row.get_unwrap::<usize, String>(0);
|
||||
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
|
||||
let anchor = row.get_unwrap::<usize, String>(1);
|
||||
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
|
||||
// double check anchor blob block hash matches
|
||||
assert_eq!(hash, anchor.anchor_block().hash);
|
||||
let txid = row.get_unwrap::<usize, String>(2);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
Ok((anchor, txid))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
anchors
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to read and write all [`ChangeSet`] data.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
|
||||
// no need to write anything if changeset is empty
|
||||
if changeset.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network_changeset = &changeset.network;
|
||||
let current_network = Self::select_network(&db_transaction)?;
|
||||
Self::insert_network(¤t_network, &db_transaction, network_changeset)?;
|
||||
|
||||
let chain_changeset = &changeset.chain;
|
||||
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
|
||||
|
||||
let tx_graph_changeset = &changeset.indexed_tx_graph;
|
||||
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
|
||||
db_transaction.commit().map_err(Error::Sqlite)
|
||||
}
|
||||
|
||||
fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network = Self::select_network(&db_transaction)?;
|
||||
let chain = Self::select_blocks(&db_transaction)?;
|
||||
let keychains_added = Self::select_keychains(&db_transaction)?;
|
||||
let last_revealed = Self::select_last_revealed(&db_transaction)?;
|
||||
let txs = Self::select_txs(&db_transaction)?;
|
||||
let last_seen = Self::select_last_seen(&db_transaction)?;
|
||||
let txouts = Self::select_txouts(&db_transaction)?;
|
||||
let anchors = Self::select_anchors(&db_transaction)?;
|
||||
|
||||
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
|
||||
txs,
|
||||
txouts,
|
||||
anchors,
|
||||
last_seen,
|
||||
};
|
||||
|
||||
let indexer: keychain::ChangeSet<K> = keychain::ChangeSet {
|
||||
keychains_added,
|
||||
last_revealed,
|
||||
};
|
||||
|
||||
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>> =
|
||||
indexed_tx_graph::ChangeSet { graph, indexer };
|
||||
|
||||
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(CombinedChangeSet {
|
||||
chain,
|
||||
indexed_tx_graph,
|
||||
network,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::store::Append;
|
||||
use bdk_chain::bitcoin::consensus::encode::deserialize;
|
||||
use bdk_chain::bitcoin::constants::genesis_block;
|
||||
use bdk_chain::bitcoin::hashes::hex::FromHex;
|
||||
use bdk_chain::bitcoin::transaction::Transaction;
|
||||
use bdk_chain::bitcoin::Network::Testnet;
|
||||
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
|
||||
use bdk_chain::miniscript::Descriptor;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
|
||||
ConfirmationTimeHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use bdk_persist::PersistBackend;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
|
||||
enum Keychain {
|
||||
External { account: u32, name: String },
|
||||
Internal { account: u32, name: String },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor(
|
||||
) -> anyhow::Result<()> {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
|
||||
confirmation_height: height,
|
||||
confirmation_time: time,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write_changes(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() -> anyhow::Result<()>
|
||||
{
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
|
||||
confirmation_height: height,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write_changes(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_blockid_anchor() -> anyhow::Result<()> {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write_changes(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_test_changesets<A: Anchor + Copy>(
|
||||
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
|
||||
) -> (
|
||||
Vec<CombinedChangeSet<Keychain, A>>,
|
||||
CombinedChangeSet<Keychain, A>,
|
||||
) {
|
||||
let secp = &secp256k1::Secp256k1::signing_only();
|
||||
|
||||
let network_changeset = Some(Testnet);
|
||||
|
||||
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
|
||||
let block_hash_1 =
|
||||
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
|
||||
.unwrap();
|
||||
let block_hash_2 =
|
||||
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
|
||||
.unwrap();
|
||||
|
||||
let block_changeset = [
|
||||
(0, Some(block_hash_0)),
|
||||
(1, Some(block_hash_1)),
|
||||
(2, Some(block_hash_2)),
|
||||
]
|
||||
.into();
|
||||
|
||||
let ext_keychain = Keychain::External {
|
||||
account: 0,
|
||||
name: "ext test".to_string(),
|
||||
};
|
||||
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
|
||||
let ext_desc_id = ext_desc.descriptor_id();
|
||||
let int_keychain = Keychain::Internal {
|
||||
account: 0,
|
||||
name: "int test".to_string(),
|
||||
};
|
||||
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
|
||||
let int_desc_id = int_desc.descriptor_id();
|
||||
|
||||
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
|
||||
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
|
||||
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
|
||||
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
|
||||
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
|
||||
|
||||
let outpoint0_0 = OutPoint::new(tx0.txid(), 0);
|
||||
let txout0_0 = tx0.output.first().unwrap().clone();
|
||||
let outpoint1_0 = OutPoint::new(tx1.txid(), 0);
|
||||
let txout1_0 = tx1.output.first().unwrap().clone();
|
||||
|
||||
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
|
||||
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
|
||||
|
||||
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx0.clone(), tx1.clone()].into(),
|
||||
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
|
||||
anchors: [(anchor1, tx0.txid()), (anchor1, tx1.txid())].into(),
|
||||
last_seen: [
|
||||
(tx0.txid(), 1598918400),
|
||||
(tx1.txid(), 1598919121),
|
||||
(tx2.txid(), 1608919121),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
|
||||
let keychain_changeset = keychain::ChangeSet {
|
||||
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
|
||||
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset,
|
||||
indexer: keychain_changeset,
|
||||
};
|
||||
|
||||
// test changesets to write to db
|
||||
let mut changesets = Vec::new();
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: block_changeset,
|
||||
indexed_tx_graph: graph_changeset,
|
||||
network: network_changeset,
|
||||
});
|
||||
|
||||
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
|
||||
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx2.clone()].into(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: BTreeSet::default(),
|
||||
last_seen: [(tx2.txid(), 1708919121)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset2,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset2,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// create changeset that adds a new anchor2 for tx0 and tx1
|
||||
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
|
||||
txs: BTreeSet::default(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: [(anchor2, tx0.txid()), (anchor2, tx1.txid())].into(),
|
||||
last_seen: BTreeMap::default(),
|
||||
};
|
||||
|
||||
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset3,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset3,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// aggregated test changesets
|
||||
let agg_test_changesets =
|
||||
changesets
|
||||
.iter()
|
||||
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
|
||||
i.append(cs.clone());
|
||||
i
|
||||
});
|
||||
|
||||
(changesets, agg_test_changesets)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -13,7 +13,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.14", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.15", default-features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
name = "bdk_wallet"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.11"
|
||||
version = "1.0.0-alpha.12"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -19,8 +19,8 @@ miniscript = { version = "11.0.0", features = ["serde"], default-features = fals
|
||||
bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.14.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_persist = { path = "../persist", version = "0.2.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.15.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_persist = { path = "../persist", version = "0.3.0", features = ["miniscript", "serde"], default-features = false }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
@@ -45,6 +45,7 @@ dev-getrandom-wasm = ["getrandom/js"]
|
||||
lazy_static = "1.4"
|
||||
assert_matches = "1.5.0"
|
||||
tempfile = "3"
|
||||
bdk_sqlite = { path = "../sqlite" }
|
||||
bdk_file_store = { path = "../file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://docs.rs/bdk_wallet"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
@@ -20,13 +20,13 @@
|
||||
<h4>
|
||||
<a href="https://bitcoindevkit.org">Project Homepage</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.rs/bdk">Documentation</a>
|
||||
<a href="https://docs.rs/bdk_wallet">Documentation</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
## `bdk`
|
||||
# BDK Wallet
|
||||
|
||||
The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
|
||||
The `bdk_wallet` crate provides the [`Wallet`] type which is a simple, high-level
|
||||
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
||||
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
||||
construct a wallet. It has two keychains (external and internal) which are defined by
|
||||
@@ -36,7 +36,7 @@ can create and sign transactions.
|
||||
|
||||
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
|
||||
|
||||
### Blockchain data
|
||||
## Blockchain data
|
||||
|
||||
In order to get blockchain data for `Wallet` to consume, you should configure a client from
|
||||
an available chain source. Typically you make a request to the chain source and get a response
|
||||
@@ -55,7 +55,7 @@ that the `Wallet` can use to update its view of the chain.
|
||||
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
||||
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
|
||||
|
||||
### Persistence
|
||||
## Persistence
|
||||
|
||||
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
|
||||
|
||||
@@ -67,7 +67,7 @@ To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`
|
||||
|
||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||
```rust,compile_fail
|
||||
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
||||
use bdk_wallet::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
||||
|
||||
fn main() {
|
||||
// Create a new file `Store`.
|
||||
@@ -85,13 +85,13 @@ fn main() {
|
||||
<!-- ### Sync the balance of a descriptor -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk::SyncOptions; -->
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
<!-- use bdk_wallet::Wallet; -->
|
||||
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk_wallet::SyncOptions; -->
|
||||
<!-- use bdk_wallet::electrum_client::Client; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
@@ -109,11 +109,11 @@ fn main() {
|
||||
<!-- ### Generate a few addresses -->
|
||||
|
||||
<!-- ```rust -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
<!-- use bdk_wallet::Wallet; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
@@ -131,17 +131,17 @@ fn main() {
|
||||
<!-- ### Create a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk_wallet::{FeeRate, Wallet, SyncOptions}; -->
|
||||
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
|
||||
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::electrum_client::Client; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk::bitcoin::consensus::serialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
@@ -172,13 +172,13 @@ fn main() {
|
||||
<!-- ### Sign a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::{Wallet, SignOptions}; -->
|
||||
<!-- use bdk_wallet::{Wallet, SignOptions}; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk::bitcoin::consensus::deserialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
<!-- use bdk_wallet::bitcoin::consensus::deserialize; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
|
||||
@@ -202,7 +202,7 @@ fn main() {
|
||||
cargo test
|
||||
```
|
||||
|
||||
## License
|
||||
# License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
@@ -211,15 +211,15 @@ Licensed under either of
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
# Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_persist/latest/bdk_persist/trait.PersistBackend.html
|
||||
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||
@@ -9,7 +9,7 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bdk_wallet;
|
||||
extern crate bitcoin;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
@@ -21,7 +21,7 @@ use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
use bdk_wallet::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
@@ -7,14 +7,14 @@
|
||||
// licenses.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bdk::bitcoin::bip32::DerivationPath;
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor;
|
||||
use bdk::descriptor::IntoWalletDescriptor;
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::Tap;
|
||||
use bdk_wallet::bitcoin::bip32::DerivationPath;
|
||||
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::descriptor;
|
||||
use bdk_wallet::descriptor::IntoWalletDescriptor;
|
||||
use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk_wallet::miniscript::Tap;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates how to generate a mnemonic phrase
|
||||
@@ -9,14 +9,14 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bdk_wallet;
|
||||
use std::error::Error;
|
||||
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk::wallet::signer::SignersContainer;
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk_wallet::wallet::signer::SignersContainer;
|
||||
|
||||
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
|
||||
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
|
||||
///
|
||||
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
|
||||
/// This is useful to express complex miniscript spending conditions into more human readable form.
|
||||
@@ -34,11 +34,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
|
||||
// Use the descriptor string to derive the full descriptor and a keymap.
|
||||
// The wallet descriptor can be used to create a new bdk::wallet.
|
||||
// The wallet descriptor can be used to create a new bdk_wallet::wallet.
|
||||
// While the `keymap` can be used to create a `SignerContainer`.
|
||||
//
|
||||
// The `SignerContainer` can sign for `PSBT`s.
|
||||
// a bdk::wallet internally uses these to handle transaction signing.
|
||||
// a bdk_wallet::wallet internally uses these to handle transaction signing.
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
@@ -423,7 +423,7 @@ macro_rules! apply_modifier {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
|
||||
/// let (my_descriptor, my_keys_map, networks) = bdk_wallet::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -444,7 +444,7 @@ macro_rules! apply_modifier {
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
/// let my_timelock = 50;
|
||||
///
|
||||
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
|
||||
/// let (descriptor_a, key_map_a, networks) = bdk_wallet::descriptor! {
|
||||
/// wsh (
|
||||
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
|
||||
/// )
|
||||
@@ -452,11 +452,12 @@ macro_rules! apply_modifier {
|
||||
///
|
||||
/// #[rustfmt::skip]
|
||||
/// let b_items = vec![
|
||||
/// bdk::fragment!(pk(my_key_1))?,
|
||||
/// bdk::fragment!(s:pk(my_key_2))?,
|
||||
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
|
||||
/// bdk_wallet::fragment!(pk(my_key_1))?,
|
||||
/// bdk_wallet::fragment!(s:pk(my_key_2))?,
|
||||
/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?,
|
||||
/// ];
|
||||
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||
/// let (descriptor_b, mut key_map_b, networks) =
|
||||
/// bdk_wallet::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||
///
|
||||
/// assert_eq!(descriptor_a, descriptor_b);
|
||||
/// assert_eq!(key_map_a.len(), key_map_b.len());
|
||||
@@ -475,7 +476,7 @@ macro_rules! apply_modifier {
|
||||
/// let my_key_2 =
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
///
|
||||
/// let (descriptor, key_map, networks) = bdk::descriptor! {
|
||||
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! {
|
||||
/// wsh (
|
||||
/// multi(2, my_key_1, my_key_2)
|
||||
/// )
|
||||
@@ -491,7 +492,7 @@ macro_rules! apply_modifier {
|
||||
/// let my_key =
|
||||
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
|
||||
///
|
||||
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
|
||||
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -20,10 +20,10 @@
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use bdk::descriptor::*;
|
||||
//! # use bdk::wallet::signer::*;
|
||||
//! # use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk::descriptor::policy::BuildSatisfaction;
|
||||
//! # use bdk_wallet::descriptor::*;
|
||||
//! # use bdk_wallet::wallet::signer::*;
|
||||
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
|
||||
//! let secp = Secp256k1::new();
|
||||
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
|
||||
//!
|
||||
@@ -36,17 +36,17 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::descriptor::error::Error as DescriptorError;
|
||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk::miniscript::Legacy;
|
||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bdk_wallet::descriptor::error::Error as DescriptorError;
|
||||
/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk_wallet::miniscript::Legacy;
|
||||
/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bitcoin::Network;
|
||||
///
|
||||
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
||||
///
|
||||
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
||||
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
||||
/// Ok(bdk_wallet::descriptor!(pkh(self.0))?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -72,10 +72,10 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Pkh;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Pkh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
@@ -102,10 +102,10 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Wpkh_P2Sh;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Wpkh_P2Sh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
@@ -133,10 +133,10 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2Wpkh;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet};
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Wpkh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
@@ -163,10 +163,10 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// use bdk::template::P2TR;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2TR;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
@@ -198,9 +198,9 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip44;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
@@ -234,9 +234,9 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip44Public;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
@@ -271,9 +271,9 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip49;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
@@ -307,9 +307,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip49Public;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
@@ -344,9 +344,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip84;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
@@ -380,9 +380,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip84Public;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
@@ -417,9 +417,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip86;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
@@ -453,9 +453,9 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// use bdk::template::Bip86Public;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
@@ -97,7 +97,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
|
||||
// This method is used internally by `bdk_wallet::fragment!` and `bdk_wallet::descriptor!`. It has to be
|
||||
// public because it is effectively called by external crates once the macros are expanded,
|
||||
// but since it is not meant to be part of the public api we hide it from the docs.
|
||||
#[doc(hidden)]
|
||||
@@ -206,9 +206,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type valid in any context:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
@@ -224,9 +224,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type that is only valid on mainnet:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{
|
||||
/// use bdk_wallet::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||
/// ScriptContext, SinglePub, SinglePubKey,
|
||||
/// };
|
||||
@@ -251,9 +251,11 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
/// use bdk_wallet::keys::{
|
||||
/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// is_legacy: bool,
|
||||
@@ -279,17 +281,17 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// makes the compiler (correctly) fail.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use bdk_wallet::bitcoin::PublicKey;
|
||||
/// use core::str::FromStr;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
///
|
||||
/// pub struct MySegwitOnlyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
/// }
|
||||
///
|
||||
/// impl IntoDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
|
||||
/// impl IntoDescriptorKey<bdk_wallet::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk_wallet::miniscript::Segwitv0>, KeyError> {
|
||||
/// self.pubkey.into_descriptor_key()
|
||||
/// }
|
||||
/// }
|
||||
@@ -297,8 +299,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// let key = MySegwitOnlyKeyType {
|
||||
/// pubkey: PublicKey::from_str("...")?,
|
||||
/// };
|
||||
/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?;
|
||||
/// // ^^^^^ changing this to `wpkh` would make it compile
|
||||
/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?;
|
||||
/// // ^^^^^ changing this to `wpkh` would make it compile
|
||||
///
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
@@ -387,9 +389,9 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||
/// use bdk_wallet::bitcoin;
|
||||
/// use bdk_wallet::bitcoin::bip32;
|
||||
/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// struct MyCustomKeyType {
|
||||
/// key_data: bitcoin::PrivateKey,
|
||||
@@ -418,9 +420,9 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
/// [`Xpriv`] or [`Xpub`] will be considered valid.
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{
|
||||
/// use bdk_wallet::bitcoin;
|
||||
/// use bdk_wallet::bitcoin::bip32;
|
||||
/// use bdk_wallet::keys::{
|
||||
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
///
|
||||
@@ -469,9 +471,9 @@ pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||
This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
|
||||
like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
|
||||
```rust
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::keys::{DerivableKey, ExtendedKey};
|
||||
use bdk::keys::bip39::{Mnemonic, Language};
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::keys::{DerivableKey, ExtendedKey};
|
||||
use bdk_wallet::keys::bip39::{Mnemonic, Language};
|
||||
|
||||
# fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let xkey: ExtendedKey =
|
||||
@@ -764,7 +766,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((pks, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
|
||||
// Used internally by `bdk_wallet::fragment!` to build `pk_k()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
descriptor_key: Pk,
|
||||
@@ -778,7 +780,7 @@ pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk::fragment!` to build `pk_h()` fragments
|
||||
// Used internally by `bdk_wallet::fragment!` to build `pk_h()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
descriptor_key: Pk,
|
||||
@@ -792,7 +794,7 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
||||
// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_multi<
|
||||
Pk: IntoDescriptorKey<Ctx>,
|
||||
@@ -812,7 +814,7 @@ pub fn make_multi<
|
||||
Ok((minisc, key_map, valid_networks))
|
||||
}
|
||||
|
||||
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
|
||||
// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments
|
||||
#[doc(hidden)]
|
||||
pub fn make_sortedmulti<Pk, Ctx, F>(
|
||||
thresh: usize,
|
||||
@@ -834,7 +836,7 @@ where
|
||||
Ok((descriptor, key_map, valid_networks))
|
||||
}
|
||||
|
||||
/// The "identity" conversion is used internally by some `bdk::fragment`s
|
||||
/// The "identity" conversion is used internally by some `bdk_wallet::fragment`s
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
Ok(self)
|
||||
@@ -26,11 +26,11 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk_persist::PersistBackend;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::coin_selection::decide_change;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::coin_selection::decide_change;
|
||||
//! # use anyhow::Error;
|
||||
//! #[derive(Debug)]
|
||||
//! struct AlwaysSpendEverything;
|
||||
@@ -20,8 +20,8 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! let import = r#"{
|
||||
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
|
||||
//! "blockheight":1782088,
|
||||
@@ -40,8 +40,8 @@
|
||||
//! ### Export a `Wallet`
|
||||
//! ```
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! let wallet = Wallet::new_no_persist(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
@@ -189,6 +189,7 @@ impl FullyNodedExport {
|
||||
WshInner::SortedMulti(_) => Ok(()),
|
||||
WshInner::Ms(ms) => check_ms(&ms.node),
|
||||
},
|
||||
Descriptor::Tr(_) => Ok(()),
|
||||
_ => Err("The descriptor is not compatible with Bitcoin Core"),
|
||||
}
|
||||
}
|
||||
@@ -314,6 +315,18 @@ mod test {
|
||||
assert_eq!(export.label, "Test Label");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_tr() {
|
||||
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
||||
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||
assert_eq!(export.blockheight, 5000);
|
||||
assert_eq!(export.label, "Test Label");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_to_json() {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
@@ -14,11 +14,11 @@
|
||||
//! This module 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::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk_wallet::wallet::AddressIndex::New;
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
@@ -22,7 +22,7 @@ use alloc::{
|
||||
pub use bdk_chain::keychain::Balance;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph,
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
keychain::KeychainTxOutIndex,
|
||||
local_chain::{
|
||||
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
|
||||
},
|
||||
@@ -59,7 +59,7 @@ pub use utils::IsDust;
|
||||
|
||||
use coin_selection::DefaultCoinSelectionAlgorithm;
|
||||
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
||||
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
||||
use tx_builder::{FeePolicy, TxBuilder, TxParams};
|
||||
use utils::{check_nsequence_rbf, After, Older, SecpCtx};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
@@ -134,72 +134,7 @@ impl From<SyncResult> for Update {
|
||||
}
|
||||
|
||||
/// The changes made to a wallet by applying an [`Update`].
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct ChangeSet {
|
||||
/// Changes to the [`LocalChain`].
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
pub chain: local_chain::ChangeSet,
|
||||
|
||||
/// Changes to [`IndexedTxGraph`].
|
||||
///
|
||||
/// [`IndexedTxGraph`]: bdk_chain::indexed_tx_graph::IndexedTxGraph
|
||||
pub indexed_tx_graph: indexed_tx_graph::ChangeSet<
|
||||
ConfirmationTimeHeightAnchor,
|
||||
keychain::ChangeSet<KeychainKind>,
|
||||
>,
|
||||
|
||||
/// Stores the network type of the wallet.
|
||||
pub network: Option<Network>,
|
||||
}
|
||||
|
||||
impl Append for ChangeSet {
|
||||
fn append(&mut self, other: Self) {
|
||||
Append::append(&mut self.chain, other.chain);
|
||||
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network type must be consistent"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.chain.is_empty() && self.indexed_tx_graph.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<local_chain::ChangeSet> for ChangeSet {
|
||||
fn from(chain: local_chain::ChangeSet) -> Self {
|
||||
Self {
|
||||
chain,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
From<
|
||||
indexed_tx_graph::ChangeSet<
|
||||
ConfirmationTimeHeightAnchor,
|
||||
keychain::ChangeSet<KeychainKind>,
|
||||
>,
|
||||
> for ChangeSet
|
||||
{
|
||||
fn from(
|
||||
indexed_tx_graph: indexed_tx_graph::ChangeSet<
|
||||
ConfirmationTimeHeightAnchor,
|
||||
keychain::ChangeSet<KeychainKind>,
|
||||
>,
|
||||
) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type ChangeSet = bdk_persist::CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>;
|
||||
|
||||
/// A derived address and the index it was found at.
|
||||
/// For convenience this automatically derefs to `Address`
|
||||
@@ -526,17 +461,18 @@ impl Wallet {
|
||||
/// manually to the [`Wallet`]:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::signer::{SignersContainer, SignerOrdering};
|
||||
/// # use bdk::descriptor::Descriptor;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
|
||||
/// # use bdk_wallet::descriptor::Descriptor;
|
||||
/// # use bitcoin::key::Secp256k1;
|
||||
/// # use bdk::KeychainKind;
|
||||
/// # use bdk_file_store::Store;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// # use bdk_sqlite::{Store, rusqlite::Connection};
|
||||
/// #
|
||||
/// # fn main() -> Result<(), anyhow::Error> {
|
||||
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
/// # let file_path = temp_dir.path().join("store.db");
|
||||
/// # let db: Store<bdk::wallet::ChangeSet> = Store::create_new(&[], &file_path).expect("must create db");
|
||||
/// # let conn = Connection::open(file_path).expect("must open connection");
|
||||
/// # let db = Store::new(conn).expect("must create db");
|
||||
/// let secp = Secp256k1::new();
|
||||
///
|
||||
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
|
||||
@@ -1052,7 +988,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Txid;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let txid:Txid = todo!();
|
||||
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
||||
@@ -1061,7 +997,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Psbt;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let mut psbt: Psbt = todo!();
|
||||
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||
@@ -1083,7 +1019,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Txid;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let txid:Txid = todo!();
|
||||
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
|
||||
@@ -1092,7 +1028,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Psbt;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let mut psbt: Psbt = todo!();
|
||||
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||
@@ -1114,7 +1050,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Txid;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let txid:Txid = todo!();
|
||||
/// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx;
|
||||
@@ -1123,7 +1059,7 @@ impl Wallet {
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use bitcoin::Psbt;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # let mut wallet: Wallet = todo!();
|
||||
/// # let mut psbt: Psbt = todo!();
|
||||
/// let tx = &psbt.clone().extract_tx().expect("tx");
|
||||
@@ -1144,8 +1080,8 @@ impl Wallet {
|
||||
/// the transaction was last seen in the mempool is provided.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use bdk::{chain::ChainPosition, Wallet};
|
||||
/// use bdk_chain::Anchor;
|
||||
/// use bdk_wallet::{chain::ChainPosition, Wallet};
|
||||
/// # let wallet: Wallet = todo!();
|
||||
/// # let my_txid: bitcoin::Txid = todo!();
|
||||
///
|
||||
@@ -1311,8 +1247,8 @@ impl Wallet {
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::bitcoin::Network;
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::bitcoin::Network;
|
||||
/// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?;
|
||||
/// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) {
|
||||
/// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
|
||||
@@ -1337,9 +1273,9 @@ impl Wallet {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
@@ -1357,12 +1293,11 @@ impl Wallet {
|
||||
/// ```
|
||||
///
|
||||
/// [`TxBuilder`]: crate::TxBuilder
|
||||
pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> {
|
||||
pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm> {
|
||||
TxBuilder {
|
||||
wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
|
||||
params: TxParams::default(),
|
||||
coin_selection: DefaultCoinSelectionAlgorithm::default(),
|
||||
phantom: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,9 +1649,9 @@ impl Wallet {
|
||||
/// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first.
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
@@ -1748,7 +1683,7 @@ impl Wallet {
|
||||
pub fn build_fee_bump(
|
||||
&mut self,
|
||||
txid: Txid,
|
||||
) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
|
||||
) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm>, BuildFeeBumpError> {
|
||||
let graph = self.indexed_graph.graph();
|
||||
let txout_index = &self.indexed_graph.index;
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
@@ -1872,7 +1807,6 @@ impl Wallet {
|
||||
wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
|
||||
params,
|
||||
coin_selection: DefaultCoinSelectionAlgorithm::default(),
|
||||
phantom: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1889,9 +1823,9 @@ impl Wallet {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
@@ -19,8 +19,8 @@
|
||||
//! # use core::str::FromStr;
|
||||
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::signer::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk_wallet::signer::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct CustomHSM;
|
||||
//! # impl CustomHSM {
|
||||
@@ -16,10 +16,9 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::ChangeSet;
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk::wallet::tx_builder::CreateTx;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::ChangeSet;
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk_persist::PersistBackend;
|
||||
//! # use anyhow::Error;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
@@ -43,31 +42,16 @@
|
||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use super::coin_selection::CoinSelectionAlgorithm;
|
||||
use super::{CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
|
||||
/// Context in which the [`TxBuilder`] is valid
|
||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||
|
||||
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
|
||||
/// to bumping the fee of an existing one).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CreateTx;
|
||||
impl TxBuilderContext for CreateTx {}
|
||||
|
||||
/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BumpFee;
|
||||
impl TxBuilderContext for BumpFee {}
|
||||
|
||||
/// A transaction builder
|
||||
///
|
||||
/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After
|
||||
@@ -78,12 +62,12 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// as in the following example:
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::tx_builder::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::tx_builder::*;
|
||||
/// # use bitcoin::*;
|
||||
/// # use core::str::FromStr;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
@@ -123,11 +107,10 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`coin_selection`]: Self::coin_selection
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, Cs, Ctx> {
|
||||
pub struct TxBuilder<'a, Cs> {
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet>>,
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
/// The parameters for transaction creation sans coin selection algorithm.
|
||||
@@ -175,19 +158,18 @@ impl Default for FeePolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> {
|
||||
impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> {
|
||||
fn clone(&self) -> Self {
|
||||
TxBuilder {
|
||||
wallet: self.wallet.clone(),
|
||||
params: self.params.clone(),
|
||||
coin_selection: self.coin_selection.clone(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
// Methods supported for any CoinSelectionAlgorithm.
|
||||
impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
/// Set a custom fee rate.
|
||||
///
|
||||
/// This method sets the mining fee paid by the transaction as a rate on its size.
|
||||
@@ -212,8 +194,8 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
/// Note that this is really a minimum absolute fee -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
/// excess might not be viable.
|
||||
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
|
||||
pub fn fee_absolute(&mut self, fee_amount: Amount) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount.to_sat()));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -263,7 +245,7 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
@@ -553,18 +535,14 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
|
||||
/// Choose the coin selection algorithm
|
||||
///
|
||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`].
|
||||
/// Overrides the [`CoinSelectionAlgorithm`].
|
||||
///
|
||||
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(
|
||||
self,
|
||||
coin_selection: P,
|
||||
) -> TxBuilder<'a, P, Ctx> {
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(self, coin_selection: P) -> TxBuilder<'a, P> {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
params: self.params,
|
||||
coin_selection,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,9 +590,84 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
self.params.allow_dust = allow_dust;
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
|
||||
self.params.recipients = recipients
|
||||
.into_iter()
|
||||
.map(|(script, amount)| (script, amount.to_sat()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
|
||||
self.params
|
||||
.recipients
|
||||
.push((script_pubkey, amount.to_sat()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, Amount::ZERO);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to *drain* excess coins to.
|
||||
///
|
||||
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
|
||||
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
|
||||
/// coins are too small) it will not be included in the resulting transaction. The only
|
||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||
///
|
||||
/// If you choose not to set any recipients, you should provide the utxos that the
|
||||
/// transaction should spend via [`add_utxos`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||
/// single address.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut tx_builder = wallet.build_tx();
|
||||
///
|
||||
/// tx_builder
|
||||
/// // Spend all outputs in this wallet.
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
/// .enable_rbf();
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> {
|
||||
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
@@ -689,142 +742,6 @@ impl fmt::Display for AddForeignUtxoError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AddForeignUtxoError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::allow_shrinking`]
|
||||
pub enum AllowShrinkingError {
|
||||
/// Script/PubKey was not in the original transaction
|
||||
MissingScriptPubKey(ScriptBuf),
|
||||
}
|
||||
|
||||
impl fmt::Display for AllowShrinkingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingScriptPubKey(script_buf) => write!(
|
||||
f,
|
||||
"Script/PubKey was not in the original transaction: {}",
|
||||
script_buf,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AllowShrinkingError {}
|
||||
|
||||
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
|
||||
self.params.recipients = recipients
|
||||
.into_iter()
|
||||
.map(|(script, amount)| (script, amount.to_sat()))
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
|
||||
self.params
|
||||
.recipients
|
||||
.push((script_pubkey, amount.to_sat()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, Amount::ZERO);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to *drain* excess coins to.
|
||||
///
|
||||
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
|
||||
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
|
||||
/// coins are too small) it will not be included in the resulting transaction. The only
|
||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||
///
|
||||
/// If you choose not to set any recipients, you should either provide the utxos that the
|
||||
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
|
||||
///
|
||||
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||
/// single address.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk::wallet::tx_builder::CreateTx;
|
||||
/// # use bdk_persist::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut tx_builder = wallet.build_tx();
|
||||
///
|
||||
/// tx_builder
|
||||
/// // Spend all outputs in this wallet.
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
/// .enable_rbf();
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
impl<'a> TxBuilder<'a, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
|
||||
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||
/// will attempt to find a change output to shrink instead.
|
||||
///
|
||||
/// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is
|
||||
/// preserved then it is currently not guaranteed to be in the same position as it was
|
||||
/// originally.
|
||||
///
|
||||
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
|
||||
/// transaction we are bumping.
|
||||
pub fn allow_shrinking(
|
||||
&mut self,
|
||||
script_pubkey: ScriptBuf,
|
||||
) -> Result<&mut Self, AllowShrinkingError> {
|
||||
match self
|
||||
.params
|
||||
.recipients
|
||||
.iter()
|
||||
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
|
||||
{
|
||||
Some(position) => {
|
||||
self.params.recipients.remove(position);
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
Ok(self)
|
||||
}
|
||||
None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordering of the transaction's inputs and outputs
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum TxOrdering {
|
||||
@@ -1,8 +1,8 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use bdk::{KeychainKind, LocalOutput, Wallet};
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{
|
||||
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
|
||||
@@ -1,5 +1,5 @@
|
||||
use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||
use bdk::{psbt, KeychainKind, SignOptions};
|
||||
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||
use bdk_wallet::{psbt, KeychainKind, SignOptions};
|
||||
use core::str::FromStr;
|
||||
mod common;
|
||||
use common::*;
|
||||
@@ -156,8 +156,8 @@ fn test_psbt_fee_rate_with_missing_txout() {
|
||||
|
||||
#[test]
|
||||
fn test_psbt_multiple_internalkey_signers() {
|
||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk::KeychainKind;
|
||||
use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk_wallet::KeychainKind;
|
||||
use bitcoin::key::TapTweak;
|
||||
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
||||
@@ -1,18 +1,21 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use bdk::descriptor::{calc_checksum, IntoWalletDescriptor};
|
||||
use bdk::psbt::PsbtUtils;
|
||||
use bdk::signer::{SignOptions, SignerError};
|
||||
use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection};
|
||||
use bdk::wallet::error::CreateTxError;
|
||||
use bdk::wallet::tx_builder::AddForeignUtxoError;
|
||||
use bdk::wallet::NewError;
|
||||
use bdk::wallet::{AddressInfo, Balance, Wallet};
|
||||
use bdk::KeychainKind;
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::COINBASE_MATURITY;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bdk_persist::PersistBackend;
|
||||
use bdk_sqlite::rusqlite::Connection;
|
||||
use bdk_wallet::descriptor::{calc_checksum, IntoWalletDescriptor};
|
||||
use bdk_wallet::psbt::PsbtUtils;
|
||||
use bdk_wallet::signer::{SignOptions, SignerError};
|
||||
use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection};
|
||||
use bdk_wallet::wallet::error::CreateTxError;
|
||||
use bdk_wallet::wallet::tx_builder::AddForeignUtxoError;
|
||||
use bdk_wallet::wallet::NewError;
|
||||
use bdk_wallet::wallet::{AddressInfo, Balance, Wallet};
|
||||
use bdk_wallet::KeychainKind;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::key::Secp256k1;
|
||||
use bitcoin::psbt;
|
||||
@@ -69,166 +72,198 @@ const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
|
||||
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
|
||||
|
||||
#[test]
|
||||
fn load_recovers_wallet() {
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join("store.db");
|
||||
|
||||
// create new wallet
|
||||
let wallet_spk_index = {
|
||||
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
|
||||
let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet)
|
||||
.expect("must init wallet");
|
||||
|
||||
wallet.reveal_next_address(KeychainKind::External).unwrap();
|
||||
wallet.spk_index().clone()
|
||||
};
|
||||
|
||||
// recover wallet
|
||||
fn load_recovers_wallet() -> anyhow::Result<()> {
|
||||
fn run<B, FN, FR>(filename: &str, create_new: FN, recover: FR) -> anyhow::Result<()>
|
||||
where
|
||||
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
|
||||
FN: Fn(&Path) -> anyhow::Result<B>,
|
||||
FR: Fn(&Path) -> anyhow::Result<B>,
|
||||
{
|
||||
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
|
||||
let wallet = Wallet::load(db).expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert_eq!(
|
||||
wallet.spk_index().keychains().collect::<Vec<_>>(),
|
||||
wallet_spk_index.keychains().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
wallet.spk_index().last_revealed_indices(),
|
||||
wallet_spk_index.last_revealed_indices()
|
||||
);
|
||||
let secp = Secp256k1::new();
|
||||
assert_eq!(
|
||||
*wallet.get_descriptor_for_keychain(KeychainKind::External),
|
||||
get_test_tr_single_sig_xprv()
|
||||
.into_wallet_descriptor(&secp, wallet.network())
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join(filename);
|
||||
|
||||
// create new wallet
|
||||
let wallet_spk_index = {
|
||||
let db = create_new(&file_path).expect("must create db");
|
||||
let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet)
|
||||
.expect("must init wallet");
|
||||
|
||||
wallet.reveal_next_address(KeychainKind::External).unwrap();
|
||||
wallet.spk_index().clone()
|
||||
};
|
||||
|
||||
// recover wallet
|
||||
{
|
||||
let db = recover(&file_path).expect("must recover db");
|
||||
let wallet = Wallet::load(db).expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert_eq!(
|
||||
wallet.spk_index().keychains().collect::<Vec<_>>(),
|
||||
wallet_spk_index.keychains().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
wallet.spk_index().last_revealed_indices(),
|
||||
wallet_spk_index.last_revealed_indices()
|
||||
);
|
||||
let secp = Secp256k1::new();
|
||||
assert_eq!(
|
||||
*wallet.get_descriptor_for_keychain(KeychainKind::External),
|
||||
get_test_tr_single_sig_xprv()
|
||||
.into_wallet_descriptor(&secp, wallet.network())
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
}
|
||||
|
||||
// `new` can only be called on empty db
|
||||
{
|
||||
let db = recover(&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)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// `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)));
|
||||
}
|
||||
run(
|
||||
"store.db",
|
||||
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|
||||
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
|
||||
)?;
|
||||
run(
|
||||
"store.sqlite",
|
||||
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|
||||
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_or_load() {
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join("store.db");
|
||||
|
||||
// init wallet when non-existent
|
||||
let wallet_keychains: BTreeMap<_, _> = {
|
||||
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
|
||||
.expect("must create db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must init wallet");
|
||||
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
|
||||
};
|
||||
|
||||
// wrong network
|
||||
fn new_or_load() -> anyhow::Result<()> {
|
||||
fn run<B, F>(filename: &str, new_or_load: F) -> anyhow::Result<()>
|
||||
where
|
||||
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
|
||||
F: Fn(&Path) -> anyhow::Result<B>,
|
||||
{
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
|
||||
.expect_err("wrong network");
|
||||
assert!(
|
||||
matches!(
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join(filename);
|
||||
|
||||
// init wallet when non-existent
|
||||
let wallet_keychains: BTreeMap<_, _> = {
|
||||
let db = new_or_load(&file_path).expect("must create db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must init wallet");
|
||||
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
|
||||
};
|
||||
|
||||
// wrong network
|
||||
{
|
||||
let db = new_or_load(&file_path).expect("must create db");
|
||||
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
|
||||
.expect_err("wrong network");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||
got: Some(Network::Testnet),
|
||||
expected: Network::Bitcoin
|
||||
}
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||
got: Some(Network::Testnet),
|
||||
expected: Network::Bitcoin
|
||||
}
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// wrong genesis hash
|
||||
{
|
||||
let exp_blockhash = BlockHash::all_zeros();
|
||||
let got_blockhash =
|
||||
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
|
||||
// wrong genesis hash
|
||||
{
|
||||
let exp_blockhash = BlockHash::all_zeros();
|
||||
let got_blockhash =
|
||||
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
|
||||
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load_with_genesis_hash(
|
||||
get_test_wpkh(),
|
||||
None,
|
||||
db,
|
||||
Network::Testnet,
|
||||
exp_blockhash,
|
||||
)
|
||||
.expect_err("wrong genesis hash");
|
||||
assert!(
|
||||
matches!(
|
||||
let db = new_or_load(&file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load_with_genesis_hash(
|
||||
get_test_wpkh(),
|
||||
None,
|
||||
db,
|
||||
Network::Testnet,
|
||||
exp_blockhash,
|
||||
)
|
||||
.expect_err("wrong genesis hash");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
|
||||
if got == Some(got_blockhash) && expected == exp_blockhash
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
|
||||
if got == Some(got_blockhash) && expected == exp_blockhash
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// wrong external descriptor
|
||||
{
|
||||
let exp_descriptor = get_test_tr_single_sig();
|
||||
let got_descriptor = get_test_wpkh()
|
||||
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
|
||||
.unwrap()
|
||||
.0;
|
||||
// wrong external descriptor
|
||||
{
|
||||
let exp_descriptor = get_test_tr_single_sig();
|
||||
let got_descriptor = get_test_wpkh()
|
||||
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet)
|
||||
.expect_err("wrong external descriptor");
|
||||
assert!(
|
||||
matches!(
|
||||
let db = new_or_load(&file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet)
|
||||
.expect_err("wrong external descriptor");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
|
||||
if got == &Some(got_descriptor) && keychain == KeychainKind::External
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
|
||||
if got == &Some(got_descriptor) && keychain == KeychainKind::External
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// wrong internal descriptor
|
||||
{
|
||||
let exp_descriptor = Some(get_test_tr_single_sig());
|
||||
let got_descriptor = None;
|
||||
// wrong internal descriptor
|
||||
{
|
||||
let exp_descriptor = Some(get_test_tr_single_sig());
|
||||
let got_descriptor = None;
|
||||
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet)
|
||||
.expect_err("wrong internal descriptor");
|
||||
assert!(
|
||||
matches!(
|
||||
let db = new_or_load(&file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet)
|
||||
.expect_err("wrong internal descriptor");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
|
||||
if got == &got_descriptor && keychain == KeychainKind::Internal
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
|
||||
if got == &got_descriptor && keychain == KeychainKind::Internal
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
// all parameters match
|
||||
{
|
||||
let db = new_or_load(&file_path).expect("must open db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert!(wallet
|
||||
.keychains()
|
||||
.map(|(k, v)| (*k, v.clone()))
|
||||
.eq(wallet_keychains));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// all parameters match
|
||||
{
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert!(wallet
|
||||
.keychains()
|
||||
.map(|(k, v)| (*k, v.clone()))
|
||||
.eq(wallet_keychains));
|
||||
}
|
||||
run("store.db", |path| {
|
||||
Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?)
|
||||
})?;
|
||||
run("store.sqlite", |path| {
|
||||
Ok(bdk_sqlite::Store::new(Connection::open(path)?)?)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -744,7 +779,7 @@ fn test_create_tx_absolute_fee() {
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.fee_absolute(100);
|
||||
.fee_absolute(Amount::from_sat(100));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
|
||||
@@ -764,7 +799,7 @@ fn test_create_tx_absolute_zero_fee() {
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.fee_absolute(0);
|
||||
.fee_absolute(Amount::ZERO);
|
||||
let psbt = builder.finish().unwrap();
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
|
||||
@@ -785,13 +820,13 @@ fn test_create_tx_absolute_high_fee() {
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.fee_absolute(60_000);
|
||||
.fee_absolute(Amount::from_sat(60_000));
|
||||
let _ = builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_add_change() {
|
||||
use bdk::wallet::tx_builder::TxOrdering;
|
||||
use bdk_wallet::wallet::tx_builder::TxOrdering;
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
|
||||
@@ -846,7 +881,7 @@ fn test_create_tx_ordering_respected() {
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
|
||||
.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000))
|
||||
.ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic);
|
||||
.ordering(bdk_wallet::wallet::tx_builder::TxOrdering::Bip69Lexicographic);
|
||||
let psbt = builder.finish().unwrap();
|
||||
let fee = check_fee!(wallet, psbt);
|
||||
|
||||
@@ -1623,7 +1658,7 @@ fn test_bump_fee_low_abs() {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_absolute(10);
|
||||
builder.fee_absolute(Amount::from_sat(10));
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -1645,7 +1680,7 @@ fn test_bump_fee_zero_abs() {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_absolute(0);
|
||||
builder.fee_absolute(Amount::ZERO);
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -1707,7 +1742,7 @@ fn test_bump_fee_reduce_change() {
|
||||
assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_absolute(200);
|
||||
builder.fee_absolute(Amount::from_sat(200));
|
||||
builder.enable_rbf();
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received =
|
||||
@@ -1772,8 +1807,12 @@ fn test_bump_fee_reduce_single_recipient() {
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.fee_rate(feerate)
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap();
|
||||
// remove original tx drain_to address and amount
|
||||
.set_recipients(Vec::new())
|
||||
// set back original drain_to address
|
||||
.drain_to(addr.script_pubkey())
|
||||
// drain wallet output amount will be re-calculated with new fee rate
|
||||
.drain_wallet();
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received =
|
||||
wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
|
||||
@@ -1814,9 +1853,13 @@ fn test_bump_fee_absolute_reduce_single_recipient() {
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap()
|
||||
.fee_absolute(300);
|
||||
.fee_absolute(Amount::from_sat(300))
|
||||
// remove original tx drain_to address and amount
|
||||
.set_recipients(Vec::new())
|
||||
// set back original drain_to address
|
||||
.drain_to(addr.script_pubkey())
|
||||
// drain wallet output amount will be re-calculated with new fee rate
|
||||
.drain_wallet();
|
||||
let psbt = builder.finish().unwrap();
|
||||
let tx = &psbt.unsigned_tx;
|
||||
let sent_received = wallet.sent_and_received(tx);
|
||||
@@ -1888,8 +1931,6 @@ fn test_bump_fee_drain_wallet() {
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.drain_wallet()
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap()
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx"));
|
||||
@@ -1905,7 +1946,7 @@ fn test_bump_fee_remove_output_manually_selected_only() {
|
||||
// them, and make sure that `bump_fee` doesn't try to add more. This fails because we've
|
||||
// told the wallet it's not allowed to add more inputs AND it can't reduce the value of the
|
||||
// existing output. In other words, bump_fee + manually_selected_only is always an error
|
||||
// unless you've also set "allow_shrinking" OR there is a change output.
|
||||
// unless there is a change output.
|
||||
let init_tx = Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
@@ -2057,7 +2098,7 @@ fn test_bump_fee_absolute_add_input() {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_absolute(6_000);
|
||||
builder.fee_absolute(Amount::from_sat(6_000));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received =
|
||||
wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
|
||||
@@ -2122,8 +2163,8 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
||||
.unwrap();
|
||||
|
||||
// now bump the fees without using `allow_shrinking`. the wallet should add an
|
||||
// extra input and a change output, and leave the original output untouched
|
||||
// Now bump the fees, the wallet should add an extra input and a change output, and leave
|
||||
// the original output untouched.
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
||||
let psbt = builder.finish().unwrap();
|
||||
@@ -2334,7 +2375,10 @@ fn test_bump_fee_absolute_force_add_input() {
|
||||
// the new fee_rate is low enough that just reducing the change would be fine, but we force
|
||||
// the addition of an extra input with `add_utxo()`
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.add_utxo(incoming_op).unwrap().fee_absolute(250);
|
||||
builder
|
||||
.add_utxo(incoming_op)
|
||||
.unwrap()
|
||||
.fee_absolute(Amount::from_sat(250));
|
||||
let psbt = builder.finish().unwrap();
|
||||
let sent_received =
|
||||
wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
|
||||
@@ -2443,8 +2487,12 @@ fn test_bump_fee_unconfirmed_input() {
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.fee_rate(FeeRate::from_sat_per_vb_unchecked(15))
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap();
|
||||
// remove original tx drain_to address and amount
|
||||
.set_recipients(Vec::new())
|
||||
// set back original drain_to address
|
||||
.drain_to(addr.script_pubkey())
|
||||
// drain wallet output amount will be re-calculated with new fee rate
|
||||
.drain_wallet();
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
@@ -2951,7 +2999,7 @@ fn test_sending_to_bip350_bech32m_address() {
|
||||
|
||||
#[test]
|
||||
fn test_get_address() {
|
||||
use bdk::descriptor::template::Bip84;
|
||||
use bdk_wallet::descriptor::template::Bip84;
|
||||
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let wallet = Wallet::new_no_persist(
|
||||
Bip84(key, KeychainKind::External),
|
||||
@@ -3022,7 +3070,7 @@ fn test_reveal_addresses() {
|
||||
|
||||
#[test]
|
||||
fn test_get_address_no_reuse_single_descriptor() {
|
||||
use bdk::descriptor::template::Bip84;
|
||||
use bdk_wallet::descriptor::template::Bip84;
|
||||
use std::collections::HashSet;
|
||||
|
||||
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
@@ -3384,7 +3432,7 @@ fn test_taproot_script_spend() {
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_all_leaves() {
|
||||
use bdk::signer::TapLeavesOptions;
|
||||
use bdk_wallet::signer::TapLeavesOptions;
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
|
||||
|
||||
@@ -3413,7 +3461,7 @@ fn test_taproot_script_spend_sign_all_leaves() {
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_include_some_leaves() {
|
||||
use bdk::signer::TapLeavesOptions;
|
||||
use bdk_wallet::signer::TapLeavesOptions;
|
||||
use bitcoin::taproot::TapLeafHash;
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
@@ -3453,7 +3501,7 @@ fn test_taproot_script_spend_sign_include_some_leaves() {
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
||||
use bdk::signer::TapLeavesOptions;
|
||||
use bdk_wallet::signer::TapLeavesOptions;
|
||||
use bitcoin::taproot::TapLeafHash;
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
@@ -3493,7 +3541,7 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_no_leaves() {
|
||||
use bdk::signer::TapLeavesOptions;
|
||||
use bdk_wallet::signer::TapLeavesOptions;
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_electrum = { path = "../../crates/electrum" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -6,19 +6,20 @@ const BATCH_SIZE: usize = 5;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::bitcoin::{Address, Amount};
|
||||
use bdk::chain::collections::HashSet;
|
||||
use bdk::{bitcoin::Network, Wallet};
|
||||
use bdk::{KeychainKind, SignOptions};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, ElectrumApi},
|
||||
ElectrumExt,
|
||||
};
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::bitcoin::{Address, Amount};
|
||||
use bdk_wallet::chain::collections::HashSet;
|
||||
use bdk_wallet::{bitcoin::Network, Wallet};
|
||||
use bdk_wallet::{KeychainKind, SignOptions};
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = std::env::temp_dir().join("bdk-electrum-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db =
|
||||
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_sqlite = { path = "../../crates/sqlite" }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
|
||||
use bdk::{
|
||||
use bdk_esplora::{esplora_client, EsploraAsyncExt};
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Address, Amount, Network, Script},
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
use bdk_esplora::{esplora_client, EsploraAsyncExt};
|
||||
use bdk_file_store::Store;
|
||||
|
||||
const DB_MAGIC: &str = "bdk_wallet_esplora_async_example";
|
||||
use bdk_sqlite::{rusqlite::Connection, Store};
|
||||
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const PARALLEL_REQUESTS: usize = 5;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db_path = "bdk-esplora-async-example.sqlite";
|
||||
let conn = Connection::open(db_path)?;
|
||||
let db = Store::new(conn)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
@@ -23,7 +24,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
external_descriptor,
|
||||
Some(internal_descriptor),
|
||||
db,
|
||||
Network::Testnet,
|
||||
Network::Signet,
|
||||
)?;
|
||||
|
||||
let address = wallet.next_unused_address(KeychainKind::External)?;
|
||||
@@ -33,8 +34,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
println!("Wallet balance before syncing: {} sats", balance.total());
|
||||
|
||||
print!("Syncing...");
|
||||
let client =
|
||||
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_async()?;
|
||||
let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
|
||||
|
||||
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
|
||||
let mut once = Some(());
|
||||
@@ -90,7 +90,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
}
|
||||
|
||||
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
|
||||
.require_network(Network::Testnet)?;
|
||||
.require_network(Network::Signet)?;
|
||||
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
tx_builder
|
||||
|
||||
@@ -7,7 +7,7 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -5,16 +5,17 @@ const PARALLEL_REQUESTS: usize = 1;
|
||||
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
|
||||
use bdk::{
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Address, Amount, Network},
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
use bdk_file_store::Store;
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = std::env::temp_dir().join("bdk-esplora-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db =
|
||||
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../../crates/bdk" }
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
$ cargo run --bin wallet_rpc -- --help
|
||||
|
||||
wallet_rpc 0.1.0
|
||||
Bitcoind RPC example using `bdk::Wallet`
|
||||
Bitcoind RPC example using `bdk_wallet::Wallet`
|
||||
|
||||
USAGE:
|
||||
wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use bdk::{
|
||||
bitcoin::{Block, Network, Transaction},
|
||||
wallet::Wallet,
|
||||
};
|
||||
use bdk_bitcoind_rpc::{
|
||||
bitcoincore_rpc::{Auth, Client, RpcApi},
|
||||
Emitter,
|
||||
};
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Block, Network, Transaction},
|
||||
wallet::Wallet,
|
||||
};
|
||||
use clap::{self, Parser};
|
||||
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
|
||||
|
||||
const DB_MAGIC: &str = "bdk-rpc-wallet-example";
|
||||
|
||||
/// Bitcoind RPC example using `bdk::Wallet`.
|
||||
/// Bitcoind RPC example using `bdk_wallet::Wallet`.
|
||||
///
|
||||
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
|
||||
/// count.
|
||||
@@ -89,7 +89,10 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
&args.descriptor,
|
||||
args.change_descriptor.as_ref(),
|
||||
Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?,
|
||||
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(
|
||||
DB_MAGIC.as_bytes(),
|
||||
args.db_path,
|
||||
)?,
|
||||
args.network,
|
||||
)?;
|
||||
println!(
|
||||
|
||||
@@ -305,341 +305,341 @@ where
|
||||
}?
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "miniscript"))]
|
||||
mod test {
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
|
||||
use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind};
|
||||
|
||||
use super::{
|
||||
coin_select_bnb,
|
||||
evaluate_cs::{Evaluation, EvaluationError},
|
||||
tester::Tester,
|
||||
CoinSelector, CoinSelectorOpt, Vec, WeightedValue,
|
||||
};
|
||||
|
||||
fn tester() -> Tester {
|
||||
const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
|
||||
Tester::new(&Secp256k1::default(), DESC_STR)
|
||||
}
|
||||
|
||||
fn evaluate_bnb(
|
||||
initial_selector: CoinSelector,
|
||||
max_tries: usize,
|
||||
) -> Result<Evaluation, EvaluationError> {
|
||||
evaluate(initial_selector, |cs| {
|
||||
coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| {
|
||||
*cs = new_cs;
|
||||
true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_enough_coins() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 100_000).into(),
|
||||
t.gen_candidate(1, 100_000).into(),
|
||||
];
|
||||
let opts = t.gen_opts(200_000);
|
||||
let selector = CoinSelector::new(&candidates, &opts);
|
||||
assert!(!coin_select_bnb(10_000, selector).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_enough_coins_preselected() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 100_000).into(), // to preselect
|
||||
t.gen_candidate(1, 100_000).into(), // to preselect
|
||||
t.gen_candidate(2, 100_000).into(),
|
||||
];
|
||||
let opts = CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(200_000)
|
||||
};
|
||||
let selector = {
|
||||
let mut selector = CoinSelector::new(&candidates, &opts);
|
||||
selector.select(0); // preselect
|
||||
selector.select(1); // preselect
|
||||
selector
|
||||
};
|
||||
|
||||
let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed");
|
||||
println!("{}", evaluation);
|
||||
assert_eq!(evaluation.solution.selected, (0..=1).collect());
|
||||
assert_eq!(evaluation.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
/// `cost_of_change` acts as the upper-bound in Bnb; we check whether these boundaries are
|
||||
/// enforced in code
|
||||
#[test]
|
||||
fn cost_of_change() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 200_000).into(),
|
||||
t.gen_candidate(1, 200_000).into(),
|
||||
t.gen_candidate(2, 200_000).into(),
|
||||
];
|
||||
|
||||
// lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming
|
||||
// that we want 2 candidates selected
|
||||
let (lowest_opts, highest_opts) = {
|
||||
let opts = t.gen_opts(0);
|
||||
|
||||
let fee_from_inputs =
|
||||
(candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2;
|
||||
let fee_from_template =
|
||||
((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64;
|
||||
|
||||
let lowest_opts = CoinSelectorOpt {
|
||||
target_value: Some(
|
||||
400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64,
|
||||
),
|
||||
..opts
|
||||
};
|
||||
|
||||
let highest_opts = CoinSelectorOpt {
|
||||
target_value: Some(400_000 - fee_from_inputs - fee_from_template),
|
||||
..opts
|
||||
};
|
||||
|
||||
(lowest_opts, highest_opts)
|
||||
};
|
||||
|
||||
// test lowest possible target we can select
|
||||
let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000);
|
||||
assert!(lowest_eval.is_ok());
|
||||
let lowest_eval = lowest_eval.unwrap();
|
||||
println!("LB {}", lowest_eval);
|
||||
assert_eq!(lowest_eval.solution.selected.len(), 2);
|
||||
assert_eq!(lowest_eval.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
lowest_eval
|
||||
.feerate_offset(ExcessStrategyKind::ToFee)
|
||||
.floor(),
|
||||
0.0
|
||||
);
|
||||
|
||||
// test the highest possible target we can select
|
||||
let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000);
|
||||
assert!(highest_eval.is_ok());
|
||||
let highest_eval = highest_eval.unwrap();
|
||||
println!("UB {}", highest_eval);
|
||||
assert_eq!(highest_eval.solution.selected.len(), 2);
|
||||
assert_eq!(highest_eval.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
highest_eval
|
||||
.feerate_offset(ExcessStrategyKind::ToFee)
|
||||
.floor(),
|
||||
0.0
|
||||
);
|
||||
|
||||
// test lower out of bounds
|
||||
let loob_opts = CoinSelectorOpt {
|
||||
target_value: lowest_opts.target_value.map(|v| v - 1),
|
||||
..lowest_opts
|
||||
};
|
||||
let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000);
|
||||
assert!(loob_eval.is_err());
|
||||
println!("Lower OOB: {}", loob_eval.unwrap_err());
|
||||
|
||||
// test upper out of bounds
|
||||
let uoob_opts = CoinSelectorOpt {
|
||||
target_value: highest_opts.target_value.map(|v| v + 1),
|
||||
..highest_opts
|
||||
};
|
||||
let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000);
|
||||
assert!(uoob_eval.is_err());
|
||||
println!("Upper OOB: {}", uoob_eval.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_select() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 300_000).into(),
|
||||
t.gen_candidate(1, 300_000).into(),
|
||||
t.gen_candidate(2, 300_000).into(),
|
||||
t.gen_candidate(3, 200_000).into(),
|
||||
t.gen_candidate(4, 200_000).into(),
|
||||
];
|
||||
let make_opts = |v: u64| -> CoinSelectorOpt {
|
||||
CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(v)
|
||||
}
|
||||
};
|
||||
|
||||
let test_cases = vec![
|
||||
(make_opts(100_000), false, 0),
|
||||
(make_opts(200_000), true, 1),
|
||||
(make_opts(300_000), true, 1),
|
||||
(make_opts(500_000), true, 2),
|
||||
(make_opts(1_000_000), true, 4),
|
||||
(make_opts(1_200_000), false, 0),
|
||||
(make_opts(1_300_000), true, 5),
|
||||
(make_opts(1_400_000), false, 0),
|
||||
];
|
||||
|
||||
for (opts, expect_solution, expect_selected) in test_cases {
|
||||
let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000);
|
||||
assert_eq!(res.is_ok(), expect_solution);
|
||||
|
||||
match res {
|
||||
Ok(eval) => {
|
||||
println!("{}", eval);
|
||||
assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0);
|
||||
assert_eq!(eval.solution.selected.len(), expect_selected as _);
|
||||
}
|
||||
Err(err) => println!("expected failure: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn early_bailout_optimization() {
|
||||
let t = tester();
|
||||
|
||||
// target: 300_000
|
||||
// candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000
|
||||
// expected solution: 2x 125_000, 1x 50_000
|
||||
// set bnb max tries: 1100, should succeed
|
||||
let candidates = {
|
||||
let mut candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 125_000).into(),
|
||||
t.gen_candidate(1, 125_000).into(),
|
||||
t.gen_candidate(2, 50_000).into(),
|
||||
];
|
||||
(3..3 + 1000_u32)
|
||||
.for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into()));
|
||||
candidates
|
||||
};
|
||||
let opts = CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(300_000)
|
||||
};
|
||||
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let eval = result.unwrap();
|
||||
println!("{}", eval);
|
||||
assert_eq!(eval.solution.selected, (0..=2).collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_exhaust_iteration() {
|
||||
static MAX_TRIES: usize = 1000;
|
||||
let t = tester();
|
||||
let candidates = (0..MAX_TRIES + 1)
|
||||
.map(|index| t.gen_candidate(index as _, 10_000).into())
|
||||
.collect::<Vec<WeightedValue>>();
|
||||
let opts = t.gen_opts(10_001 * MAX_TRIES as u64);
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES);
|
||||
assert!(result.is_err());
|
||||
println!("error as expected: {}", result.unwrap_err());
|
||||
}
|
||||
|
||||
/// Solution should have fee >= min_absolute_fee (or no solution at all)
|
||||
#[test]
|
||||
fn min_absolute_fee() {
|
||||
let t = tester();
|
||||
let candidates = {
|
||||
let mut candidates = Vec::new();
|
||||
t.gen_weighted_values(&mut candidates, 5, 10_000);
|
||||
t.gen_weighted_values(&mut candidates, 5, 20_000);
|
||||
t.gen_weighted_values(&mut candidates, 5, 30_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_300);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_500);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_700);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_900);
|
||||
t.gen_weighted_values(&mut candidates, 10, 11_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 12_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 13_000);
|
||||
candidates
|
||||
};
|
||||
let mut opts = CoinSelectorOpt {
|
||||
min_absolute_fee: 1,
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
(1..=120_u64).for_each(|fee_factor| {
|
||||
opts.min_absolute_fee = fee_factor * 31;
|
||||
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000);
|
||||
match result {
|
||||
Ok(result) => {
|
||||
println!("Solution {}", result);
|
||||
let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee;
|
||||
assert!(fee >= opts.min_absolute_fee);
|
||||
assert_eq!(result.solution.excess_strategies.len(), 1);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("No Solution: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// For a decreasing feerate (long-term feerate is lower than effective feerate), we should
|
||||
/// select less. For increasing feerate (long-term feerate is higher than effective feerate), we
|
||||
/// should select more.
|
||||
#[test]
|
||||
fn feerate_difference() {
|
||||
let t = tester();
|
||||
let candidates = {
|
||||
let mut candidates = Vec::new();
|
||||
t.gen_weighted_values(&mut candidates, 10, 2_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 5_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 20_000);
|
||||
candidates
|
||||
};
|
||||
|
||||
let decreasing_feerate_opts = CoinSelectorOpt {
|
||||
target_feerate: 1.25,
|
||||
long_term_feerate: Some(0.25),
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
let increasing_feerate_opts = CoinSelectorOpt {
|
||||
target_feerate: 0.25,
|
||||
long_term_feerate: Some(1.25),
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
let decreasing_res = evaluate_bnb(
|
||||
CoinSelector::new(&candidates, &decreasing_feerate_opts),
|
||||
21_000,
|
||||
)
|
||||
.expect("no result");
|
||||
let decreasing_len = decreasing_res.solution.selected.len();
|
||||
|
||||
let increasing_res = evaluate_bnb(
|
||||
CoinSelector::new(&candidates, &increasing_feerate_opts),
|
||||
21_000,
|
||||
)
|
||||
.expect("no result");
|
||||
let increasing_len = increasing_res.solution.selected.len();
|
||||
|
||||
println!("decreasing_len: {}", decreasing_len);
|
||||
println!("increasing_len: {}", increasing_len);
|
||||
assert!(decreasing_len < increasing_len);
|
||||
}
|
||||
|
||||
/// TODO: UNIMPLEMENTED TESTS:
|
||||
/// * Excess strategies:
|
||||
/// * We should always have `ExcessStrategy::ToFee`.
|
||||
/// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`.
|
||||
/// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`.
|
||||
/// * Fuzz
|
||||
/// * Solution feerate should never be lower than target feerate
|
||||
/// * Solution fee should never be lower than `min_absolute_fee`.
|
||||
/// * Preselected should always remain selected
|
||||
fn _todo() {}
|
||||
}
|
||||
// #[cfg(all(test, feature = "miniscript"))]
|
||||
// mod test {
|
||||
// use bitcoin::secp256k1::Secp256k1;
|
||||
//
|
||||
// use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind};
|
||||
//
|
||||
// use super::{
|
||||
// coin_select_bnb,
|
||||
// evaluate_cs::{Evaluation, EvaluationError},
|
||||
// tester::Tester,
|
||||
// CoinSelector, CoinSelectorOpt, Vec, WeightedValue,
|
||||
// };
|
||||
//
|
||||
// fn tester() -> Tester {
|
||||
// const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
|
||||
// Tester::new(&Secp256k1::default(), DESC_STR)
|
||||
// }
|
||||
//
|
||||
// fn evaluate_bnb(
|
||||
// initial_selector: CoinSelector,
|
||||
// max_tries: usize,
|
||||
// ) -> Result<Evaluation, EvaluationError> {
|
||||
// evaluate(initial_selector, |cs| {
|
||||
// coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| {
|
||||
// *cs = new_cs;
|
||||
// true
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn not_enough_coins() {
|
||||
// let t = tester();
|
||||
// let candidates: Vec<WeightedValue> = vec![
|
||||
// t.gen_candidate(0, 100_000).into(),
|
||||
// t.gen_candidate(1, 100_000).into(),
|
||||
// ];
|
||||
// let opts = t.gen_opts(200_000);
|
||||
// let selector = CoinSelector::new(&candidates, &opts);
|
||||
// assert!(!coin_select_bnb(10_000, selector).is_some());
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn exactly_enough_coins_preselected() {
|
||||
// let t = tester();
|
||||
// let candidates: Vec<WeightedValue> = vec![
|
||||
// t.gen_candidate(0, 100_000).into(), // to preselect
|
||||
// t.gen_candidate(1, 100_000).into(), // to preselect
|
||||
// t.gen_candidate(2, 100_000).into(),
|
||||
// ];
|
||||
// let opts = CoinSelectorOpt {
|
||||
// target_feerate: 0.0,
|
||||
// ..t.gen_opts(200_000)
|
||||
// };
|
||||
// let selector = {
|
||||
// let mut selector = CoinSelector::new(&candidates, &opts);
|
||||
// selector.select(0); // preselect
|
||||
// selector.select(1); // preselect
|
||||
// selector
|
||||
// };
|
||||
//
|
||||
// let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed");
|
||||
// println!("{}", evaluation);
|
||||
// assert_eq!(evaluation.solution.selected, (0..=1).collect());
|
||||
// assert_eq!(evaluation.solution.excess_strategies.len(), 1);
|
||||
// assert_eq!(
|
||||
// evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(),
|
||||
// 0.0
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// /// `cost_of_change` acts as the upper-bound in Bnb; we check whether these boundaries are
|
||||
// /// enforced in code
|
||||
// #[test]
|
||||
// fn cost_of_change() {
|
||||
// let t = tester();
|
||||
// let candidates: Vec<WeightedValue> = vec![
|
||||
// t.gen_candidate(0, 200_000).into(),
|
||||
// t.gen_candidate(1, 200_000).into(),
|
||||
// t.gen_candidate(2, 200_000).into(),
|
||||
// ];
|
||||
//
|
||||
// // lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming
|
||||
// // that we want 2 candidates selected
|
||||
// let (lowest_opts, highest_opts) = {
|
||||
// let opts = t.gen_opts(0);
|
||||
//
|
||||
// let fee_from_inputs =
|
||||
// (candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2;
|
||||
// let fee_from_template =
|
||||
// ((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64;
|
||||
//
|
||||
// let lowest_opts = CoinSelectorOpt {
|
||||
// target_value: Some(
|
||||
// 400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64,
|
||||
// ),
|
||||
// ..opts
|
||||
// };
|
||||
//
|
||||
// let highest_opts = CoinSelectorOpt {
|
||||
// target_value: Some(400_000 - fee_from_inputs - fee_from_template),
|
||||
// ..opts
|
||||
// };
|
||||
//
|
||||
// (lowest_opts, highest_opts)
|
||||
// };
|
||||
//
|
||||
// // test lowest possible target we can select
|
||||
// let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000);
|
||||
// assert!(lowest_eval.is_ok());
|
||||
// let lowest_eval = lowest_eval.unwrap();
|
||||
// println!("LB {}", lowest_eval);
|
||||
// assert_eq!(lowest_eval.solution.selected.len(), 2);
|
||||
// assert_eq!(lowest_eval.solution.excess_strategies.len(), 1);
|
||||
// assert_eq!(
|
||||
// lowest_eval
|
||||
// .feerate_offset(ExcessStrategyKind::ToFee)
|
||||
// .floor(),
|
||||
// 0.0
|
||||
// );
|
||||
//
|
||||
// // test the highest possible target we can select
|
||||
// let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000);
|
||||
// assert!(highest_eval.is_ok());
|
||||
// let highest_eval = highest_eval.unwrap();
|
||||
// println!("UB {}", highest_eval);
|
||||
// assert_eq!(highest_eval.solution.selected.len(), 2);
|
||||
// assert_eq!(highest_eval.solution.excess_strategies.len(), 1);
|
||||
// assert_eq!(
|
||||
// highest_eval
|
||||
// .feerate_offset(ExcessStrategyKind::ToFee)
|
||||
// .floor(),
|
||||
// 0.0
|
||||
// );
|
||||
//
|
||||
// // test lower out of bounds
|
||||
// let loob_opts = CoinSelectorOpt {
|
||||
// target_value: lowest_opts.target_value.map(|v| v - 1),
|
||||
// ..lowest_opts
|
||||
// };
|
||||
// let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000);
|
||||
// assert!(loob_eval.is_err());
|
||||
// println!("Lower OOB: {}", loob_eval.unwrap_err());
|
||||
//
|
||||
// // test upper out of bounds
|
||||
// let uoob_opts = CoinSelectorOpt {
|
||||
// target_value: highest_opts.target_value.map(|v| v + 1),
|
||||
// ..highest_opts
|
||||
// };
|
||||
// let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000);
|
||||
// assert!(uoob_eval.is_err());
|
||||
// println!("Upper OOB: {}", uoob_eval.unwrap_err());
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn try_select() {
|
||||
// let t = tester();
|
||||
// let candidates: Vec<WeightedValue> = vec![
|
||||
// t.gen_candidate(0, 300_000).into(),
|
||||
// t.gen_candidate(1, 300_000).into(),
|
||||
// t.gen_candidate(2, 300_000).into(),
|
||||
// t.gen_candidate(3, 200_000).into(),
|
||||
// t.gen_candidate(4, 200_000).into(),
|
||||
// ];
|
||||
// let make_opts = |v: u64| -> CoinSelectorOpt {
|
||||
// CoinSelectorOpt {
|
||||
// target_feerate: 0.0,
|
||||
// ..t.gen_opts(v)
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// let test_cases = vec![
|
||||
// (make_opts(100_000), false, 0),
|
||||
// (make_opts(200_000), true, 1),
|
||||
// (make_opts(300_000), true, 1),
|
||||
// (make_opts(500_000), true, 2),
|
||||
// (make_opts(1_000_000), true, 4),
|
||||
// (make_opts(1_200_000), false, 0),
|
||||
// (make_opts(1_300_000), true, 5),
|
||||
// (make_opts(1_400_000), false, 0),
|
||||
// ];
|
||||
//
|
||||
// for (opts, expect_solution, expect_selected) in test_cases {
|
||||
// let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000);
|
||||
// assert_eq!(res.is_ok(), expect_solution);
|
||||
//
|
||||
// match res {
|
||||
// Ok(eval) => {
|
||||
// println!("{}", eval);
|
||||
// assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0);
|
||||
// assert_eq!(eval.solution.selected.len(), expect_selected as _);
|
||||
// }
|
||||
// Err(err) => println!("expected failure: {}", err),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn early_bailout_optimization() {
|
||||
// let t = tester();
|
||||
//
|
||||
// // target: 300_000
|
||||
// // candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000
|
||||
// // expected solution: 2x 125_000, 1x 50_000
|
||||
// // set bnb max tries: 1100, should succeed
|
||||
// let candidates = {
|
||||
// let mut candidates: Vec<WeightedValue> = vec![
|
||||
// t.gen_candidate(0, 125_000).into(),
|
||||
// t.gen_candidate(1, 125_000).into(),
|
||||
// t.gen_candidate(2, 50_000).into(),
|
||||
// ];
|
||||
// (3..3 + 1000_u32)
|
||||
// .for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into()));
|
||||
// candidates
|
||||
// };
|
||||
// let opts = CoinSelectorOpt {
|
||||
// target_feerate: 0.0,
|
||||
// ..t.gen_opts(300_000)
|
||||
// };
|
||||
//
|
||||
// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100);
|
||||
// assert!(result.is_ok());
|
||||
//
|
||||
// let eval = result.unwrap();
|
||||
// println!("{}", eval);
|
||||
// assert_eq!(eval.solution.selected, (0..=2).collect());
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn should_exhaust_iteration() {
|
||||
// static MAX_TRIES: usize = 1000;
|
||||
// let t = tester();
|
||||
// let candidates = (0..MAX_TRIES + 1)
|
||||
// .map(|index| t.gen_candidate(index as _, 10_000).into())
|
||||
// .collect::<Vec<WeightedValue>>();
|
||||
// let opts = t.gen_opts(10_001 * MAX_TRIES as u64);
|
||||
// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES);
|
||||
// assert!(result.is_err());
|
||||
// println!("error as expected: {}", result.unwrap_err());
|
||||
// }
|
||||
//
|
||||
// /// Solution should have fee >= min_absolute_fee (or no solution at all)
|
||||
// #[test]
|
||||
// fn min_absolute_fee() {
|
||||
// let t = tester();
|
||||
// let candidates = {
|
||||
// let mut candidates = Vec::new();
|
||||
// t.gen_weighted_values(&mut candidates, 5, 10_000);
|
||||
// t.gen_weighted_values(&mut candidates, 5, 20_000);
|
||||
// t.gen_weighted_values(&mut candidates, 5, 30_000);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 10_300);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 10_500);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 10_700);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 10_900);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 11_000);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 12_000);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 13_000);
|
||||
// candidates
|
||||
// };
|
||||
// let mut opts = CoinSelectorOpt {
|
||||
// min_absolute_fee: 1,
|
||||
// ..t.gen_opts(100_000)
|
||||
// };
|
||||
//
|
||||
// (1..=120_u64).for_each(|fee_factor| {
|
||||
// opts.min_absolute_fee = fee_factor * 31;
|
||||
//
|
||||
// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000);
|
||||
// match result {
|
||||
// Ok(result) => {
|
||||
// println!("Solution {}", result);
|
||||
// let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee;
|
||||
// assert!(fee >= opts.min_absolute_fee);
|
||||
// assert_eq!(result.solution.excess_strategies.len(), 1);
|
||||
// }
|
||||
// Err(err) => {
|
||||
// println!("No Solution: {}", err);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// /// For a decreasing feerate (long-term feerate is lower than effective feerate), we should
|
||||
// /// select less. For increasing feerate (long-term feerate is higher than effective feerate), we
|
||||
// /// should select more.
|
||||
// #[test]
|
||||
// fn feerate_difference() {
|
||||
// let t = tester();
|
||||
// let candidates = {
|
||||
// let mut candidates = Vec::new();
|
||||
// t.gen_weighted_values(&mut candidates, 10, 2_000);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 5_000);
|
||||
// t.gen_weighted_values(&mut candidates, 10, 20_000);
|
||||
// candidates
|
||||
// };
|
||||
//
|
||||
// let decreasing_feerate_opts = CoinSelectorOpt {
|
||||
// target_feerate: 1.25,
|
||||
// long_term_feerate: Some(0.25),
|
||||
// ..t.gen_opts(100_000)
|
||||
// };
|
||||
//
|
||||
// let increasing_feerate_opts = CoinSelectorOpt {
|
||||
// target_feerate: 0.25,
|
||||
// long_term_feerate: Some(1.25),
|
||||
// ..t.gen_opts(100_000)
|
||||
// };
|
||||
//
|
||||
// let decreasing_res = evaluate_bnb(
|
||||
// CoinSelector::new(&candidates, &decreasing_feerate_opts),
|
||||
// 21_000,
|
||||
// )
|
||||
// .expect("no result");
|
||||
// let decreasing_len = decreasing_res.solution.selected.len();
|
||||
//
|
||||
// let increasing_res = evaluate_bnb(
|
||||
// CoinSelector::new(&candidates, &increasing_feerate_opts),
|
||||
// 21_000,
|
||||
// )
|
||||
// .expect("no result");
|
||||
// let increasing_len = increasing_res.solution.selected.len();
|
||||
//
|
||||
// println!("decreasing_len: {}", decreasing_len);
|
||||
// println!("increasing_len: {}", increasing_len);
|
||||
// assert!(decreasing_len < increasing_len);
|
||||
// }
|
||||
//
|
||||
// /// TODO: UNIMPLEMENTED TESTS:
|
||||
// /// * Excess strategies:
|
||||
// /// * We should always have `ExcessStrategy::ToFee`.
|
||||
// /// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`.
|
||||
// /// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`.
|
||||
// /// * Fuzz
|
||||
// /// * Solution feerate should never be lower than target feerate
|
||||
// /// * Solution fee should never be lower than `min_absolute_fee`.
|
||||
// /// * Preselected should always remain selected
|
||||
// fn _todo() {}
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user