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
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
[workspace]
resolver = "2"
members = [
- "crates/bdk",
+ "crates/wallet",
"crates/chain",
"crates/file_store",
"crates/electrum",
</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>
<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>
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.
- [`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.
+++ /dev/null
-[package]
-name = "bdk"
-homepage = "https://bitcoindevkit.org"
-version = "1.0.0-alpha.11"
-repository = "https://github.com/bitcoindevkit/bdk"
-documentation = "https://docs.rs/bdk"
-description = "A modern, lightweight, descriptor-based wallet library"
-keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
-readme = "README.md"
-license = "MIT OR Apache-2.0"
-authors = ["Bitcoin Dev Kit Developers"]
-edition = "2021"
-rust-version = "1.63"
-
-[dependencies]
-anyhow = { version = "1", default-features = false }
-rand = "^0.8"
-miniscript = { version = "11.0.0", features = ["serde"], default-features = false }
-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" }
-
-# Optional dependencies
-bip39 = { version = "2.0", optional = true }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-getrandom = "0.2"
-js-sys = "0.3"
-
-[features]
-default = ["std"]
-std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
-compiler = ["miniscript/compiler"]
-all-keys = ["keys-bip39"]
-keys-bip39 = ["bip39"]
-
-# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
-# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
-# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
-dev-getrandom-wasm = ["getrandom/js"]
-
-[dev-dependencies]
-lazy_static = "1.4"
-assert_matches = "1.5.0"
-tempfile = "3"
-bdk_file_store = { path = "../file_store" }
-anyhow = "1"
-
-[package.metadata.docs.rs]
-all-features = true
-rustdoc-args = ["--cfg", "docsrs"]
-
-[[example]]
-name = "mnemonic_to_descriptors"
-path = "examples/mnemonic_to_descriptors.rs"
-required-features = ["all-keys"]
-
-[[example]]
-name = "miniscriptc"
-path = "examples/compiler.rs"
-required-features = ["compiler"]
+++ /dev/null
-<div align="center">
- <h1>BDK</h1>
-
- <img src="https://raw.githubusercontent.com/bitcoindevkit/bdk/master/static/bdk.png" width="220" />
-
- <p>
- <strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
- </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://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://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>
-
- <h4>
- <a href="https://bitcoindevkit.org">Project Homepage</a>
- <span> | </span>
- <a href="https://docs.rs/bdk">Documentation</a>
- </h4>
-</div>
-
-## `bdk`
-
-The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
-interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
-for many simple applications as well as a good demonstration of how to use the other mechanisms to
-construct a wallet. It has two keychains (external and internal) which are defined by
-[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it
-chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
-can create and sign transactions.
-
-For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
-
-### Blockchain data
-
-In order to get blockchain data for `Wallet` to consume, you should configure a client from
-an available chain source. Typically you make a request to the chain source and get a response
-that the `Wallet` can use to update its view of the chain.
-
-**Blockchain Data Sources**
-
-* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
-* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
-* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
-
-**Examples**
-
-* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
-* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
-* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
-* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
-
-### Persistence
-
-To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
-
-**Implementations**
-
-* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
-
-**Example**
-
-<!-- compile_fail because outpoint and txout are fake variables -->
-```rust,compile_fail
-use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
-
-fn main() {
- // Create a new file `Store`.
- let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
-
- let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
- let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
-
- // Insert a single `TxOut` at `OutPoint` into the wallet.
- let _ = wallet.insert_txout(outpoint, txout);
- wallet.commit().expect("must write to database");
-}
-```
-
-<!-- ### 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; -->
-
-<!-- fn main() -> Result<(), bdk::Error> { -->
-<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
-<!-- let wallet = Wallet::new( -->
-<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
-<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
-<!-- Network::Testnet, -->
-<!-- )?; -->
-
-<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
-
-<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
-
-<!-- Ok(()) -->
-<!-- } -->
-<!-- ``` -->
-<!-- ### Generate a few addresses -->
-
-<!-- ```rust -->
-<!-- use bdk::Wallet; -->
-<!-- use bdk::wallet::AddressIndex::New; -->
-<!-- use bdk::bitcoin::Network; -->
-
-<!-- fn main() -> Result<(), bdk::Error> { -->
-<!-- let wallet = Wallet::new_no_persist( -->
-<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
-<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
-<!-- Network::Testnet, -->
-<!-- )?; -->
-
-<!-- println!("Address #0: {}", wallet.get_address(New)); -->
-<!-- println!("Address #1: {}", wallet.get_address(New)); -->
-<!-- println!("Address #2: {}", wallet.get_address(New)); -->
-
-<!-- Ok(()) -->
-<!-- } -->
-<!-- ``` -->
-
-<!-- ### Create a transaction -->
-
-<!-- ```rust,no_run -->
-<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
-<!-- use bdk::blockchain::ElectrumBlockchain; -->
-
-<!-- use bdk::electrum_client::Client; -->
-<!-- use bdk::wallet::AddressIndex::New; -->
-
-<!-- use bitcoin::base64; -->
-<!-- use bdk::bitcoin::consensus::serialize; -->
-<!-- use bdk::bitcoin::Network; -->
-
-<!-- fn main() -> Result<(), bdk::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/*)", -->
-<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
-<!-- Network::Testnet, -->
-<!-- )?; -->
-
-<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
-
-<!-- let send_to = wallet.get_address(New); -->
-<!-- let (psbt, details) = { -->
-<!-- let mut builder = wallet.build_tx(); -->
-<!-- builder -->
-<!-- .add_recipient(send_to.script_pubkey(), 50_000) -->
-<!-- .enable_rbf() -->
-<!-- .do_not_spend_change() -->
-<!-- .fee_rate(FeeRate::from_sat_per_vb(5.0)); -->
-<!-- builder.finish()? -->
-<!-- }; -->
-
-<!-- println!("Transaction details: {:#?}", details); -->
-<!-- println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); -->
-
-<!-- Ok(()) -->
-<!-- } -->
-<!-- ``` -->
-
-<!-- ### Sign a transaction -->
-
-<!-- ```rust,no_run -->
-<!-- use bdk::{Wallet, SignOptions}; -->
-
-<!-- use bitcoin::base64; -->
-<!-- use bdk::bitcoin::consensus::deserialize; -->
-<!-- use bdk::bitcoin::Network; -->
-
-<!-- fn main() -> Result<(), bdk::Error> { -->
-<!-- let wallet = Wallet::new_no_persist( -->
-<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
-<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
-<!-- Network::Testnet, -->
-<!-- )?; -->
-
-<!-- let psbt = "..."; -->
-<!-- let mut psbt = deserialize(&base64::decode(psbt).unwrap())?; -->
-
-<!-- let _finalized = wallet.sign(&mut psbt, SignOptions::default())?; -->
-
-<!-- Ok(()) -->
-<!-- } -->
-<!-- ``` -->
-
-## Testing
-
-### Unit testing
-
-```bash
-cargo test
-```
-
-## License
-
-Licensed under either of
-
-* Apache License, Version 2.0, ([LICENSE-APACHE](../../LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
-* MIT license ([LICENSE-MIT](../../LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
-
-at your option.
-
-### 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
-[`bdk_chain`]: https://docs.rs/bdk_chain/latest
-[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
-[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
-[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
-[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
-[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-extern crate bdk;
-extern crate bitcoin;
-extern crate miniscript;
-extern crate serde_json;
-
-use std::error::Error;
-use std::str::FromStr;
-
-use bitcoin::Network;
-use miniscript::policy::Concrete;
-use miniscript::Descriptor;
-
-use bdk::{KeychainKind, Wallet};
-
-/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
-/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
-/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
-/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
-/// can be derived from the policy.
-///
-/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
-
-fn main() -> Result<(), Box<dyn Error>> {
- // We start with a generic miniscript policy string
- let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
- println!("Compiling policy: \n{}", policy_str);
-
- // Parse the string as a [`Concrete`] type miniscript policy.
- let policy = Concrete::<String>::from_str(policy_str)?;
-
- // Create a `wsh` type descriptor from the policy.
- // `policy.compile()` returns the resulting miniscript from the policy.
- let descriptor = Descriptor::new_wsh(policy.compile()?)?;
-
- println!("Compiled into following Descriptor: \n{}", descriptor);
-
- // Create a new wallet from this descriptor
- let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
-
- println!(
- "First derived address from the descriptor: \n{}",
- wallet.next_unused_address(KeychainKind::External)?,
- );
-
- // BDK also has it's own `Policy` structure to represent the spending condition in a more
- // human readable json format.
- let spending_policy = wallet.policies(KeychainKind::External)?;
- println!(
- "The BDK spending policy: \n{}",
- serde_json::to_string_pretty(&spending_policy)?
- );
-
- Ok(())
-}
+++ /dev/null
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-use 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 std::str::FromStr;
-
-/// This example demonstrates how to generate a mnemonic phrase
-/// using BDK and use that to generate a descriptor string.
-fn main() -> Result<(), anyhow::Error> {
- let secp = Secp256k1::new();
-
- // In this example we are generating a 12 words mnemonic phrase
- // but it is also possible generate 15, 18, 21 and 24 words
- // using their respective `WordCount` variant.
- let mnemonic: GeneratedKey<_, Tap> =
- Mnemonic::generate((WordCount::Words12, Language::English))
- .map_err(|_| anyhow!("Mnemonic generation error"))?;
-
- println!("Mnemonic phrase: {}", *mnemonic);
- let mnemonic_with_passphrase = (mnemonic, None);
-
- // define external and internal derivation key path
- let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
- let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
-
- // generate external and internal descriptor from mnemonic
- let (external_descriptor, ext_keymap) =
- descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
- .into_wallet_descriptor(&secp, Network::Testnet)?;
- let (internal_descriptor, int_keymap) =
- descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
- .into_wallet_descriptor(&secp, Network::Testnet)?;
-
- println!("tpub external descriptor: {}", external_descriptor);
- println!("tpub internal descriptor: {}", internal_descriptor);
- println!(
- "tprv external descriptor: {}",
- external_descriptor.to_string_with_secret(&ext_keymap)
- );
- println!(
- "tprv internal descriptor: {}",
- internal_descriptor.to_string_with_secret(&int_keymap)
- );
-
- Ok(())
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-extern crate bdk;
-use std::error::Error;
-
-use bdk::bitcoin::Network;
-use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
-use bdk::wallet::signer::SignersContainer;
-
-/// This example describes the use of the BDK's [`bdk::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.
-/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
-/// to spend from.
-///
-/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
-/// one of the Extend Private key.
-
-fn main() -> Result<(), Box<dyn Error>> {
- let secp = bitcoin::secp256k1::Secp256k1::new();
-
- // The descriptor used in the example
- // The form is "wsh(multi(2, <privkey>, <pubkey>))"
- 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.
- // 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.
- // But they can be used as independent tools also.
- let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
-
- println!("Example Descriptor for policy analysis : {}", wallet_desc);
-
- // Create the signer with the keymap and descriptor.
- let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
-
- // Extract the Policy from the given descriptor and signer.
- // Note that Policy is a wallet specific structure. It depends on the the descriptor, and
- // what the concerned wallet with a given signer can sign for.
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
- .expect("We expect a policy");
-
- println!("Derived Policy for the descriptor {:#?}", policy);
-
- Ok(())
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptor checksum
-//!
-//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
-//! checksum of a descriptor
-
-use crate::descriptor::DescriptorError;
-use alloc::string::String;
-
-const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
-const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
-
-fn poly_mod(mut c: u64, val: u64) -> u64 {
- let c0 = c >> 35;
- c = ((c & 0x7ffffffff) << 5) ^ val;
- if c0 & 1 > 0 {
- c ^= 0xf5dee51989
- };
- if c0 & 2 > 0 {
- c ^= 0xa9fdca3312
- };
- if c0 & 4 > 0 {
- c ^= 0x1bab10e32d
- };
- if c0 & 8 > 0 {
- c ^= 0x3706b1677a
- };
- if c0 & 16 > 0 {
- c ^= 0x644d626ffd
- };
-
- c
-}
-
-/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
-pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
- let mut c = 1;
- let mut cls = 0;
- let mut clscount = 0;
-
- let mut original_checksum = None;
- if let Some(split) = desc.split_once('#') {
- desc = split.0;
- original_checksum = Some(split.1);
- }
-
- for ch in desc.as_bytes() {
- let pos = INPUT_CHARSET
- .iter()
- .position(|b| b == ch)
- .ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
- c = poly_mod(c, pos & 31);
- cls = cls * 3 + (pos >> 5);
- clscount += 1;
- if clscount == 3 {
- c = poly_mod(c, cls);
- cls = 0;
- clscount = 0;
- }
- }
- if clscount > 0 {
- c = poly_mod(c, cls);
- }
- (0..8).for_each(|_| c = poly_mod(c, 0));
- c ^= 1;
-
- let mut checksum = [0_u8; 8];
- for j in 0..8 {
- checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
- }
-
- // if input data already had a checksum, check calculated checksum against original checksum
- if let Some(original_checksum) = original_checksum {
- if original_checksum.as_bytes() != checksum {
- return Err(DescriptorError::InvalidDescriptorChecksum);
- }
- }
-
- Ok(checksum)
-}
-
-/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
-pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
- // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
- calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use crate::descriptor::calc_checksum;
- use assert_matches::assert_matches;
-
- // test calc_checksum() function; it should return the same value as Bitcoin Core
- #[test]
- fn test_calc_checksum() {
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
- assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
-
- let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
- assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
- }
-
- // test calc_checksum() function; it should return the same value as Bitcoin Core even if the
- // descriptor string includes a checksum hash
- #[test]
- fn test_calc_checksum_with_checksum_hash() {
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
- assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
-
- let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
- assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
- assert_matches!(
- calc_checksum(desc),
- Err(DescriptorError::InvalidDescriptorChecksum)
- );
-
- let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
- assert_matches!(
- calc_checksum(desc),
- Err(DescriptorError::InvalidDescriptorChecksum)
- );
- }
-
- #[test]
- fn test_calc_checksum_invalid_character() {
- let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
- let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
-
- assert_matches!(
- calc_checksum(&invalid_desc),
- Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
- );
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptors DSL
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_top_level_sh {
- // disallow `sortedmulti` in `bare()`
- ( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => {
- compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands");
- };
- ( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => {
- compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands");
- };
-
- ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{
- use core::marker::PhantomData;
-
- use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
- use $crate::miniscript::$ctx;
-
- let build_desc = |k, pks| {
- Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
- };
-
- $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*)
- }};
- ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{
- use core::marker::PhantomData;
-
- use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
- use $crate::miniscript::$ctx;
-
- let build_desc = |k, pks| {
- Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
- };
-
- $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*)
- }};
-
- ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{
- use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
-
- $crate::fragment!($( $minisc )*)
- .and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks)))
- .and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::<DescriptorPublicKey>::$inner_struct(inner), key_map, valid_networks)))
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_top_level_pk {
- ( $inner_type:ident, $ctx:ty, $key:expr ) => {{
- use $crate::miniscript::descriptor::$inner_type;
-
- #[allow(unused_imports)]
- use $crate::keys::{DescriptorKey, IntoDescriptorKey};
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
-
- $key.into_descriptor_key()
- .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp))
- .map_err($crate::descriptor::DescriptorError::Key)
- .map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks))
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_top_level_tr {
- ( $internal_key:expr, $tap_tree:expr ) => {{
- use $crate::miniscript::descriptor::{
- Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr,
- };
- use $crate::miniscript::Tap;
-
- #[allow(unused_imports)]
- use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
-
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
-
- $internal_key
- .into_descriptor_key()
- .and_then(|key: DescriptorKey<Tap>| key.extract(&secp))
- .map_err($crate::descriptor::DescriptorError::Key)
- .and_then(|(pk, mut key_map, mut valid_networks)| {
- let tap_tree = $tap_tree.map(
- |(tap_tree, tree_keymap, tree_networks): (
- TapTree<DescriptorPublicKey>,
- KeyMap,
- ValidNetworks,
- )| {
- key_map.extend(tree_keymap.into_iter());
- valid_networks =
- $crate::keys::merge_networks(&valid_networks, &tree_networks);
-
- tap_tree
- },
- );
-
- Ok((
- Descriptor::<DescriptorPublicKey>::Tr(Tr::new(pk, tap_tree)?),
- key_map,
- valid_networks,
- ))
- })
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_leaf_opcode {
- ( $terminal_variant:ident ) => {{
- use $crate::descriptor::CheckMiniscript;
-
- $crate::miniscript::Miniscript::from_ast(
- $crate::miniscript::miniscript::decode::Terminal::$terminal_variant,
- )
- .map_err($crate::descriptor::DescriptorError::Miniscript)
- .and_then(|minisc| {
- minisc.check_miniscript()?;
- Ok(minisc)
- })
- .map(|minisc| {
- (
- minisc,
- $crate::miniscript::descriptor::KeyMap::default(),
- $crate::keys::any_network(),
- )
- })
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_leaf_opcode_value {
- ( $terminal_variant:ident, $value:expr ) => {{
- use $crate::descriptor::CheckMiniscript;
-
- $crate::miniscript::Miniscript::from_ast(
- $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value),
- )
- .map_err($crate::descriptor::DescriptorError::Miniscript)
- .and_then(|minisc| {
- minisc.check_miniscript()?;
- Ok(minisc)
- })
- .map(|minisc| {
- (
- minisc,
- $crate::miniscript::descriptor::KeyMap::default(),
- $crate::keys::any_network(),
- )
- })
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_leaf_opcode_value_two {
- ( $terminal_variant:ident, $one:expr, $two:expr ) => {{
- use $crate::descriptor::CheckMiniscript;
-
- $crate::miniscript::Miniscript::from_ast(
- $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two),
- )
- .map_err($crate::descriptor::DescriptorError::Miniscript)
- .and_then(|minisc| {
- minisc.check_miniscript()?;
- Ok(minisc)
- })
- .map(|minisc| {
- (
- minisc,
- $crate::miniscript::descriptor::KeyMap::default(),
- $crate::keys::any_network(),
- )
- })
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_node_opcode_two {
- ( $terminal_variant:ident, $( $inner:tt )* ) => ({
- use $crate::descriptor::CheckMiniscript;
-
- let inner = $crate::fragment_internal!( @t $( $inner )* );
- let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened();
-
- a
- .and_then(|a| Ok((a, b?)))
- .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| {
- // join key_maps
- a_keymap.extend(b_keymap.into_iter());
-
- let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
- $crate::alloc::sync::Arc::new(a_minisc),
- $crate::alloc::sync::Arc::new(b_minisc),
- ))?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
- })
- });
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_node_opcode_three {
- ( $terminal_variant:ident, $( $inner:tt )* ) => ({
- use $crate::descriptor::CheckMiniscript;
-
- let inner = $crate::fragment_internal!( @t $( $inner )* );
- let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened();
-
- a
- .and_then(|a| Ok((a, b?, c?)))
- .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| {
- // join key_maps
- a_keymap.extend(b_keymap.into_iter());
- a_keymap.extend(c_keymap.into_iter());
-
- let networks = $crate::keys::merge_networks(&a_networks, &b_networks);
- let networks = $crate::keys::merge_networks(&networks, &c_networks);
-
- let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
- $crate::alloc::sync::Arc::new(a_minisc),
- $crate::alloc::sync::Arc::new(b_minisc),
- $crate::alloc::sync::Arc::new(c_minisc),
- ))?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, a_keymap, networks))
- })
- });
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! impl_sortedmulti {
- ( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
- $crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp)
- });
- ( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({
- use $crate::keys::IntoDescriptorKey;
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
-
- let keys = vec![
- $(
- $key.into_descriptor_key(),
- )*
- ];
-
- keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
- .map_err($crate::descriptor::DescriptorError::Key)
- .and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp))
- });
-
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! parse_tap_tree {
- ( @merge $tree_a:expr, $tree_b:expr) => {{
- use $crate::miniscript::descriptor::TapTree;
-
- $tree_a
- .and_then(|tree_a| Ok((tree_a, $tree_b?)))
- .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
- a_keymap.extend(b_keymap.into_iter());
- Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
- })
-
- }};
-
- // Two sub-trees
- ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{
- let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
- let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
-
- $crate::parse_tap_tree!(@merge tree_a, tree_b)
- }};
-
- // One leaf and a sub-tree
- ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{
- let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
- let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
-
- $crate::parse_tap_tree!(@merge tree_a, tree_b)
- }};
- ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
- let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
- let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
-
- $crate::parse_tap_tree!(@merge tree_a, tree_b)
- }};
-
- // Two leaves
- ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
- let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
- let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
-
- $crate::parse_tap_tree!(@merge tree_a, tree_b)
- }};
-
- // Single leaf
- ( $op:ident ( $( $minisc:tt )* ) ) => {{
- use $crate::alloc::sync::Arc;
- use $crate::miniscript::descriptor::TapTree;
-
- $crate::fragment!( $op ( $( $minisc )* ) )
- .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks))
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! apply_modifier {
- ( $terminal_variant:ident, $inner:expr ) => {{
- use $crate::descriptor::CheckMiniscript;
-
- $inner
- .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() })
- .and_then(|(minisc, keymap, networks)| {
- let minisc = $crate::miniscript::Miniscript::from_ast(
- $crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
- $crate::alloc::sync::Arc::new(minisc),
- ),
- )?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, keymap, networks))
- })
- }};
-
- ( a: $inner:expr ) => {{
- $crate::apply_modifier!(Alt, $inner)
- }};
- ( s: $inner:expr ) => {{
- $crate::apply_modifier!(Swap, $inner)
- }};
- ( c: $inner:expr ) => {{
- $crate::apply_modifier!(Check, $inner)
- }};
- ( d: $inner:expr ) => {{
- $crate::apply_modifier!(DupIf, $inner)
- }};
- ( v: $inner:expr ) => {{
- $crate::apply_modifier!(Verify, $inner)
- }};
- ( j: $inner:expr ) => {{
- $crate::apply_modifier!(NonZero, $inner)
- }};
- ( n: $inner:expr ) => {{
- $crate::apply_modifier!(ZeroNotEqual, $inner)
- }};
-
- // Modifiers expanded to other operators
- ( t: $inner:expr ) => {{
- $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
- $crate::impl_leaf_opcode_value_two!(
- AndV,
- $crate::alloc::sync::Arc::new(a_minisc),
- $crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0)
- )
- .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
- })
- }};
- ( l: $inner:expr ) => {{
- $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
- $crate::impl_leaf_opcode_value_two!(
- OrI,
- $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0),
- $crate::alloc::sync::Arc::new(a_minisc)
- )
- .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
- })
- }};
- ( u: $inner:expr ) => {{
- $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
- $crate::impl_leaf_opcode_value_two!(
- OrI,
- $crate::alloc::sync::Arc::new(a_minisc),
- $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0)
- )
- .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
- })
- }};
-}
-
-/// Macro to write full descriptors with code
-///
-/// This macro expands to a `Result` of
-/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError)
-///
-/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers
-/// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be
-/// broken up to `s:d:v:older(144)`.
-///
-/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements
-/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that
-/// case they must be wrapped in quotes, which is another difference compared to the standard
-/// descriptor syntax.
-///
-/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey
-///
-/// ## Example
-///
-/// Signature plus timelock descriptor:
-///
-/// ```
-/// # use std::str::FromStr;
-/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
-/// # Ok::<(), Box<dyn std::error::Error>>(())
-/// ```
-///
-/// -------
-///
-/// 2-of-3 that becomes a 1-of-3 after a timelock has expired. Both `descriptor_a` and `descriptor_b` are equivalent: the first
-/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
-/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
-///
-/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))`
-///
-/// ```
-/// # use std::str::FromStr;
-/// let my_key_1 = bitcoin::PublicKey::from_str(
-/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c",
-/// )?;
-/// let my_key_2 =
-/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
-/// let my_timelock = 50;
-///
-/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
-/// wsh (
-/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
-/// )
-/// }?;
-///
-/// #[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))?,
-/// ];
-/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
-///
-/// assert_eq!(descriptor_a, descriptor_b);
-/// assert_eq!(key_map_a.len(), key_map_b.len());
-/// # Ok::<(), Box<dyn std::error::Error>>(())
-/// ```
-///
-/// ------
-///
-/// Simple 2-of-2 multi-signature, equivalent to: `wsh(multi(2, ...))`
-///
-/// ```
-/// # use std::str::FromStr;
-/// let my_key_1 = bitcoin::PublicKey::from_str(
-/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c",
-/// )?;
-/// let my_key_2 =
-/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
-///
-/// let (descriptor, key_map, networks) = bdk::descriptor! {
-/// wsh (
-/// multi(2, my_key_1, my_key_2)
-/// )
-/// }?;
-/// # Ok::<(), Box<dyn std::error::Error>>(())
-/// ```
-///
-/// ------
-///
-/// Native-Segwit single-sig, equivalent to: `wpkh(...)`
-///
-/// ```
-/// let my_key =
-/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
-///
-/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
-/// # Ok::<(), Box<dyn std::error::Error>>(())
-/// ```
-///
-/// [`Vec`]: alloc::vec::Vec
-#[macro_export]
-macro_rules! descriptor {
- ( bare ( $( $minisc:tt )* ) ) => ({
- $crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*)
- });
- ( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({
- $crate::descriptor!(shwsh ($( $minisc )*))
- });
- ( shwsh ( $( $minisc:tt )* ) ) => ({
- $crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*)
- });
- ( pk ( $key:expr ) ) => ({
- // `pk()` is actually implemented as `bare(pk())`
- $crate::descriptor!( bare ( pk ( $key ) ) )
- });
- ( pkh ( $key:expr ) ) => ({
- use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
-
- $crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key)
- .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
- .map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Pkh(a), b, c))
- });
- ( wpkh ( $key:expr ) ) => ({
- use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
-
- $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
- .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
- .map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Wpkh(a), b, c))
- });
- ( sh ( wpkh ( $key:expr ) ) ) => ({
- $crate::descriptor!(shwpkh ( $key ))
- });
- ( shwpkh ( $key:expr ) ) => ({
- use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh};
-
- $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
- .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
- .and_then(|(a, b, c)| Ok((Descriptor::<DescriptorPublicKey>::Sh(Sh::new_wpkh(a.into_inner())?), b, c)))
- });
- ( sh ( $( $minisc:tt )* ) ) => ({
- $crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*)
- });
- ( wsh ( $( $minisc:tt )* ) ) => ({
- $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
- });
-
- ( tr ( $internal_key:expr ) ) => ({
- $crate::impl_top_level_tr!($internal_key, None)
- });
- ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({
- let tap_tree = $crate::parse_tap_tree!( $( $taptree )* );
- tap_tree
- .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree)))
- });
-}
-
-#[doc(hidden)]
-pub struct TupleTwo<A, B> {
- pub a: A,
- pub b: B,
-}
-
-impl<A, B> TupleTwo<A, B> {
- pub fn flattened(self) -> (A, B) {
- (self.a, self.b)
- }
-}
-
-impl<A, B> From<(A, (B, ()))> for TupleTwo<A, B> {
- fn from((a, (b, _)): (A, (B, ()))) -> Self {
- TupleTwo { a, b }
- }
-}
-
-#[doc(hidden)]
-pub struct TupleThree<A, B, C> {
- pub a: A,
- pub b: B,
- pub c: C,
-}
-
-impl<A, B, C> TupleThree<A, B, C> {
- pub fn flattened(self) -> (A, B, C) {
- (self.a, self.b, self.c)
- }
-}
-
-impl<A, B, C> From<(A, (B, (C, ())))> for TupleThree<A, B, C> {
- fn from((a, (b, (c, _))): (A, (B, (C, ())))) -> Self {
- TupleThree { a, b, c }
- }
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! group_multi_keys {
- ( $( $key:expr ),+ ) => {{
- use $crate::keys::IntoDescriptorKey;
-
- let keys = vec![
- $(
- $key.into_descriptor_key(),
- )*
- ];
-
- keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
- .map_err($crate::descriptor::DescriptorError::Key)
- }};
-}
-
-#[doc(hidden)]
-#[macro_export]
-macro_rules! fragment_internal {
- // The @v prefix is used to parse a sequence of operands and return them in a vector. This is
- // used by operands that take a variable number of arguments, like `thresh()` and `multi()`.
- ( @v $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({
- let mut v = vec![$crate::fragment!( $op ( $( $args )* ) )];
- v.append(&mut $crate::fragment_internal!( @v $( $tail )* ));
-
- v
- });
- // Match modifiers
- ( @v $modif:tt : $( $tail:tt )* ) => ({
- let mut v = $crate::fragment_internal!( @v $( $tail )* );
- let first = v.drain(..1).next().unwrap();
-
- let first = $crate::apply_modifier!($modif:first);
-
- let mut v_final = vec![first];
- v_final.append(&mut v);
-
- v_final
- });
- // Remove commas between operands
- ( @v , $( $tail:tt )* ) => ({
- $crate::fragment_internal!( @v $( $tail )* )
- });
- ( @v ) => ({
- vec![]
- });
-
- // The @t prefix is used to parse a sequence of operands and return them in a tuple. This
- // allows checking at compile-time the number of arguments passed to an operand. For this
- // reason it's used by `and_*()`, `or_*()`, etc.
- //
- // Unfortunately, due to the fact that concatenating tuples is pretty hard, the final result
- // adds in the first spot the parsed operand and in the second spot the result of parsing
- // all the following ones. For two operands the type then corresponds to: (X, (X, ())). For
- // three operands it's (X, (X, (X, ()))), etc.
- //
- // To check that the right number of arguments has been passed we can "cast" those tuples to
- // more convenient structures like `TupleTwo`. If the conversion succeeds, the right number of
- // args was passed. Otherwise the compilation fails entirely.
- ( @t $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({
- ($crate::fragment!( $op ( $( $args )* ) ), $crate::fragment_internal!( @t $( $tail )* ))
- });
- // Match modifiers
- ( @t $modif:tt : $( $tail:tt )* ) => ({
- let (first, tail) = $crate::fragment_internal!( @t $( $tail )* );
- ($crate::apply_modifier!($modif:first), tail)
- });
- // Remove commas between operands
- ( @t , $( $tail:tt )* ) => ({
- $crate::fragment_internal!( @t $( $tail )* )
- });
- ( @t ) => ({});
-
- // Fallback to calling `fragment!()`
- ( $( $tokens:tt )* ) => ({
- $crate::fragment!($( $tokens )*)
- });
-}
-
-/// Macro to write descriptor fragments with code
-///
-/// This macro will be expanded to an object of type `Result<(Miniscript<DescriptorPublicKey, _>, KeyMap, ValidNetworks), DescriptorError>`. It allows writing
-/// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec(m, ...))`.
-///
-/// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro.
-#[macro_export]
-macro_rules! fragment {
- // Modifiers
- ( $modif:tt : $( $tail:tt )* ) => ({
- let op = $crate::fragment!( $( $tail )* );
- $crate::apply_modifier!($modif:op)
- });
-
- // Miniscript
- ( true ) => ({
- $crate::impl_leaf_opcode!(True)
- });
- ( false ) => ({
- $crate::impl_leaf_opcode!(False)
- });
- ( pk_k ( $key:expr ) ) => ({
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
- $crate::keys::make_pk($key, &secp)
- });
- ( pk ( $key:expr ) ) => ({
- $crate::fragment!(c:pk_k ( $key ))
- });
- ( pk_h ( $key:expr ) ) => ({
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
- $crate::keys::make_pkh($key, &secp)
- });
- ( after ( $value:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
- });
- ( older ( $value:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
- });
- ( sha256 ( $hash:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(Sha256, $hash)
- });
- ( hash256 ( $hash:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(Hash256, $hash)
- });
- ( ripemd160 ( $hash:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(Ripemd160, $hash)
- });
- ( hash160 ( $hash:expr ) ) => ({
- $crate::impl_leaf_opcode_value!(Hash160, $hash)
- });
- ( and_v ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(AndV, $( $inner )*)
- });
- ( and_b ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(AndB, $( $inner )*)
- });
- ( and_or ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_three!(AndOr, $( $inner )*)
- });
- ( andor ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_three!(AndOr, $( $inner )*)
- });
- ( or_b ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(OrB, $( $inner )*)
- });
- ( or_d ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(OrD, $( $inner )*)
- });
- ( or_c ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(OrC, $( $inner )*)
- });
- ( or_i ( $( $inner:tt )* ) ) => ({
- $crate::impl_node_opcode_two!(OrI, $( $inner )*)
- });
- ( thresh_vec ( $thresh:expr, $items:expr ) ) => ({
- use $crate::miniscript::descriptor::KeyMap;
-
- let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
- let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect();
-
- let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| {
- keys_acc.extend(key.into_iter());
- let net_acc = $crate::keys::merge_networks(&net_acc, &net);
-
- (keys_acc, net_acc)
- });
-
- $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items)
- .map(|(minisc, _, _)| (minisc, key_maps, valid_networks))
- });
- ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
- let items = $crate::fragment_internal!( @v $( $inner )* );
-
- items.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
- .and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
- });
- ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
-
- $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
- });
- ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
- $crate::group_multi_keys!( $( $key ),* )
- .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) ))
- });
- ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
- let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
-
- $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
- });
- ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
- $crate::group_multi_keys!( $( $key ),* )
- .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) ))
- });
-
- // `sortedmulti()` is handled separately
- ( sortedmulti ( $( $inner:tt )* ) ) => ({
- compile_error!("`sortedmulti` can only be used as the root operand of a descriptor");
- });
- ( sortedmulti_vec ( $( $inner:tt )* ) ) => ({
- compile_error!("`sortedmulti_vec` can only be used as the root operand of a descriptor");
- });
-}
-
-#[cfg(test)]
-mod test {
- use alloc::string::ToString;
- use bitcoin::secp256k1::Secp256k1;
- use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
- use miniscript::{Descriptor, Legacy, Segwitv0};
-
- use core::str::FromStr;
-
- use crate::descriptor::{DescriptorError, DescriptorMeta};
- use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
- use bitcoin::bip32;
- use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
- use bitcoin::PrivateKey;
-
- // test the descriptor!() macro
-
- // verify descriptor generates expected script(s) (if bare or pk) or address(es)
- fn check(
- desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
- is_witness: bool,
- is_fixed: bool,
- expected: &[&str],
- ) {
- let (desc, _key_map, _networks) = desc.unwrap();
- assert_eq!(desc.is_witness(), is_witness);
- assert_eq!(!desc.has_wildcard(), is_fixed);
- for i in 0..expected.len() {
- let child_desc = desc
- .at_derivation_index(i as u32)
- .expect("i is not hardened");
- let address = child_desc.address(Regtest);
- if let Ok(address) = address {
- assert_eq!(address.to_string(), *expected.get(i).unwrap());
- } else {
- let script = child_desc.script_pubkey();
- assert_eq!(script.to_hex_string(), *expected.get(i).unwrap());
- }
- }
- }
-
- // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
- // - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
-
- // expected script for pk and bare manually created
- // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses`
-
- #[test]
- fn test_fixed_legacy_descriptors() {
- let pubkey1 = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- let pubkey2 = bitcoin::PublicKey::from_str(
- "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
- )
- .unwrap();
-
- check(
- descriptor!(bare(multi(1,pubkey1,pubkey2))),
- false,
- true,
- &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"],
- );
- check(
- descriptor!(pk(pubkey1)),
- false,
- true,
- &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"],
- );
- check(
- descriptor!(pkh(pubkey1)),
- false,
- true,
- &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
- );
- check(
- descriptor!(sh(multi(1, pubkey1, pubkey2))),
- false,
- true,
- &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"],
- );
- }
-
- #[test]
- fn test_fixed_segwitv0_descriptors() {
- let pubkey1 = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- let pubkey2 = bitcoin::PublicKey::from_str(
- "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
- )
- .unwrap();
-
- check(
- descriptor!(wpkh(pubkey1)),
- true,
- true,
- &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
- );
- check(
- descriptor!(sh(wpkh(pubkey1))),
- true,
- true,
- &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
- );
- check(
- descriptor!(wsh(multi(1, pubkey1, pubkey2))),
- true,
- true,
- &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"],
- );
- check(
- descriptor!(sh(wsh(multi(1, pubkey1, pubkey2)))),
- true,
- true,
- &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"],
- );
- }
-
- #[test]
- fn test_fixed_threeop_descriptors() {
- let redeem_key = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- let move_key = bitcoin::PublicKey::from_str(
- "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
- )
- .unwrap();
-
- check(
- descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))),
- true,
- true,
- &["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"],
- );
- }
-
- #[test]
- fn test_bip32_legacy_descriptors() {
- let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
-
- let path = bip32::DerivationPath::from_str("m/0").unwrap();
- let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
- check(
- descriptor!(pk(desc_key)),
- false,
- false,
- &[
- "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac",
- "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac",
- "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac",
- ],
- );
-
- let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
- check(
- descriptor!(pkh(desc_key)),
- false,
- false,
- &[
- "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP",
- "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj",
- "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn",
- ],
- );
-
- let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
- let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
- let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
-
- check(
- descriptor!(sh(multi(1, desc_key1, desc_key2))),
- false,
- false,
- &[
- "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8",
- "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At",
- "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr",
- ],
- );
- }
-
- #[test]
- fn test_bip32_segwitv0_descriptors() {
- let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
-
- let path = bip32::DerivationPath::from_str("m/0").unwrap();
- let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
- check(
- descriptor!(wpkh(desc_key)),
- true,
- false,
- &[
- "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0",
- "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad",
- "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r",
- ],
- );
-
- let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
- check(
- descriptor!(sh(wpkh(desc_key))),
- true,
- false,
- &[
- "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763",
- "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn",
- "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx",
- ],
- );
-
- let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
- let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap();
- let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap();
- check(
- descriptor!(wsh(multi(1, desc_key1, desc_key2))),
- true,
- false,
- &[
- "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49",
- "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k",
- "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7",
- ],
- );
-
- let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
- let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
- check(
- descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))),
- true,
- false,
- &[
- "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz",
- "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw",
- "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC",
- ],
- );
- }
-
- #[test]
- fn test_dsl_sortedmulti() {
- let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
-
- let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
- let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
-
- let desc_key1 = (key_1, path_1);
- let desc_key2 = (key_2, path_2);
-
- check(
- descriptor!(sh(sortedmulti(1, desc_key1.clone(), desc_key2.clone()))),
- false,
- false,
- &[
- "2MsxzPEJDBzpGffJXPaDpfXZAUNnZhaMh2N",
- "2My3x3DLPK3UbGWGpxrXr1RnbD8MNC4FpgS",
- "2NByEuiQT7YLqHCTNxL5KwYjvtuCYcXNBSC",
- "2N1TGbP81kj2VUKTSWgrwxoMfuWjvfUdyu7",
- "2N3Bomq2fpAcLRNfZnD3bCWK9quan28CxCR",
- "2N9nrZaEzEFDqEAU9RPvDnXGT6AVwBDKAQb",
- ],
- );
-
- check(
- descriptor!(sh(wsh(sortedmulti(
- 1,
- desc_key1.clone(),
- desc_key2.clone()
- )))),
- true,
- false,
- &[
- "2NCogc5YyM4N6ruv1hUa7WLMW1BPeCK7N9B",
- "2N6mkSAKi1V2oaBXby7XHdvBMKEDRQcFpNe",
- "2NFmTSttm9v6bXeoWaBvpMcgfPQcZhNn3Eh",
- "2Mvib87RBPUHXNEpX5S5Kv1qqrhBfgBGsJM",
- "2MtMv5mcK2EjcLsH8Txpx2JxLLzHr4ttczL",
- "2MsWCB56rb4T6yPv8QudZGHERTwNgesE4f6",
- ],
- );
-
- check(
- descriptor!(wsh(sortedmulti_vec(1, vec![desc_key1, desc_key2]))),
- true,
- false,
- &[
- "bcrt1qcvq0lg8q7a47ytrd7zk5y7uls7mulrenjgvflwylpppgwf8029es4vhpnj",
- "bcrt1q80yn8sdt6l7pjvkz25lglyaqctlmsq9ugk80rmxt8yu0npdsj97sc7l4de",
- "bcrt1qrvf6024v9s50qhffe3t2fr2q9ckdhx2g6jz32chm2pp24ymgtr5qfrdmct",
- "bcrt1q6srfmra0ynypym35c7jvsxt2u4yrugeajq95kg2ps7lk6h2gaunsq9lzxn",
- "bcrt1qhl8rrzzcdpu7tcup3lcg7tge52sqvwy5fcv4k78v6kxtwmqf3v6qpvyjza",
- "bcrt1ql2elz9mhm9ll27ddpewhxs732xyl2fk2kpkqz9gdyh33wgcun4vstrd49k",
- ],
- );
- }
-
- // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
- #[test]
- fn test_valid_networks() {
- let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let path = bip32::DerivationPath::from_str("m/0").unwrap();
- let desc_key = (xprv, path).into_descriptor_key().unwrap();
-
- let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap();
- assert_eq!(
- valid_networks,
- [Testnet, Regtest, Signet].iter().cloned().collect()
- );
-
- let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
- let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
- let desc_key = (xprv, path).into_descriptor_key().unwrap();
-
- let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap();
- assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect());
- }
-
- // - verify the key_maps are correctly merged together
- #[test]
- fn test_key_maps_merged() {
- let secp = Secp256k1::new();
-
- let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
- let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
-
- let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
- let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
- let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
-
- let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
- let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
- let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
-
- let (_desc, key_map, _valid_networks) =
- descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap();
- assert_eq!(key_map.len(), 3);
-
- let desc_key1: DescriptorKey<Segwitv0> = (xprv1, path1).into_descriptor_key().unwrap();
- let desc_key2: DescriptorKey<Segwitv0> = (xprv2, path2).into_descriptor_key().unwrap();
- let desc_key3: DescriptorKey<Segwitv0> = (xprv3, path3).into_descriptor_key().unwrap();
-
- let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap();
- let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap();
- let (key3, _key_map, _valid_networks) = desc_key3.extract(&secp).unwrap();
- assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*");
- assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*");
- assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*");
- }
-
- // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey<Segwitv0> to a pkh() descriptor should throw a compilation error
- #[test]
- fn test_script_context_validation() {
- // this compiles
- let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let path = bip32::DerivationPath::from_str("m/0").unwrap();
- let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
-
- let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
- assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2");
-
- // as expected this does not compile due to invalid context
- //let desc_key:DescriptorKey<Segwitv0> = (xprv, path.clone()).into_descriptor_key().unwrap();
- //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
- }
-
- #[test]
- fn test_dsl_modifiers() {
- let private_key =
- PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
- let (descriptor, _, _) =
- descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
-
- assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
- }
-
- #[test]
- #[should_panic(expected = "Miniscript(ContextError(UncompressedKeysNotAllowed))")]
- fn test_dsl_miniscript_checks() {
- let mut uncompressed_pk =
- PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
- uncompressed_pk.compressed = false;
-
- descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
- }
-
- #[test]
- fn test_dsl_tr_only_key() {
- let private_key =
- PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
- let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap();
-
- assert_eq!(
- descriptor.to_string(),
- "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v"
- )
- }
-
- #[test]
- fn test_dsl_tr_simple_tree() {
- let private_key =
- PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
- let (descriptor, _, _) =
- descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap();
-
- assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d")
- }
-
- #[test]
- fn test_dsl_tr_single_leaf() {
- let private_key =
- PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
- let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap();
-
- assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7")
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptor errors
-use core::fmt;
-
-/// Errors related to the parsing and usage of descriptors
-#[derive(Debug)]
-pub enum Error {
- /// Invalid HD Key path, such as having a wildcard but a length != 1
- InvalidHdKeyPath,
- /// The provided descriptor doesn't match its checksum
- InvalidDescriptorChecksum,
- /// The descriptor contains hardened derivation steps on public extended keys
- HardenedDerivationXpub,
- /// The descriptor contains multipath keys
- MultiPath,
-
- /// Error thrown while working with [`keys`](crate::keys)
- Key(crate::keys::KeyError),
- /// Error while extracting and manipulating policies
- Policy(crate::descriptor::policy::PolicyError),
-
- /// Invalid byte found in the descriptor checksum
- InvalidDescriptorCharacter(u8),
-
- /// BIP32 error
- Bip32(bitcoin::bip32::Error),
- /// Error during base58 decoding
- Base58(bitcoin::base58::Error),
- /// Key-related error
- Pk(bitcoin::key::Error),
- /// Miniscript error
- Miniscript(miniscript::Error),
- /// Hex decoding error
- Hex(bitcoin::hex::HexToBytesError),
-}
-
-impl From<crate::keys::KeyError> for Error {
- fn from(key_error: crate::keys::KeyError) -> Error {
- match key_error {
- crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
- crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
- e => Error::Key(e),
- }
- }
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"),
- Self::InvalidDescriptorChecksum => {
- write!(f, "The provided descriptor doesn't match its checksum")
- }
- Self::HardenedDerivationXpub => write!(
- f,
- "The descriptor contains hardened derivation steps on public extended keys"
- ),
- Self::MultiPath => write!(
- f,
- "The descriptor contains multipath keys, which are not supported yet"
- ),
- Self::Key(err) => write!(f, "Key error: {}", err),
- Self::Policy(err) => write!(f, "Policy error: {}", err),
- Self::InvalidDescriptorCharacter(char) => {
- write!(f, "Invalid descriptor character: {}", char)
- }
- Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
- Self::Base58(err) => write!(f, "Base58 error: {}", err),
- Self::Pk(err) => write!(f, "Key-related error: {}", err),
- Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
- Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for Error {}
-
-impl From<bitcoin::bip32::Error> for Error {
- fn from(err: bitcoin::bip32::Error) -> Self {
- Error::Bip32(err)
- }
-}
-
-impl From<bitcoin::base58::Error> for Error {
- fn from(err: bitcoin::base58::Error) -> Self {
- Error::Base58(err)
- }
-}
-
-impl From<bitcoin::key::Error> for Error {
- fn from(err: bitcoin::key::Error) -> Self {
- Error::Pk(err)
- }
-}
-
-impl From<miniscript::Error> for Error {
- fn from(err: miniscript::Error) -> Self {
- Error::Miniscript(err)
- }
-}
-
-impl From<bitcoin::hex::HexToBytesError> for Error {
- fn from(err: bitcoin::hex::HexToBytesError) -> Self {
- Error::Hex(err)
- }
-}
-
-impl From<crate::descriptor::policy::PolicyError> for Error {
- fn from(err: crate::descriptor::policy::PolicyError) -> Self {
- Error::Policy(err)
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptors
-//!
-//! This module contains generic utilities to work with descriptors, plus some re-exported types
-//! from [`miniscript`].
-
-use crate::collections::BTreeMap;
-use alloc::string::String;
-use alloc::vec::Vec;
-
-use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
-use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
-use bitcoin::{psbt, taproot};
-use bitcoin::{Network, TxOut};
-
-use miniscript::descriptor::{
- DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType,
- DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard,
-};
-pub use miniscript::{
- Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
-};
-use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
-
-use crate::descriptor::policy::BuildSatisfaction;
-
-pub mod checksum;
-#[doc(hidden)]
-pub mod dsl;
-pub mod error;
-pub mod policy;
-pub mod template;
-
-pub use self::checksum::calc_checksum;
-use self::checksum::calc_checksum_bytes;
-pub use self::error::Error as DescriptorError;
-pub use self::policy::Policy;
-use self::template::DescriptorTemplateOut;
-use crate::keys::{IntoDescriptorKey, KeyError};
-use crate::wallet::signer::SignersContainer;
-use crate::wallet::utils::SecpCtx;
-
-/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`]
-pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
-
-/// Alias for a [`Descriptor`] that contains extended **derived** keys
-pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
-
-/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
-/// [`psbt::Output`]
-///
-/// [`psbt::Input`]: bitcoin::psbt::Input
-/// [`psbt::Output`]: bitcoin::psbt::Output
-pub type HdKeyPaths = BTreeMap<secp256k1::PublicKey, KeySource>;
-
-/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or
-/// [`psbt::Output`]
-///
-/// [`psbt::Input`]: bitcoin::psbt::Input
-/// [`psbt::Output`]: bitcoin::psbt::Output
-pub type TapKeyOrigins = BTreeMap<XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
-
-/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
-pub trait IntoWalletDescriptor {
- /// Convert to wallet descriptor
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>;
-}
-
-impl IntoWalletDescriptor for &str {
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- let descriptor = match self.split_once('#') {
- Some((desc, original_checksum)) => {
- let checksum = calc_checksum_bytes(desc)?;
- if original_checksum.as_bytes() != checksum {
- return Err(DescriptorError::InvalidDescriptorChecksum);
- }
- desc
- }
- None => self,
- };
-
- ExtendedDescriptor::parse_descriptor(secp, descriptor)?
- .into_wallet_descriptor(secp, network)
- }
-}
-
-impl IntoWalletDescriptor for &String {
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- self.as_str().into_wallet_descriptor(secp, network)
- }
-}
-
-impl IntoWalletDescriptor for ExtendedDescriptor {
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- (self, KeyMap::default()).into_wallet_descriptor(secp, network)
- }
-}
-
-impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- use crate::keys::DescriptorKey;
-
- struct Translator<'s, 'd> {
- secp: &'s SecpCtx,
- descriptor: &'d ExtendedDescriptor,
- network: Network,
- }
-
- impl<'s, 'd> miniscript::Translator<DescriptorPublicKey, String, DescriptorError>
- for Translator<'s, 'd>
- {
- fn pk(&mut self, pk: &DescriptorPublicKey) -> Result<String, DescriptorError> {
- let secp = &self.secp;
-
- let (_, _, networks) = if self.descriptor.is_taproot() {
- let descriptor_key: DescriptorKey<miniscript::Tap> =
- pk.clone().into_descriptor_key()?;
- descriptor_key.extract(secp)?
- } else if self.descriptor.is_witness() {
- let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
- pk.clone().into_descriptor_key()?;
- descriptor_key.extract(secp)?
- } else {
- let descriptor_key: DescriptorKey<miniscript::Legacy> =
- pk.clone().into_descriptor_key()?;
- descriptor_key.extract(secp)?
- };
-
- if networks.contains(&self.network) {
- Ok(Default::default())
- } else {
- Err(DescriptorError::Key(KeyError::InvalidNetwork))
- }
- }
- fn sha256(
- &mut self,
- _sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
- ) -> Result<String, DescriptorError> {
- Ok(Default::default())
- }
- fn hash256(
- &mut self,
- _hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
- ) -> Result<String, DescriptorError> {
- Ok(Default::default())
- }
- fn ripemd160(
- &mut self,
- _ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
- ) -> Result<String, DescriptorError> {
- Ok(Default::default())
- }
- fn hash160(
- &mut self,
- _hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
- ) -> Result<String, DescriptorError> {
- Ok(Default::default())
- }
- }
-
- // check the network for the keys
- use miniscript::TranslateErr;
- match self.0.translate_pk(&mut Translator {
- secp,
- network,
- descriptor: &self.0,
- }) {
- Ok(_) => {}
- Err(TranslateErr::TranslatorErr(e)) => return Err(e),
- Err(TranslateErr::OuterError(e)) => return Err(e.into()),
- }
-
- Ok(self)
- }
-}
-
-impl IntoWalletDescriptor for DescriptorTemplateOut {
- fn into_wallet_descriptor(
- self,
- _secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- struct Translator {
- network: Network,
- }
-
- impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
- for Translator
- {
- fn pk(
- &mut self,
- pk: &DescriptorPublicKey,
- ) -> Result<DescriptorPublicKey, DescriptorError> {
- // workaround for xpubs generated by other key types, like bip39: since when the
- // conversion is made one network has to be chosen, what we generally choose
- // "mainnet", but then override the set of valid networks to specify that all of
- // them are valid. here we reset the network to make sure the wallet struct gets a
- // descriptor with the right network everywhere.
- let pk = match pk {
- DescriptorPublicKey::XPub(ref xpub) => {
- let mut xpub = xpub.clone();
- xpub.xkey.network = self.network;
-
- DescriptorPublicKey::XPub(xpub)
- }
- other => other.clone(),
- };
-
- Ok(pk)
- }
- miniscript::translate_hash_clone!(
- DescriptorPublicKey,
- DescriptorPublicKey,
- DescriptorError
- );
- }
-
- let (desc, keymap, networks) = self;
-
- if !networks.contains(&network) {
- return Err(DescriptorError::Key(KeyError::InvalidNetwork));
- }
-
- // fixup the network for keys that need it in the descriptor
- use miniscript::TranslateErr;
- let translated = match desc.translate_pk(&mut Translator { network }) {
- Ok(descriptor) => descriptor,
- Err(TranslateErr::TranslatorErr(e)) => return Err(e),
- Err(TranslateErr::OuterError(e)) => return Err(e.into()),
- };
- // ...and in the key map
- let fixed_keymap = keymap
- .into_iter()
- .map(|(mut k, mut v)| {
- match (&mut k, &mut v) {
- (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
- xpub.xkey.network = network;
- xprv.xkey.network = network;
- }
- (_, DescriptorSecretKey::Single(key)) => {
- key.key.network = network;
- }
- _ => {}
- }
-
- (k, v)
- })
- .collect();
-
- Ok((translated, fixed_keymap))
- }
-}
-
-/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
-/// descriptor
-pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
- inner: T,
- secp: &SecpCtx,
- network: Network,
-) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
-
- // Ensure the keys don't contain any hardened derivation steps or hardened wildcards
- let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
- if let DescriptorPublicKey::XPub(DescriptorXKey {
- derivation_path,
- wildcard,
- ..
- }) = k
- {
- return *wildcard == Wildcard::Hardened
- || derivation_path.into_iter().any(ChildNumber::is_hardened);
- }
-
- false
- });
- if descriptor_contains_hardened_steps {
- return Err(DescriptorError::HardenedDerivationXpub);
- }
-
- if descriptor.is_multipath() {
- return Err(DescriptorError::MultiPath);
- }
-
- // Run miniscript's sanity check, which will look for duplicated keys and other potential
- // issues
- descriptor.sanity_check()?;
-
- Ok((descriptor, keymap))
-}
-
-#[doc(hidden)]
-/// Used internally mainly by the `descriptor!()` and `fragment!()` macros
-pub trait CheckMiniscript<Ctx: miniscript::ScriptContext> {
- fn check_miniscript(&self) -> Result<(), miniscript::Error>;
-}
-
-impl<Ctx: miniscript::ScriptContext, Pk: miniscript::MiniscriptKey> CheckMiniscript<Ctx>
- for miniscript::Miniscript<Pk, Ctx>
-{
- fn check_miniscript(&self) -> Result<(), miniscript::Error> {
- Ctx::check_global_validity(self)?;
-
- Ok(())
- }
-}
-
-/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`]
-pub trait ExtractPolicy {
- /// Extract the spending [`policy`]
- fn extract_policy(
- &self,
- signers: &SignersContainer,
- psbt: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Result<Option<Policy>, DescriptorError>;
-}
-
-pub(crate) trait XKeyUtils {
- fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
-}
-
-impl<T> XKeyUtils for DescriptorMultiXKey<T>
-where
- T: InnerXKey,
-{
- fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
- match self.origin {
- Some((fingerprint, _)) => fingerprint,
- None => self.xkey.xkey_fingerprint(secp),
- }
- }
-}
-
-impl<T> XKeyUtils for DescriptorXKey<T>
-where
- T: InnerXKey,
-{
- fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
- match self.origin {
- Some((fingerprint, _)) => fingerprint,
- None => self.xkey.xkey_fingerprint(secp),
- }
- }
-}
-
-pub(crate) trait DescriptorMeta {
- fn is_witness(&self) -> bool;
- fn is_taproot(&self) -> bool;
- fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>>;
- fn derive_from_hd_keypaths(
- &self,
- hd_keypaths: &HdKeyPaths,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor>;
- fn derive_from_tap_key_origins(
- &self,
- tap_key_origins: &TapKeyOrigins,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor>;
- fn derive_from_psbt_key_origins(
- &self,
- key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor>;
- fn derive_from_psbt_input(
- &self,
- psbt_input: &psbt::Input,
- utxo: Option<TxOut>,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor>;
-}
-
-impl DescriptorMeta for ExtendedDescriptor {
- fn is_witness(&self) -> bool {
- matches!(
- self.desc_type(),
- DescriptorType::Wpkh
- | DescriptorType::ShWpkh
- | DescriptorType::Wsh
- | DescriptorType::ShWsh
- | DescriptorType::ShWshSortedMulti
- | DescriptorType::WshSortedMulti
- )
- }
-
- fn is_taproot(&self) -> bool {
- self.desc_type() == DescriptorType::Tr
- }
-
- fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>> {
- let mut answer = Vec::new();
-
- self.for_each_key(|pk| {
- if let DescriptorPublicKey::XPub(xpub) = pk {
- answer.push(xpub.clone());
- }
-
- true
- });
-
- answer
- }
-
- fn derive_from_psbt_key_origins(
- &self,
- key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor> {
- // Ensure that deriving `xpub` with `path` yields `expected`
- let verify_key =
- |xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
- let derived = xpub
- .xkey
- .derive_pub(secp, path)
- .expect("The path should never contain hardened derivation steps")
- .public_key;
-
- match expected {
- SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
- SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
- _ => false,
- }
- };
-
- let mut path_found = None;
-
- // using `for_any_key` should make this stop as soon as we return `true`
- self.for_any_key(|key| {
- if let DescriptorPublicKey::XPub(xpub) = key {
- // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
- // return the "prefix" that matched, so we remove that prefix from the full path
- // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
- // path of length 1 if the key is `wildcard` and an empty path otherwise.
- let root_fingerprint = xpub.root_fingerprint(secp);
- let derive_path = key_origins
- .get_key_value(&root_fingerprint)
- .and_then(|(fingerprint, (path, expected))| {
- xpub.matches(&(*fingerprint, (*path).clone()), secp)
- .zip(Some((path, expected)))
- })
- .and_then(|(prefix, (full_path, expected))| {
- let derive_path = full_path
- .into_iter()
- .skip(prefix.into_iter().count())
- .cloned()
- .collect::<DerivationPath>();
-
- // `derive_path` only contains the replacement index for the wildcard, if present, or
- // an empty path for fixed descriptors. To verify the key we also need the normal steps
- // that come before the wildcard, so we take them directly from `xpub` and then append
- // the final index
- if verify_key(
- xpub,
- &xpub.derivation_path.extend(derive_path.clone()),
- expected,
- ) {
- Some(derive_path)
- } else {
- None
- }
- });
-
- match derive_path {
- Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
- // Ignore hardened wildcards
- if let ChildNumber::Normal { index } = path[0] {
- path_found = Some(index);
- return true;
- }
- }
- Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
- path_found = Some(0);
- return true;
- }
- _ => {}
- }
- }
-
- false
- });
-
- path_found.map(|path| {
- self.at_derivation_index(path)
- .expect("We ignore hardened wildcards")
- })
- }
-
- fn derive_from_hd_keypaths(
- &self,
- hd_keypaths: &HdKeyPaths,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor> {
- // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
- let key_origins = hd_keypaths
- .iter()
- .map(|(pk, (fingerprint, path))| {
- (
- *fingerprint,
- (path, SinglePubKey::FullKey(PublicKey::new(*pk))),
- )
- })
- .collect();
- self.derive_from_psbt_key_origins(key_origins, secp)
- }
-
- fn derive_from_tap_key_origins(
- &self,
- tap_key_origins: &TapKeyOrigins,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor> {
- // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
- let key_origins = tap_key_origins
- .iter()
- .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk))))
- .collect();
- self.derive_from_psbt_key_origins(key_origins, secp)
- }
-
- fn derive_from_psbt_input(
- &self,
- psbt_input: &psbt::Input,
- utxo: Option<TxOut>,
- secp: &SecpCtx,
- ) -> Option<DerivedDescriptor> {
- if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
- return Some(derived);
- }
- if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
- return Some(derived);
- }
- if self.has_wildcard() {
- // We can't try to bruteforce the derivation index, exit here
- return None;
- }
-
- let descriptor = self.at_derivation_index(0).expect("0 is not hardened");
- match descriptor.desc_type() {
- // TODO: add pk() here
- DescriptorType::Pkh
- | DescriptorType::Wpkh
- | DescriptorType::ShWpkh
- | DescriptorType::Tr
- if utxo.is_some()
- && descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
- {
- Some(descriptor)
- }
- DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
- if psbt_input.redeem_script.is_some()
- && &descriptor.explicit_script().unwrap()
- == psbt_input.redeem_script.as_ref().unwrap() =>
- {
- Some(descriptor)
- }
- DescriptorType::Wsh
- | DescriptorType::ShWsh
- | DescriptorType::ShWshSortedMulti
- | DescriptorType::WshSortedMulti
- if psbt_input.witness_script.is_some()
- && &descriptor.explicit_script().unwrap()
- == psbt_input.witness_script.as_ref().unwrap() =>
- {
- Some(descriptor)
- }
- _ => None,
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use alloc::string::ToString;
- use core::str::FromStr;
-
- use assert_matches::assert_matches;
- use bitcoin::hex::FromHex;
- use bitcoin::secp256k1::Secp256k1;
- use bitcoin::ScriptBuf;
- use bitcoin::{bip32, Psbt};
-
- use super::*;
- use crate::psbt::PsbtUtils;
-
- #[test]
- fn test_derive_from_psbt_input_wpkh_wif() {
- let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
- "wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)",
- )
- .unwrap();
- let psbt = Psbt::deserialize(
- &Vec::<u8>::from_hex(
- "70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\
- 11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\
- 001493ce48570b55c42c2af816aeaba06cfee1224fae000000000001011fa086\
- 01000000000016001493ce48570b55c42c2af816aeaba06cfee1224fae010304\
- 010000000000",
- )
- .unwrap(),
- )
- .unwrap();
-
- assert!(descriptor
- .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
- .is_some());
- }
-
- #[test]
- fn test_derive_from_psbt_input_pkh_tpub() {
- let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
- "pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)",
- )
- .unwrap();
- let psbt = Psbt::deserialize(
- &Vec::<u8>::from_hex(
- "70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\
- e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\
- a91432bb94283282f72b2e034709e348c44d5a4db0ef8700000000000100f902\
- 0000000001010167e99c0eb67640f3a1b6805f2d8be8238c947f8aaf49eb0a9c\
- bee6a42c984200000000171600142b29a22019cca05b9c2b2d283a4c4489e1cf\
- 9f8ffeffffff02a01dced06100000017a914e2abf033cadbd74f0f4c74946201\
- decd20d5c43c8780969800000000001976a9148b0fce5fb1264e599a65387313\
- 3c95478b902eb288ac02473044022015d9211576163fa5b001e84dfa3d44efd9\
- 86b8f3a0d3d2174369288b2b750906022048dacc0e5d73ae42512fd2b97e2071\
- a8d0bce443b390b1fe0b8128fe70ec919e01210232dad1c5a67dcb0116d407e2\
- 52584228ab7ec00e8b9779d0c3ffe8114fc1a7d2c80600000103040100000022\
- 0603433b83583f8c4879b329dd08bbc7da935e4cc02f637ff746e05f0466ffb2\
- a6a2180f0569432c00008000000080000000800a000000000000000000",
- )
- .unwrap(),
- )
- .unwrap();
-
- assert!(descriptor
- .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
- .is_some());
- }
-
- #[test]
- fn test_derive_from_psbt_input_wsh() {
- let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
- "wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))",
- )
- .unwrap();
- let psbt = Psbt::deserialize(
- &Vec::<u8>::from_hex(
- "70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\
- 67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\
- a914ad105f61102e0d01d7af40d06d6a5c3ae2f7fde387000000000001012b80\
- 969800000000002200203ca72f106a72234754890ca7640c43f65d2174e44d33\
- 336030f9059345091044010304010000000105252103b6633fef2397a0a9de9d\
- 7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14ad56b20000",
- )
- .unwrap(),
- )
- .unwrap();
-
- assert!(descriptor
- .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
- .is_some());
- }
-
- #[test]
- fn test_derive_from_psbt_input_sh() {
- let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
- "sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))",
- )
- .unwrap();
- let psbt = Psbt::deserialize(
- &Vec::<u8>::from_hex(
- "70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\
- a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\
- a91457b148ba4d3e5fa8608a8657875124e3d1c9390887f09c0900000100e002\
- 0000000001016ba1bbe05cc93574a0d611ec7d93ad0ab6685b28d0cd80e8a82d\
- debb326643c90100000000feffffff02809698000000000017a914d9a6e8c455\
- 8e16c8253afe53ce37ad61cf4c38c487403504cf6100000017a9144044fb6e0b\
- 757dfc1b34886b6a95aef4d3db137e870247304402202a9b72d939bcde8ba2a1\
- e0980597e47af4f5c152a78499143c3d0a78ac2286a602207a45b1df9e93b8c9\
- 6f09f5c025fe3e413ca4b905fe65ee55d32a3276439a9b8f012102dc1fcc2636\
- 4da1aa718f03d8d9bd6f2ff410ed2cf1245a168aa3bcc995ac18e0a806000001\
- 03040100000001042821021403881a5587297818fcaf17d239cefca22fce84a4\
- 5b3b1d23e836c4af671dbbad03f09c09b10000",
- )
- .unwrap(),
- )
- .unwrap();
-
- assert!(descriptor
- .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
- .is_some());
- }
-
- #[test]
- fn test_to_wallet_descriptor_fixup_networks() {
- use crate::keys::{any_network, IntoDescriptorKey};
-
- let secp = Secp256k1::new();
-
- let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
- let path = bip32::DerivationPath::from_str("m/0").unwrap();
-
- // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
- // we are using an "xpub"
- let key = (xprv, path.clone()).into_descriptor_key().unwrap();
- // override it with any. this happens in some key conversions, like bip39
- let key = key.override_valid_networks(any_network());
-
- // make a descriptor out of it
- let desc = crate::descriptor!(wpkh(key)).unwrap();
- // this should convert the key that supports "any_network" to the right network (testnet)
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
-
- let mut xprv_testnet = xprv;
- xprv_testnet.network = Network::Testnet;
-
- let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet);
- let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
- xkey: xpub_testnet,
- origin: None,
- derivation_path: path,
- wildcard: Wildcard::Unhardened,
- });
-
- assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
- assert_eq!(
- keymap
- .get(&desc_pubkey)
- .map(|key| key.to_public(&secp).unwrap()),
- Some(desc_pubkey)
- );
- }
-
- // test IntoWalletDescriptor trait from &str with and without checksum appended
- #[test]
- fn test_descriptor_from_str_with_checksum() {
- let secp = Secp256k1::new();
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
- }
-
- // test IntoWalletDescriptor trait from &str with keys from right and wrong network
- #[test]
- fn test_descriptor_from_str_with_keys_network() {
- let secp = Secp256k1::new();
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Regtest);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Regtest);
- assert!(desc.is_ok());
-
- let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
- .into_wallet_descriptor(&secp, Network::Testnet);
- assert!(desc.is_ok());
-
- let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
- .into_wallet_descriptor(&secp, Network::Bitcoin);
- assert!(desc.is_ok());
-
- let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Bitcoin);
- assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
-
- let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
- .into_wallet_descriptor(&secp, Network::Bitcoin);
- assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
- }
-
- // test IntoWalletDescriptor trait from the output of the descriptor!() macro
- #[test]
- fn test_descriptor_from_str_from_output_of_macro() {
- let secp = Secp256k1::new();
-
- let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
- let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
- let key = (tpub, path).into_descriptor_key().unwrap();
-
- // make a descriptor out of it
- let desc = crate::descriptor!(wpkh(key)).unwrap();
-
- let (wallet_desc, _) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let wallet_desc_str = wallet_desc.to_string();
- assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw");
-
- let (wallet_desc2, _) = wallet_desc_str
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- assert_eq!(wallet_desc, wallet_desc2)
- }
-
- #[test]
- fn test_into_wallet_descriptor_checked() {
- let secp = Secp256k1::new();
-
- let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
- let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
-
- assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
-
- let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
- let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
-
- assert_matches!(result, Err(DescriptorError::MultiPath));
-
- // repeated pubkeys
- let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
- let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
-
- assert!(result.is_err());
- }
-
- #[test]
- fn test_sh_wsh_sortedmulti_redeemscript() {
- use miniscript::psbt::PsbtInputExt;
-
- let secp = Secp256k1::new();
-
- let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
- let (descriptor, _) =
- into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
-
- let descriptor = descriptor.at_derivation_index(0).unwrap();
-
- let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
-
- let mut psbt_input = psbt::Input::default();
- psbt_input
- .update_with_descriptor_unchecked(&descriptor)
- .unwrap();
-
- assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh()));
- assert_eq!(psbt_input.witness_script, Some(script));
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptor policy
-//!
-//! This module implements the logic to extract and represent the spending policies of a descriptor
-//! in a more human-readable format.
-//!
-//! This is an **EXPERIMENTAL** feature, API and other major changes are expected.
-//!
-//! ## Example
-//!
-//! ```
-//! # use std::sync::Arc;
-//! # use bdk::descriptor::*;
-//! # use bdk::wallet::signer::*;
-//! # use bdk::bitcoin::secp256k1::Secp256k1;
-//! use bdk::descriptor::policy::BuildSatisfaction;
-//! let secp = Secp256k1::new();
-//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
-//!
-//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?;
-//! println!("{:?}", extended_desc);
-//!
-//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
-//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
-//! println!("policy: {}", serde_json::to_string(&policy).unwrap());
-//! # Ok::<(), anyhow::Error>(())
-//! ```
-
-use crate::collections::{BTreeMap, HashSet, VecDeque};
-use alloc::string::String;
-use alloc::vec::Vec;
-use core::cmp::max;
-
-use core::fmt;
-
-use serde::ser::SerializeMap;
-use serde::{Serialize, Serializer};
-
-use bitcoin::bip32::Fingerprint;
-use bitcoin::hashes::{hash160, ripemd160, sha256};
-use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
-
-use miniscript::descriptor::{
- DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
-};
-use miniscript::hash256;
-use miniscript::{
- Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
-};
-
-use crate::descriptor::ExtractPolicy;
-use crate::keys::ExtScriptContext;
-use crate::wallet::signer::{SignerId, SignersContainer};
-use crate::wallet::utils::{After, Older, SecpCtx};
-
-use super::checksum::calc_checksum;
-use super::error::Error;
-use super::XKeyUtils;
-use bitcoin::psbt::{self, Psbt};
-use miniscript::psbt::PsbtInputSatisfier;
-
-/// A unique identifier for a key
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum PkOrF {
- /// A legacy public key
- Pubkey(PublicKey),
- /// A x-only public key
- XOnlyPubkey(XOnlyPublicKey),
- /// An extended key fingerprint
- Fingerprint(Fingerprint),
-}
-
-impl PkOrF {
- fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
- match k {
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::FullKey(pk),
- ..
- }) => PkOrF::Pubkey(*pk),
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::XOnly(pk),
- ..
- }) => PkOrF::XOnlyPubkey(*pk),
- DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
- DescriptorPublicKey::MultiXPub(multi) => {
- PkOrF::Fingerprint(multi.root_fingerprint(secp))
- }
- }
- }
-}
-
-/// An item that needs to be satisfied
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-#[serde(tag = "type", rename_all = "UPPERCASE")]
-pub enum SatisfiableItem {
- // Leaves
- /// ECDSA Signature for a raw public key
- EcdsaSignature(PkOrF),
- /// Schnorr Signature for a raw public key
- SchnorrSignature(PkOrF),
- /// SHA256 preimage hash
- Sha256Preimage {
- /// The digest value
- hash: sha256::Hash,
- },
- /// Double SHA256 preimage hash
- Hash256Preimage {
- /// The digest value
- hash: hash256::Hash,
- },
- /// RIPEMD160 preimage hash
- Ripemd160Preimage {
- /// The digest value
- hash: ripemd160::Hash,
- },
- /// SHA256 then RIPEMD160 preimage hash
- Hash160Preimage {
- /// The digest value
- hash: hash160::Hash,
- },
- /// Absolute timeclock timestamp
- AbsoluteTimelock {
- /// The timelock value
- value: absolute::LockTime,
- },
- /// Relative timelock locktime
- RelativeTimelock {
- /// The timelock value
- value: Sequence,
- },
- /// Multi-signature public keys with threshold count
- Multisig {
- /// The raw public key or extended key fingerprint
- keys: Vec<PkOrF>,
- /// The required threshold count
- threshold: usize,
- },
-
- // Complex item
- /// Threshold items with threshold count
- Thresh {
- /// The policy items
- items: Vec<Policy>,
- /// The required threshold count
- threshold: usize,
- },
-}
-
-impl SatisfiableItem {
- /// Returns whether the [`SatisfiableItem`] is a leaf item
- pub fn is_leaf(&self) -> bool {
- !matches!(
- self,
- SatisfiableItem::Thresh {
- items: _,
- threshold: _,
- }
- )
- }
-
- /// Returns a unique id for the [`SatisfiableItem`]
- pub fn id(&self) -> String {
- calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
- .expect("Failed to compute a SatisfiableItem id")
- }
-}
-
-fn combinations(vec: &[usize], size: usize) -> Vec<Vec<usize>> {
- assert!(vec.len() >= size);
-
- let mut answer = Vec::new();
-
- let mut queue = VecDeque::new();
- for (index, val) in vec.iter().enumerate() {
- let mut new_vec = Vec::with_capacity(size);
- new_vec.push(*val);
- queue.push_back((index, new_vec));
- }
-
- while let Some((index, vals)) = queue.pop_front() {
- if vals.len() >= size {
- answer.push(vals);
- } else {
- for (new_index, val) in vec.iter().skip(index + 1).enumerate() {
- let mut cloned = vals.clone();
- cloned.push(*val);
- queue.push_front((new_index, cloned));
- }
- }
- }
-
- answer
-}
-
-fn mix<T: Clone>(vec: Vec<Vec<T>>) -> Vec<Vec<T>> {
- if vec.is_empty() || vec.iter().any(Vec::is_empty) {
- return vec![];
- }
-
- let mut answer = Vec::new();
- let size = vec.len();
-
- let mut queue = VecDeque::new();
- for i in &vec[0] {
- let mut new_vec = Vec::with_capacity(size);
- new_vec.push(i.clone());
- queue.push_back(new_vec);
- }
-
- while let Some(vals) = queue.pop_front() {
- if vals.len() >= size {
- answer.push(vals);
- } else {
- let level = vals.len();
- for i in &vec[level] {
- let mut cloned = vals.clone();
- cloned.push(i.clone());
- queue.push_front(cloned);
- }
- }
- }
-
- answer
-}
-
-/// Type for a map of sets of [`Condition`] items keyed by each set's index
-pub type ConditionMap = BTreeMap<usize, HashSet<Condition>>;
-/// Type for a map of folded sets of [`Condition`] items keyed by a vector of the combined set's indexes
-pub type FoldedConditionMap = BTreeMap<Vec<usize>, HashSet<Condition>>;
-
-fn serialize_folded_cond_map<S>(
- input_map: &FoldedConditionMap,
- serializer: S,
-) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- let mut map = serializer.serialize_map(Some(input_map.len()))?;
- for (k, v) in input_map {
- let k_string = format!("{:?}", k);
- map.serialize_entry(&k_string, v)?;
- }
- map.end()
-}
-
-/// Represent if and how much a policy item is satisfied by the wallet's descriptor
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-#[serde(tag = "type", rename_all = "UPPERCASE")]
-pub enum Satisfaction {
- /// Only a partial satisfaction of some kind of threshold policy
- Partial {
- /// Total number of items
- n: usize,
- /// Threshold
- m: usize,
- /// The items that can be satisfied by the descriptor or are satisfied in the PSBT
- items: Vec<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// Whether the items are sorted in lexicographic order (used by `sortedmulti`)
- sorted: Option<bool>,
- #[serde(skip_serializing_if = "BTreeMap::is_empty")]
- /// Extra conditions that also need to be satisfied
- conditions: ConditionMap,
- },
- /// Can reach the threshold of some kind of threshold policy
- PartialComplete {
- /// Total number of items
- n: usize,
- /// Threshold
- m: usize,
- /// The items that can be satisfied by the descriptor
- items: Vec<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// Whether the items are sorted in lexicographic order (used by `sortedmulti`)
- sorted: Option<bool>,
- #[serde(
- serialize_with = "serialize_folded_cond_map",
- skip_serializing_if = "BTreeMap::is_empty"
- )]
- /// Extra conditions that also need to be satisfied
- conditions: FoldedConditionMap,
- },
-
- /// Can satisfy the policy item
- Complete {
- /// Extra conditions that also need to be satisfied
- condition: Condition,
- },
- /// Cannot satisfy or contribute to the policy item
- None,
-}
-
-impl Satisfaction {
- /// Returns whether the [`Satisfaction`] is a leaf item
- pub fn is_leaf(&self) -> bool {
- match self {
- Satisfaction::None | Satisfaction::Complete { .. } => true,
- Satisfaction::PartialComplete { .. } | Satisfaction::Partial { .. } => false,
- }
- }
-
- // add `inner` as one of self's partial items. this only makes sense on partials
- fn add(&mut self, inner: &Satisfaction, inner_index: usize) -> Result<(), PolicyError> {
- match self {
- Satisfaction::None | Satisfaction::Complete { .. } => Err(PolicyError::AddOnLeaf),
- Satisfaction::PartialComplete { .. } => Err(PolicyError::AddOnPartialComplete),
- Satisfaction::Partial {
- n,
- ref mut conditions,
- ref mut items,
- ..
- } => {
- if inner_index >= *n || items.contains(&inner_index) {
- return Err(PolicyError::IndexOutOfRange(inner_index));
- }
-
- match inner {
- // not relevant if not completed yet
- Satisfaction::None | Satisfaction::Partial { .. } => return Ok(()),
- Satisfaction::Complete { condition } => {
- items.push(inner_index);
- conditions.insert(inner_index, vec![*condition].into_iter().collect());
- }
- Satisfaction::PartialComplete {
- conditions: other_conditions,
- ..
- } => {
- items.push(inner_index);
- let conditions_set = other_conditions
- .values()
- .fold(HashSet::new(), |set, i| set.union(i).cloned().collect());
- conditions.insert(inner_index, conditions_set);
- }
- }
-
- Ok(())
- }
- }
- }
-
- fn finalize(&mut self) {
- // if partial try to bump it to a partialcomplete
- if let Satisfaction::Partial {
- n,
- m,
- items,
- conditions,
- sorted,
- } = self
- {
- if items.len() >= *m {
- let mut map = BTreeMap::new();
- let indexes = combinations(items, *m);
- // `indexes` at this point is a Vec<Vec<usize>>, with the "n choose k" of items (m of n)
- indexes
- .into_iter()
- // .inspect(|x| println!("--- orig --- {:?}", x))
- // we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one
- // condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we
- // consider every possible options and check whether or not they are compatible.
- // since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out
- .flat_map(|i_vec| {
- mix(i_vec
- .iter()
- .map(|i| {
- conditions
- .get(i)
- .map(|set| set.clone().into_iter().collect())
- .unwrap_or_default()
- })
- .collect())
- .into_iter()
- .map(|x| (i_vec.clone(), x))
- .collect::<Vec<(Vec<usize>, Vec<Condition>)>>()
- })
- // .inspect(|x| println!("flat {:?}", x))
- // try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err
- .map(|(key, val)| {
- (
- key,
- val.into_iter()
- .try_fold(Condition::default(), |acc, v| acc.merge(&v)),
- )
- })
- // .inspect(|x| println!("try_fold {:?}", x))
- // filter out all the incompatible combinations
- .filter(|(_, val)| val.is_ok())
- // .inspect(|x| println!("filter {:?}", x))
- // push them into the map
- .for_each(|(key, val)| {
- map.entry(key)
- .or_insert_with(HashSet::new)
- .insert(val.unwrap());
- });
- // TODO: if the map is empty, the conditions are not compatible, return an error?
- *self = Satisfaction::PartialComplete {
- n: *n,
- m: *m,
- items: items.clone(),
- conditions: map,
- sorted: *sorted,
- };
- }
- }
- }
-}
-
-impl From<bool> for Satisfaction {
- fn from(other: bool) -> Self {
- if other {
- Satisfaction::Complete {
- condition: Default::default(),
- }
- } else {
- Satisfaction::None
- }
- }
-}
-
-/// Descriptor spending policy
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct Policy {
- /// Identifier for this policy node
- pub id: String,
-
- /// Type of this policy node
- #[serde(flatten)]
- pub item: SatisfiableItem,
- /// How much a given PSBT already satisfies this policy node in terms of signatures
- pub satisfaction: Satisfaction,
- /// How the wallet's descriptor can satisfy this policy node
- pub contribution: Satisfaction,
-}
-
-/// An extra condition that must be satisfied but that is out of control of the user
-/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
-#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
-pub struct Condition {
- /// Optional CheckSequenceVerify condition
- #[serde(skip_serializing_if = "Option::is_none")]
- pub csv: Option<Sequence>,
- /// Optional timelock condition
- #[serde(skip_serializing_if = "Option::is_none")]
- pub timelock: Option<absolute::LockTime>,
-}
-
-impl Condition {
- fn merge_nlocktime(
- a: absolute::LockTime,
- b: absolute::LockTime,
- ) -> Result<absolute::LockTime, PolicyError> {
- if !a.is_same_unit(b) {
- Err(PolicyError::MixedTimelockUnits)
- } else if a > b {
- Ok(a)
- } else {
- Ok(b)
- }
- }
-
- fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
- if a.is_time_locked() != b.is_time_locked() {
- Err(PolicyError::MixedTimelockUnits)
- } else {
- Ok(max(a, b))
- }
- }
-
- pub(crate) fn merge(mut self, other: &Condition) -> Result<Self, PolicyError> {
- match (self.csv, other.csv) {
- (Some(a), Some(b)) => self.csv = Some(Self::merge_nsequence(a, b)?),
- (None, any) => self.csv = any,
- _ => {}
- }
-
- match (self.timelock, other.timelock) {
- (Some(a), Some(b)) => self.timelock = Some(Self::merge_nlocktime(a, b)?),
- (None, any) => self.timelock = any,
- _ => {}
- }
-
- Ok(self)
- }
-
- /// Returns `true` if there are no extra conditions to verify
- pub fn is_null(&self) -> bool {
- self.csv.is_none() && self.timelock.is_none()
- }
-}
-
-/// Errors that can happen while extracting and manipulating policies
-#[derive(Debug, PartialEq, Eq)]
-pub enum PolicyError {
- /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`]
- NotEnoughItemsSelected(String),
- /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`]
- IndexOutOfRange(usize),
- /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`]
- AddOnLeaf,
- /// Can not add to an item that is [`Satisfaction::PartialComplete`]
- AddOnPartialComplete,
- /// Can not merge CSV or timelock values unless both are less than or both are equal or greater than 500_000_000
- MixedTimelockUnits,
- /// Incompatible conditions (not currently used)
- IncompatibleConditions,
-}
-
-impl fmt::Display for PolicyError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err),
- Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index),
- Self::AddOnLeaf => write!(f, "Add on leaf"),
- Self::AddOnPartialComplete => write!(f, "Add on partial complete"),
- Self::MixedTimelockUnits => write!(f, "Mixed timelock units"),
- Self::IncompatibleConditions => write!(f, "Incompatible conditions"),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for PolicyError {}
-
-impl Policy {
- fn new(item: SatisfiableItem) -> Self {
- Policy {
- id: item.id(),
- item,
- satisfaction: Satisfaction::None,
- contribution: Satisfaction::None,
- }
- }
-
- fn make_and(a: Option<Policy>, b: Option<Policy>) -> Result<Option<Policy>, PolicyError> {
- match (a, b) {
- (None, None) => Ok(None),
- (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
- (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2),
- }
- }
-
- fn make_or(a: Option<Policy>, b: Option<Policy>) -> Result<Option<Policy>, PolicyError> {
- match (a, b) {
- (None, None) => Ok(None),
- (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
- (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1),
- }
- }
-
- fn make_thresh(items: Vec<Policy>, threshold: usize) -> Result<Option<Policy>, PolicyError> {
- if threshold == 0 {
- return Ok(None);
- }
-
- let mut contribution = Satisfaction::Partial {
- n: items.len(),
- m: threshold,
- items: vec![],
- conditions: Default::default(),
- sorted: None,
- };
- let mut satisfaction = contribution.clone();
- for (index, item) in items.iter().enumerate() {
- contribution.add(&item.contribution, index)?;
- satisfaction.add(&item.satisfaction, index)?;
- }
-
- contribution.finalize();
- satisfaction.finalize();
-
- let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
- policy.contribution = contribution;
- policy.satisfaction = satisfaction;
-
- Ok(Some(policy))
- }
-
- fn make_multisig<Ctx: ScriptContext + 'static>(
- keys: &[DescriptorPublicKey],
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- threshold: usize,
- sorted: bool,
- secp: &SecpCtx,
- ) -> Result<Option<Policy>, PolicyError> {
- if threshold == 0 {
- return Ok(None);
- }
-
- let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect();
-
- let mut contribution = Satisfaction::Partial {
- n: keys.len(),
- m: threshold,
- items: vec![],
- conditions: Default::default(),
- sorted: Some(sorted),
- };
- let mut satisfaction = contribution.clone();
-
- for (index, key) in keys.iter().enumerate() {
- if signers.find(signer_id(key, secp)).is_some() {
- contribution.add(
- &Satisfaction::Complete {
- condition: Default::default(),
- },
- index,
- )?;
- }
-
- if let Some(psbt) = build_sat.psbt() {
- if Ctx::find_signature(psbt, key, secp) {
- satisfaction.add(
- &Satisfaction::Complete {
- condition: Default::default(),
- },
- index,
- )?;
- }
- }
- }
- satisfaction.finalize();
- contribution.finalize();
-
- let mut policy: Policy = SatisfiableItem::Multisig {
- keys: parsed_keys,
- threshold,
- }
- .into();
- policy.contribution = contribution;
- policy.satisfaction = satisfaction;
-
- Ok(Some(policy))
- }
-
- /// Return whether or not a specific path in the policy tree is required to unambiguously
- /// create a transaction
- ///
- /// What this means is that for some spending policies the user should select which paths in
- /// the tree it intends to satisfy while signing, because the transaction must be created differently based
- /// on that.
- pub fn requires_path(&self) -> bool {
- self.get_condition(&BTreeMap::new()).is_err()
- }
-
- /// Return the conditions that are set by the spending policy for a given path in the
- /// policy tree
- pub fn get_condition(
- &self,
- path: &BTreeMap<String, Vec<usize>>,
- ) -> Result<Condition, PolicyError> {
- // if items.len() == threshold, selected can be omitted and we take all of them by default
- let default = match &self.item {
- SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => {
- (0..*threshold).collect()
- }
- SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(),
- _ => HashSet::new(),
- };
- let selected: HashSet<_> = match path.get(&self.id) {
- Some(arr) => arr.iter().copied().collect(),
- _ => default,
- };
-
- match &self.item {
- SatisfiableItem::Thresh { items, threshold } => {
- let mapped_req = items
- .iter()
- .map(|i| i.get_condition(path))
- .collect::<Vec<_>>();
-
- // if all the requirements are null we don't care about `selected` because there
- // are no requirements
- if mapped_req
- .iter()
- .all(|cond| matches!(cond, Ok(c) if c.is_null()))
- {
- return Ok(Condition::default());
- }
-
- // make sure all the indexes in the `selected` list are within range
- for index in &selected {
- if *index >= items.len() {
- return Err(PolicyError::IndexOutOfRange(*index));
- }
- }
-
- // if we have something, make sure we have enough items. note that the user can set
- // an empty value for this step in case of n-of-n, because `selected` is set to all
- // the elements above
- if selected.len() < *threshold {
- return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
- }
-
- // check the selected items, see if there are conflicting requirements
- mapped_req
- .into_iter()
- .enumerate()
- .filter(|(index, _)| selected.contains(index))
- .try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?))
- }
- SatisfiableItem::Multisig { keys, threshold } => {
- if selected.len() < *threshold {
- return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
- }
- if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) {
- return Err(PolicyError::IndexOutOfRange(item));
- }
-
- Ok(Condition::default())
- }
- SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition {
- csv: None,
- timelock: Some(*value),
- }),
- SatisfiableItem::RelativeTimelock { value } => Ok(Condition {
- csv: Some(*value),
- timelock: None,
- }),
- _ => Ok(Condition::default()),
- }
- }
-}
-
-impl From<SatisfiableItem> for Policy {
- fn from(other: SatisfiableItem) -> Self {
- Self::new(other)
- }
-}
-
-fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
- // For consistency we always compute the key hash in "ecdsa" form (with the leading sign
- // prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
- // for a key, so it doesn't really matter how the identifier is computed.
- match key {
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::FullKey(pk),
- ..
- }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::XOnly(pk),
- ..
- }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
- DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
- DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(),
- }
-}
-
-fn make_generic_signature<M: Fn() -> SatisfiableItem, F: Fn(&Psbt) -> bool>(
- key: &DescriptorPublicKey,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- make_policy: M,
- find_sig: F,
-) -> Policy {
- let mut policy: Policy = make_policy().into();
-
- policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
- Satisfaction::Complete {
- condition: Default::default(),
- }
- } else {
- Satisfaction::None
- };
-
- if let Some(psbt) = build_sat.psbt() {
- policy.satisfaction = if find_sig(psbt) {
- Satisfaction::Complete {
- condition: Default::default(),
- }
- } else {
- Satisfaction::None
- };
- }
-
- policy
-}
-
-fn generic_sig_in_psbt<
- // C is for "check", it's a closure we use to *check* if a psbt input contains the signature
- // for a specific key
- C: Fn(&psbt::Input, &SinglePubKey) -> bool,
- // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input
- E: Fn(&psbt::Input, Fingerprint) -> Option<SinglePubKey>,
->(
- psbt: &Psbt,
- key: &DescriptorPublicKey,
- secp: &SecpCtx,
- check: C,
- extract: E,
-) -> bool {
- //TODO check signature validity
- psbt.inputs.iter().all(|input| match key {
- DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
- DescriptorPublicKey::XPub(xpub) => {
- //TODO check actual derivation matches
- match extract(input, xpub.root_fingerprint(secp)) {
- Some(pubkey) => check(input, &pubkey),
- None => false,
- }
- }
- DescriptorPublicKey::MultiXPub(xpub) => {
- //TODO check actual derivation matches
- match extract(input, xpub.root_fingerprint(secp)) {
- Some(pubkey) => check(input, &pubkey),
- None => false,
- }
- }
- })
-}
-
-trait SigExt: ScriptContext {
- fn make_signature(
- key: &DescriptorPublicKey,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Policy;
-
- fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool;
-}
-
-impl<T: ScriptContext + 'static> SigExt for T {
- fn make_signature(
- key: &DescriptorPublicKey,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Policy {
- if T::as_enum().is_taproot() {
- make_generic_signature(
- key,
- signers,
- build_sat,
- secp,
- || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)),
- |psbt| Self::find_signature(psbt, key, secp),
- )
- } else {
- make_generic_signature(
- key,
- signers,
- build_sat,
- secp,
- || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)),
- |psbt| Self::find_signature(psbt, key, secp),
- )
- }
- }
-
- fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
- if T::as_enum().is_taproot() {
- generic_sig_in_psbt(
- psbt,
- key,
- secp,
- |input, pk| {
- let pk = match pk {
- SinglePubKey::XOnly(pk) => pk,
- _ => return false,
- };
-
- if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() {
- true
- } else {
- input.tap_script_sigs.keys().any(|(sk, _)| sk == pk)
- }
- },
- |input, fing| {
- input
- .tap_key_origins
- .iter()
- .find(|(_, (_, (f, _)))| f == &fing)
- .map(|(pk, _)| SinglePubKey::XOnly(*pk))
- },
- )
- } else {
- generic_sig_in_psbt(
- psbt,
- key,
- secp,
- |input, pk| match pk {
- SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk),
- _ => false,
- },
- |input, fing| {
- input
- .bip32_derivation
- .iter()
- .find(|(_, (f, _))| f == &fing)
- .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk)))
- },
- )
- }
- }
-}
-
-impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
- fn extract_policy(
- &self,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Result<Option<Policy>, Error> {
- Ok(match &self.node {
- // Leaves
- Terminal::True | Terminal::False => None,
- Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)),
- Terminal::PkH(pubkey_hash) => {
- Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
- }
- Terminal::After(value) => {
- let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
- value: (*value).into(),
- }
- .into();
- policy.contribution = Satisfaction::Complete {
- condition: Condition {
- timelock: Some((*value).into()),
- csv: None,
- },
- };
- if let BuildSatisfaction::PsbtTimelocks {
- current_height,
- psbt,
- ..
- } = build_sat
- {
- let after = After::new(Some(current_height), false);
- let after_sat =
- Satisfier::<bitcoin::PublicKey>::check_after(&after, (*value).into());
- let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
- Satisfier::<bitcoin::PublicKey>::check_after(&sat, (*value).into())
- });
- if after_sat && inputs_sat {
- policy.satisfaction = policy.contribution.clone();
- }
- }
-
- Some(policy)
- }
- Terminal::Older(value) => {
- let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into();
- policy.contribution = Satisfaction::Complete {
- condition: Condition {
- timelock: None,
- csv: Some(*value),
- },
- };
- if let BuildSatisfaction::PsbtTimelocks {
- current_height,
- input_max_height,
- psbt,
- } = build_sat
- {
- let older = Older::new(Some(current_height), Some(input_max_height), false);
- let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
- let inputs_sat = psbt_inputs_sat(psbt)
- .all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
- if older_sat && inputs_sat {
- policy.satisfaction = policy.contribution.clone();
- }
- }
-
- Some(policy)
- }
- Terminal::Sha256(hash) => Some(SatisfiableItem::Sha256Preimage { hash: *hash }.into()),
- Terminal::Hash256(hash) => {
- Some(SatisfiableItem::Hash256Preimage { hash: *hash }.into())
- }
- Terminal::Ripemd160(hash) => {
- Some(SatisfiableItem::Ripemd160Preimage { hash: *hash }.into())
- }
- Terminal::Hash160(hash) => {
- Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
- }
- Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
- Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
- }
- // Identities
- Terminal::Alt(inner)
- | Terminal::Swap(inner)
- | Terminal::Check(inner)
- | Terminal::DupIf(inner)
- | Terminal::Verify(inner)
- | Terminal::NonZero(inner)
- | Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?,
- // Complex policies
- Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and(
- a.extract_policy(signers, build_sat, secp)?,
- b.extract_policy(signers, build_sat, secp)?,
- )?,
- Terminal::AndOr(x, y, z) => Policy::make_or(
- Policy::make_and(
- x.extract_policy(signers, build_sat, secp)?,
- y.extract_policy(signers, build_sat, secp)?,
- )?,
- z.extract_policy(signers, build_sat, secp)?,
- )?,
- Terminal::OrB(a, b)
- | Terminal::OrD(a, b)
- | Terminal::OrC(a, b)
- | Terminal::OrI(a, b) => Policy::make_or(
- a.extract_policy(signers, build_sat, secp)?,
- b.extract_policy(signers, build_sat, secp)?,
- )?,
- Terminal::Thresh(k, nodes) => {
- let mut threshold = *k;
- let mapped: Vec<_> = nodes
- .iter()
- .map(|n| n.extract_policy(signers, build_sat, secp))
- .collect::<Result<Vec<_>, _>>()?
- .into_iter()
- .flatten()
- .collect();
-
- if mapped.len() < nodes.len() {
- threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
- None => return Ok(None),
- Some(x) => x,
- };
- }
-
- Policy::make_thresh(mapped, threshold)?
- }
-
- // Unsupported
- Terminal::RawPkH(_) => None,
- })
- }
-}
-
-fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator<Item = PsbtInputSatisfier> {
- (0..psbt.inputs.len()).map(move |i| PsbtInputSatisfier::new(psbt, i))
-}
-
-/// Options to build the satisfaction field in the policy
-#[derive(Debug, Clone, Copy)]
-pub enum BuildSatisfaction<'a> {
- /// Don't generate `satisfaction` field
- None,
- /// Analyze the given PSBT to check for existing signatures
- Psbt(&'a Psbt),
- /// Like `Psbt` variant and also check for expired timelocks
- PsbtTimelocks {
- /// Given PSBT
- psbt: &'a Psbt,
- /// Current blockchain height
- current_height: u32,
- /// The highest confirmation height between the inputs
- /// CSV should consider different inputs, but we consider the worst condition for the tx as whole
- input_max_height: u32,
- },
-}
-impl<'a> BuildSatisfaction<'a> {
- fn psbt(&self) -> Option<&'a Psbt> {
- match self {
- BuildSatisfaction::None => None,
- BuildSatisfaction::Psbt(psbt) => Some(psbt),
- BuildSatisfaction::PsbtTimelocks { psbt, .. } => Some(psbt),
- }
- }
-}
-
-impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
- fn extract_policy(
- &self,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Result<Option<Policy>, Error> {
- fn make_sortedmulti<Ctx: ScriptContext + 'static>(
- keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
- signers: &SignersContainer,
- build_sat: BuildSatisfaction,
- secp: &SecpCtx,
- ) -> Result<Option<Policy>, Error> {
- Ok(Policy::make_multisig::<Ctx>(
- keys.pks.as_ref(),
- signers,
- build_sat,
- keys.k,
- true,
- secp,
- )?)
- }
-
- match self {
- Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature(
- pk.as_inner(),
- signers,
- build_sat,
- secp,
- ))),
- Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
- pk.as_inner(),
- signers,
- build_sat,
- secp,
- ))),
- Descriptor::Sh(sh) => match sh.as_inner() {
- ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
- pk.as_inner(),
- signers,
- build_sat,
- secp,
- ))),
- ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
- ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
- ShInner::Wsh(wsh) => match wsh.as_inner() {
- WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
- WshInner::SortedMulti(ref keys) => {
- make_sortedmulti(keys, signers, build_sat, secp)
- }
- },
- },
- Descriptor::Wsh(wsh) => match wsh.as_inner() {
- WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
- WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
- },
- Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
- Descriptor::Tr(tr) => {
- // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh`
- // node with threshold = 1 and the key spend signature plus all the tree leaves
- let key_spend_sig =
- miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
-
- if tr.tap_tree().is_none() {
- Ok(Some(key_spend_sig))
- } else {
- let mut items = vec![key_spend_sig];
- items.append(
- &mut tr
- .iter_scripts()
- .filter_map(|(_, ms)| {
- ms.extract_policy(signers, build_sat, secp).transpose()
- })
- .collect::<Result<Vec<_>, _>>()?,
- );
-
- Ok(Policy::make_thresh(items, 1)?)
- }
- }
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use crate::descriptor;
- use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
-
- use super::*;
- use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
- use crate::keys::{DescriptorKey, IntoDescriptorKey};
- use crate::wallet::signer::SignersContainer;
- use alloc::{string::ToString, sync::Arc};
- use assert_matches::assert_matches;
- use bitcoin::bip32;
- use bitcoin::secp256k1::Secp256k1;
- use bitcoin::Network;
- use core::str::FromStr;
-
- const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
- const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
-
- const PATH: &str = "m/44'/1'/0'/0";
-
- fn setup_keys<Ctx: ScriptContext>(
- tprv: &str,
- path: &str,
- secp: &SecpCtx,
- ) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
- let path = bip32::DerivationPath::from_str(path).unwrap();
- let tprv = bip32::Xpriv::from_str(tprv).unwrap();
- let tpub = bip32::Xpub::from_priv(secp, &tprv);
- let fingerprint = tprv.fingerprint(secp);
- let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
- let pubkey = (tpub, path).into_descriptor_key().unwrap();
-
- (prvkey, pubkey, fingerprint)
- }
-
- // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi())
-
- #[test]
- fn test_extract_policy_for_wpkh() {
- let secp = Secp256k1::new();
-
- let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp);
- let desc = descriptor!(wpkh(pubkey)).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
- assert_matches!(&policy.contribution, Satisfaction::None);
-
- let desc = descriptor!(wpkh(prvkey)).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
- assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
- }
-
- // 2 pub keys descriptor, required 2 prv keys
- #[test]
- fn test_extract_policy_for_sh_multi_partial_0of2() {
- let secp = Secp256k1::new();
- let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let desc = descriptor!(sh(multi(2, pubkey0, pubkey1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
- && keys[0] == PkOrF::Fingerprint(fingerprint0)
- && keys[1] == PkOrF::Fingerprint(fingerprint1)
- );
- // TODO should this be "Satisfaction::None" since we have no prv keys?
- // TODO should items and conditions not be empty?
- assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
- && m == &2usize
- && items.is_empty()
- && conditions.is_empty()
- );
- }
-
- // 1 prv and 1 pub key descriptor, required 2 prv keys
- #[test]
- fn test_extract_policy_for_sh_multi_partial_1of2() {
- let secp = Secp256k1::new();
- let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let desc = descriptor!(sh(multi(2, prvkey0, pubkey1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
- assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
- && keys[0] == PkOrF::Fingerprint(fingerprint0)
- && keys[1] == PkOrF::Fingerprint(fingerprint1)
- );
-
- assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
- && m == &2usize
- && items.len() == 1
- && conditions.contains_key(&0)
- );
- }
-
- // 1 prv and 1 pub key descriptor, required 1 prv keys
- #[test]
- #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
- fn test_extract_policy_for_sh_multi_complete_1of2() {
- let secp = Secp256k1::new();
-
- let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
- && keys[0] == PkOrF::Fingerprint(fingerprint0)
- && keys[1] == PkOrF::Fingerprint(fingerprint1)
- );
- assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
- && m == &1
- && items.len() == 2
- && conditions.contains_key(&vec![0])
- && conditions.contains_key(&vec![1])
- );
- }
-
- // 2 prv keys descriptor, required 2 prv keys
- #[test]
- fn test_extract_policy_for_sh_multi_complete_2of2() {
- let secp = Secp256k1::new();
-
- let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let desc = descriptor!(sh(multi(2, prvkey0, prvkey1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
- && keys[0] == PkOrF::Fingerprint(fingerprint0)
- && keys[1] == PkOrF::Fingerprint(fingerprint1)
- );
-
- assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
- && m == &2
- && items.len() == 2
- && conditions.contains_key(&vec![0,1])
- );
- }
-
- // test ExtractPolicy trait with extended and single keys
-
- #[test]
- fn test_extract_policy_for_single_wpkh() {
- let secp = Secp256k1::new();
-
- let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp);
- let desc = descriptor!(wpkh(pubkey)).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
- assert_matches!(&policy.contribution, Satisfaction::None);
-
- let desc = descriptor!(wpkh(prvkey)).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint);
- assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
- }
-
- // single key, 1 prv and 1 pub key descriptor, required 1 prv keys
- #[test]
- #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
- fn test_extract_policy_for_single_wsh_multi_complete_1of2() {
- let secp = Secp256k1::new();
-
- let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1
- && keys[0] == PkOrF::Fingerprint(fingerprint0)
- && keys[1] == PkOrF::Fingerprint(fingerprint1)
- );
- assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
- && m == 1
- && items.len() == 2
- && conditions.contains_key(&vec![0])
- && conditions.contains_key(&vec![1])
- );
- }
-
- // test ExtractPolicy trait with descriptors containing timelocks in a thresh()
-
- #[test]
- #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
- fn test_extract_policy_for_wsh_multi_timelock() {
- let secp = Secp256k1::new();
-
- let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
- let sequence = 50;
- #[rustfmt::skip]
- let desc = descriptor!(wsh(thresh(
- 2,
- pk(prvkey0),
- s:pk(pubkey1),
- s:d:v:older(sequence)
- )))
- .unwrap();
-
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2);
-
- assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
- && m == &2
- && items.len() == 3
- && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
- && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
- && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
- );
- }
-
- // - mixed timelocks should fail
-
- #[test]
- #[ignore]
- fn test_extract_policy_for_wsh_mixed_timelocks() {
- let secp = Secp256k1::new();
- let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds
- let locktime_blocks = 100;
- let locktime_seconds = locktime_blocks + locktime_threshold;
- let desc = descriptor!(sh(and_v(
- v: pk(prvkey0),
- and_v(v: after(locktime_seconds), after(locktime_blocks))
- )))
- .unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let _policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
- // println!("desc policy = {:?}", policy); // TODO remove
- // TODO how should this fail with mixed timelocks?
- }
-
- // - multiple timelocks of the same type should be correctly merged together
- #[test]
- #[ignore]
- fn test_extract_policy_for_multiple_same_timelocks() {
- let secp = Secp256k1::new();
- let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
- let locktime_blocks0 = 100;
- let locktime_blocks1 = 200;
- let desc = descriptor!(sh(and_v(
- v: pk(prvkey0),
- and_v(v: after(locktime_blocks0), after(locktime_blocks1))
- )))
- .unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let _policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
- // println!("desc policy = {:?}", policy); // TODO remove
- // TODO how should this merge timelocks?
- let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp);
- let locktime_seconds0 = 500000100;
- let locktime_seconds1 = 500000200;
- let desc = descriptor!(sh(and_v(
- v: pk(prvkey1),
- and_v(v: after(locktime_seconds0), after(locktime_seconds1))
- )))
- .unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
- let _policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- // println!("desc policy = {:?}", policy); // TODO remove
-
- // TODO how should this merge timelocks?
- }
-
- #[test]
- fn test_get_condition_multisig() {
- let secp = Secp256k1::new();
-
- let (_, pk0, _) = setup_keys(TPRV0_STR, PATH, &secp);
- let (_, pk1, _) = setup_keys(TPRV1_STR, PATH, &secp);
-
- let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- // no args, choose the default
- let no_args = policy.get_condition(&vec![].into_iter().collect());
- assert_eq!(no_args, Ok(Condition::default()));
-
- // enough args
- let eq_thresh =
- policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect());
- assert_eq!(eq_thresh, Ok(Condition::default()));
-
- // more args, it doesn't really change anything
- let gt_thresh =
- policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect());
- assert_eq!(gt_thresh, Ok(Condition::default()));
-
- // not enough args, error
- let lt_thresh =
- policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect());
- assert_eq!(
- lt_thresh,
- Err(PolicyError::NotEnoughItemsSelected(policy.id.clone()))
- );
-
- // index out of range
- let out_of_range =
- policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect());
- assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5)));
- }
-
- const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
- const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
- const CAROL_TPRV_STR:&str = "tprv8ZgxMBicQKsPdC3CicFifuLCEyVVdXVUNYorxUWj3iGZ6nimnLAYAY9SYB7ib8rKzRxrCKFcEytCt6szwd2GHnGPRCBLAEAoSVDefSNk4Bt";
- const ALICE_BOB_PATH: &str = "m/0'";
-
- #[test]
- fn test_extract_satisfaction() {
- const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
- const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA==";
- const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA";
-
- let secp = Secp256k1::new();
-
- let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(wsh(multi(2, prvkey_alice, prvkey_bob))).unwrap();
-
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
-
- let addr = wallet_desc
- .at_derivation_index(0)
- .unwrap()
- .address(Network::Testnet)
- .unwrap();
- assert_eq!(
- "tb1qg3cwv3xt50gdg875qvjjpfgaps86gtk4rz0ejvp6ttc5ldnlxuvqlcn0xk",
- addr.to_string()
- );
-
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
-
- let policy_alice_psbt = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
- .unwrap()
- .unwrap();
- //println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
-
- assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
- && m == &2
- && items == &vec![0]
- );
-
- let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
- let policy_bob_psbt = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
- .unwrap()
- .unwrap();
- //println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
-
- assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
- && m == &2
- && items == &vec![1]
- );
-
- let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
- let policy_alice_bob_psbt = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
- .unwrap()
- .unwrap();
- assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
- && m == &2
- && items == &vec![0, 1]
- );
- }
-
- #[test]
- fn test_extract_satisfaction_timelock() {
- //const PSBT_POLICY_CONSIDER_TIMELOCK_NOT_EXPIRED: &str = "cHNidP8BAFMBAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAD/////ATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
- const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED: &str = "cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
- const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED: &str ="cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstIMEUCIQCtZxNm6H3Ux3pnc64DSpgohMdBj+57xhFHcURYt2BpPAIgG3OnI7bcj/3GtWX1HHyYGSI7QGa/zq5YnsmK1Cw29NABAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEIoAQASDBFAiEArWcTZuh91Md6Z3OuA0qYKITHQY/ue8YRR3FEWLdgaTwCIBtzpyO23I/9xrVl9Rx8mBkiO0Bmv86uWJ7JitQsNvTQAQEBUnZjUrJpaHwhA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLrJN8IQL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiKyTUocAAA==";
-
- let secp = Secp256k1::new();
-
- let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc =
- descriptor!(wsh(thresh(2,n:d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
-
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let addr = wallet_desc
- .at_derivation_index(0)
- .unwrap()
- .address(Network::Testnet)
- .unwrap();
- assert_eq!(
- "tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58",
- addr.to_string()
- );
-
- let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap();
-
- let build_sat = BuildSatisfaction::PsbtTimelocks {
- psbt: &psbt,
- current_height: 10,
- input_max_height: 9,
- };
-
- let policy = wallet_desc
- .extract_policy(&signers_container, build_sat, &secp)
- .unwrap()
- .unwrap();
- assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
- && m == &2
- && items.is_empty()
- );
- //println!("{}", serde_json::to_string(&policy).unwrap());
-
- let build_sat_expired = BuildSatisfaction::PsbtTimelocks {
- psbt: &psbt,
- current_height: 12,
- input_max_height: 9,
- };
-
- let policy_expired = wallet_desc
- .extract_policy(&signers_container, build_sat_expired, &secp)
- .unwrap()
- .unwrap();
- assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
- && m == &2
- && items == &vec![0]
- );
- //println!("{}", serde_json::to_string(&policy_expired).unwrap());
-
- let psbt_signed = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap();
-
- let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks {
- psbt: &psbt_signed,
- current_height: 12,
- input_max_height: 9,
- };
-
- let policy_expired_signed = wallet_desc
- .extract_policy(&signers_container, build_sat_expired_signed, &secp)
- .unwrap()
- .unwrap();
- assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
- && m == &2
- && items == &vec![0, 1]
- );
- //println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
- }
-
- #[test]
- fn test_extract_pkh() {
- let secp = Secp256k1::new();
-
- let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (prvkey_carol, _, _) = setup_keys(CAROL_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(wsh(c: andor(
- pk(prvkey_alice),
- pk_k(prvkey_bob),
- pk_h(prvkey_carol),
- )))
- .unwrap();
-
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp);
- assert!(policy.is_ok());
- }
-
- #[test]
- fn test_extract_tr_key_spend() {
- let secp = Secp256k1::new();
-
- let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(tr(prvkey)).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap();
- assert_eq!(
- policy,
- Some(Policy {
- id: "48u0tz0n".to_string(),
- item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)),
- satisfaction: Satisfaction::None,
- contribution: Satisfaction::Complete {
- condition: Condition::default()
- }
- })
- );
- }
-
- #[test]
- fn test_extract_tr_script_spend() {
- let secp = Secp256k1::new();
-
- let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
- let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
-
- let policy = wallet_desc
- .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
- .unwrap()
- .unwrap();
-
- assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
- assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]);
-
- let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
- let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
-
- let thresh_items = match policy.item {
- SatisfiableItem::Thresh { items, .. } => items,
- _ => unreachable!(),
- };
-
- assert_eq!(thresh_items[0].item, bob_sig);
- assert_eq!(thresh_items[1].item, alice_sig);
- }
-
- #[test]
- fn test_extract_tr_satisfaction_key_spend() {
- const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA==";
- const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA=";
-
- let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
- let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
-
- let secp = Secp256k1::new();
-
- let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(tr(pubkey)).unwrap();
- let (wallet_desc, _) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
-
- let policy_unsigned = wallet_desc
- .extract_policy(
- &SignersContainer::default(),
- BuildSatisfaction::Psbt(&unsigned_psbt),
- &secp,
- )
- .unwrap()
- .unwrap();
- let policy_signed = wallet_desc
- .extract_policy(
- &SignersContainer::default(),
- BuildSatisfaction::Psbt(&signed_psbt),
- &secp,
- )
- .unwrap()
- .unwrap();
-
- assert_eq!(policy_unsigned.satisfaction, Satisfaction::None);
- assert_eq!(
- policy_signed.satisfaction,
- Satisfaction::Complete {
- condition: Default::default()
- }
- );
- }
-
- #[test]
- fn test_extract_tr_satisfaction_script_spend() {
- const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
- const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
-
- let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
- let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
-
- let secp = Secp256k1::new();
-
- let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
- let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
-
- let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap();
- let (wallet_desc, _) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
-
- let policy_unsigned = wallet_desc
- .extract_policy(
- &SignersContainer::default(),
- BuildSatisfaction::Psbt(&unsigned_psbt),
- &secp,
- )
- .unwrap()
- .unwrap();
- let policy_signed = wallet_desc
- .extract_policy(
- &SignersContainer::default(),
- BuildSatisfaction::Psbt(&signed_psbt),
- &secp,
- )
- .unwrap()
- .unwrap();
-
- assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
- assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty());
-
- assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
- assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]);
-
- let satisfied_items = match policy_signed.item {
- SatisfiableItem::Thresh { items, .. } => items,
- _ => unreachable!(),
- };
-
- assert_eq!(
- satisfied_items[0].satisfaction,
- Satisfaction::Complete {
- condition: Default::default()
- }
- );
- assert_eq!(
- satisfied_items[1].satisfaction,
- Satisfaction::Complete {
- condition: Default::default()
- }
- );
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Descriptor templates
-//!
-//! This module contains the definition of various common script templates that are ready to be
-//! used. See the documentation of each template for an example.
-
-use bitcoin::bip32;
-use bitcoin::Network;
-
-use miniscript::{Legacy, Segwitv0, Tap};
-
-use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap};
-use crate::descriptor::DescriptorError;
-use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks};
-use crate::wallet::utils::SecpCtx;
-use crate::{descriptor, KeychainKind};
-
-/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others
-pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
-
-/// Trait for descriptor templates that can be built into a full descriptor
-///
-/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be
-/// passed directly to the [`Wallet`](crate::Wallet) constructor.
-///
-/// ## Example
-///
-/// ```
-/// use bdk::descriptor::error::Error as DescriptorError;
-/// use bdk::keys::{IntoDescriptorKey, KeyError};
-/// use bdk::miniscript::Legacy;
-/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
-/// use bitcoin::Network;
-///
-/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
-///
-/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
-/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
-/// Ok(bdk::descriptor!(pkh(self.0))?)
-/// }
-/// }
-/// ```
-pub trait DescriptorTemplate {
- /// Build the complete descriptor
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError>;
-}
-
-/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
-/// [`build`](DescriptorTemplate::build) method
-impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
- fn into_wallet_descriptor(
- self,
- secp: &SecpCtx,
- network: Network,
- ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- self.build(network)?.into_wallet_descriptor(secp, network)
- }
-}
-
-/// P2PKH template. Expands to a descriptor `pkh(key)`
-///
-/// ## Example
-///
-/// ```
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::Wallet;
-/// # use bdk::KeychainKind;
-/// use bdk::template::P2Pkh;
-///
-/// let key =
-/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
-/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
-///
-/// assert_eq!(
-/// wallet
-/// .next_unused_address(KeychainKind::External)?
-/// .to_string(),
-/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
-/// );
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
-
-impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
- fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- descriptor!(pkh(self.0))
- }
-}
-
-/// P2WPKH-P2SH template. Expands to a descriptor `sh(wpkh(key))`
-///
-/// ## Example
-///
-/// ```
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::Wallet;
-/// # use bdk::KeychainKind;
-/// use bdk::template::P2Wpkh_P2Sh;
-///
-/// let key =
-/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
-/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
-///
-/// assert_eq!(
-/// wallet
-/// .next_unused_address(KeychainKind::External)?
-/// .to_string(),
-/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
-/// );
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-#[allow(non_camel_case_types)]
-pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
-
-impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
- fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- descriptor!(sh(wpkh(self.0)))
- }
-}
-
-/// P2WPKH template. Expands to a descriptor `wpkh(key)`
-///
-/// ## Example
-///
-/// ```
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet};
-/// # use bdk::KeychainKind;
-/// use bdk::template::P2Wpkh;
-///
-/// let key =
-/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
-/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
-///
-/// assert_eq!(
-/// wallet
-/// .next_unused_address(KeychainKind::External)?
-/// .to_string(),
-/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
-/// );
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
-
-impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
- fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- descriptor!(wpkh(self.0))
- }
-}
-
-/// P2TR template. Expands to a descriptor `tr(key)`
-///
-/// ## Example
-///
-/// ```
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::Wallet;
-/// # use bdk::KeychainKind;
-/// use bdk::template::P2TR;
-///
-/// let key =
-/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
-/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
-///
-/// assert_eq!(
-/// wallet
-/// .next_unused_address(KeychainKind::External)?
-/// .to_string(),
-/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
-/// );
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
-
-impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
- fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- descriptor!(tr(self.0))
- }
-}
-
-/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
-///
-/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
-///
-/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip44;
-///
-/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip44(key.clone(), KeychainKind::External),
-/// Some(Bip44(key, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
-
-impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
- }
-}
-
-/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
-///
-/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet.
-///
-/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
-///
-/// See [`Bip44`] for a template that does the full derivation, but requires private data
-/// for the key.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip44Public;
-///
-/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
-/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
-/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
-
-impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Pkh(legacy::make_bipxx_public(
- 44, self.0, self.1, self.2, network,
- )?)
- .build(network)
- }
-}
-
-/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))`
-///
-/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
-///
-/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip49;
-///
-/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip49(key.clone(), KeychainKind::External),
-/// Some(Bip49(key, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
-
-impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
- }
-}
-
-/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))`
-///
-/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet.
-///
-/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
-///
-/// See [`Bip49`] for a template that does the full derivation, but requires private data
-/// for the key.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip49Public;
-///
-/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
-/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
-/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
-
-impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(
- 49, self.0, self.1, self.2, network,
- )?)
- .build(network)
- }
-}
-
-/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)`
-///
-/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
-///
-/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip84;
-///
-/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip84(key.clone(), KeychainKind::External),
-/// Some(Bip84(key, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
-
-impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
- }
-}
-
-/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)`
-///
-/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet.
-///
-/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
-///
-/// See [`Bip84`] for a template that does the full derivation, but requires private data
-/// for the key.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip84Public;
-///
-/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
-/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
-/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
-
-impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2Wpkh(segwit_v0::make_bipxx_public(
- 84, self.0, self.1, self.2, network,
- )?)
- .build(network)
- }
-}
-
-/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)`
-///
-/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
-///
-/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip86;
-///
-/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip86(key.clone(), KeychainKind::External),
-/// Some(Bip86(key, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
-
-impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network)
- }
-}
-
-/// BIP86 public template. Expands to `tr(key/{0,1}/*)`
-///
-/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet.
-///
-/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
-///
-/// See [`Bip86`] for a template that does the full derivation, but requires private data
-/// for the key.
-///
-/// ## Example
-///
-/// ```
-/// # use std::str::FromStr;
-/// # use bdk::bitcoin::{PrivateKey, Network};
-/// # use bdk::{Wallet, KeychainKind};
-/// use bdk::template::Bip86Public;
-///
-/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
-/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new_no_persist(
-/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
-/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
-/// Network::Testnet,
-/// )?;
-///
-/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
-/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
-
-impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {
- fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
- P2TR(segwit_v1::make_bipxx_public(
- 86, self.0, self.1, self.2, network,
- )?)
- .build(network)
- }
-}
-
-macro_rules! expand_make_bipxx {
- ( $mod_name:ident, $ctx:ty ) => {
- mod $mod_name {
- use super::*;
-
- pub(super) fn make_bipxx_private<K: DerivableKey<$ctx>>(
- bip: u32,
- key: K,
- keychain: KeychainKind,
- network: Network,
- ) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
- let mut derivation_path = alloc::vec::Vec::with_capacity(4);
- derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
-
- match network {
- Network::Bitcoin => {
- derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
- }
- _ => {
- derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?);
- }
- }
- derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
-
- match keychain {
- KeychainKind::External => {
- derivation_path.push(bip32::ChildNumber::from_normal_idx(0)?)
- }
- KeychainKind::Internal => {
- derivation_path.push(bip32::ChildNumber::from_normal_idx(1)?)
- }
- };
-
- let derivation_path: bip32::DerivationPath = derivation_path.into();
-
- Ok((key, derivation_path))
- }
- pub(super) fn make_bipxx_public<K: DerivableKey<$ctx>>(
- bip: u32,
- key: K,
- parent_fingerprint: bip32::Fingerprint,
- keychain: KeychainKind,
- network: Network,
- ) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
- let derivation_path: bip32::DerivationPath = match keychain {
- KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
- KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(),
- };
-
- let source_path = bip32::DerivationPath::from(vec![
- bip32::ChildNumber::from_hardened_idx(bip)?,
- match network {
- Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?,
- _ => bip32::ChildNumber::from_hardened_idx(1)?,
- },
- bip32::ChildNumber::from_hardened_idx(0)?,
- ]);
-
- Ok((key, (parent_fingerprint, source_path), derivation_path))
- }
- }
- };
-}
-
-expand_make_bipxx!(legacy, Legacy);
-expand_make_bipxx!(segwit_v0, Segwitv0);
-expand_make_bipxx!(segwit_v1, Tap);
-
-#[cfg(test)]
-mod test {
- // test existing descriptor templates, make sure they are expanded to the right descriptors
-
- use alloc::{string::ToString, vec::Vec};
- use core::str::FromStr;
-
- use super::*;
- use crate::descriptor::{DescriptorError, DescriptorMeta};
- use crate::keys::ValidNetworks;
- use assert_matches::assert_matches;
- use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
- use miniscript::Descriptor;
-
- // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
- #[test]
- fn test_bip44_template_cointype() {
- use bitcoin::bip32::ChildNumber::{self, Hardened};
-
- let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
- assert_eq!(Network::Bitcoin, xprvkey.network);
- let xdesc = Bip44(xprvkey, KeychainKind::Internal)
- .build(Network::Bitcoin)
- .unwrap();
-
- if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
- let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
- let purpose = path.first().unwrap();
- assert_matches!(purpose, Hardened { index: 44 });
- let coin_type = path.get(1).unwrap();
- assert_matches!(coin_type, Hardened { index: 0 });
- }
-
- let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- assert_eq!(Network::Testnet, tprvkey.network);
- let tdesc = Bip44(tprvkey, KeychainKind::Internal)
- .build(Network::Testnet)
- .unwrap();
-
- if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
- let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
- let purpose = path.first().unwrap();
- assert_matches!(purpose, Hardened { index: 44 });
- let coin_type = path.get(1).unwrap();
- assert_matches!(coin_type, Hardened { index: 1 });
- }
- }
-
- // verify template descriptor generates expected address(es)
- fn check(
- desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
- is_witness: bool,
- is_taproot: bool,
- is_fixed: bool,
- network: Network,
- expected: &[&str],
- ) {
- let (desc, _key_map, _networks) = desc.unwrap();
- assert_eq!(desc.is_witness(), is_witness);
- assert_eq!(desc.is_taproot(), is_taproot);
- assert_eq!(!desc.has_wildcard(), is_fixed);
- for i in 0..expected.len() {
- let index = i as u32;
- let child_desc = if !desc.has_wildcard() {
- desc.at_derivation_index(0).unwrap()
- } else {
- desc.at_derivation_index(index).unwrap()
- };
- let address = child_desc.address(network).unwrap();
- assert_eq!(address.to_string(), *expected.get(i).unwrap());
- }
- }
-
- // P2PKH
- #[test]
- fn test_p2ph_template() {
- let prvkey =
- bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
- .unwrap();
- check(
- P2Pkh(prvkey).build(Network::Bitcoin),
- false,
- false,
- true,
- Network::Regtest,
- &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
- );
-
- let pubkey = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- check(
- P2Pkh(pubkey).build(Network::Bitcoin),
- false,
- false,
- true,
- Network::Regtest,
- &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
- );
- }
-
- // P2WPKH-P2SH `sh(wpkh(key))`
- #[test]
- fn test_p2wphp2sh_template() {
- let prvkey =
- bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
- .unwrap();
- check(
- P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
- true,
- false,
- true,
- Network::Regtest,
- &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
- );
-
- let pubkey = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- check(
- P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
- true,
- false,
- true,
- Network::Regtest,
- &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
- );
- }
-
- // P2WPKH `wpkh(key)`
- #[test]
- fn test_p2wph_template() {
- let prvkey =
- bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
- .unwrap();
- check(
- P2Wpkh(prvkey).build(Network::Bitcoin),
- true,
- false,
- true,
- Network::Regtest,
- &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
- );
-
- let pubkey = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- check(
- P2Wpkh(pubkey).build(Network::Bitcoin),
- true,
- false,
- true,
- Network::Regtest,
- &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
- );
- }
-
- // P2TR `tr(key)`
- #[test]
- fn test_p2tr_template() {
- let prvkey =
- bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
- .unwrap();
- check(
- P2TR(prvkey).build(Network::Bitcoin),
- false,
- true,
- true,
- Network::Regtest,
- &["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"],
- );
-
- let pubkey = bitcoin::PublicKey::from_str(
- "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
- )
- .unwrap();
- check(
- P2TR(pubkey).build(Network::Bitcoin),
- false,
- true,
- true,
- Network::Regtest,
- &["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"],
- );
- }
-
- // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
- #[test]
- fn test_bip44_template() {
- let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- check(
- Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
- false,
- false,
- false,
- Network::Regtest,
- &[
- "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5",
- "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP",
- "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ",
- ],
- );
- check(
- Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
- false,
- false,
- false,
- Network::Regtest,
- &[
- "muHF98X9KxEzdKrnFAX85KeHv96eXopaip",
- "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR",
- "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA",
- ],
- );
- }
-
- // BIP44 public `pkh(key/{0,1}/*)`
- #[test]
- fn test_bip44_public_template() {
- let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
- let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
- check(
- Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
- false,
- false,
- false,
- Network::Regtest,
- &[
- "miNG7dJTzJqNbFS19svRdTCisC65dsubtR",
- "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg",
- "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g",
- ],
- );
- check(
- Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
- false,
- false,
- false,
- Network::Regtest,
- &[
- "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H",
- "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG",
- "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo",
- ],
- );
- }
-
- // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
- #[test]
- fn test_bip49_template() {
- let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- check(
- Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV",
- "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS",
- "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4",
- ],
- );
- check(
- Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG",
- "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p",
- "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn",
- ],
- );
- }
-
- // BIP49 public `sh(wpkh(key/{0,1}/*))`
- #[test]
- fn test_bip49_public_template() {
- let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
- let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
- check(
- Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt",
- "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX",
- "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7",
- ],
- );
- check(
- Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ",
- "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH",
- "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp",
- ],
- );
- }
-
- // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
- #[test]
- fn test_bip84_template() {
- let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- check(
- Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s",
- "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp",
- "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp",
- ],
- );
- check(
- Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa",
- "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45",
- "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce",
- ],
- );
- }
-
- // BIP84 public `wpkh(key/{0,1}/*)`
- #[test]
- fn test_bip84_public_template() {
- let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
- let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
- check(
- Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h",
- "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana",
- "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9",
- ],
- );
- check(
- Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
- true,
- false,
- false,
- Network::Regtest,
- &[
- "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2",
- "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp",
- "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp",
- ],
- );
- }
-
- // BIP86 `tr(key/86'/0'/0'/{0,1}/*)`
- // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
- #[test]
- fn test_bip86_template() {
- let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
- check(
- Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
- false,
- true,
- false,
- Network::Bitcoin,
- &[
- "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
- "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
- "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
- ],
- );
- check(
- Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
- false,
- true,
- false,
- Network::Bitcoin,
- &[
- "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
- "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
- "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
- ],
- );
- }
-
- // BIP86 public `tr(key/{0,1}/*)`
- // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
- #[test]
- fn test_bip86_public_template() {
- let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
- let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
- check(
- Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
- false,
- true,
- false,
- Network::Bitcoin,
- &[
- "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
- "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
- "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
- ],
- );
- check(
- Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
- false,
- true,
- false,
- Network::Bitcoin,
- &[
- "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
- "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
- "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
- ],
- );
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! BIP-0039
-
-// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for
-// something that should be fairly simple to re-implement.
-
-use alloc::string::String;
-use bitcoin::bip32;
-use bitcoin::Network;
-
-use miniscript::ScriptContext;
-
-pub use bip39::{Error, Language, Mnemonic};
-
-type Seed = [u8; 64];
-
-/// Type describing entropy length (aka word count) in the mnemonic
-pub enum WordCount {
- /// 12 words mnemonic (128 bits entropy)
- Words12 = 128,
- /// 15 words mnemonic (160 bits entropy)
- Words15 = 160,
- /// 18 words mnemonic (192 bits entropy)
- Words18 = 192,
- /// 21 words mnemonic (224 bits entropy)
- Words21 = 224,
- /// 24 words mnemonic (256 bits entropy)
- Words24 = 256,
-}
-
-use super::{
- any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError,
-};
-
-fn set_valid_on_any_network<Ctx: ScriptContext>(
- descriptor_key: DescriptorKey<Ctx>,
-) -> DescriptorKey<Ctx> {
- // We have to pick one network to build the xprv, but since the bip39 standard doesn't
- // encode the network, the xprv we create is actually valid everywhere. So we override the
- // valid networks with `any_network()`.
- descriptor_key.override_valid_networks(any_network())
-}
-
-/// Type for a BIP39 mnemonic with an optional passphrase
-pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
-
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
- }
-
- fn into_descriptor_key(
- self,
- source: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- let descriptor_key = self
- .into_extended_key()?
- .into_descriptor_key(source, derivation_path)?;
-
- Ok(set_valid_on_any_network(descriptor_key))
- }
-}
-
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- let (mnemonic, passphrase) = self;
- let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or(""));
-
- seed.into_extended_key()
- }
-
- fn into_descriptor_key(
- self,
- source: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- let descriptor_key = self
- .into_extended_key()?
- .into_descriptor_key(source, derivation_path)?;
-
- Ok(set_valid_on_any_network(descriptor_key))
- }
-}
-
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>) {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- let (mnemonic, passphrase) = self;
- (mnemonic.into_key(), passphrase).into_extended_key()
- }
-
- fn into_descriptor_key(
- self,
- source: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- let (mnemonic, passphrase) = self;
- (mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path)
- }
-}
-
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- (self, None).into_extended_key()
- }
-
- fn into_descriptor_key(
- self,
- source: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- let descriptor_key = self
- .into_extended_key()?
- .into_descriptor_key(source, derivation_path)?;
-
- Ok(set_valid_on_any_network(descriptor_key))
- }
-}
-
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
- type Entropy = [u8; 32];
-
- type Options = (WordCount, Language);
- type Error = Option<bip39::Error>;
-
- fn generate_with_entropy(
- (word_count, language): Self::Options,
- entropy: Self::Entropy,
- ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- let entropy = &entropy[..(word_count as usize / 8)];
- let mnemonic = Mnemonic::from_entropy_in(language, entropy)?;
-
- Ok(GeneratedKey::new(mnemonic, any_network()))
- }
-}
-
-#[cfg(test)]
-mod test {
- use alloc::string::ToString;
- use core::str::FromStr;
-
- use bitcoin::bip32;
-
- use bip39::{Language, Mnemonic};
-
- use crate::keys::{any_network, GeneratableKey, GeneratedKey};
-
- use super::WordCount;
-
- #[test]
- fn test_keys_bip39_mnemonic() {
- let mnemonic =
- "aim bunker wash balance finish force paper analyst cabin spoon stable organ";
- let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
- let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
-
- let key = (mnemonic, path);
- let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
- assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv");
- assert_eq!(keys.len(), 1);
- assert_eq!(networks.len(), 4);
- }
-
- #[test]
- fn test_keys_bip39_mnemonic_passphrase() {
- let mnemonic =
- "aim bunker wash balance finish force paper analyst cabin spoon stable organ";
- let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
- let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
-
- let key = ((mnemonic, Some("passphrase".into())), path);
- let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
- assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m");
- assert_eq!(keys.len(), 1);
- assert_eq!(networks.len(), 4);
- }
-
- #[test]
- fn test_keys_generate_bip39() {
- let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
- Mnemonic::generate_with_entropy(
- (WordCount::Words12, Language::English),
- crate::keys::test::TEST_ENTROPY,
- )
- .unwrap();
- assert_eq!(generated_mnemonic.valid_networks, any_network());
- assert_eq!(
- generated_mnemonic.to_string(),
- "primary fetch primary fetch primary fetch primary fetch primary fetch primary fever"
- );
-
- let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
- Mnemonic::generate_with_entropy(
- (WordCount::Words24, Language::English),
- crate::keys::test::TEST_ENTROPY,
- )
- .unwrap();
- assert_eq!(generated_mnemonic.valid_networks, any_network());
- assert_eq!(generated_mnemonic.to_string(), "primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary foster");
- }
-
- #[test]
- fn test_keys_generate_bip39_random() {
- let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
- Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
- assert_eq!(generated_mnemonic.valid_networks, any_network());
-
- let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
- Mnemonic::generate((WordCount::Words24, Language::English)).unwrap();
- assert_eq!(generated_mnemonic.valid_networks, any_network());
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Key formats
-
-use crate::collections::HashSet;
-use alloc::string::{String, ToString};
-use alloc::vec::Vec;
-use core::any::TypeId;
-use core::fmt;
-use core::marker::PhantomData;
-use core::ops::Deref;
-use core::str::FromStr;
-
-use bitcoin::secp256k1::{self, Secp256k1, Signing};
-
-use bitcoin::bip32;
-use bitcoin::{key::XOnlyPublicKey, Network, PrivateKey, PublicKey};
-
-use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
-pub use miniscript::descriptor::{
- DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
- SortedMultiVec,
-};
-pub use miniscript::ScriptContext;
-use miniscript::{Miniscript, Terminal};
-
-use crate::descriptor::{CheckMiniscript, DescriptorError};
-use crate::wallet::utils::SecpCtx;
-
-#[cfg(feature = "keys-bip39")]
-#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
-pub mod bip39;
-
-/// Set of valid networks for a key
-pub type ValidNetworks = HashSet<Network>;
-
-/// Create a set containing mainnet, testnet, signet, and regtest
-pub fn any_network() -> ValidNetworks {
- vec![
- Network::Bitcoin,
- Network::Testnet,
- Network::Regtest,
- Network::Signet,
- ]
- .into_iter()
- .collect()
-}
-/// Create a set only containing mainnet
-pub fn mainnet_network() -> ValidNetworks {
- vec![Network::Bitcoin].into_iter().collect()
-}
-/// Create a set containing testnet and regtest
-pub fn test_networks() -> ValidNetworks {
- vec![Network::Testnet, Network::Regtest, Network::Signet]
- .into_iter()
- .collect()
-}
-/// Compute the intersection of two sets
-pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks {
- a.intersection(b).cloned().collect()
-}
-
-/// Container for public or secret keys
-#[derive(Debug)]
-pub enum DescriptorKey<Ctx: ScriptContext> {
- #[doc(hidden)]
- Public(DescriptorPublicKey, ValidNetworks, PhantomData<Ctx>),
- #[doc(hidden)]
- Secret(DescriptorSecretKey, ValidNetworks, PhantomData<Ctx>),
-}
-
-impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
- /// Create an instance given a public key and a set of valid networks
- pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self {
- DescriptorKey::Public(public, networks, PhantomData)
- }
-
- /// Create an instance given a secret key and a set of valid networks
- pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self {
- DescriptorKey::Secret(secret, networks, PhantomData)
- }
-
- /// Override the computed set of valid networks
- pub fn override_valid_networks(self, networks: ValidNetworks) -> Self {
- match self {
- DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData),
- DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData),
- }
- }
-
- // This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
- // public because it is effectively called by external crates once the macros are expanded,
- // but since it is not meant to be part of the public api we hide it from the docs.
- #[doc(hidden)]
- pub fn extract(
- self,
- secp: &SecpCtx,
- ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> {
- match self {
- DescriptorKey::Public(public, valid_networks, _) => {
- Ok((public, KeyMap::default(), valid_networks))
- }
- DescriptorKey::Secret(secret, valid_networks, _) => {
- let mut key_map = KeyMap::new();
-
- let public = secret
- .to_public(secp)
- .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
- key_map.insert(public.clone(), secret);
-
- Ok((public, key_map, valid_networks))
- }
- }
- }
-}
-
-/// Enum representation of the known valid [`ScriptContext`]s
-#[derive(Debug, Eq, PartialEq, Copy, Clone)]
-pub enum ScriptContextEnum {
- /// Legacy scripts
- Legacy,
- /// Segwitv0 scripts
- Segwitv0,
- /// Taproot scripts
- Tap,
-}
-
-impl ScriptContextEnum {
- /// Returns whether the script context is [`ScriptContextEnum::Legacy`]
- pub fn is_legacy(&self) -> bool {
- self == &ScriptContextEnum::Legacy
- }
-
- /// Returns whether the script context is [`ScriptContextEnum::Segwitv0`]
- pub fn is_segwit_v0(&self) -> bool {
- self == &ScriptContextEnum::Segwitv0
- }
-
- /// Returns whether the script context is [`ScriptContextEnum::Tap`]
- pub fn is_taproot(&self) -> bool {
- self == &ScriptContextEnum::Tap
- }
-}
-
-/// Trait that adds extra useful methods to [`ScriptContext`]s
-pub trait ExtScriptContext: ScriptContext {
- /// Returns the [`ScriptContext`] as a [`ScriptContextEnum`]
- fn as_enum() -> ScriptContextEnum;
-
- /// Returns whether the script context is [`Legacy`](miniscript::Legacy)
- fn is_legacy() -> bool {
- Self::as_enum().is_legacy()
- }
-
- /// Returns whether the script context is [`Segwitv0`](miniscript::Segwitv0)
- fn is_segwit_v0() -> bool {
- Self::as_enum().is_segwit_v0()
- }
-
- /// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1
- fn is_taproot() -> bool {
- Self::as_enum().is_taproot()
- }
-}
-
-impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
- fn as_enum() -> ScriptContextEnum {
- match TypeId::of::<Ctx>() {
- t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
- t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
- t if t == TypeId::of::<miniscript::Tap>() => ScriptContextEnum::Tap,
- _ => unimplemented!("Unknown ScriptContext type"),
- }
- }
-}
-
-/// Trait for objects that can be turned into a public or secret [`DescriptorKey`]
-///
-/// The generic type `Ctx` is used to define the context in which the key is valid: some key
-/// formats, like the mnemonics used by Electrum wallets, encode internally whether the wallet is
-/// legacy or segwit. Thus, trying to turn a valid legacy mnemonic into a `DescriptorKey`
-/// that would become part of a segwit descriptor should fail.
-///
-/// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful
-/// methods that can be used to check at runtime which `Ctx` is being used.
-///
-/// For key types that can do this check statically (because they can only work within a
-/// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type
-/// checking.
-///
-/// Keys also have control over the networks they support: constructing the return object with
-/// [`DescriptorKey::from_public`] or [`DescriptorKey::from_secret`] allows to specify a set of
-/// [`ValidNetworks`].
-///
-/// ## Examples
-///
-/// Key type valid in any context:
-///
-/// ```
-/// use bdk::bitcoin::PublicKey;
-///
-/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
-///
-/// pub struct MyKeyType {
-/// pubkey: PublicKey,
-/// }
-///
-/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
-/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
-/// self.pubkey.into_descriptor_key()
-/// }
-/// }
-/// ```
-///
-/// Key type that is only valid on mainnet:
-///
-/// ```
-/// use bdk::bitcoin::PublicKey;
-///
-/// use bdk::keys::{
-/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
-/// ScriptContext, SinglePub, SinglePubKey,
-/// };
-///
-/// pub struct MyKeyType {
-/// pubkey: PublicKey,
-/// }
-///
-/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
-/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
-/// Ok(DescriptorKey::from_public(
-/// DescriptorPublicKey::Single(SinglePub {
-/// origin: None,
-/// key: SinglePubKey::FullKey(self.pubkey),
-/// }),
-/// mainnet_network(),
-/// ))
-/// }
-/// }
-/// ```
-///
-/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
-///
-/// ```
-/// use bdk::bitcoin::PublicKey;
-///
-/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
-///
-/// pub struct MyKeyType {
-/// is_legacy: bool,
-/// pubkey: PublicKey,
-/// }
-///
-/// impl<Ctx: ScriptContext + 'static> IntoDescriptorKey<Ctx> for MyKeyType {
-/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
-/// if Ctx::is_legacy() == self.is_legacy {
-/// self.pubkey.into_descriptor_key()
-/// } else {
-/// Err(KeyError::InvalidScriptContext)
-/// }
-/// }
-/// }
-/// ```
-///
-/// Key type that can only work within [`miniscript::Segwitv0`] context. Only the specialized version
-/// of the trait is implemented.
-///
-/// This example deliberately fails to compile, to demonstrate how the compiler can catch when keys
-/// are misused. In this case, the "segwit-only" key is used to build a `pkh()` descriptor, which
-/// makes the compiler (correctly) fail.
-///
-/// ```compile_fail
-/// use bdk::bitcoin::PublicKey;
-/// use core::str::FromStr;
-///
-/// use bdk::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> {
-/// self.pubkey.into_descriptor_key()
-/// }
-/// }
-///
-/// let key = MySegwitOnlyKeyType {
-/// pubkey: PublicKey::from_str("...")?,
-/// };
-/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?;
-/// // ^^^^^ changing this to `wpkh` would make it compile
-///
-/// # Ok::<_, Box<dyn std::error::Error>>(())
-/// ```
-pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
- /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`]
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError>;
-}
-
-/// Enum for extended keys that can be either `xprv` or `xpub`
-///
-/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
-/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
-///
-/// Defaults to the [`Legacy`](miniscript::Legacy) context.
-pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
- /// A private extended key, aka an `xprv`
- Private((bip32::Xpriv, PhantomData<Ctx>)),
- /// A public extended key, aka an `xpub`
- Public((bip32::Xpub, PhantomData<Ctx>)),
-}
-
-impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
- /// Return whether or not the key contains the private data
- pub fn has_secret(&self) -> bool {
- match self {
- ExtendedKey::Private(_) => true,
- ExtendedKey::Public(_) => false,
- }
- }
-
- /// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
- /// given [`Network`], if the key contains the private data
- pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
- match self {
- ExtendedKey::Private((mut xprv, _)) => {
- xprv.network = network;
- Some(xprv)
- }
- ExtendedKey::Public(_) => None,
- }
- }
-
- /// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
- /// given [`Network`]
- pub fn into_xpub<C: Signing>(
- self,
- network: bitcoin::Network,
- secp: &Secp256k1<C>,
- ) -> bip32::Xpub {
- let mut xpub = match self {
- ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
- ExtendedKey::Public((xpub, _)) => xpub,
- };
-
- xpub.network = network;
- xpub
- }
-}
-
-impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
- fn from(xpub: bip32::Xpub) -> Self {
- ExtendedKey::Public((xpub, PhantomData))
- }
-}
-
-impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
- fn from(xprv: bip32::Xpriv) -> Self {
- ExtendedKey::Private((xprv, PhantomData))
- }
-}
-
-/// Trait for keys that can be derived.
-///
-/// When extra metadata are provided, a [`DerivableKey`] can be transformed into a
-/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented
-/// for `(DerivableKey, DerivationPath)` and
-/// `(DerivableKey, KeySource, DerivationPath)` tuples.
-///
-/// For key types that don't encode any indication about the path to use (like bip39), it's
-/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same
-/// rules regarding script context and valid networks apply.
-///
-/// ## Examples
-///
-/// Key types that can be directly converted into an [`Xpriv`] or
-/// 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};
-///
-/// struct MyCustomKeyType {
-/// key_data: bitcoin::PrivateKey,
-/// chain_code: [u8; 32],
-/// network: bitcoin::Network,
-/// }
-///
-/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
-/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
-/// let xprv = bip32::Xpriv {
-/// network: self.network,
-/// depth: 0,
-/// parent_fingerprint: bip32::Fingerprint::default(),
-/// private_key: self.key_data.inner,
-/// chain_code: bip32::ChainCode::from(&self.chain_code),
-/// child_number: bip32::ChildNumber::Normal { index: 0 },
-/// };
-///
-/// xprv.into_extended_key()
-/// }
-/// }
-/// ```
-///
-/// Types that don't internally encode the [`Network`] in which they are valid need some extra
-/// steps to override the set of valid networks, otherwise only the network specified in the
-/// [`Xpriv`] or [`Xpub`] will be considered valid.
-///
-/// ```
-/// use bdk::bitcoin;
-/// use bdk::bitcoin::bip32;
-/// use bdk::keys::{
-/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
-/// };
-///
-/// struct MyCustomKeyType {
-/// key_data: bitcoin::PrivateKey,
-/// chain_code: [u8; 32],
-/// }
-///
-/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
-/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
-/// let xprv = bip32::Xpriv {
-/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
-/// depth: 0,
-/// parent_fingerprint: bip32::Fingerprint::default(),
-/// private_key: self.key_data.inner,
-/// chain_code: bip32::ChainCode::from(&self.chain_code),
-/// child_number: bip32::ChildNumber::Normal { index: 0 },
-/// };
-///
-/// xprv.into_extended_key()
-/// }
-///
-/// fn into_descriptor_key(
-/// self,
-/// source: Option<bip32::KeySource>,
-/// derivation_path: bip32::DerivationPath,
-/// ) -> Result<DescriptorKey<Ctx>, KeyError> {
-/// let descriptor_key = self
-/// .into_extended_key()?
-/// .into_descriptor_key(source, derivation_path)?;
-///
-/// // Override the set of valid networks here
-/// Ok(descriptor_key.override_valid_networks(any_network()))
-/// }
-/// }
-/// ```
-///
-/// [`DerivationPath`]: (bip32::DerivationPath)
-/// [`Xpriv`]: (bip32::Xpriv)
-/// [`Xpub`]: (bip32::Xpub)
-pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
- /// Consume `self` and turn it into an [`ExtendedKey`]
- #[cfg_attr(
- feature = "keys-bip39",
- doc = r##"
-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};
-
-# fn main() -> Result<(), Box<dyn std::error::Error>> {
-let xkey: ExtendedKey =
- Mnemonic::parse_in(
- Language::English,
- "jelly crash boy whisper mouse ecology tuna soccer memory million news short",
- )?
- .into_extended_key()?;
-let xprv = xkey.into_xprv(Network::Bitcoin).unwrap();
-# Ok(()) }
-```
-"##
- )]
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError>;
-
- /// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as
- /// key origin and derivation path
- fn into_descriptor_key(
- self,
- origin: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- match self.into_extended_key()? {
- ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey {
- origin,
- xkey: xprv,
- derivation_path,
- wildcard: Wildcard::Unhardened,
- })
- .into_descriptor_key(),
- ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey {
- origin,
- xkey: xpub,
- derivation_path,
- wildcard: Wildcard::Unhardened,
- })
- .into_descriptor_key(),
- }
- }
-}
-
-/// Identity conversion
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- Ok(self)
- }
-}
-
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- Ok(self.into())
- }
-}
-
-impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- Ok(self.into())
- }
-}
-
-/// Output of a [`GeneratableKey`] key generation
-pub struct GeneratedKey<K, Ctx: ScriptContext> {
- key: K,
- valid_networks: ValidNetworks,
- phantom: PhantomData<Ctx>,
-}
-
-impl<K, Ctx: ScriptContext> GeneratedKey<K, Ctx> {
- fn new(key: K, valid_networks: ValidNetworks) -> Self {
- GeneratedKey {
- key,
- valid_networks,
- phantom: PhantomData,
- }
- }
-
- /// Consumes `self` and returns the key
- pub fn into_key(self) -> K {
- self.key
- }
-}
-
-impl<K, Ctx: ScriptContext> Deref for GeneratedKey<K, Ctx> {
- type Target = K;
-
- fn deref(&self) -> &Self::Target {
- &self.key
- }
-}
-
-impl<K: Clone, Ctx: ScriptContext> Clone for GeneratedKey<K, Ctx> {
- fn clone(&self) -> GeneratedKey<K, Ctx> {
- GeneratedKey {
- key: self.key.clone(),
- valid_networks: self.valid_networks.clone(),
- phantom: self.phantom,
- }
- }
-}
-
-// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
-// right `valid_networks`.
-impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
-where
- Ctx: ScriptContext,
- K: DerivableKey<Ctx>,
-{
- fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
- self.key.into_extended_key()
- }
-
- fn into_descriptor_key(
- self,
- origin: Option<bip32::KeySource>,
- derivation_path: bip32::DerivationPath,
- ) -> Result<DescriptorKey<Ctx>, KeyError> {
- let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?;
- Ok(descriptor_key.override_valid_networks(self.valid_networks))
- }
-}
-
-// Make generated keys directly usable in descriptors, and make sure they get assigned the right
-// `valid_networks`.
-impl<Ctx, K> IntoDescriptorKey<Ctx> for GeneratedKey<K, Ctx>
-where
- Ctx: ScriptContext,
- K: IntoDescriptorKey<Ctx>,
-{
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- let desc_key = self.key.into_descriptor_key()?;
- Ok(desc_key.override_valid_networks(self.valid_networks))
- }
-}
-
-/// Trait for keys that can be generated
-///
-/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply.
-///
-/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self`
-/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for
-/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also
-/// [`IntoDescriptorKey`].
-pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
- /// Type specifying the amount of entropy required e.g. `[u8;32]`
- type Entropy: AsMut<[u8]> + Default;
-
- /// Extra options required by the `generate_with_entropy`
- type Options;
- /// Returned error in case of failure
- type Error: core::fmt::Debug;
-
- /// Generate a key given the extra options and the entropy
- fn generate_with_entropy(
- options: Self::Options,
- entropy: Self::Entropy,
- ) -> Result<GeneratedKey<Self, Ctx>, Self::Error>;
-
- /// Generate a key given the options with a random entropy
- fn generate(options: Self::Options) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- use rand::{thread_rng, Rng};
-
- let mut entropy = Self::Entropy::default();
- thread_rng().fill(entropy.as_mut());
- Self::generate_with_entropy(options, entropy)
- }
-}
-
-/// Trait that allows generating a key with the default options
-///
-/// This trait is automatically implemented if the [`GeneratableKey::Options`] implements [`Default`].
-pub trait GeneratableDefaultOptions<Ctx>: GeneratableKey<Ctx>
-where
- Ctx: ScriptContext,
- <Self as GeneratableKey<Ctx>>::Options: Default,
-{
- /// Generate a key with the default options and a given entropy
- fn generate_with_entropy_default(
- entropy: Self::Entropy,
- ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- Self::generate_with_entropy(Default::default(), entropy)
- }
-
- /// Generate a key with the default options and a random entropy
- fn generate_default() -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- Self::generate(Default::default())
- }
-}
-
-/// Automatic implementation of [`GeneratableDefaultOptions`] for [`GeneratableKey`]s where
-/// `Options` implements `Default`
-impl<Ctx, K> GeneratableDefaultOptions<Ctx> for K
-where
- Ctx: ScriptContext,
- K: GeneratableKey<Ctx>,
- <K as GeneratableKey<Ctx>>::Options: Default,
-{
-}
-
-impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
- type Entropy = [u8; 32];
-
- type Options = ();
- type Error = bip32::Error;
-
- fn generate_with_entropy(
- _: Self::Options,
- entropy: Self::Entropy,
- ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- // pick a arbitrary network here, but say that we support all of them
- let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
- Ok(GeneratedKey::new(xprv, any_network()))
- }
-}
-
-/// Options for generating a [`PrivateKey`]
-///
-/// Defaults to creating compressed keys, which save on-chain bytes and fees
-#[derive(Debug, Copy, Clone)]
-pub struct PrivateKeyGenerateOptions {
- /// Whether the generated key should be "compressed" or not
- pub compressed: bool,
-}
-
-impl Default for PrivateKeyGenerateOptions {
- fn default() -> Self {
- PrivateKeyGenerateOptions { compressed: true }
- }
-}
-
-impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
- type Entropy = [u8; secp256k1::constants::SECRET_KEY_SIZE];
-
- type Options = PrivateKeyGenerateOptions;
- type Error = bip32::Error;
-
- fn generate_with_entropy(
- options: Self::Options,
- entropy: Self::Entropy,
- ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
- // pick a arbitrary network here, but say that we support all of them
- let inner = secp256k1::SecretKey::from_slice(&entropy)?;
- let private_key = PrivateKey {
- compressed: options.compressed,
- network: Network::Bitcoin,
- inner,
- };
-
- Ok(GeneratedKey::new(private_key, any_network()))
- }
-}
-
-impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
- for (T, bip32::DerivationPath)
-{
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- self.0.into_descriptor_key(None, self.1)
- }
-}
-
-impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
- for (T, bip32::KeySource, bip32::DerivationPath)
-{
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- self.0.into_descriptor_key(Some(self.1), self.2)
- }
-}
-
-fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
- pks: Vec<Pk>,
- secp: &SecpCtx,
-) -> Result<(Vec<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError> {
- let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks
- .into_iter()
- .map(|key| key.into_descriptor_key()?.extract(secp))
- .collect::<Result<Vec<_>, _>>()?
- .into_iter()
- .map(|(a, b, c)| (a, (b, c)))
- .unzip();
-
- let (key_map, valid_networks) = key_maps_networks.into_iter().fold(
- (KeyMap::default(), any_network()),
- |(mut keys_acc, net_acc), (key, net)| {
- keys_acc.extend(key);
- let net_acc = merge_networks(&net_acc, &net);
-
- (keys_acc, net_acc)
- },
- );
-
- Ok((pks, key_map, valid_networks))
-}
-
-// Used internally by `bdk::fragment!` to build `pk_k()` fragments
-#[doc(hidden)]
-pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
- descriptor_key: Pk,
- secp: &SecpCtx,
-) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
- let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?;
- let minisc = Miniscript::from_ast(Terminal::PkK(key))?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, key_map, valid_networks))
-}
-
-// Used internally by `bdk::fragment!` to build `pk_h()` fragments
-#[doc(hidden)]
-pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
- descriptor_key: Pk,
- secp: &SecpCtx,
-) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
- let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?;
- let minisc = Miniscript::from_ast(Terminal::PkH(key))?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, key_map, valid_networks))
-}
-
-// Used internally by `bdk::fragment!` to build `multi()` fragments
-#[doc(hidden)]
-pub fn make_multi<
- Pk: IntoDescriptorKey<Ctx>,
- Ctx: ScriptContext,
- V: Fn(usize, Vec<DescriptorPublicKey>) -> Terminal<DescriptorPublicKey, Ctx>,
->(
- thresh: usize,
- variant: V,
- pks: Vec<Pk>,
- secp: &SecpCtx,
-) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
- let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
- let minisc = Miniscript::from_ast(variant(thresh, pks))?;
-
- minisc.check_miniscript()?;
-
- Ok((minisc, key_map, valid_networks))
-}
-
-// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
-#[doc(hidden)]
-pub fn make_sortedmulti<Pk, Ctx, F>(
- thresh: usize,
- pks: Vec<Pk>,
- build_desc: F,
- secp: &SecpCtx,
-) -> Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>
-where
- Pk: IntoDescriptorKey<Ctx>,
- Ctx: ScriptContext,
- F: Fn(
- usize,
- Vec<DescriptorPublicKey>,
- ) -> Result<(Descriptor<DescriptorPublicKey>, PhantomData<Ctx>), DescriptorError>,
-{
- let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
- let descriptor = build_desc(thresh, pks)?.0;
-
- Ok((descriptor, key_map, valid_networks))
-}
-
-/// The "identity" conversion is used internally by some `bdk::fragment`s
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- Ok(self)
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- let networks = match self {
- DescriptorPublicKey::Single(_) => any_network(),
- DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
- if xkey.network == Network::Bitcoin =>
- {
- mainnet_network()
- }
- _ => test_networks(),
- };
-
- Ok(DescriptorKey::from_public(self, networks))
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::FullKey(self),
- origin: None,
- })
- .into_descriptor_key()
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- DescriptorPublicKey::Single(SinglePub {
- key: SinglePubKey::XOnly(self),
- origin: None,
- })
- .into_descriptor_key()
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- let networks = match &self {
- DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
- mainnet_network()
- }
- DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
- if xkey.network == Network::Bitcoin =>
- {
- mainnet_network()
- }
- _ => test_networks(),
- };
-
- Ok(DescriptorKey::from_secret(self, networks))
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- DescriptorSecretKey::from_str(self)
- .map_err(|e| KeyError::Message(e.to_string()))?
- .into_descriptor_key()
- }
-}
-
-impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
- fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
- DescriptorSecretKey::Single(SinglePriv {
- key: self,
- origin: None,
- })
- .into_descriptor_key()
- }
-}
-
-/// Errors thrown while working with [`keys`](crate::keys)
-#[derive(Debug)]
-pub enum KeyError {
- /// The key cannot exist in the given script context
- InvalidScriptContext,
- /// The key is not valid for the given network
- InvalidNetwork,
- /// The key has an invalid checksum
- InvalidChecksum,
-
- /// Custom error message
- Message(String),
-
- /// BIP32 error
- Bip32(bitcoin::bip32::Error),
- /// Miniscript error
- Miniscript(miniscript::Error),
-}
-
-impl From<miniscript::Error> for KeyError {
- fn from(err: miniscript::Error) -> Self {
- KeyError::Miniscript(err)
- }
-}
-
-impl From<bip32::Error> for KeyError {
- fn from(err: bip32::Error) -> Self {
- KeyError::Bip32(err)
- }
-}
-
-impl fmt::Display for KeyError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidScriptContext => write!(f, "Invalid script context"),
- Self::InvalidNetwork => write!(f, "Invalid network"),
- Self::InvalidChecksum => write!(f, "Invalid checksum"),
- Self::Message(err) => write!(f, "{}", err),
- Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
- Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for KeyError {}
-
-#[cfg(test)]
-pub mod test {
- use bitcoin::bip32;
-
- use super::*;
-
- pub const TEST_ENTROPY: [u8; 32] = [0xAA; 32];
-
- #[test]
- fn test_keys_generate_xprv() {
- let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
- bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
-
- assert_eq!(generated_xprv.valid_networks, any_network());
- assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
- }
-
- #[test]
- fn test_keys_generate_wif() {
- let generated_wif: GeneratedKey<_, miniscript::Segwitv0> =
- bitcoin::PrivateKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
-
- assert_eq!(generated_wif.valid_networks, any_network());
- assert_eq!(
- generated_wif.to_string(),
- "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch"
- );
- }
-
- #[cfg(feature = "keys-bip39")]
- #[test]
- fn test_keys_wif_network_bip39() {
- let xkey: ExtendedKey = bip39::Mnemonic::parse_in(
- bip39::Language::English,
- "jelly crash boy whisper mouse ecology tuna soccer memory million news short",
- )
- .unwrap()
- .into_extended_key()
- .unwrap();
- let xprv = xkey.into_xprv(Network::Testnet).unwrap();
-
- assert_eq!(xprv.network, Network::Testnet);
- }
-}
+++ /dev/null
-#![doc = include_str!("../README.md")]
-// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
-#![cfg_attr(docsrs, feature(doc_cfg))]
-#![cfg_attr(
- docsrs,
- doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
-)]
-#![no_std]
-#![warn(missing_docs)]
-
-#[cfg(feature = "std")]
-#[macro_use]
-extern crate std;
-
-#[doc(hidden)]
-#[macro_use]
-pub extern crate alloc;
-
-pub extern crate bitcoin;
-pub extern crate miniscript;
-extern crate serde;
-extern crate serde_json;
-
-#[cfg(feature = "keys-bip39")]
-extern crate bip39;
-
-pub mod descriptor;
-pub mod keys;
-pub mod psbt;
-pub(crate) mod types;
-pub mod wallet;
-
-pub use descriptor::template;
-pub use descriptor::HdKeyPaths;
-pub use types::*;
-pub use wallet::signer;
-pub use wallet::signer::SignOptions;
-pub use wallet::tx_builder::TxBuilder;
-pub use wallet::Wallet;
-
-/// Get the version of BDK at runtime
-pub fn version() -> &'static str {
- env!("CARGO_PKG_VERSION", "unknown")
-}
-
-pub use bdk_chain as chain;
-pub(crate) use bdk_chain::collections;
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Additional functions on the `rust-bitcoin` `Psbt` structure.
-
-use alloc::vec::Vec;
-use bitcoin::Amount;
-use bitcoin::FeeRate;
-use bitcoin::Psbt;
-use bitcoin::TxOut;
-
-// TODO upstream the functions here to `rust-bitcoin`?
-
-/// Trait to add functions to extract utxos and calculate fees.
-pub trait PsbtUtils {
- /// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
- fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
-
- /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
- /// If the PSBT is missing a TxOut for an input returns None.
- fn fee_amount(&self) -> Option<u64>;
-
- /// The transaction's fee rate. This value will only be accurate if calculated AFTER the
- /// `Psbt` is finalized and all witness/signature data is added to the
- /// transaction.
- /// If the PSBT is missing a TxOut for an input returns None.
- fn fee_rate(&self) -> Option<FeeRate>;
-}
-
-impl PsbtUtils for Psbt {
- fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
- let tx = &self.unsigned_tx;
- let input = self.inputs.get(input_index)?;
-
- match (&input.witness_utxo, &input.non_witness_utxo) {
- (Some(_), _) => input.witness_utxo.clone(),
- (_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
- in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
- }),
- _ => None,
- }
- }
-
- fn fee_amount(&self) -> Option<u64> {
- let tx = &self.unsigned_tx;
- let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
-
- utxos.map(|inputs| {
- let input_amount: u64 = inputs.iter().map(|i| i.value.to_sat()).sum();
- let output_amount: u64 = self
- .unsigned_tx
- .output
- .iter()
- .map(|o| o.value.to_sat())
- .sum();
- input_amount
- .checked_sub(output_amount)
- .expect("input amount must be greater than output amount")
- })
- }
-
- fn fee_rate(&self) -> Option<FeeRate> {
- let fee_amount = self.fee_amount();
- let weight = self.clone().extract_tx().ok()?.weight();
- fee_amount.map(|fee| Amount::from_sat(fee) / weight)
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-use alloc::boxed::Box;
-use core::convert::AsRef;
-
-use bdk_chain::ConfirmationTime;
-use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
-use bitcoin::psbt;
-
-use serde::{Deserialize, Serialize};
-
-/// Types of keychains
-#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
-pub enum KeychainKind {
- /// External keychain, used for deriving recipient addresses.
- External = 0,
- /// Internal keychain, used for deriving change addresses.
- Internal = 1,
-}
-
-impl KeychainKind {
- /// Return [`KeychainKind`] as a byte
- pub fn as_byte(&self) -> u8 {
- match self {
- KeychainKind::External => b'e',
- KeychainKind::Internal => b'i',
- }
- }
-}
-
-impl AsRef<[u8]> for KeychainKind {
- fn as_ref(&self) -> &[u8] {
- match self {
- KeychainKind::External => b"e",
- KeychainKind::Internal => b"i",
- }
- }
-}
-
-/// An unspent output owned by a [`Wallet`].
-///
-/// [`Wallet`]: crate::Wallet
-#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
-pub struct LocalOutput {
- /// Reference to a transaction output
- pub outpoint: OutPoint,
- /// Transaction output
- pub txout: TxOut,
- /// Type of keychain
- pub keychain: KeychainKind,
- /// Whether this UTXO is spent or not
- pub is_spent: bool,
- /// The derivation index for the script pubkey in the wallet
- pub derivation_index: u32,
- /// The confirmation time for transaction containing this utxo
- pub confirmation_time: ConfirmationTime,
-}
-
-/// A [`Utxo`] with its `satisfaction_weight`.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WeightedUtxo {
- /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
- /// properly maintain the feerate when adding this input to a transaction during coin selection.
- ///
- /// [weight units]: https://en.bitcoin.it/wiki/Weight_units
- pub satisfaction_weight: usize,
- /// The UTXO
- pub utxo: Utxo,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-/// An unspent transaction output (UTXO).
-pub enum Utxo {
- /// A UTXO owned by the local wallet.
- Local(LocalOutput),
- /// A UTXO owned by another wallet.
- Foreign {
- /// The location of the output.
- outpoint: OutPoint,
- /// The nSequence value to set for this input.
- sequence: Option<Sequence>,
- /// The information about the input we require to add it to a PSBT.
- // Box it to stop the type being too big.
- psbt_input: Box<psbt::Input>,
- },
-}
-
-impl Utxo {
- /// Get the location of the UTXO
- pub fn outpoint(&self) -> OutPoint {
- match &self {
- Utxo::Local(local) => local.outpoint,
- Utxo::Foreign { outpoint, .. } => *outpoint,
- }
- }
-
- /// Get the `TxOut` of the UTXO
- pub fn txout(&self) -> &TxOut {
- match &self {
- Utxo::Local(local) => &local.txout,
- Utxo::Foreign {
- outpoint,
- psbt_input,
- ..
- } => {
- if let Some(prev_tx) = &psbt_input.non_witness_utxo {
- return &prev_tx.output[outpoint.vout as usize];
- }
-
- if let Some(txout) = &psbt_input.witness_utxo {
- return txout;
- }
-
- unreachable!("Foreign UTXOs will always have one of these set")
- }
- }
- }
-
- /// Get the sequence number if an explicit sequence number has to be set for this input.
- pub fn sequence(&self) -> Option<Sequence> {
- match self {
- Utxo::Local(_) => None,
- Utxo::Foreign { sequence, .. } => *sequence,
- }
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Coin selection
-//!
-//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to
-//! define custom coin selection algorithms.
-//!
-//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on
-//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will
-//! be used if it is not explicitly set.
-//!
-//! [`TxBuilder`]: super::tx_builder::TxBuilder
-//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection
-//!
-//! ## Example
-//!
-//! ```
-//! # use std::str::FromStr;
-//! # use bitcoin::*;
-//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
-//! # use bdk::wallet::error::CreateTxError;
-//! # use bdk_persist::PersistBackend;
-//! # use bdk::*;
-//! # use bdk::wallet::coin_selection::decide_change;
-//! # use anyhow::Error;
-//! #[derive(Debug)]
-//! struct AlwaysSpendEverything;
-//!
-//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
-//! fn coin_select(
-//! &self,
-//! required_utxos: Vec<WeightedUtxo>,
-//! optional_utxos: Vec<WeightedUtxo>,
-//! fee_rate: FeeRate,
-//! target_amount: u64,
-//! drain_script: &Script,
-//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
-//! let mut selected_amount = 0;
-//! let mut additional_weight = Weight::ZERO;
-//! let all_utxos_selected = required_utxos
-//! .into_iter()
-//! .chain(optional_utxos)
-//! .scan(
-//! (&mut selected_amount, &mut additional_weight),
-//! |(selected_amount, additional_weight), weighted_utxo| {
-//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
-//! **additional_weight += Weight::from_wu(
-//! (TxIn::default().segwit_weight().to_wu()
-//! + weighted_utxo.satisfaction_weight as u64)
-//! as u64,
-//! );
-//! Some(weighted_utxo.utxo)
-//! },
-//! )
-//! .collect::<Vec<_>>();
-//! let additional_fees = (fee_rate * additional_weight).to_sat();
-//! let amount_needed_with_fees = additional_fees + target_amount;
-//! if selected_amount < amount_needed_with_fees {
-//! return Err(coin_selection::Error::InsufficientFunds {
-//! needed: amount_needed_with_fees,
-//! available: selected_amount,
-//! });
-//! }
-//!
-//! let remaining_amount = selected_amount - amount_needed_with_fees;
-//!
-//! let excess = decide_change(remaining_amount, fee_rate, drain_script);
-//!
-//! Ok(CoinSelectionResult {
-//! selected: all_utxos_selected,
-//! fee_amount: additional_fees,
-//! excess,
-//! })
-//! }
-//! }
-//!
-//! # let mut wallet = doctest_wallet!();
-//! // create wallet, sync, ...
-//!
-//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
-//! .unwrap()
-//! .require_network(Network::Testnet)
-//! .unwrap();
-//! let psbt = {
-//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
-//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
-//! builder.finish()?
-//! };
-//!
-//! // inspect, sign, broadcast, ...
-//!
-//! # Ok::<(), anyhow::Error>(())
-//! ```
-
-use crate::chain::collections::HashSet;
-use crate::wallet::utils::IsDust;
-use crate::Utxo;
-use crate::WeightedUtxo;
-use bitcoin::FeeRate;
-
-use alloc::vec::Vec;
-use bitcoin::consensus::encode::serialize;
-use bitcoin::OutPoint;
-use bitcoin::TxIn;
-use bitcoin::{Script, Weight};
-
-use core::convert::TryInto;
-use core::fmt::{self, Formatter};
-use rand::seq::SliceRandom;
-
-/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
-/// overridden
-pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
-
-/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
-#[derive(Debug)]
-pub enum Error {
- /// Wallet's UTXO set is not enough to cover recipient's requested plus fee
- InsufficientFunds {
- /// Sats needed for some transaction
- needed: u64,
- /// Sats available for spending
- available: u64,
- },
- /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for
- /// the desired outputs plus fee, if there is not such combination this error is thrown
- BnBNoExactMatch,
- /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow
- /// exponentially, thus a limit is set, and when hit, this error is thrown
- BnBTotalTriesExceeded,
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- match self {
- Self::InsufficientFunds { needed, available } => write!(
- f,
- "Insufficient funds: {} sat available of {} sat needed",
- available, needed
- ),
- Self::BnBTotalTriesExceeded => {
- write!(f, "Branch and bound coin selection: total tries exceeded")
- }
- Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for Error {}
-
-#[derive(Debug)]
-/// Remaining amount after performing coin selection
-pub enum Excess {
- /// It's not possible to create spendable output from excess using the current drain output
- NoChange {
- /// Threshold to consider amount as dust for this particular change script_pubkey
- dust_threshold: u64,
- /// Exceeding amount of current selection over outgoing value and fee costs
- remaining_amount: u64,
- /// The calculated fee for the drain TxOut with the selected script_pubkey
- change_fee: u64,
- },
- /// It's possible to create spendable output from excess using the current drain output
- Change {
- /// Effective amount available to create change after deducting the change output fee
- amount: u64,
- /// The deducted change output fee
- fee: u64,
- },
-}
-
-/// Result of a successful coin selection
-#[derive(Debug)]
-pub struct CoinSelectionResult {
- /// List of outputs selected for use as inputs
- pub selected: Vec<Utxo>,
- /// Total fee amount for the selected utxos in satoshis
- pub fee_amount: u64,
- /// Remaining amount after deducing fees and outgoing outputs
- pub excess: Excess,
-}
-
-impl CoinSelectionResult {
- /// The total value of the inputs selected.
- pub fn selected_amount(&self) -> u64 {
- self.selected.iter().map(|u| u.txout().value.to_sat()).sum()
- }
-
- /// The total value of the inputs selected from the local wallet.
- pub fn local_selected_amount(&self) -> u64 {
- self.selected
- .iter()
- .filter_map(|u| match u {
- Utxo::Local(_) => Some(u.txout().value.to_sat()),
- _ => None,
- })
- .sum()
- }
-}
-
-/// Trait for generalized coin selection algorithms
-///
-/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
-/// selection algorithm when it creates transactions.
-///
-/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
-pub trait CoinSelectionAlgorithm: core::fmt::Debug {
- /// Perform the coin selection
- ///
- /// - `database`: a reference to the wallet's database that can be used to lookup additional
- /// details for a specific UTXO
- /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their
- /// weight cost
- /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their
- /// weight cost
- /// - `fee_rate`: fee rate to use
- /// - `target_amount`: the outgoing amount in satoshis and the fees already
- /// accumulated from added outputs and transaction’s header.
- /// - `drain_script`: the script to use in case of change
- #[allow(clippy::too_many_arguments)]
- fn coin_select(
- &self,
- required_utxos: Vec<WeightedUtxo>,
- optional_utxos: Vec<WeightedUtxo>,
- fee_rate: FeeRate,
- target_amount: u64,
- drain_script: &Script,
- ) -> Result<CoinSelectionResult, Error>;
-}
-
-/// Simple and dumb coin selection
-///
-/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
-/// from the largest ones until the required amount is reached.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct LargestFirstCoinSelection;
-
-impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
- fn coin_select(
- &self,
- required_utxos: Vec<WeightedUtxo>,
- mut optional_utxos: Vec<WeightedUtxo>,
- fee_rate: FeeRate,
- target_amount: u64,
- drain_script: &Script,
- ) -> Result<CoinSelectionResult, Error> {
- // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
- // initially smallest to largest, before being reversed with `.rev()`.
- let utxos = {
- optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
- required_utxos
- .into_iter()
- .map(|utxo| (true, utxo))
- .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
- };
-
- select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
- }
-}
-
-/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next
-///
-/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting
-/// from the oldest ones until the required amount is reached.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct OldestFirstCoinSelection;
-
-impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
- fn coin_select(
- &self,
- required_utxos: Vec<WeightedUtxo>,
- mut optional_utxos: Vec<WeightedUtxo>,
- fee_rate: FeeRate,
- target_amount: u64,
- drain_script: &Script,
- ) -> Result<CoinSelectionResult, Error> {
- // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
- // oldest to newest according to blocktime
- // For utxo that doesn't exist in DB, they will have lowest priority to be selected
- let utxos = {
- optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
- Utxo::Local(local) => Some(local.confirmation_time),
- Utxo::Foreign { .. } => None,
- });
-
- required_utxos
- .into_iter()
- .map(|utxo| (true, utxo))
- .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
- };
-
- select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
- }
-}
-
-/// Decide if change can be created
-///
-/// - `remaining_amount`: the amount in which the selected coins exceed the target amount
-/// - `fee_rate`: required fee rate for the current selection
-/// - `drain_script`: script to consider change creation
-pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
- // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
- let drain_output_len = serialize(drain_script).len() + 8usize;
- let change_fee =
- (fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
- let drain_val = remaining_amount.saturating_sub(change_fee);
-
- if drain_val.is_dust(drain_script) {
- let dust_threshold = drain_script.dust_value().to_sat();
- Excess::NoChange {
- dust_threshold,
- change_fee,
- remaining_amount,
- }
- } else {
- Excess::Change {
- amount: drain_val,
- fee: change_fee,
- }
- }
-}
-
-fn select_sorted_utxos(
- utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
- fee_rate: FeeRate,
- target_amount: u64,
- drain_script: &Script,
-) -> Result<CoinSelectionResult, Error> {
- let mut selected_amount = 0;
- let mut fee_amount = 0;
- let selected = utxos
- .scan(
- (&mut selected_amount, &mut fee_amount),
- |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
- if must_use || **selected_amount < target_amount + **fee_amount {
- **fee_amount += (fee_rate
- * Weight::from_wu(
- TxIn::default().segwit_weight().to_wu()
- + weighted_utxo.satisfaction_weight as u64,
- ))
- .to_sat();
- **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
- Some(weighted_utxo.utxo)
- } else {
- None
- }
- },
- )
- .collect::<Vec<_>>();
-
- let amount_needed_with_fees = target_amount + fee_amount;
- if selected_amount < amount_needed_with_fees {
- return Err(Error::InsufficientFunds {
- needed: amount_needed_with_fees,
- available: selected_amount,
- });
- }
-
- let remaining_amount = selected_amount - amount_needed_with_fees;
-
- let excess = decide_change(remaining_amount, fee_rate, drain_script);
-
- Ok(CoinSelectionResult {
- selected,
- fee_amount,
- excess,
- })
-}
-
-#[derive(Debug, Clone)]
-// Adds fee information to an UTXO.
-struct OutputGroup {
- weighted_utxo: WeightedUtxo,
- // Amount of fees for spending a certain utxo, calculated using a certain FeeRate
- fee: u64,
- // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
- effective_value: i64,
-}
-
-impl OutputGroup {
- fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
- let fee = (fee_rate
- * Weight::from_wu(
- TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
- ))
- .to_sat();
- let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
- OutputGroup {
- weighted_utxo,
- fee,
- effective_value,
- }
- }
-}
-
-/// Branch and bound coin selection
-///
-/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: <http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
-#[derive(Debug, Clone)]
-pub struct BranchAndBoundCoinSelection {
- size_of_change: u64,
-}
-
-impl Default for BranchAndBoundCoinSelection {
- fn default() -> Self {
- Self {
- // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes)
- size_of_change: 8 + 1 + 22,
- }
- }
-}
-
-impl BranchAndBoundCoinSelection {
- /// Create new instance with target size for change output
- pub fn new(size_of_change: u64) -> Self {
- Self { size_of_change }
- }
-}
-
-const BNB_TOTAL_TRIES: usize = 100_000;
-
-impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
- fn coin_select(
- &self,
- required_utxos: Vec<WeightedUtxo>,
- optional_utxos: Vec<WeightedUtxo>,
- fee_rate: FeeRate,
- target_amount: u64,
- drain_script: &Script,
- ) -> Result<CoinSelectionResult, Error> {
- // Mapping every (UTXO, usize) to an output group
- let required_utxos: Vec<OutputGroup> = required_utxos
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative
- // effective value
- let optional_utxos: Vec<OutputGroup> = optional_utxos
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .filter(|u| u.effective_value.is_positive())
- .collect();
-
- let curr_value = required_utxos
- .iter()
- .fold(0, |acc, x| acc + x.effective_value);
-
- let curr_available_value = optional_utxos
- .iter()
- .fold(0, |acc, x| acc + x.effective_value);
-
- let cost_of_change =
- (Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
-
- // `curr_value` and `curr_available_value` are both the sum of *effective_values* of
- // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
- // negative effective value, so it will always be positive.
- //
- // Since we are required to spend the required UTXOs (curr_value) we have to consider
- // all their effective values, even when negative, which means that curr_value could
- // be negative as well.
- //
- // If the sum of curr_value and curr_available_value is negative or lower than our target,
- // we can immediately exit with an error, as it's guaranteed we will never find a solution
- // if we actually run the BnB.
- let total_value: Result<u64, _> = (curr_available_value + curr_value).try_into();
- match total_value {
- Ok(v) if v >= target_amount => {}
- _ => {
- // Assume we spend all the UTXOs we can (all the required + all the optional with
- // positive effective value), sum their value and their fee cost.
- let (utxo_fees, utxo_value) = required_utxos
- .iter()
- .chain(optional_utxos.iter())
- .fold((0, 0), |(mut fees, mut value), utxo| {
- fees += utxo.fee;
- value += utxo.weighted_utxo.utxo.txout().value.to_sat();
-
- (fees, value)
- });
-
- // Add to the target the fee cost of the UTXOs
- return Err(Error::InsufficientFunds {
- needed: target_amount + utxo_fees,
- available: utxo_value,
- });
- }
- }
-
- let target_amount = target_amount
- .try_into()
- .expect("Bitcoin amount to fit into i64");
-
- if curr_value > target_amount {
- // remaining_amount can't be negative as that would mean the
- // selection wasn't successful
- // target_amount = amount_needed + (fee_amount - vin_fees)
- let remaining_amount = (curr_value - target_amount) as u64;
-
- let excess = decide_change(remaining_amount, fee_rate, drain_script);
-
- return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
- vec![],
- required_utxos,
- excess,
- ));
- }
-
- Ok(self
- .bnb(
- required_utxos.clone(),
- optional_utxos.clone(),
- curr_value,
- curr_available_value,
- target_amount,
- cost_of_change,
- drain_script,
- fee_rate,
- )
- .unwrap_or_else(|_| {
- self.single_random_draw(
- required_utxos,
- optional_utxos,
- curr_value,
- target_amount,
- drain_script,
- fee_rate,
- )
- }))
- }
-}
-
-impl BranchAndBoundCoinSelection {
- // TODO: make this more Rust-onic :)
- // (And perhaps refactor with less arguments?)
- #[allow(clippy::too_many_arguments)]
- fn bnb(
- &self,
- required_utxos: Vec<OutputGroup>,
- mut optional_utxos: Vec<OutputGroup>,
- mut curr_value: i64,
- mut curr_available_value: i64,
- target_amount: i64,
- cost_of_change: u64,
- drain_script: &Script,
- fee_rate: FeeRate,
- ) -> Result<CoinSelectionResult, Error> {
- // current_selection[i] will contain true if we are using optional_utxos[i],
- // false otherwise. Note that current_selection.len() could be less than
- // optional_utxos.len(), it just means that we still haven't decided if we should keep
- // certain optional_utxos or not.
- let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
-
- // Sort the utxo_pool
- optional_utxos.sort_unstable_by_key(|a| a.effective_value);
- optional_utxos.reverse();
-
- // Contains the best selection we found
- let mut best_selection = Vec::new();
- let mut best_selection_value = None;
-
- // Depth First search loop for choosing the UTXOs
- for _ in 0..BNB_TOTAL_TRIES {
- // Conditions for starting a backtrack
- let mut backtrack = false;
- // Cannot possibly reach target with the amount remaining in the curr_available_value,
- // or the selected value is out of range.
- // Go back and try other branch
- if curr_value + curr_available_value < target_amount
- || curr_value > target_amount + cost_of_change as i64
- {
- backtrack = true;
- } else if curr_value >= target_amount {
- // Selected value is within range, there's no point in going forward. Start
- // backtracking
- backtrack = true;
-
- // If we found a solution better than the previous one, or if there wasn't previous
- // solution, update the best solution
- if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
- best_selection.clone_from(¤t_selection);
- best_selection_value = Some(curr_value);
- }
-
- // If we found a perfect match, break here
- if curr_value == target_amount {
- break;
- }
- }
-
- // Backtracking, moving backwards
- if backtrack {
- // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
- while let Some(false) = current_selection.last() {
- current_selection.pop();
- curr_available_value += optional_utxos[current_selection.len()].effective_value;
- }
-
- if current_selection.last_mut().is_none() {
- // We have walked back to the first utxo and no branch is untraversed. All solutions searched
- // If best selection is empty, then there's no exact match
- if best_selection.is_empty() {
- return Err(Error::BnBNoExactMatch);
- }
- break;
- }
-
- if let Some(c) = current_selection.last_mut() {
- // Output was included on previous iterations, try excluding now.
- *c = false;
- }
-
- let utxo = &optional_utxos[current_selection.len() - 1];
- curr_value -= utxo.effective_value;
- } else {
- // Moving forwards, continuing down this branch
- let utxo = &optional_utxos[current_selection.len()];
-
- // Remove this utxo from the curr_available_value utxo amount
- curr_available_value -= utxo.effective_value;
-
- // Inclusion branch first (Largest First Exploration)
- current_selection.push(true);
- curr_value += utxo.effective_value;
- }
- }
-
- // Check for solution
- if best_selection.is_empty() {
- return Err(Error::BnBTotalTriesExceeded);
- }
-
- // Set output set
- let selected_utxos = optional_utxos
- .into_iter()
- .zip(best_selection)
- .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
- .collect::<Vec<OutputGroup>>();
-
- let selected_amount = best_selection_value.unwrap();
-
- // remaining_amount can't be negative as that would mean the
- // selection wasn't successful
- // target_amount = amount_needed + (fee_amount - vin_fees)
- let remaining_amount = (selected_amount - target_amount) as u64;
-
- let excess = decide_change(remaining_amount, fee_rate, drain_script);
-
- Ok(BranchAndBoundCoinSelection::calculate_cs_result(
- selected_utxos,
- required_utxos,
- excess,
- ))
- }
-
- #[allow(clippy::too_many_arguments)]
- fn single_random_draw(
- &self,
- required_utxos: Vec<OutputGroup>,
- mut optional_utxos: Vec<OutputGroup>,
- curr_value: i64,
- target_amount: i64,
- drain_script: &Script,
- fee_rate: FeeRate,
- ) -> CoinSelectionResult {
- optional_utxos.shuffle(&mut rand::thread_rng());
- let selected_utxos = optional_utxos.into_iter().fold(
- (curr_value, vec![]),
- |(mut amount, mut utxos), utxo| {
- if amount >= target_amount {
- (amount, utxos)
- } else {
- amount += utxo.effective_value;
- utxos.push(utxo);
- (amount, utxos)
- }
- },
- );
-
- // remaining_amount can't be negative as that would mean the
- // selection wasn't successful
- // target_amount = amount_needed + (fee_amount - vin_fees)
- let remaining_amount = (selected_utxos.0 - target_amount) as u64;
-
- let excess = decide_change(remaining_amount, fee_rate, drain_script);
-
- BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
- }
-
- fn calculate_cs_result(
- mut selected_utxos: Vec<OutputGroup>,
- mut required_utxos: Vec<OutputGroup>,
- excess: Excess,
- ) -> CoinSelectionResult {
- selected_utxos.append(&mut required_utxos);
- let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::<u64>();
- let selected = selected_utxos
- .into_iter()
- .map(|u| u.weighted_utxo.utxo)
- .collect::<Vec<_>>();
-
- CoinSelectionResult {
- selected,
- fee_amount,
- excess,
- }
- }
-}
-
-/// Remove duplicate UTXOs.
-///
-/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
-pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
-where
- I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
-{
- let mut visited = HashSet::<OutPoint>::new();
- let required = required
- .into_iter()
- .filter(|utxo| visited.insert(utxo.utxo.outpoint()))
- .collect::<I>();
- let optional = optional
- .into_iter()
- .filter(|utxo| visited.insert(utxo.utxo.outpoint()))
- .collect::<I>();
- (required, optional)
-}
-
-#[cfg(test)]
-mod test {
- use assert_matches::assert_matches;
- use core::str::FromStr;
-
- use bdk_chain::ConfirmationTime;
- use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
-
- use super::*;
- use crate::types::*;
- use crate::wallet::coin_selection::filter_duplicates;
-
- use rand::rngs::StdRng;
- use rand::seq::SliceRandom;
- use rand::{Rng, RngCore, SeedableRng};
-
- // signature len (1WU) + signature and sighash (72WU)
- // + pubkey len (1WU) + pubkey (33WU)
- const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
-
- const FEE_AMOUNT: u64 = 50;
-
- fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo {
- assert!(index < 10);
- let outpoint = OutPoint::from_str(&format!(
- "000000000000000000000000000000000000000000000000000000000000000{}:0",
- index
- ))
- .unwrap();
- WeightedUtxo {
- satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
- utxo: Utxo::Local(LocalOutput {
- outpoint,
- txout: TxOut {
- value: Amount::from_sat(value),
- script_pubkey: ScriptBuf::new(),
- },
- keychain: KeychainKind::External,
- is_spent: false,
- derivation_index: 42,
- confirmation_time,
- }),
- }
- }
-
- fn get_test_utxos() -> Vec<WeightedUtxo> {
- vec![
- utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }),
- utxo(
- FEE_AMOUNT - 40,
- 1,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- ),
- utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }),
- ]
- }
-
- fn get_oldest_first_test_utxos() -> Vec<WeightedUtxo> {
- // ensure utxos are from different tx
- let utxo1 = utxo(
- 120_000,
- 1,
- ConfirmationTime::Confirmed {
- height: 1,
- time: 1231006505,
- },
- );
- let utxo2 = utxo(
- 80_000,
- 2,
- ConfirmationTime::Confirmed {
- height: 2,
- time: 1231006505,
- },
- );
- let utxo3 = utxo(
- 300_000,
- 3,
- ConfirmationTime::Confirmed {
- height: 3,
- time: 1231006505,
- },
- );
- vec![utxo1, utxo2, utxo3]
- }
-
- fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
- let mut res = Vec::new();
- for i in 0..utxos_number {
- res.push(WeightedUtxo {
- satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
- utxo: Utxo::Local(LocalOutput {
- outpoint: OutPoint::from_str(&format!(
- "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
- i
- ))
- .unwrap(),
- txout: TxOut {
- value: Amount::from_sat(rng.gen_range(0..200000000)),
- script_pubkey: ScriptBuf::new(),
- },
- keychain: KeychainKind::External,
- is_spent: false,
- derivation_index: rng.next_u32(),
- confirmation_time: if rng.gen_bool(0.5) {
- ConfirmationTime::Confirmed {
- height: rng.next_u32(),
- time: rng.next_u64(),
- }
- } else {
- ConfirmationTime::Unconfirmed { last_seen: 0 }
- },
- }),
- });
- }
- res
- }
-
- fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
- (0..utxos_number)
- .map(|i| WeightedUtxo {
- satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
- utxo: Utxo::Local(LocalOutput {
- outpoint: OutPoint::from_str(&format!(
- "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
- i
- ))
- .unwrap(),
- txout: TxOut {
- value: Amount::from_sat(utxos_value),
- script_pubkey: ScriptBuf::new(),
- },
- keychain: KeychainKind::External,
- is_spent: false,
- derivation_index: 42,
- confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
- }),
- })
- .collect()
- }
-
- fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
- let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
- utxos.shuffle(&mut rng);
- utxos[..utxos_picked_len]
- .iter()
- .map(|u| u.utxo.txout().value.to_sat())
- .sum()
- }
-
- #[test]
- fn test_largest_first_coin_selection_success() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 250_000 + FEE_AMOUNT;
-
- let result = LargestFirstCoinSelection
- .coin_select(
- utxos,
- vec![],
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 3);
- assert_eq!(result.selected_amount(), 300_010);
- assert_eq!(result.fee_amount, 204)
- }
-
- #[test]
- fn test_largest_first_coin_selection_use_all() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let result = LargestFirstCoinSelection
- .coin_select(
- utxos,
- vec![],
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 3);
- assert_eq!(result.selected_amount(), 300_010);
- assert_eq!(result.fee_amount, 204);
- }
-
- #[test]
- fn test_largest_first_coin_selection_use_only_necessary() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let result = LargestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 1);
- assert_eq!(result.selected_amount(), 200_000);
- assert_eq!(result.fee_amount, 68);
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_largest_first_coin_selection_insufficient_funds() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 500_000 + FEE_AMOUNT;
-
- LargestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 250_000 + FEE_AMOUNT;
-
- LargestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1000),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- fn test_oldest_first_coin_selection_success() {
- let utxos = get_oldest_first_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 180_000 + FEE_AMOUNT;
-
- let result = OldestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 2);
- assert_eq!(result.selected_amount(), 200_000);
- assert_eq!(result.fee_amount, 136)
- }
-
- #[test]
- fn test_oldest_first_coin_selection_use_all() {
- let utxos = get_oldest_first_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let result = OldestFirstCoinSelection
- .coin_select(
- utxos,
- vec![],
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 3);
- assert_eq!(result.selected_amount(), 500_000);
- assert_eq!(result.fee_amount, 204);
- }
-
- #[test]
- fn test_oldest_first_coin_selection_use_only_necessary() {
- let utxos = get_oldest_first_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let result = OldestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 1);
- assert_eq!(result.selected_amount(), 120_000);
- assert_eq!(result.fee_amount, 68);
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_oldest_first_coin_selection_insufficient_funds() {
- let utxos = get_oldest_first_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 600_000 + FEE_AMOUNT;
-
- OldestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
- let utxos = get_oldest_first_test_utxos();
-
- let target_amount: u64 = utxos
- .iter()
- .map(|wu| wu.utxo.txout().value.to_sat())
- .sum::<u64>()
- - 50;
- let drain_script = ScriptBuf::default();
-
- OldestFirstCoinSelection
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1000),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- fn test_bnb_coin_selection_success() {
- // In this case bnb won't find a suitable match and single random draw will
- // select three outputs
- let utxos = generate_same_value_utxos(100_000, 20);
-
- let drain_script = ScriptBuf::default();
-
- let target_amount = 250_000 + FEE_AMOUNT;
-
- let result = BranchAndBoundCoinSelection::default()
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 3);
- assert_eq!(result.selected_amount(), 300_000);
- assert_eq!(result.fee_amount, 204);
- }
-
- #[test]
- fn test_bnb_coin_selection_required_are_enough() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let result = BranchAndBoundCoinSelection::default()
- .coin_select(
- utxos.clone(),
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 3);
- assert_eq!(result.selected_amount(), 300_010);
- assert_eq!(result.fee_amount, 204);
- }
-
- #[test]
- fn test_bnb_coin_selection_optional_are_enough() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 299756 + FEE_AMOUNT;
-
- let result = BranchAndBoundCoinSelection::default()
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 2);
- assert_eq!(result.selected_amount(), 300000);
- assert_eq!(result.fee_amount, 136);
- }
-
- #[test]
- #[ignore]
- fn test_bnb_coin_selection_required_not_enough() {
- let utxos = get_test_utxos();
-
- let required = vec![utxos[0].clone()];
- let mut optional = utxos[1..].to_vec();
- optional.push(utxo(
- 500_000,
- 3,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- ));
-
- // Defensive assertions, for sanity and in case someone changes the test utxos vector.
- let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
- assert_eq!(amount, 100_000);
- let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
- assert!(amount > 150_000);
- let drain_script = ScriptBuf::default();
-
- let target_amount = 150_000 + FEE_AMOUNT;
-
- let result = BranchAndBoundCoinSelection::default()
- .coin_select(
- required,
- optional,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
-
- assert_eq!(result.selected.len(), 2);
- assert_eq!(result.selected_amount(), 300_000);
- assert_eq!(result.fee_amount, 136);
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_bnb_coin_selection_insufficient_funds() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 500_000 + FEE_AMOUNT;
-
- BranchAndBoundCoinSelection::default()
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- #[should_panic(expected = "InsufficientFunds")]
- fn test_bnb_coin_selection_insufficient_funds_high_fees() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 250_000 + FEE_AMOUNT;
-
- BranchAndBoundCoinSelection::default()
- .coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(1000),
- target_amount,
- &drain_script,
- )
- .unwrap();
- }
-
- #[test]
- fn test_bnb_coin_selection_check_fee_rate() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
- let target_amount = 99932; // first utxo's effective value
- let feerate = FeeRate::BROADCAST_MIN;
-
- let result = BranchAndBoundCoinSelection::new(0)
- .coin_select(vec![], utxos, feerate, target_amount, &drain_script)
- .unwrap();
-
- assert_eq!(result.selected.len(), 1);
- assert_eq!(result.selected_amount(), 100_000);
- let input_weight =
- TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64;
- // the final fee rate should be exactly the same as the fee rate given
- let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
- assert_eq!(result_feerate, feerate);
- }
-
- #[test]
- fn test_bnb_coin_selection_exact_match() {
- let seed = [0; 32];
- let mut rng: StdRng = SeedableRng::from_seed(seed);
-
- for _i in 0..200 {
- let mut optional_utxos = generate_random_utxos(&mut rng, 16);
- let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
- let drain_script = ScriptBuf::default();
- let result = BranchAndBoundCoinSelection::new(0)
- .coin_select(
- vec![],
- optional_utxos,
- FeeRate::ZERO,
- target_amount,
- &drain_script,
- )
- .unwrap();
- assert_eq!(result.selected_amount(), target_amount);
- }
- }
-
- #[test]
- #[should_panic(expected = "BnBNoExactMatch")]
- fn test_bnb_function_no_exact_match() {
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
- let utxos: Vec<OutputGroup> = get_test_utxos()
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
-
- let size_of_change = 31;
- let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
-
- let drain_script = ScriptBuf::default();
- let target_amount = 20_000 + FEE_AMOUNT;
- BranchAndBoundCoinSelection::new(size_of_change)
- .bnb(
- vec![],
- utxos,
- 0,
- curr_available_value,
- target_amount as i64,
- cost_of_change,
- &drain_script,
- fee_rate,
- )
- .unwrap();
- }
-
- #[test]
- #[should_panic(expected = "BnBTotalTriesExceeded")]
- fn test_bnb_function_tries_exceeded() {
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
- let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
-
- let size_of_change = 31;
- let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
- let target_amount = 20_000 + FEE_AMOUNT;
-
- let drain_script = ScriptBuf::default();
-
- BranchAndBoundCoinSelection::new(size_of_change)
- .bnb(
- vec![],
- utxos,
- 0,
- curr_available_value,
- target_amount as i64,
- cost_of_change,
- &drain_script,
- fee_rate,
- )
- .unwrap();
- }
-
- // The match won't be exact but still in the range
- #[test]
- fn test_bnb_function_almost_exact_match_with_fees() {
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
- let size_of_change = 31;
- let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
-
- let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- let curr_value = 0;
-
- let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
-
- // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
- // cost_of_change + 5.
- let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
-
- let drain_script = ScriptBuf::default();
-
- let result = BranchAndBoundCoinSelection::new(size_of_change)
- .bnb(
- vec![],
- utxos,
- curr_value,
- curr_available_value,
- target_amount,
- cost_of_change,
- &drain_script,
- fee_rate,
- )
- .unwrap();
- assert_eq!(result.selected_amount(), 100_000);
- assert_eq!(result.fee_amount, 136);
- }
-
- // TODO: bnb() function should be optimized, and this test should be done with more utxos
- #[test]
- fn test_bnb_function_exact_match_more_utxos() {
- let seed = [0; 32];
- let mut rng: StdRng = SeedableRng::from_seed(seed);
- let fee_rate = FeeRate::ZERO;
-
- for _ in 0..200 {
- let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- let curr_value = 0;
-
- let curr_available_value = optional_utxos
- .iter()
- .fold(0, |acc, x| acc + x.effective_value);
-
- let target_amount =
- optional_utxos[3].effective_value + optional_utxos[23].effective_value;
-
- let drain_script = ScriptBuf::default();
-
- let result = BranchAndBoundCoinSelection::new(0)
- .bnb(
- vec![],
- optional_utxos,
- curr_value,
- curr_available_value,
- target_amount,
- 0,
- &drain_script,
- fee_rate,
- )
- .unwrap();
- assert_eq!(result.selected_amount(), target_amount as u64);
- }
- }
-
- #[test]
- fn test_single_random_draw_function_success() {
- let seed = [0; 32];
- let mut rng: StdRng = SeedableRng::from_seed(seed);
- let mut utxos = generate_random_utxos(&mut rng, 300);
- let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
-
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
- let utxos: Vec<OutputGroup> = utxos
- .into_iter()
- .map(|u| OutputGroup::new(u, fee_rate))
- .collect();
-
- let drain_script = ScriptBuf::default();
-
- let result = BranchAndBoundCoinSelection::default().single_random_draw(
- vec![],
- utxos,
- 0,
- target_amount as i64,
- &drain_script,
- fee_rate,
- );
-
- assert!(result.selected_amount() > target_amount);
- assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
- }
-
- #[test]
- fn test_bnb_exclude_negative_effective_value() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
-
- let selection = BranchAndBoundCoinSelection::default().coin_select(
- vec![],
- utxos,
- FeeRate::from_sat_per_vb_unchecked(10),
- 500_000,
- &drain_script,
- );
-
- assert_matches!(
- selection,
- Err(Error::InsufficientFunds {
- available: 300_000,
- ..
- })
- );
- }
-
- #[test]
- fn test_bnb_include_negative_effective_value_when_required() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
-
- let (required, optional) = utxos.into_iter().partition(
- |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
- );
-
- let selection = BranchAndBoundCoinSelection::default().coin_select(
- required,
- optional,
- FeeRate::from_sat_per_vb_unchecked(10),
- 500_000,
- &drain_script,
- );
-
- assert_matches!(
- selection,
- Err(Error::InsufficientFunds {
- available: 300_010,
- ..
- })
- );
- }
-
- #[test]
- fn test_bnb_sum_of_effective_value_negative() {
- let utxos = get_test_utxos();
- let drain_script = ScriptBuf::default();
-
- let selection = BranchAndBoundCoinSelection::default().coin_select(
- utxos,
- vec![],
- FeeRate::from_sat_per_vb_unchecked(10_000),
- 500_000,
- &drain_script,
- );
-
- assert_matches!(
- selection,
- Err(Error::InsufficientFunds {
- available: 300_010,
- ..
- })
- );
- }
-
- #[test]
- fn test_filter_duplicates() {
- fn utxo(txid: &str, value: u64) -> WeightedUtxo {
- WeightedUtxo {
- satisfaction_weight: 0,
- utxo: Utxo::Local(LocalOutput {
- outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
- txout: TxOut {
- value: Amount::from_sat(value),
- script_pubkey: ScriptBuf::new(),
- },
- keychain: KeychainKind::External,
- is_spent: false,
- derivation_index: 0,
- confirmation_time: ConfirmationTime::Confirmed {
- height: 12345,
- time: 12345,
- },
- }),
- }
- }
-
- fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
- let mut v = utxos
- .iter()
- .map(|&(txid, value)| utxo(txid, value))
- .collect::<Vec<_>>();
- v.sort_by_key(|u| u.utxo.outpoint());
- v
- }
-
- struct TestCase<'a> {
- name: &'a str,
- required: &'a [(&'a str, u64)],
- optional: &'a [(&'a str, u64)],
- exp_required: &'a [(&'a str, u64)],
- exp_optional: &'a [(&'a str, u64)],
- }
-
- let test_cases = [
- TestCase {
- name: "no_duplicates",
- required: &[("A", 1000), ("B", 2100)],
- optional: &[("C", 1000)],
- exp_required: &[("A", 1000), ("B", 2100)],
- exp_optional: &[("C", 1000)],
- },
- TestCase {
- name: "duplicate_required_utxos",
- required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
- optional: &[("D", 2100)],
- exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
- exp_optional: &[("D", 2100)],
- },
- TestCase {
- name: "duplicate_optional_utxos",
- required: &[("A", 3000), ("B", 1200)],
- optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
- exp_required: &[("A", 3000), ("B", 1200)],
- exp_optional: &[("C", 5000), ("D", 1300)],
- },
- TestCase {
- name: "duplicate_across_required_and_optional_utxos",
- required: &[("A", 3000), ("B", 1200), ("C", 2100)],
- optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
- exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
- exp_optional: &[("D", 1200), ("E", 5000)],
- },
- ];
-
- for (i, t) in test_cases.into_iter().enumerate() {
- println!("Case {}: {}", i, t.name);
- let (required, optional) =
- filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
- assert_eq!(
- required,
- to_utxo_vec(t.exp_required),
- "[{}:{}] unexpected `required` result",
- i,
- t.name
- );
- assert_eq!(
- optional,
- to_utxo_vec(t.exp_optional),
- "[{}:{}] unexpected `optional` result",
- i,
- t.name
- );
- }
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
-
-use crate::descriptor::policy::PolicyError;
-use crate::descriptor::DescriptorError;
-use crate::wallet::coin_selection;
-use crate::{descriptor, KeychainKind};
-use alloc::string::String;
-use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
-use core::fmt;
-
-/// Errors returned by miniscript when updating inconsistent PSBTs
-#[derive(Debug, Clone)]
-pub enum MiniscriptPsbtError {
- /// Descriptor key conversion error
- Conversion(miniscript::descriptor::ConversionError),
- /// Return error type for PsbtExt::update_input_with_descriptor
- UtxoUpdate(miniscript::psbt::UtxoUpdateError),
- /// Return error type for PsbtExt::update_output_with_descriptor
- OutputUpdate(miniscript::psbt::OutputUpdateError),
-}
-
-impl fmt::Display for MiniscriptPsbtError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Conversion(err) => write!(f, "Conversion error: {}", err),
- Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
- Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for MiniscriptPsbtError {}
-
-#[derive(Debug)]
-/// Error returned from [`TxBuilder::finish`]
-///
-/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
-pub enum CreateTxError {
- /// There was a problem with the descriptors passed in
- Descriptor(DescriptorError),
- /// We were unable to load wallet data from or write wallet data to the persistence backend
- Persist(anyhow::Error),
- /// There was a problem while extracting and manipulating policies
- Policy(PolicyError),
- /// Spending policy is not compatible with this [`KeychainKind`]
- SpendingPolicyRequired(KeychainKind),
- /// Requested invalid transaction version '0'
- Version0,
- /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV
- Version1Csv,
- /// Requested `LockTime` is less than is required to spend from this script
- LockTime {
- /// Requested `LockTime`
- requested: absolute::LockTime,
- /// Required `LockTime`
- required: absolute::LockTime,
- },
- /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE
- RbfSequence,
- /// Cannot enable RBF with `Sequence` given a required OP_CSV
- RbfSequenceCsv {
- /// Given RBF `Sequence`
- rbf: Sequence,
- /// Required OP_CSV `Sequence`
- csv: Sequence,
- },
- /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
- FeeTooLow {
- /// Required fee absolute value (satoshi)
- required: u64,
- },
- /// When bumping a tx the fee rate requested is lower than required
- FeeRateTooLow {
- /// Required fee rate
- required: bitcoin::FeeRate,
- },
- /// `manually_selected_only` option is selected but no utxo has been passed
- NoUtxosSelected,
- /// Output created is under the dust limit, 546 satoshis
- OutputBelowDustLimit(usize),
- /// The `change_policy` was set but the wallet does not have a change_descriptor
- ChangePolicyDescriptor,
- /// There was an error with coin selection
- CoinSelection(coin_selection::Error),
- /// Wallet's UTXO set is not enough to cover recipient's requested plus fee
- InsufficientFunds {
- /// Sats needed for some transaction
- needed: u64,
- /// Sats available for spending
- available: u64,
- },
- /// Cannot build a tx without recipients
- NoRecipients,
- /// Partially signed bitcoin transaction error
- Psbt(psbt::Error),
- /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
- /// key in the descriptor must either be a master key itself (having depth = 0) or have an
- /// explicit origin provided
- ///
- /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
- MissingKeyOrigin(String),
- /// Happens when trying to spend an UTXO that is not in the internal database
- UnknownUtxo,
- /// Missing non_witness_utxo on foreign utxo for given `OutPoint`
- MissingNonWitnessUtxo(OutPoint),
- /// Miniscript PSBT error
- MiniscriptPsbt(MiniscriptPsbtError),
-}
-
-impl fmt::Display for CreateTxError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Descriptor(e) => e.fmt(f),
- Self::Persist(e) => {
- write!(
- f,
- "failed to load wallet data from or write wallet data to persistence backend: {}",
- e
- )
- }
- Self::Policy(e) => e.fmt(f),
- CreateTxError::SpendingPolicyRequired(keychain_kind) => {
- write!(f, "Spending policy required: {:?}", keychain_kind)
- }
- CreateTxError::Version0 => {
- write!(f, "Invalid version `0`")
- }
- CreateTxError::Version1Csv => {
- write!(
- f,
- "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
- )
- }
- CreateTxError::LockTime {
- requested,
- required,
- } => {
- write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested)
- }
- CreateTxError::RbfSequence => {
- write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")
- }
- CreateTxError::RbfSequenceCsv { rbf, csv } => {
- write!(
- f,
- "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
- rbf, csv
- )
- }
- CreateTxError::FeeTooLow { required } => {
- write!(f, "Fee to low: required {} sat", required)
- }
- CreateTxError::FeeRateTooLow { required } => {
- write!(
- f,
- // Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
- //"Fee rate too low: required {required:#}"
- "Fee rate too low: required {} sat/vb",
- crate::floating_rate!(required)
- )
- }
- CreateTxError::NoUtxosSelected => {
- write!(f, "No UTXO selected")
- }
- CreateTxError::OutputBelowDustLimit(limit) => {
- write!(f, "Output below the dust limit: {}", limit)
- }
- CreateTxError::ChangePolicyDescriptor => {
- write!(
- f,
- "The `change_policy` can be set only if the wallet has a change_descriptor"
- )
- }
- CreateTxError::CoinSelection(e) => e.fmt(f),
- CreateTxError::InsufficientFunds { needed, available } => {
- write!(
- f,
- "Insufficient funds: {} sat available of {} sat needed",
- available, needed
- )
- }
- CreateTxError::NoRecipients => {
- write!(f, "Cannot build tx without recipients")
- }
- CreateTxError::Psbt(e) => e.fmt(f),
- CreateTxError::MissingKeyOrigin(err) => {
- write!(f, "Missing key origin: {}", err)
- }
- CreateTxError::UnknownUtxo => {
- write!(f, "UTXO not found in the internal database")
- }
- CreateTxError::MissingNonWitnessUtxo(outpoint) => {
- write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint)
- }
- CreateTxError::MiniscriptPsbt(err) => {
- write!(f, "Miniscript PSBT error: {}", err)
- }
- }
- }
-}
-
-impl From<descriptor::error::Error> for CreateTxError {
- fn from(err: descriptor::error::Error) -> Self {
- CreateTxError::Descriptor(err)
- }
-}
-
-impl From<PolicyError> for CreateTxError {
- fn from(err: PolicyError) -> Self {
- CreateTxError::Policy(err)
- }
-}
-
-impl From<MiniscriptPsbtError> for CreateTxError {
- fn from(err: MiniscriptPsbtError) -> Self {
- CreateTxError::MiniscriptPsbt(err)
- }
-}
-
-impl From<psbt::Error> for CreateTxError {
- fn from(err: psbt::Error) -> Self {
- CreateTxError::Psbt(err)
- }
-}
-
-impl From<coin_selection::Error> for CreateTxError {
- fn from(err: coin_selection::Error) -> Self {
- CreateTxError::CoinSelection(err)
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for CreateTxError {}
-
-#[derive(Debug)]
-/// Error returned from [`Wallet::build_fee_bump`]
-///
-/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
-pub enum BuildFeeBumpError {
- /// Happens when trying to spend an UTXO that is not in the internal database
- UnknownUtxo(OutPoint),
- /// Thrown when a tx is not found in the internal database
- TransactionNotFound(Txid),
- /// Happens when trying to bump a transaction that is already confirmed
- TransactionConfirmed(Txid),
- /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
- IrreplaceableTransaction(Txid),
- /// Node doesn't have data to estimate a fee rate
- FeeRateUnavailable,
-}
-
-impl fmt::Display for BuildFeeBumpError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::UnknownUtxo(outpoint) => write!(
- f,
- "UTXO not found in the internal database with txid: {}, vout: {}",
- outpoint.txid, outpoint.vout
- ),
- Self::TransactionNotFound(txid) => {
- write!(
- f,
- "Transaction not found in the internal database with txid: {}",
- txid
- )
- }
- Self::TransactionConfirmed(txid) => {
- write!(f, "Transaction already confirmed with txid: {}", txid)
- }
- Self::IrreplaceableTransaction(txid) => {
- write!(f, "Transaction can't be replaced with txid: {}", txid)
- }
- Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for BuildFeeBumpError {}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Wallet export
-//!
-//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md).
-//!
-//! ## Examples
-//!
-//! ### Import from JSON
-//!
-//! ```
-//! # use std::str::FromStr;
-//! # use bitcoin::*;
-//! # use bdk::wallet::export::*;
-//! # use bdk::*;
-//! let import = r#"{
-//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
-//! "blockheight":1782088,
-//! "label":"testnet"
-//! }"#;
-//!
-//! let import = FullyNodedExport::from_str(import)?;
-//! let wallet = Wallet::new_no_persist(
-//! &import.descriptor(),
-//! import.change_descriptor().as_ref(),
-//! Network::Testnet,
-//! )?;
-//! # Ok::<_, Box<dyn std::error::Error>>(())
-//! ```
-//!
-//! ### Export a `Wallet`
-//! ```
-//! # use bitcoin::*;
-//! # use bdk::wallet::export::*;
-//! # use bdk::*;
-//! let wallet = Wallet::new_no_persist(
-//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
-//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
-//! Network::Testnet,
-//! )?;
-//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
-//!
-//! println!("Exported: {}", export.to_string());
-//! # Ok::<_, Box<dyn std::error::Error>>(())
-//! ```
-
-use alloc::string::String;
-use core::fmt;
-use core::str::FromStr;
-use serde::{Deserialize, Serialize};
-
-use miniscript::descriptor::{ShInner, WshInner};
-use miniscript::{Descriptor, ScriptContext, Terminal};
-
-use crate::types::KeychainKind;
-use crate::wallet::Wallet;
-
-/// Alias for [`FullyNodedExport`]
-#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")]
-pub type WalletExport = FullyNodedExport;
-
-/// Structure that contains the export of a wallet
-///
-/// For a usage example see [this module](crate::wallet::export)'s documentation.
-#[derive(Debug, Serialize, Deserialize)]
-pub struct FullyNodedExport {
- descriptor: String,
- /// Earliest block to rescan when looking for the wallet's transactions
- pub blockheight: u32,
- /// Arbitrary label for the wallet
- pub label: String,
-}
-
-impl fmt::Display for FullyNodedExport {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", serde_json::to_string(self).unwrap())
- }
-}
-
-impl FromStr for FullyNodedExport {
- type Err = serde_json::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- serde_json::from_str(s)
- }
-}
-
-fn remove_checksum(s: String) -> String {
- s.split_once('#').map(|(a, _)| String::from(a)).unwrap()
-}
-
-impl FullyNodedExport {
- /// Export a wallet
- ///
- /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
- /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44
- /// and others.
- ///
- /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database
- /// for the oldest transaction it knows and use that as the earliest block to rescan.
- ///
- /// If the database is empty or `include_blockheight` is false, the `blockheight` field
- /// returned will be `0`.
- pub fn export_wallet(
- wallet: &Wallet,
- label: &str,
- include_blockheight: bool,
- ) -> Result<Self, &'static str> {
- let descriptor = wallet
- .get_descriptor_for_keychain(KeychainKind::External)
- .to_string_with_secret(
- &wallet
- .get_signers(KeychainKind::External)
- .as_key_map(wallet.secp_ctx()),
- );
- let descriptor = remove_checksum(descriptor);
- Self::is_compatible_with_core(&descriptor)?;
-
- let blockheight = if include_blockheight {
- wallet.transactions().next().map_or(0, |canonical_tx| {
- match canonical_tx.chain_position {
- bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
- bdk_chain::ChainPosition::Unconfirmed(_) => 0,
- }
- })
- } else {
- 0
- };
-
- let export = FullyNodedExport {
- descriptor,
- label: label.into(),
- blockheight,
- };
-
- let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
- false => None,
- true => {
- let descriptor = wallet
- .get_descriptor_for_keychain(KeychainKind::Internal)
- .to_string_with_secret(
- &wallet
- .get_signers(KeychainKind::Internal)
- .as_key_map(wallet.secp_ctx()),
- );
- Some(remove_checksum(descriptor))
- }
- };
- if export.change_descriptor() != change_descriptor {
- return Err("Incompatible change descriptor");
- }
-
- Ok(export)
- }
-
- fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> {
- fn check_ms<Ctx: ScriptContext>(
- terminal: &Terminal<String, Ctx>,
- ) -> Result<(), &'static str> {
- if let Terminal::Multi(_, _) = terminal {
- Ok(())
- } else {
- Err("The descriptor contains operators not supported by Bitcoin Core")
- }
- }
-
- // pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti()
- match Descriptor::<String>::from_str(descriptor).map_err(|_| "Invalid descriptor")? {
- Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()),
- Descriptor::Sh(sh) => match sh.as_inner() {
- ShInner::Wpkh(_) => Ok(()),
- ShInner::SortedMulti(_) => Ok(()),
- ShInner::Wsh(wsh) => match wsh.as_inner() {
- WshInner::SortedMulti(_) => Ok(()),
- WshInner::Ms(ms) => check_ms(&ms.node),
- },
- ShInner::Ms(ms) => check_ms(&ms.node),
- },
- Descriptor::Wsh(wsh) => match wsh.as_inner() {
- WshInner::SortedMulti(_) => Ok(()),
- WshInner::Ms(ms) => check_ms(&ms.node),
- },
- _ => Err("The descriptor is not compatible with Bitcoin Core"),
- }
- }
-
- /// Return the external descriptor
- pub fn descriptor(&self) -> String {
- self.descriptor.clone()
- }
-
- /// Return the internal descriptor, if present
- pub fn change_descriptor(&self) -> Option<String> {
- let replaced = self.descriptor.replace("/0/*", "/1/*");
-
- if replaced != self.descriptor {
- Some(replaced)
- } else {
- None
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use core::str::FromStr;
-
- use crate::std::string::ToString;
- use bdk_chain::{BlockId, ConfirmationTime};
- use bitcoin::hashes::Hash;
- use bitcoin::{transaction, BlockHash, Network, Transaction};
-
- use super::*;
- use crate::wallet::Wallet;
-
- fn get_test_wallet(
- descriptor: &str,
- change_descriptor: Option<&str>,
- network: Network,
- ) -> Wallet {
- let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
- let transaction = Transaction {
- input: vec![],
- output: vec![],
- version: transaction::Version::non_standard(0),
- lock_time: bitcoin::absolute::LockTime::ZERO,
- };
- wallet
- .insert_checkpoint(BlockId {
- height: 5001,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
- wallet
- .insert_tx(
- transaction,
- ConfirmationTime::Confirmed {
- height: 5000,
- time: 0,
- },
- )
- .unwrap();
- wallet
- }
-
- #[test]
- fn test_export_bip44() {
- let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
- let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
-
- let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
- let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
-
- assert_eq!(export.descriptor(), descriptor);
- assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
- assert_eq!(export.blockheight, 5000);
- assert_eq!(export.label, "Test Label");
- }
-
- #[test]
- #[should_panic(expected = "Incompatible change descriptor")]
- fn test_export_no_change() {
- // This wallet explicitly doesn't have a change descriptor. It should be impossible to
- // export, because exporting this kind of external descriptor normally implies the
- // existence of an internal descriptor
-
- let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
-
- let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
- FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
- }
-
- #[test]
- #[should_panic(expected = "Incompatible change descriptor")]
- fn test_export_incompatible_change() {
- // This wallet has a change descriptor, but the derivation path is not in the "standard"
- // bip44/49/etc format
-
- let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
- let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
-
- let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
- FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
- }
-
- #[test]
- fn test_export_multi() {
- let descriptor = "wsh(multi(2,\
- [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\
- [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\
- [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\
- ))";
- let change_descriptor = "wsh(multi(2,\
- [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\
- [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\
- [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/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/*)";
- let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
-
- let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
- let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
-
- assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
- }
-
- #[test]
- fn test_export_from_json() {
- let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
- let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
-
- let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
- let export = FullyNodedExport::from_str(import_str).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");
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! HWI Signer
-//!
-//! 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 hwi::HWIClient;
-//! # use std::sync::Arc;
-//! #
-//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
-//! let mut devices = HWIClient::enumerate()?;
-//! if devices.is_empty() {
-//! panic!("No devices found!");
-//! }
-//! let first_device = devices.remove(0)?;
-//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
-//!
-//! # let mut wallet = Wallet::new_no_persist(
-//! # "",
-//! # None,
-//! # Network::Testnet,
-//! # )?;
-//! #
-//! // Adding the hardware signer to the BDK wallet
-//! wallet.add_signer(
-//! KeychainKind::External,
-//! SignerOrdering(200),
-//! Arc::new(custom_signer),
-//! );
-//!
-//! # Ok(())
-//! # }
-//! ```
-
-use bitcoin::bip32::Fingerprint;
-use bitcoin::secp256k1::{All, Secp256k1};
-use bitcoin::Psbt;
-
-use hwi::error::Error;
-use hwi::types::{HWIChain, HWIDevice};
-use hwi::HWIClient;
-
-use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
-
-#[derive(Debug)]
-/// Custom signer for Hardware Wallets
-///
-/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
-pub struct HWISigner {
- fingerprint: Fingerprint,
- client: HWIClient,
-}
-
-impl HWISigner {
- /// Create a instance from the specified device and chain
- pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
- let client = HWIClient::get_client(device, false, chain)?;
- Ok(HWISigner {
- fingerprint: device.fingerprint,
- client,
- })
- }
-}
-
-impl SignerCommon for HWISigner {
- fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
- SignerId::Fingerprint(self.fingerprint)
- }
-}
-
-/// This implementation ignores `sign_options`
-impl TransactionSigner for HWISigner {
- fn sign_transaction(
- &self,
- psbt: &mut Psbt,
- _sign_options: &crate::SignOptions,
- _secp: &crate::wallet::utils::SecpCtx,
- ) -> Result<(), SignerError> {
- psbt.combine(self.client.sign_tx(psbt)?.psbt)
- .expect("Failed to combine HW signed psbt with passed PSBT");
- Ok(())
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Wallet
-//!
-//! This module defines the [`Wallet`].
-use crate::collections::{BTreeMap, HashMap};
-use alloc::{
- boxed::Box,
- string::{String, ToString},
- sync::Arc,
- vec::Vec,
-};
-pub use bdk_chain::keychain::Balance;
-use bdk_chain::{
- indexed_tx_graph,
- keychain::{self, KeychainTxOutIndex},
- local_chain::{
- self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
- },
- spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
- tx_graph::{CanonicalTx, TxGraph},
- Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
- IndexedTxGraph,
-};
-use bdk_persist::{Persist, PersistBackend};
-use bitcoin::secp256k1::{All, Secp256k1};
-use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
-use bitcoin::{
- absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence,
- Transaction, TxOut, Txid, Witness,
-};
-use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
-use bitcoin::{constants::genesis_block, Amount};
-use core::fmt;
-use core::ops::Deref;
-use descriptor::error::Error as DescriptorError;
-use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
-
-use bdk_chain::tx_graph::CalculateFeeError;
-
-pub mod coin_selection;
-pub mod export;
-pub mod signer;
-pub mod tx_builder;
-pub(crate) mod utils;
-
-pub mod error;
-
-pub use utils::IsDust;
-
-use coin_selection::DefaultCoinSelectionAlgorithm;
-use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
-use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
-use utils::{check_nsequence_rbf, After, Older, SecpCtx};
-
-use crate::descriptor::policy::BuildSatisfaction;
-use crate::descriptor::{
- self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
- ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
-};
-use crate::psbt::PsbtUtils;
-use crate::signer::SignerError;
-use crate::types::*;
-use crate::wallet::coin_selection::Excess::{Change, NoChange};
-use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError};
-
-const COINBASE_MATURITY: u32 = 100;
-
-/// A Bitcoin wallet
-///
-/// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions.
-/// Its main components are:
-///
-/// 1. output *descriptors* from which it can derive addresses.
-/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors.
-///
-/// [`signer`]: crate::signer
-#[derive(Debug)]
-pub struct Wallet {
- signers: Arc<SignersContainer>,
- change_signers: Arc<SignersContainer>,
- chain: LocalChain,
- indexed_graph: IndexedTxGraph<ConfirmationTimeHeightAnchor, KeychainTxOutIndex<KeychainKind>>,
- persist: Persist<ChangeSet>,
- network: Network,
- secp: SecpCtx,
-}
-
-/// An update to [`Wallet`].
-///
-/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically.
-#[derive(Debug, Clone, Default)]
-pub struct Update {
- /// Contains the last active derivation indices per keychain (`K`), which is used to update the
- /// [`KeychainTxOutIndex`].
- pub last_active_indices: BTreeMap<KeychainKind, u32>,
-
- /// Update for the wallet's internal [`TxGraph`].
- pub graph: TxGraph<ConfirmationTimeHeightAnchor>,
-
- /// Update for the wallet's internal [`LocalChain`].
- ///
- /// [`LocalChain`]: local_chain::LocalChain
- pub chain: Option<CheckPoint>,
-}
-
-impl From<FullScanResult<KeychainKind>> for Update {
- fn from(value: FullScanResult<KeychainKind>) -> Self {
- Self {
- last_active_indices: value.last_active_indices,
- graph: value.graph_update,
- chain: Some(value.chain_update),
- }
- }
-}
-
-impl From<SyncResult> for Update {
- fn from(value: SyncResult) -> Self {
- Self {
- last_active_indices: BTreeMap::new(),
- graph: value.graph_update,
- chain: Some(value.chain_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()
- }
- }
-}
-
-/// A derived address and the index it was found at.
-/// For convenience this automatically derefs to `Address`
-#[derive(Debug, PartialEq, Eq)]
-pub struct AddressInfo {
- /// Child index of this address
- pub index: u32,
- /// Address
- pub address: Address,
- /// Type of keychain
- pub keychain: KeychainKind,
-}
-
-impl Deref for AddressInfo {
- type Target = Address;
-
- fn deref(&self) -> &Self::Target {
- &self.address
- }
-}
-
-impl fmt::Display for AddressInfo {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.address)
- }
-}
-
-impl Wallet {
- /// Creates a wallet that does not persist data.
- pub fn new_no_persist<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- network: Network,
- ) -> Result<Self, DescriptorError> {
- Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
- NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
- NewError::Descriptor(e) => e,
- NewError::Persist(_) => unreachable!("mock-write must always succeed"),
- })
- }
-
- /// Creates a wallet that does not persist data, with a custom genesis hash.
- pub fn new_no_persist_with_genesis_hash<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- network: Network,
- genesis_hash: BlockHash,
- ) -> Result<Self, crate::descriptor::DescriptorError> {
- Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
- .map_err(|e| match e {
- NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
- NewError::Descriptor(e) => e,
- NewError::Persist(_) => unreachable!("mock-write must always succeed"),
- })
- }
-}
-
-/// The error type when constructing a fresh [`Wallet`].
-///
-/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
-///
-/// [`new`]: Wallet::new
-/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
-#[derive(Debug)]
-pub enum NewError {
- /// Database already has data.
- NonEmptyDatabase,
- /// There was problem with the passed-in descriptor(s).
- Descriptor(crate::descriptor::DescriptorError),
- /// We were unable to write the wallet's data to the persistence backend.
- Persist(anyhow::Error),
-}
-
-impl fmt::Display for NewError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- NewError::NonEmptyDatabase => write!(
- f,
- "database already has data - use `load` or `new_or_load` methods instead"
- ),
- NewError::Descriptor(e) => e.fmt(f),
- NewError::Persist(e) => e.fmt(f),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NewError {}
-
-/// The error type when loading a [`Wallet`] from persistence.
-///
-/// Method [`load`] may return this error.
-///
-/// [`load`]: Wallet::load
-#[derive(Debug)]
-pub enum LoadError {
- /// There was a problem with the passed-in descriptor(s).
- Descriptor(crate::descriptor::DescriptorError),
- /// Loading data from the persistence backend failed.
- Persist(anyhow::Error),
- /// Wallet not initialized, persistence backend is empty.
- NotInitialized,
- /// Data loaded from persistence is missing network type.
- MissingNetwork,
- /// Data loaded from persistence is missing genesis hash.
- MissingGenesis,
- /// Data loaded from persistence is missing descriptor.
- MissingDescriptor,
-}
-
-impl fmt::Display for LoadError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- LoadError::Descriptor(e) => e.fmt(f),
- LoadError::Persist(e) => e.fmt(f),
- LoadError::NotInitialized => {
- write!(f, "wallet is not initialized, persistence backend is empty")
- }
- LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
- LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
- LoadError::MissingDescriptor => write!(f, "loaded data is missing descriptor"),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for LoadError {}
-
-/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
-///
-/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
-///
-/// [`new_or_load`]: Wallet::new_or_load
-/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
-#[derive(Debug)]
-pub enum NewOrLoadError {
- /// There is a problem with the passed-in descriptor.
- Descriptor(crate::descriptor::DescriptorError),
- /// Either writing to or loading from the persistence backend failed.
- Persist(anyhow::Error),
- /// Wallet is not initialized, persistence backend is empty.
- NotInitialized,
- /// The loaded genesis hash does not match what was provided.
- LoadedGenesisDoesNotMatch {
- /// The expected genesis block hash.
- expected: BlockHash,
- /// The block hash loaded from persistence.
- got: Option<BlockHash>,
- },
- /// The loaded network type does not match what was provided.
- LoadedNetworkDoesNotMatch {
- /// The expected network type.
- expected: Network,
- /// The network type loaded from persistence.
- got: Option<Network>,
- },
- /// The loaded desccriptor does not match what was provided.
- LoadedDescriptorDoesNotMatch {
- /// The descriptor loaded from persistence.
- got: Option<ExtendedDescriptor>,
- /// The keychain of the descriptor not matching
- keychain: KeychainKind,
- },
-}
-
-impl fmt::Display for NewOrLoadError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- NewOrLoadError::Descriptor(e) => e.fmt(f),
- NewOrLoadError::Persist(e) => write!(
- f,
- "failed to either write to or load from persistence, {}",
- e
- ),
- NewOrLoadError::NotInitialized => {
- write!(f, "wallet is not initialized, persistence backend is empty")
- }
- NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
- write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
- }
- NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
- write!(f, "loaded network type is not {}, got {:?}", expected, got)
- }
- NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
- write!(
- f,
- "loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
- got, keychain
- )
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NewOrLoadError {}
-
-/// An error that may occur when inserting a transaction into [`Wallet`].
-#[derive(Debug)]
-pub enum InsertTxError {
- /// The error variant that occurs when the caller attempts to insert a transaction with a
- /// confirmation height that is greater than the internal chain tip.
- ConfirmationHeightCannotBeGreaterThanTip {
- /// The internal chain's tip height.
- tip_height: u32,
- /// The introduced transaction's confirmation height.
- tx_height: u32,
- },
-}
-
-impl fmt::Display for InsertTxError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
- tip_height,
- tx_height,
- } => {
- write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for InsertTxError {}
-
-/// An error that may occur when applying a block to [`Wallet`].
-#[derive(Debug)]
-pub enum ApplyBlockError {
- /// Occurs when the update chain cannot connect with original chain.
- CannotConnect(CannotConnectError),
- /// Occurs when the `connected_to` hash does not match the hash derived from `block`.
- UnexpectedConnectedToHash {
- /// Block hash of `connected_to`.
- connected_to_hash: BlockHash,
- /// Expected block hash of `connected_to`, as derived from `block`.
- expected_hash: BlockHash,
- },
-}
-
-impl fmt::Display for ApplyBlockError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ApplyBlockError::CannotConnect(err) => err.fmt(f),
- ApplyBlockError::UnexpectedConnectedToHash {
- expected_hash: block_hash,
- connected_to_hash: checkpoint_hash,
- } => write!(
- f,
- "`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
- checkpoint_hash, block_hash
- ),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ApplyBlockError {}
-
-impl Wallet {
- /// Initialize an empty [`Wallet`].
- pub fn new<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- network: Network,
- ) -> Result<Self, NewError> {
- let genesis_hash = genesis_block(network).block_hash();
- Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash)
- }
-
- /// Initialize an empty [`Wallet`] with a custom genesis hash.
- ///
- /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
- /// for syncing from alternative networks.
- pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- network: Network,
- genesis_hash: BlockHash,
- ) -> Result<Self, NewError> {
- if let Ok(changeset) = db.load_from_persistence() {
- if changeset.is_some() {
- return Err(NewError::NonEmptyDatabase);
- }
- }
- let secp = Secp256k1::new();
- let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
- let mut index = KeychainTxOutIndex::<KeychainKind>::default();
-
- let (signers, change_signers) =
- create_signers(&mut index, &secp, descriptor, change_descriptor, network)
- .map_err(NewError::Descriptor)?;
-
- let indexed_graph = IndexedTxGraph::new(index);
-
- let mut persist = Persist::new(db);
- persist.stage(ChangeSet {
- chain: chain_changeset,
- indexed_tx_graph: indexed_graph.initial_changeset(),
- network: Some(network),
- });
- persist.commit().map_err(NewError::Persist)?;
-
- Ok(Wallet {
- signers,
- change_signers,
- network,
- chain,
- indexed_graph,
- persist,
- secp,
- })
- }
-
- /// Load [`Wallet`] from the given persistence backend.
- ///
- /// Note that the descriptor secret keys are not persisted to the db; this means that after
- /// calling this method the [`Wallet`] **won't** know the secret keys, and as such, won't be
- /// able to sign transactions.
- ///
- /// If you wish to use the wallet to sign transactions, you need to add the secret keys
- /// manually to the [`Wallet`]:
- ///
- /// ```rust,no_run
- /// # use bdk::Wallet;
- /// # use bdk::signer::{SignersContainer, SignerOrdering};
- /// # use bdk::descriptor::Descriptor;
- /// # use bitcoin::key::Secp256k1;
- /// # use bdk::KeychainKind;
- /// # use bdk_file_store::Store;
- /// #
- /// # 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 secp = Secp256k1::new();
- ///
- /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
- /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
- ///
- /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
- /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
- ///
- /// let mut wallet = Wallet::load(db)?;
- ///
- /// external_signer_container.signers().into_iter()
- /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
- /// internal_signer_container.signers().into_iter()
- /// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
- /// # Ok(())
- /// # }
- /// ```
- ///
- /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
- /// passed-in descriptors to the [`Wallet`].
- pub fn load(
- mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- ) -> Result<Self, LoadError> {
- let changeset = db
- .load_from_persistence()
- .map_err(LoadError::Persist)?
- .ok_or(LoadError::NotInitialized)?;
- Self::load_from_changeset(db, changeset)
- }
-
- fn load_from_changeset(
- db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- changeset: ChangeSet,
- ) -> Result<Self, LoadError> {
- let secp = Secp256k1::new();
- let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
- let chain =
- LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
- let mut index = KeychainTxOutIndex::<KeychainKind>::default();
- let descriptor = changeset
- .indexed_tx_graph
- .indexer
- .keychains_added
- .get(&KeychainKind::External)
- .ok_or(LoadError::MissingDescriptor)?
- .clone();
- let change_descriptor = changeset
- .indexed_tx_graph
- .indexer
- .keychains_added
- .get(&KeychainKind::Internal)
- .cloned();
-
- let (signers, change_signers) =
- create_signers(&mut index, &secp, descriptor, change_descriptor, network)
- .expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
-
- let mut indexed_graph = IndexedTxGraph::new(index);
- indexed_graph.apply_changeset(changeset.indexed_tx_graph);
-
- let persist = Persist::new(db);
-
- Ok(Wallet {
- signers,
- change_signers,
- chain,
- indexed_graph,
- persist,
- network,
- secp,
- })
- }
-
- /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist.
- ///
- /// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
- pub fn new_or_load<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- network: Network,
- ) -> Result<Self, NewOrLoadError> {
- let genesis_hash = genesis_block(network).block_hash();
- Self::new_or_load_with_genesis_hash(
- descriptor,
- change_descriptor,
- db,
- network,
- genesis_hash,
- )
- }
-
- /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist, using the
- /// provided descriptor, change descriptor, network, and custom genesis hash.
- ///
- /// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
- /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
- /// useful for syncing from alternative networks.
- pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: Option<E>,
- mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
- network: Network,
- genesis_hash: BlockHash,
- ) -> Result<Self, NewOrLoadError> {
- let changeset = db
- .load_from_persistence()
- .map_err(NewOrLoadError::Persist)?;
- match changeset {
- Some(changeset) => {
- let mut wallet = Self::load_from_changeset(db, changeset).map_err(|e| match e {
- LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
- LoadError::Persist(e) => NewOrLoadError::Persist(e),
- LoadError::NotInitialized => NewOrLoadError::NotInitialized,
- LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
- expected: network,
- got: None,
- },
- LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
- expected: genesis_hash,
- got: None,
- },
- LoadError::MissingDescriptor => NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: None,
- keychain: KeychainKind::External,
- },
- })?;
- if wallet.network != network {
- return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
- expected: network,
- got: Some(wallet.network),
- });
- }
- if wallet.chain.genesis_hash() != genesis_hash {
- return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
- expected: genesis_hash,
- got: Some(wallet.chain.genesis_hash()),
- });
- }
-
- let (expected_descriptor, expected_descriptor_keymap) = descriptor
- .into_wallet_descriptor(&wallet.secp, network)
- .map_err(NewOrLoadError::Descriptor)?;
- let wallet_descriptor = wallet.public_descriptor(KeychainKind::External).cloned();
- if wallet_descriptor != Some(expected_descriptor.clone()) {
- return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: wallet_descriptor,
- keychain: KeychainKind::External,
- });
- }
- // if expected descriptor has private keys add them as new signers
- if !expected_descriptor_keymap.is_empty() {
- let signer_container = SignersContainer::build(
- expected_descriptor_keymap,
- &expected_descriptor,
- &wallet.secp,
- );
- signer_container.signers().into_iter().for_each(|signer| {
- wallet.add_signer(
- KeychainKind::External,
- SignerOrdering::default(),
- signer.clone(),
- )
- });
- }
-
- let expected_change_descriptor = if let Some(c) = change_descriptor {
- Some(
- c.into_wallet_descriptor(&wallet.secp, network)
- .map_err(NewOrLoadError::Descriptor)?,
- )
- } else {
- None
- };
- let wallet_change_descriptor =
- wallet.public_descriptor(KeychainKind::Internal).cloned();
-
- match (expected_change_descriptor, wallet_change_descriptor) {
- (Some((expected_descriptor, expected_keymap)), Some(wallet_descriptor))
- if wallet_descriptor == expected_descriptor =>
- {
- // if expected change descriptor has private keys add them as new signers
- if !expected_keymap.is_empty() {
- let signer_container = SignersContainer::build(
- expected_keymap,
- &expected_descriptor,
- &wallet.secp,
- );
- signer_container.signers().into_iter().for_each(|signer| {
- wallet.add_signer(
- KeychainKind::Internal,
- SignerOrdering::default(),
- signer.clone(),
- )
- });
- }
- }
- (None, None) => (),
- (_, wallet_descriptor) => {
- return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: wallet_descriptor,
- keychain: KeychainKind::Internal,
- });
- }
- }
-
- Ok(wallet)
- }
- None => Self::new_with_genesis_hash(
- descriptor,
- change_descriptor,
- db,
- network,
- genesis_hash,
- )
- .map_err(|e| match e {
- NewError::NonEmptyDatabase => {
- unreachable!("database is already checked to have no data")
- }
- NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
- NewError::Persist(e) => NewOrLoadError::Persist(e),
- }),
- }
- }
-
- /// Get the Bitcoin network the wallet is using.
- pub fn network(&self) -> Network {
- self.network
- }
-
- /// Iterator over all keychains in this wallet
- pub fn keychains(&self) -> impl Iterator<Item = (&KeychainKind, &ExtendedDescriptor)> {
- self.indexed_graph.index.keychains()
- }
-
- /// Peek an address of the given `keychain` at `index` without revealing it.
- ///
- /// For non-wildcard descriptors this returns the same address at every provided index.
- ///
- /// # Panics
- ///
- /// This panics when the caller requests for an address of derivation index greater than the
- /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index.
- pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo {
- let keychain = self.map_keychain(keychain);
- let mut spk_iter = self
- .indexed_graph
- .index
- .unbounded_spk_iter(&keychain)
- .expect("Must exist (we called map_keychain)");
- if !spk_iter.descriptor().has_wildcard() {
- index = 0;
- }
- let (index, spk) = spk_iter
- .nth(index as usize)
- .expect("derivation index is out of bounds");
-
- AddressInfo {
- index,
- address: Address::from_script(&spk, self.network).expect("must have address form"),
- keychain,
- }
- }
-
- /// Attempt to reveal the next address of the given `keychain`.
- ///
- /// This will increment the internal derivation index. If the keychain's descriptor doesn't
- /// contain a wildcard or every address is already revealed up to the maximum derivation
- /// index defined in [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki),
- /// then returns the last revealed address.
- ///
- /// # Errors
- ///
- /// If writing to persistent storage fails.
- pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
- let keychain = self.map_keychain(keychain);
- let ((index, spk), index_changeset) = self
- .indexed_graph
- .index
- .reveal_next_spk(&keychain)
- .expect("Must exist (we called map_keychain)");
-
- self.persist
- .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
-
- Ok(AddressInfo {
- index,
- address: Address::from_script(spk, self.network).expect("must have address form"),
- keychain,
- })
- }
-
- /// Reveal addresses up to and including the target `index` and return an iterator
- /// of newly revealed addresses.
- ///
- /// If the target `index` is unreachable, we make a best effort to reveal up to the last
- /// possible index. If all addresses up to the given `index` are already revealed, then
- /// no new addresses are returned.
- ///
- /// # Errors
- ///
- /// If writing to persistent storage fails.
- pub fn reveal_addresses_to(
- &mut self,
- keychain: KeychainKind,
- index: u32,
- ) -> anyhow::Result<impl Iterator<Item = AddressInfo> + '_> {
- let keychain = self.map_keychain(keychain);
- let (spk_iter, index_changeset) = self
- .indexed_graph
- .index
- .reveal_to_target(&keychain, index)
- .expect("must exist (we called map_keychain)");
-
- self.persist
- .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
-
- Ok(spk_iter.map(move |(index, spk)| AddressInfo {
- index,
- address: Address::from_script(&spk, self.network).expect("must have address form"),
- keychain,
- }))
- }
-
- /// Get the next unused address for the given `keychain`, i.e. the address with the lowest
- /// derivation index that hasn't been used.
- ///
- /// This will attempt to derive and reveal a new address if no newly revealed addresses
- /// are available. See also [`reveal_next_address`](Self::reveal_next_address).
- ///
- /// # Errors
- ///
- /// If writing to persistent storage fails.
- pub fn next_unused_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
- let keychain = self.map_keychain(keychain);
- let ((index, spk), index_changeset) = self
- .indexed_graph
- .index
- .next_unused_spk(&keychain)
- .expect("must exist (we called map_keychain)");
-
- self.persist
- .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
-
- Ok(AddressInfo {
- index,
- address: Address::from_script(spk, self.network).expect("must have address form"),
- keychain,
- })
- }
-
- /// Marks an address used of the given `keychain` at `index`.
- ///
- /// Returns whether the given index was present and then removed from the unused set.
- pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool {
- self.indexed_graph.index.mark_used(keychain, index)
- }
-
- /// Undoes the effect of [`mark_used`] and returns whether the `index` was inserted
- /// back into the unused set.
- ///
- /// Since this is only a superficial marker, it will have no effect if the address at the given
- /// `index` was actually used, i.e. the wallet has previously indexed a tx output for the
- /// derived spk.
- ///
- /// [`mark_used`]: Self::mark_used
- pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool {
- self.indexed_graph.index.unmark_used(keychain, index)
- }
-
- /// List addresses that are revealed but unused.
- ///
- /// Note if the returned iterator is empty you can reveal more addresses
- /// by using [`reveal_next_address`](Self::reveal_next_address) or
- /// [`reveal_addresses_to`](Self::reveal_addresses_to).
- pub fn list_unused_addresses(
- &self,
- keychain: KeychainKind,
- ) -> impl DoubleEndedIterator<Item = AddressInfo> + '_ {
- let keychain = self.map_keychain(keychain);
- self.indexed_graph
- .index
- .unused_keychain_spks(&keychain)
- .map(move |(index, spk)| AddressInfo {
- index,
- address: Address::from_script(spk, self.network).expect("must have address form"),
- keychain,
- })
- }
-
- /// Return whether or not a `script` is part of this wallet (either internal or external)
- pub fn is_mine(&self, script: &Script) -> bool {
- self.indexed_graph.index.index_of_spk(script).is_some()
- }
-
- /// Finds how the wallet derived the script pubkey `spk`.
- ///
- /// Will only return `Some(_)` if the wallet has given out the spk.
- pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> {
- self.indexed_graph.index.index_of_spk(spk)
- }
-
- /// Return the list of unspent outputs of this wallet
- pub fn list_unspent(&self) -> impl Iterator<Item = LocalOutput> + '_ {
- self.indexed_graph
- .graph()
- .filter_chain_unspents(
- &self.chain,
- self.chain.tip().block_id(),
- self.indexed_graph.index.outpoints(),
- )
- .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
- }
-
- /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed).
- ///
- /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead.
- pub fn list_output(&self) -> impl Iterator<Item = LocalOutput> + '_ {
- self.indexed_graph
- .graph()
- .filter_chain_txouts(
- &self.chain,
- self.chain.tip().block_id(),
- self.indexed_graph.index.outpoints(),
- )
- .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
- }
-
- /// Get all the checkpoints the wallet is currently storing indexed by height.
- pub fn checkpoints(&self) -> CheckPointIter {
- self.chain.iter_checkpoints()
- }
-
- /// Returns the latest checkpoint.
- pub fn latest_checkpoint(&self) -> CheckPoint {
- self.chain.tip()
- }
-
- /// Get unbounded script pubkey iterators for both `Internal` and `External` keychains.
- ///
- /// This is intended to be used when doing a full scan of your addresses (e.g. after restoring
- /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g.
- /// electrum server) which will go through each address until it reaches a *stop gap*.
- ///
- /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what
- /// script pubkeys the wallet is storing internally).
- pub fn all_unbounded_spk_iters(
- &self,
- ) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, ScriptBuf)> + Clone> {
- self.indexed_graph.index.all_unbounded_spk_iters()
- }
-
- /// Get an unbounded script pubkey iterator for the given `keychain`.
- ///
- /// See [`all_unbounded_spk_iters`] for more documentation
- ///
- /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters
- pub fn unbounded_spk_iter(
- &self,
- keychain: KeychainKind,
- ) -> impl Iterator<Item = (u32, ScriptBuf)> + Clone {
- let keychain = self.map_keychain(keychain);
- self.indexed_graph
- .index
- .unbounded_spk_iter(&keychain)
- .expect("Must exist (we called map_keychain)")
- }
-
- /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
- /// wallet's database.
- pub fn get_utxo(&self, op: OutPoint) -> Option<LocalOutput> {
- let (keychain, index, _) = self.indexed_graph.index.txout(op)?;
- self.indexed_graph
- .graph()
- .filter_chain_unspents(
- &self.chain,
- self.chain.tip().block_id(),
- core::iter::once(((), op)),
- )
- .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo))
- .next()
- }
-
- /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph.
- ///
- /// This is used for providing a previous output's value so that we can use [`calculate_fee`]
- /// or [`calculate_fee_rate`] on a given transaction. Outputs inserted with this method will
- /// not be returned in [`list_unspent`] or [`list_output`].
- ///
- /// Any inserted `TxOut`s are not persisted until [`commit`] is called.
- ///
- /// **WARNING:** This should only be used to add `TxOut`s that the wallet does not own. Only
- /// insert `TxOut`s that you trust the values for!
- ///
- /// [`calculate_fee`]: Self::calculate_fee
- /// [`calculate_fee_rate`]: Self::calculate_fee_rate
- /// [`list_unspent`]: Self::list_unspent
- /// [`list_output`]: Self::list_output
- /// [`commit`]: Self::commit
- pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) {
- let additions = self.indexed_graph.insert_txout(outpoint, txout);
- self.persist.stage(ChangeSet::from(additions));
- }
-
- /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
- ///
- /// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must
- /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function.
- ///
- /// Note `tx` does not have to be in the graph for this to work.
- ///
- /// # Examples
- ///
- /// ```rust, no_run
- /// # use bitcoin::Txid;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let txid:Txid = todo!();
- /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
- /// let fee = wallet.calculate_fee(&tx).expect("fee");
- /// ```
- ///
- /// ```rust, no_run
- /// # use bitcoin::Psbt;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let mut psbt: Psbt = todo!();
- /// let tx = &psbt.clone().extract_tx().expect("tx");
- /// let fee = wallet.calculate_fee(tx).expect("fee");
- /// ```
- /// [`insert_txout`]: Self::insert_txout
- pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
- self.indexed_graph.graph().calculate_fee(tx)
- }
-
- /// Calculate the [`FeeRate`] for a given transaction.
- ///
- /// To calculate the fee rate for a [`Transaction`] with inputs not owned by this wallet you must
- /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function.
- ///
- /// Note `tx` does not have to be in the graph for this to work.
- ///
- /// # Examples
- ///
- /// ```rust, no_run
- /// # use bitcoin::Txid;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let txid:Txid = todo!();
- /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
- /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate");
- /// ```
- ///
- /// ```rust, no_run
- /// # use bitcoin::Psbt;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let mut psbt: Psbt = todo!();
- /// let tx = &psbt.clone().extract_tx().expect("tx");
- /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
- /// ```
- /// [`insert_txout`]: Self::insert_txout
- pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
- self.calculate_fee(tx)
- .map(|fee| Amount::from_sat(fee) / tx.weight())
- }
-
- /// Compute the `tx`'s sent and received [`Amount`]s.
- ///
- /// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
- /// that spend from previous txouts tracked by this wallet. Received is the summation
- /// of this tx's outputs that send to script pubkeys tracked by this wallet.
- ///
- /// # Examples
- ///
- /// ```rust, no_run
- /// # use bitcoin::Txid;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let txid:Txid = todo!();
- /// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx;
- /// let (sent, received) = wallet.sent_and_received(&tx);
- /// ```
- ///
- /// ```rust, no_run
- /// # use bitcoin::Psbt;
- /// # use bdk::Wallet;
- /// # let mut wallet: Wallet = todo!();
- /// # let mut psbt: Psbt = todo!();
- /// let tx = &psbt.clone().extract_tx().expect("tx");
- /// let (sent, received) = wallet.sent_and_received(tx);
- /// ```
- pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) {
- self.indexed_graph.index.sent_and_received(tx, ..)
- }
-
- /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists).
- ///
- /// `CanonicalTx` contains the full transaction alongside meta-data such as:
- /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist
- /// in the best chain.
- /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is
- /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the
- /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when
- /// the transaction was last seen in the mempool is provided.
- ///
- /// ```rust, no_run
- /// use bdk::{chain::ChainPosition, Wallet};
- /// use bdk_chain::Anchor;
- /// # let wallet: Wallet = todo!();
- /// # let my_txid: bitcoin::Txid = todo!();
- ///
- /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist");
- ///
- /// // get reference to full transaction
- /// println!("my tx: {:#?}", canonical_tx.tx_node.tx);
- ///
- /// // list all transaction anchors
- /// for anchor in canonical_tx.tx_node.anchors {
- /// println!(
- /// "tx is anchored by block of hash {}",
- /// anchor.anchor_block().hash
- /// );
- /// }
- ///
- /// // get confirmation status of transaction
- /// match canonical_tx.chain_position {
- /// ChainPosition::Confirmed(anchor) => println!(
- /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
- /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash,
- /// ),
- /// ChainPosition::Unconfirmed(last_seen) => println!(
- /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
- /// last_seen,
- /// ),
- /// }
- /// ```
- ///
- /// [`Anchor`]: bdk_chain::Anchor
- pub fn get_tx(
- &self,
- txid: Txid,
- ) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
- let graph = self.indexed_graph.graph();
-
- Some(CanonicalTx {
- chain_position: graph.get_chain_position(
- &self.chain,
- self.chain.tip().block_id(),
- txid,
- )?,
- tx_node: graph.get_tx_node(txid)?,
- })
- }
-
- /// Add a new checkpoint to the wallet's internal view of the chain.
- /// This stages but does not [`commit`] the change.
- ///
- /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already
- /// there).
- ///
- /// [`commit`]: Self::commit
- pub fn insert_checkpoint(
- &mut self,
- block_id: BlockId,
- ) -> Result<bool, local_chain::AlterCheckPointError> {
- let changeset = self.chain.insert_block(block_id)?;
- let changed = !changeset.is_empty();
- self.persist.stage(changeset.into());
- Ok(changed)
- }
-
- /// Add a transaction to the wallet's internal view of the chain. This stages but does not
- /// [`commit`] the change.
- ///
- /// Returns whether anything changed with the transaction insertion (e.g. `false` if the
- /// transaction was already inserted at the same position).
- ///
- /// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`].
- /// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually
- /// inserting new transactions.
- ///
- /// **WARNING:** If `position` is confirmed, we anchor the `tx` to a the lowest checkpoint that
- /// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our
- /// local view of the best chain's history.
- ///
- /// [`commit`]: Self::commit
- /// [`latest_checkpoint`]: Self::latest_checkpoint
- /// [`insert_checkpoint`]: Self::insert_checkpoint
- pub fn insert_tx(
- &mut self,
- tx: Transaction,
- position: ConfirmationTime,
- ) -> Result<bool, InsertTxError> {
- let (anchor, last_seen) = match position {
- ConfirmationTime::Confirmed { height, time } => {
- // anchor tx to checkpoint with lowest height that is >= position's height
- let anchor = self
- .chain
- .range(height..)
- .last()
- .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
- tip_height: self.chain.tip().height(),
- tx_height: height,
- })
- .map(|anchor_cp| ConfirmationTimeHeightAnchor {
- anchor_block: anchor_cp.block_id(),
- confirmation_height: height,
- confirmation_time: time,
- })?;
-
- (Some(anchor), None)
- }
- ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)),
- };
-
- let mut changeset = ChangeSet::default();
- let txid = tx.txid();
- changeset.append(self.indexed_graph.insert_tx(tx).into());
- if let Some(anchor) = anchor {
- changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into());
- }
- if let Some(last_seen) = last_seen {
- changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into());
- }
-
- let changed = !changeset.is_empty();
- self.persist.stage(changeset);
- Ok(changed)
- }
-
- /// Iterate over the transactions in the wallet.
- pub fn transactions(
- &self,
- ) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> + '_
- {
- self.indexed_graph
- .graph()
- .list_chain_txs(&self.chain, self.chain.tip().block_id())
- }
-
- /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
- /// values.
- pub fn get_balance(&self) -> Balance {
- self.indexed_graph.graph().balance(
- &self.chain,
- self.chain.tip().block_id(),
- self.indexed_graph.index.outpoints(),
- |&(k, _), _| k == KeychainKind::Internal,
- )
- }
-
- /// Add an external signer
- ///
- /// See [the `signer` module](signer) for an example.
- pub fn add_signer(
- &mut self,
- keychain: KeychainKind,
- ordering: SignerOrdering,
- signer: Arc<dyn TransactionSigner>,
- ) {
- let signers = match keychain {
- KeychainKind::External => Arc::make_mut(&mut self.signers),
- KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
- };
-
- signers.add_external(signer.id(&self.secp), ordering, signer);
- }
-
- /// Get the signers
- ///
- /// ## Example
- ///
- /// ```
- /// # use bdk::{Wallet, KeychainKind};
- /// # use bdk::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/*
- /// println!("secret_key: {}", secret_key);
- /// }
- ///
- /// Ok::<(), Box<dyn std::error::Error>>(())
- /// ```
- pub fn get_signers(&self, keychain: KeychainKind) -> Arc<SignersContainer> {
- match keychain {
- KeychainKind::External => Arc::clone(&self.signers),
- KeychainKind::Internal => Arc::clone(&self.change_signers),
- }
- }
-
- /// Start building a transaction.
- ///
- /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
- ///
- /// ## Example
- ///
- /// ```
- /// # use std::str::FromStr;
- /// # use bitcoin::*;
- /// # use bdk::*;
- /// # use bdk::wallet::ChangeSet;
- /// # use bdk::wallet::error::CreateTxError;
- /// # use bdk_persist::PersistBackend;
- /// # use anyhow::Error;
- /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
- /// # let mut wallet = doctest_wallet!();
- /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
- /// let psbt = {
- /// let mut builder = wallet.build_tx();
- /// builder
- /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
- /// builder.finish()?
- /// };
- ///
- /// // sign and broadcast ...
- /// # Ok::<(), anyhow::Error>(())
- /// ```
- ///
- /// [`TxBuilder`]: crate::TxBuilder
- pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> {
- TxBuilder {
- wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
- params: TxParams::default(),
- coin_selection: DefaultCoinSelectionAlgorithm::default(),
- phantom: core::marker::PhantomData,
- }
- }
-
- pub(crate) fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm>(
- &mut self,
- coin_selection: Cs,
- params: TxParams,
- ) -> Result<Psbt, CreateTxError> {
- let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect();
- let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist");
- let internal_descriptor = keychains.get(&KeychainKind::Internal);
-
- let external_policy = external_descriptor
- .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
- .unwrap();
- let internal_policy = internal_descriptor
- .as_ref()
- .map(|desc| {
- Ok::<_, CreateTxError>(
- desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
- .unwrap(),
- )
- })
- .transpose()?;
-
- // The policy allows spending external outputs, but it requires a policy path that hasn't been
- // provided
- if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange
- && external_policy.requires_path()
- && params.external_policy_path.is_none()
- {
- return Err(CreateTxError::SpendingPolicyRequired(
- KeychainKind::External,
- ));
- };
- // Same for the internal_policy path, if present
- if let Some(internal_policy) = &internal_policy {
- if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden
- && internal_policy.requires_path()
- && params.internal_policy_path.is_none()
- {
- return Err(CreateTxError::SpendingPolicyRequired(
- KeychainKind::Internal,
- ));
- };
- }
-
- let external_requirements = external_policy.get_condition(
- params
- .external_policy_path
- .as_ref()
- .unwrap_or(&BTreeMap::new()),
- )?;
- let internal_requirements = internal_policy
- .map(|policy| {
- Ok::<_, CreateTxError>(
- policy.get_condition(
- params
- .internal_policy_path
- .as_ref()
- .unwrap_or(&BTreeMap::new()),
- )?,
- )
- })
- .transpose()?;
-
- let requirements =
- external_requirements.merge(&internal_requirements.unwrap_or_default())?;
-
- let version = match params.version {
- Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0),
- Some(tx_builder::Version(1)) if requirements.csv.is_some() => {
- return Err(CreateTxError::Version1Csv)
- }
- Some(tx_builder::Version(x)) => x,
- None if requirements.csv.is_some() => 2,
- None => 1,
- };
-
- // We use a match here instead of a unwrap_or_else as it's way more readable :)
- let current_height = match params.current_height {
- // If they didn't tell us the current height, we assume it's the latest sync height.
- None => {
- let tip_height = self.chain.tip().height();
- absolute::LockTime::from_height(tip_height).expect("invalid height")
- }
- Some(h) => h,
- };
-
- let lock_time = match params.locktime {
- // When no nLockTime is specified, we try to prevent fee sniping, if possible
- None => {
- // Fee sniping can be partially prevented by setting the timelock
- // to current_height. If we don't know the current_height,
- // we default to 0.
- let fee_sniping_height = current_height;
-
- // We choose the biggest between the required nlocktime and the fee sniping
- // height
- match requirements.timelock {
- // No requirement, just use the fee_sniping_height
- None => fee_sniping_height,
- // There's a block-based requirement, but the value is lower than the fee_sniping_height
- Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => {
- fee_sniping_height
- }
- // There's a time-based requirement or a block-based requirement greater
- // than the fee_sniping_height use that value
- Some(value) => value,
- }
- }
- // Specific nLockTime required and we have no constraints, so just set to that value
- Some(x) if requirements.timelock.is_none() => x,
- // Specific nLockTime required and it's compatible with the constraints
- Some(x)
- if requirements.timelock.unwrap().is_same_unit(x)
- && x >= requirements.timelock.unwrap() =>
- {
- x
- }
- // Invalid nLockTime required
- Some(x) => {
- return Err(CreateTxError::LockTime {
- requested: x,
- required: requirements.timelock.unwrap(),
- })
- }
- };
-
- // The nSequence to be by default for inputs unless an explicit sequence is specified.
- let n_sequence = match (params.rbf, requirements.csv) {
- // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
- (None, None) if lock_time != absolute::LockTime::ZERO => {
- Sequence::ENABLE_LOCKTIME_NO_RBF
- }
- // No RBF, CSV or nLockTime, make the transaction final
- (None, None) => Sequence::MAX,
-
- // No RBF requested, use the value from CSV. Note that this value is by definition
- // non-final, so even if a timelock is enabled this nSequence is fine, hence why we
- // don't bother checking for it here. The same is true for all the other branches below
- (None, Some(csv)) => csv,
-
- // RBF with a specific value but that value is too high
- (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
- return Err(CreateTxError::RbfSequence)
- }
- // RBF with a specific value requested, but the value is incompatible with CSV
- (Some(tx_builder::RbfValue::Value(rbf)), Some(csv))
- if !check_nsequence_rbf(rbf, csv) =>
- {
- return Err(CreateTxError::RbfSequenceCsv { rbf, csv })
- }
-
- // RBF enabled with the default value with CSV also enabled. CSV takes precedence
- (Some(tx_builder::RbfValue::Default), Some(csv)) => csv,
- // Valid RBF, either default or with a specific value. We ignore the `CSV` value
- // because we've already checked it before
- (Some(rbf), _) => rbf.get_value(),
- };
-
- let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() {
- //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
- FeePolicy::FeeAmount(fee) => {
- if let Some(previous_fee) = params.bumping_fee {
- if fee < previous_fee.absolute {
- return Err(CreateTxError::FeeTooLow {
- required: previous_fee.absolute,
- });
- }
- }
- (FeeRate::ZERO, fee)
- }
- FeePolicy::FeeRate(rate) => {
- if let Some(previous_fee) = params.bumping_fee {
- let required_feerate = FeeRate::from_sat_per_kwu(
- previous_fee.rate.to_sat_per_kwu()
- + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb
- );
- if rate < required_feerate {
- return Err(CreateTxError::FeeRateTooLow {
- required: required_feerate,
- });
- }
- }
- (rate, 0)
- }
- };
-
- let mut tx = Transaction {
- version: transaction::Version::non_standard(version),
- lock_time,
- input: vec![],
- output: vec![],
- };
-
- if params.manually_selected_only && params.utxos.is_empty() {
- return Err(CreateTxError::NoUtxosSelected);
- }
-
- // we keep it as a float while we accumulate it, and only round it at the end
- let mut outgoing: u64 = 0;
- let mut received: u64 = 0;
-
- let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
-
- for (index, (script_pubkey, value)) in recipients.enumerate() {
- if !params.allow_dust
- && value.is_dust(script_pubkey)
- && !script_pubkey.is_provably_unspendable()
- {
- return Err(CreateTxError::OutputBelowDustLimit(index));
- }
-
- if self.is_mine(script_pubkey) {
- received += value;
- }
-
- let new_out = TxOut {
- script_pubkey: script_pubkey.clone(),
- value: Amount::from_sat(value),
- };
-
- tx.output.push(new_out);
-
- outgoing += value;
- }
-
- fee_amount += (fee_rate * tx.weight()).to_sat();
-
- if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
- && internal_descriptor.is_none()
- {
- return Err(CreateTxError::ChangePolicyDescriptor);
- }
-
- let (required_utxos, optional_utxos) =
- self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32()));
-
- // get drain script
- let drain_script = match params.drain_to {
- Some(ref drain_recipient) => drain_recipient.clone(),
- None => {
- let change_keychain = self.map_keychain(KeychainKind::Internal);
- let ((index, spk), index_changeset) = self
- .indexed_graph
- .index
- .next_unused_spk(&change_keychain)
- .expect("Keychain exists (we called map_keychain)");
- let spk = spk.into();
- self.indexed_graph.index.mark_used(change_keychain, index);
- self.persist
- .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
- index_changeset,
- )));
- self.persist.commit().map_err(CreateTxError::Persist)?;
- spk
- }
- };
-
- let (required_utxos, optional_utxos) =
- coin_selection::filter_duplicates(required_utxos, optional_utxos);
-
- let coin_selection = coin_selection.coin_select(
- required_utxos,
- optional_utxos,
- fee_rate,
- outgoing + fee_amount,
- &drain_script,
- )?;
- fee_amount += coin_selection.fee_amount;
- let excess = &coin_selection.excess;
-
- tx.input = coin_selection
- .selected
- .iter()
- .map(|u| bitcoin::TxIn {
- previous_output: u.outpoint(),
- script_sig: ScriptBuf::default(),
- sequence: u.sequence().unwrap_or(n_sequence),
- witness: Witness::new(),
- })
- .collect();
-
- if tx.output.is_empty() {
- // Uh oh, our transaction has no outputs.
- // We allow this when:
- // - We have a drain_to address and the utxos we must spend (this happens,
- // for example, when we RBF)
- // - We have a drain_to address and drain_wallet set
- // Otherwise, we don't know who we should send the funds to, and how much
- // we should send!
- if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) {
- if let NoChange {
- dust_threshold,
- remaining_amount,
- change_fee,
- } = excess
- {
- return Err(CreateTxError::InsufficientFunds {
- needed: *dust_threshold,
- available: remaining_amount.saturating_sub(*change_fee),
- });
- }
- } else {
- return Err(CreateTxError::NoRecipients);
- }
- }
-
- match excess {
- NoChange {
- remaining_amount, ..
- } => fee_amount += remaining_amount,
- Change { amount, fee } => {
- if self.is_mine(&drain_script) {
- received += amount;
- }
- fee_amount += fee;
-
- // create drain output
- let drain_output = TxOut {
- value: Amount::from_sat(*amount),
- script_pubkey: drain_script,
- };
-
- // TODO: We should pay attention when adding a new output: this might increase
- // the length of the "number of vouts" parameter by 2 bytes, potentially making
- // our feerate too low
- tx.output.push(drain_output);
- }
- };
-
- // sort input/outputs according to the chosen algorithm
- params.ordering.sort_tx(&mut tx);
-
- let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
- Ok(psbt)
- }
-
- /// Bump the fee of a transaction previously created with this wallet.
- ///
- /// Returns an error if the transaction is already confirmed or doesn't explicitly signal
- /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`]
- /// pre-populated with the inputs and outputs of the original transaction.
- ///
- /// ## Example
- ///
- /// ```no_run
- /// # // 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_persist::PersistBackend;
- /// # use anyhow::Error;
- /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
- /// # let mut wallet = doctest_wallet!();
- /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
- /// let mut psbt = {
- /// let mut builder = wallet.build_tx();
- /// builder
- /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
- /// .enable_rbf();
- /// builder.finish()?
- /// };
- /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
- /// let tx = psbt.clone().extract_tx().expect("tx");
- /// // broadcast tx but it's taking too long to confirm so we want to bump the fee
- /// let mut psbt = {
- /// let mut builder = wallet.build_fee_bump(tx.txid())?;
- /// builder
- /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));
- /// builder.finish()?
- /// };
- ///
- /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
- /// let fee_bumped_tx = psbt.extract_tx();
- /// // broadcast fee_bumped_tx to replace original
- /// # Ok::<(), anyhow::Error>(())
- /// ```
- // TODO: support for merging multiple transactions while bumping the fees
- pub fn build_fee_bump(
- &mut self,
- txid: Txid,
- ) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
- let graph = self.indexed_graph.graph();
- let txout_index = &self.indexed_graph.index;
- let chain_tip = self.chain.tip().block_id();
-
- let mut tx = graph
- .get_tx(txid)
- .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
- .as_ref()
- .clone();
-
- let pos = graph
- .get_chain_position(&self.chain, chain_tip, txid)
- .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
- if let ChainPosition::Confirmed(_) = pos {
- return Err(BuildFeeBumpError::TransactionConfirmed(txid));
- }
-
- if !tx
- .input
- .iter()
- .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
- {
- return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid()));
- }
-
- let fee = self
- .calculate_fee(&tx)
- .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
- let fee_rate = self
- .calculate_fee_rate(&tx)
- .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
-
- // remove the inputs from the tx and process them
- let original_txin = tx.input.drain(..).collect::<Vec<_>>();
- let original_utxos = original_txin
- .iter()
- .map(|txin| -> Result<_, BuildFeeBumpError> {
- let prev_tx = graph
- .get_tx(txin.previous_output.txid)
- .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
- let txout = &prev_tx.output[txin.previous_output.vout as usize];
-
- let confirmation_time: ConfirmationTime = graph
- .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
- .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
- .cloned()
- .into();
-
- let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
- Some((keychain, derivation_index)) => {
- let satisfaction_weight = self
- .get_descriptor_for_keychain(keychain)
- .max_weight_to_satisfy()
- .unwrap();
- WeightedUtxo {
- utxo: Utxo::Local(LocalOutput {
- outpoint: txin.previous_output,
- txout: txout.clone(),
- keychain,
- is_spent: true,
- derivation_index,
- confirmation_time,
- }),
- satisfaction_weight,
- }
- }
- None => {
- let satisfaction_weight =
- serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len();
- WeightedUtxo {
- utxo: Utxo::Foreign {
- outpoint: txin.previous_output,
- sequence: Some(txin.sequence),
- psbt_input: Box::new(psbt::Input {
- witness_utxo: Some(txout.clone()),
- non_witness_utxo: Some(prev_tx.as_ref().clone()),
- ..Default::default()
- }),
- },
- satisfaction_weight,
- }
- }
- };
-
- Ok(weighted_utxo)
- })
- .collect::<Result<Vec<_>, _>>()?;
-
- if tx.output.len() > 1 {
- let mut change_index = None;
- for (index, txout) in tx.output.iter().enumerate() {
- let change_type = self.map_keychain(KeychainKind::Internal);
- match txout_index.index_of_spk(&txout.script_pubkey) {
- Some((keychain, _)) if keychain == change_type => change_index = Some(index),
- _ => {}
- }
- }
-
- if let Some(change_index) = change_index {
- tx.output.remove(change_index);
- }
- }
-
- let params = TxParams {
- // TODO: figure out what rbf option should be?
- version: Some(tx_builder::Version(tx.version.0)),
- recipients: tx
- .output
- .into_iter()
- .map(|txout| (txout.script_pubkey, txout.value.to_sat()))
- .collect(),
- utxos: original_utxos,
- bumping_fee: Some(tx_builder::PreviousFee {
- absolute: fee,
- rate: fee_rate,
- }),
- ..Default::default()
- };
-
- Ok(TxBuilder {
- wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
- params,
- coin_selection: DefaultCoinSelectionAlgorithm::default(),
- phantom: core::marker::PhantomData,
- })
- }
-
- /// Sign a transaction with all the wallet's signers, in the order specified by every signer's
- /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that has the value true if the PSBT was finalized, or false otherwise.
- ///
- /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way
- /// the transaction is finalized at the end. Note that it can't be guaranteed that *every*
- /// signers will follow the options, but the "software signers" (WIF keys and `xprv`) defined
- /// in this library will.
- ///
- /// ## Example
- ///
- /// ```
- /// # use std::str::FromStr;
- /// # use bitcoin::*;
- /// # use bdk::*;
- /// # use bdk::wallet::ChangeSet;
- /// # use bdk::wallet::error::CreateTxError;
- /// # use bdk_persist::PersistBackend;
- /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
- /// # let mut wallet = doctest_wallet!();
- /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
- /// let mut psbt = {
- /// let mut builder = wallet.build_tx();
- /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
- /// builder.finish()?
- /// };
- /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
- /// assert!(finalized, "we should have signed all the inputs");
- /// # Ok::<(),anyhow::Error>(())
- pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result<bool, SignerError> {
- // This adds all the PSBT metadata for the inputs, which will help us later figure out how
- // to derive our keys
- self.update_psbt_with_descriptor(psbt)
- .map_err(SignerError::MiniscriptPsbt)?;
-
- // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
- // has the `non_witness_utxo`
- if !sign_options.trust_witness_utxo
- && psbt
- .inputs
- .iter()
- .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none())
- .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
- .any(|i| i.non_witness_utxo.is_none())
- {
- return Err(SignerError::MissingNonWitnessUtxo);
- }
-
- // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
- // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot
- if !sign_options.allow_all_sighashes
- && !psbt.inputs.iter().all(|i| {
- i.sighash_type.is_none()
- || i.sighash_type == Some(EcdsaSighashType::All.into())
- || i.sighash_type == Some(TapSighashType::All.into())
- || i.sighash_type == Some(TapSighashType::Default.into())
- })
- {
- return Err(SignerError::NonStandardSighash);
- }
-
- for signer in self
- .signers
- .signers()
- .iter()
- .chain(self.change_signers.signers().iter())
- {
- signer.sign_transaction(psbt, &sign_options, &self.secp)?;
- }
-
- // attempt to finalize
- if sign_options.try_finalize {
- self.finalize_psbt(psbt, sign_options)
- } else {
- Ok(false)
- }
- }
-
- /// Return the spending policies for the wallet's descriptor
- pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, DescriptorError> {
- let signers = match keychain {
- KeychainKind::External => &self.signers,
- KeychainKind::Internal => &self.change_signers,
- };
-
- match self.public_descriptor(keychain) {
- Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?),
- None => Ok(None),
- }
- }
-
- /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has
- /// the same structure but with every secret key removed
- ///
- /// This can be used to build a watch-only version of a wallet
- pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> {
- self.indexed_graph
- .index
- .keychains()
- .find(|(k, _)| *k == &keychain)
- .map(|(_, d)| d)
- }
-
- /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass
- /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to
- /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer)
- /// for further information.
- ///
- /// Returns `true` if the PSBT could be finalized, and `false` otherwise.
- ///
- /// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
- pub fn finalize_psbt(
- &self,
- psbt: &mut Psbt,
- sign_options: SignOptions,
- ) -> Result<bool, SignerError> {
- let chain_tip = self.chain.tip().block_id();
-
- let tx = &psbt.unsigned_tx;
- let mut finished = true;
-
- for (n, input) in tx.input.iter().enumerate() {
- let psbt_input = &psbt
- .inputs
- .get(n)
- .ok_or(SignerError::InputIndexOutOfRange)?;
- if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
- continue;
- }
- let confirmation_height = self
- .indexed_graph
- .graph()
- .get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
- .map(|chain_position| match chain_position {
- ChainPosition::Confirmed(a) => a.confirmation_height,
- ChainPosition::Unconfirmed(_) => u32::MAX,
- });
- let current_height = sign_options
- .assume_height
- .unwrap_or_else(|| self.chain.tip().height());
-
- // - Try to derive the descriptor by looking at the txout. If it's in our database, we
- // know exactly which `keychain` to use, and which derivation index it is
- // - If that fails, try to derive it by looking at the psbt input: the complete logic
- // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`,
- // `redeem_script` and `witness_script` to determine the right derivation
- // - If that also fails, it will try it on the internal descriptor, if present
- let desc = psbt
- .get_utxo_for(n)
- .and_then(|txout| self.get_descriptor_for_txout(&txout))
- .or_else(|| {
- self.indexed_graph.index.keychains().find_map(|(_, desc)| {
- desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp)
- })
- });
-
- match desc {
- Some(desc) => {
- let mut tmp_input = bitcoin::TxIn::default();
- match desc.satisfy(
- &mut tmp_input,
- (
- PsbtInputSatisfier::new(psbt, n),
- After::new(Some(current_height), false),
- Older::new(Some(current_height), confirmation_height, false),
- ),
- ) {
- Ok(_) => {
- let psbt_input = &mut psbt.inputs[n];
- psbt_input.final_script_sig = Some(tmp_input.script_sig);
- psbt_input.final_script_witness = Some(tmp_input.witness);
- if sign_options.remove_partial_sigs {
- psbt_input.partial_sigs.clear();
- }
- if sign_options.remove_taproot_extras {
- // We just constructed the final witness, clear these fields.
- psbt_input.tap_key_sig = None;
- psbt_input.tap_script_sigs.clear();
- psbt_input.tap_scripts.clear();
- psbt_input.tap_key_origins.clear();
- psbt_input.tap_internal_key = None;
- psbt_input.tap_merkle_root = None;
- }
- }
- Err(_) => finished = false,
- }
- }
- None => finished = false,
- }
- }
-
- if finished && sign_options.remove_taproot_extras {
- for output in &mut psbt.outputs {
- output.tap_key_origins.clear();
- }
- }
-
- Ok(finished)
- }
-
- /// Return the secp256k1 context used for all signing operations
- pub fn secp_ctx(&self) -> &SecpCtx {
- &self.secp
- }
-
- /// Returns the descriptor used to create addresses for a particular `keychain`.
- pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor {
- self.public_descriptor(self.map_keychain(keychain))
- .expect("we mapped it to external if it doesn't exist")
- }
-
- /// The derivation index of this wallet. It will return `None` if it has not derived any addresses.
- /// Otherwise, it will return the index of the highest address it has derived.
- pub fn derivation_index(&self, keychain: KeychainKind) -> Option<u32> {
- self.indexed_graph.index.last_revealed_index(&keychain)
- }
-
- /// The index of the next address that you would get if you were to ask the wallet for a new address
- pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 {
- let keychain = self.map_keychain(keychain);
- self.indexed_graph
- .index
- .next_index(&keychain)
- .expect("Keychain must exist (we called map_keychain)")
- .0
- }
-
- /// Informs the wallet that you no longer intend to broadcast a tx that was built from it.
- ///
- /// This frees up the change address used when creating the tx for use in future transactions.
- // TODO: Make this free up reserved utxos when that's implemented
- pub fn cancel_tx(&mut self, tx: &Transaction) {
- let txout_index = &mut self.indexed_graph.index;
- for txout in &tx.output {
- if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) {
- // NOTE: unmark_used will **not** make something unused if it has actually been used
- // by a tx in the tracker. It only removes the superficial marking.
- txout_index.unmark_used(keychain, index);
- }
- }
- }
-
- fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind {
- if keychain == KeychainKind::Internal
- && self.public_descriptor(KeychainKind::Internal).is_none()
- {
- KeychainKind::External
- } else {
- keychain
- }
- }
-
- fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
- let (keychain, child) = self
- .indexed_graph
- .index
- .index_of_spk(&txout.script_pubkey)?;
- let descriptor = self.get_descriptor_for_keychain(keychain);
- descriptor.at_derivation_index(child).ok()
- }
-
- fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> {
- self.list_unspent()
- .map(|utxo| {
- let keychain = utxo.keychain;
- (utxo, {
- self.get_descriptor_for_keychain(keychain)
- .max_weight_to_satisfy()
- .unwrap()
- })
- })
- .collect()
- }
-
- /// Given the options returns the list of utxos that must be used to form the
- /// transaction and any further that may be used if needed.
- fn preselect_utxos(
- &self,
- params: &TxParams,
- current_height: Option<u32>,
- ) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
- let TxParams {
- change_policy,
- unspendable,
- utxos,
- drain_wallet,
- manually_selected_only,
- bumping_fee,
- ..
- } = params;
-
- let manually_selected = utxos.clone();
- // we mandate confirmed transactions if we're bumping the fee
- let must_only_use_confirmed_tx = bumping_fee.is_some();
- let must_use_all_available = *drain_wallet;
-
- let chain_tip = self.chain.tip().block_id();
- // must_spend <- manually selected utxos
- // may_spend <- all other available utxos
- let mut may_spend = self.get_available_utxos();
-
- may_spend.retain(|may_spend| {
- !manually_selected
- .iter()
- .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
- });
- let mut must_spend = manually_selected;
-
- // NOTE: we are intentionally ignoring `unspendable` here. i.e manual
- // selection overrides unspendable.
- if *manually_selected_only {
- return (must_spend, vec![]);
- }
-
- let satisfies_confirmed = may_spend
- .iter()
- .map(|u| -> bool {
- let txid = u.0.outpoint.txid;
- let tx = match self.indexed_graph.graph().get_tx(txid) {
- Some(tx) => tx,
- None => return false,
- };
- let confirmation_time: ConfirmationTime = match self
- .indexed_graph
- .graph()
- .get_chain_position(&self.chain, chain_tip, txid)
- {
- Some(chain_position) => chain_position.cloned().into(),
- None => return false,
- };
-
- // Whether the UTXO is mature and, if needed, confirmed
- let mut spendable = true;
- if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
- return false;
- }
- if tx.is_coinbase() {
- debug_assert!(
- confirmation_time.is_confirmed(),
- "coinbase must always be confirmed"
- );
- if let Some(current_height) = current_height {
- match confirmation_time {
- ConfirmationTime::Confirmed { height, .. } => {
- // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
- spendable &=
- (current_height.saturating_sub(height)) >= COINBASE_MATURITY;
- }
- ConfirmationTime::Unconfirmed { .. } => spendable = false,
- }
- }
- }
- spendable
- })
- .collect::<Vec<_>>();
-
- let mut i = 0;
- may_spend.retain(|u| {
- let retain = change_policy.is_satisfied_by(&u.0)
- && !unspendable.contains(&u.0.outpoint)
- && satisfies_confirmed[i];
- i += 1;
- retain
- });
-
- let mut may_spend = may_spend
- .into_iter()
- .map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
- satisfaction_weight,
- utxo: Utxo::Local(local_utxo),
- })
- .collect();
-
- if must_use_all_available {
- must_spend.append(&mut may_spend);
- }
-
- (must_spend, may_spend)
- }
-
- fn complete_transaction(
- &self,
- tx: Transaction,
- selected: Vec<Utxo>,
- params: TxParams,
- ) -> Result<Psbt, CreateTxError> {
- let mut psbt = Psbt::from_unsigned_tx(tx)?;
-
- if params.add_global_xpubs {
- let all_xpubs = self
- .keychains()
- .flat_map(|(_, desc)| desc.get_extended_keys())
- .collect::<Vec<_>>();
-
- for xpub in all_xpubs {
- let origin = match xpub.origin {
- Some(origin) => origin,
- None if xpub.xkey.depth == 0 => {
- (xpub.root_fingerprint(&self.secp), vec![].into())
- }
- _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())),
- };
-
- psbt.xpub.insert(xpub.xkey, origin);
- }
- }
-
- let mut lookup_output = selected
- .into_iter()
- .map(|utxo| (utxo.outpoint(), utxo))
- .collect::<HashMap<_, _>>();
-
- // add metadata for the inputs
- for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) {
- let utxo = match lookup_output.remove(&input.previous_output) {
- Some(utxo) => utxo,
- None => continue,
- };
-
- match utxo {
- Utxo::Local(utxo) => {
- *psbt_input =
- match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
- Ok(psbt_input) => psbt_input,
- Err(e) => match e {
- CreateTxError::UnknownUtxo => psbt::Input {
- sighash_type: params.sighash,
- ..psbt::Input::default()
- },
- _ => return Err(e),
- },
- }
- }
- Utxo::Foreign {
- outpoint,
- psbt_input: foreign_psbt_input,
- ..
- } => {
- let is_taproot = foreign_psbt_input
- .witness_utxo
- .as_ref()
- .map(|txout| txout.script_pubkey.is_p2tr())
- .unwrap_or(false);
- if !is_taproot
- && !params.only_witness_utxo
- && foreign_psbt_input.non_witness_utxo.is_none()
- {
- return Err(CreateTxError::MissingNonWitnessUtxo(outpoint));
- }
- *psbt_input = *foreign_psbt_input;
- }
- }
- }
-
- self.update_psbt_with_descriptor(&mut psbt)?;
-
- Ok(psbt)
- }
-
- /// get the corresponding PSBT Input for a LocalUtxo
- pub fn get_psbt_input(
- &self,
- utxo: LocalOutput,
- sighash_type: Option<psbt::PsbtSighashType>,
- only_witness_utxo: bool,
- ) -> Result<psbt::Input, CreateTxError> {
- // Try to find the prev_script in our db to figure out if this is internal or external,
- // and the derivation index
- let (keychain, child) = self
- .indexed_graph
- .index
- .index_of_spk(&utxo.txout.script_pubkey)
- .ok_or(CreateTxError::UnknownUtxo)?;
-
- let mut psbt_input = psbt::Input {
- sighash_type,
- ..psbt::Input::default()
- };
-
- let desc = self.get_descriptor_for_keychain(keychain);
- let derived_descriptor = desc
- .at_derivation_index(child)
- .expect("child can't be hardened");
-
- psbt_input
- .update_with_descriptor_unchecked(&derived_descriptor)
- .map_err(MiniscriptPsbtError::Conversion)?;
-
- let prev_output = utxo.outpoint;
- if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) {
- if desc.is_witness() || desc.is_taproot() {
- psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
- }
- if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
- psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone());
- }
- }
- Ok(psbt_input)
- }
-
- fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> {
- // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
- // the input utxos and outputs
- let utxos = (0..psbt.inputs.len())
- .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
- .chain(
- psbt.unsigned_tx
- .output
- .iter()
- .enumerate()
- .map(|(i, out)| (false, i, out.clone())),
- )
- .collect::<Vec<_>>();
-
- // Try to figure out the keychain and derivation for every input and output
- for (is_input, index, out) in utxos.into_iter() {
- if let Some((keychain, child)) =
- self.indexed_graph.index.index_of_spk(&out.script_pubkey)
- {
- let desc = self.get_descriptor_for_keychain(keychain);
- let desc = desc
- .at_derivation_index(child)
- .expect("child can't be hardened");
-
- if is_input {
- psbt.update_input_with_descriptor(index, &desc)
- .map_err(MiniscriptPsbtError::UtxoUpdate)?;
- } else {
- psbt.update_output_with_descriptor(index, &desc)
- .map_err(MiniscriptPsbtError::OutputUpdate)?;
- }
- }
- }
-
- Ok(())
- }
-
- /// Return the checksum of the public descriptor associated to `keychain`
- ///
- /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor
- pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String {
- self.get_descriptor_for_keychain(keychain)
- .to_string()
- .split_once('#')
- .unwrap()
- .1
- .to_string()
- }
-
- /// Applies an update to the wallet and stages the changes (but does not [`commit`] them).
- ///
- /// Usually you create an `update` by interacting with some blockchain data source and inserting
- /// transactions related to your wallet into it.
- ///
- /// [`commit`]: Self::commit
- pub fn apply_update(&mut self, update: impl Into<Update>) -> Result<(), CannotConnectError> {
- let update = update.into();
- let mut changeset = match update.chain {
- Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?),
- None => ChangeSet::default(),
- };
-
- let (_, index_changeset) = self
- .indexed_graph
- .index
- .reveal_to_target_multi(&update.last_active_indices);
- changeset.append(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
- index_changeset,
- )));
- changeset.append(ChangeSet::from(
- self.indexed_graph.apply_update(update.graph),
- ));
- self.persist.stage(changeset);
- Ok(())
- }
-
- /// Commits all currently [`staged`] changed to the persistence backend returning and error when
- /// this fails.
- ///
- /// This returns whether the `update` resulted in any changes.
- ///
- /// [`staged`]: Self::staged
- pub fn commit(&mut self) -> anyhow::Result<bool> {
- self.persist.commit().map(|c| c.is_some())
- }
-
- /// Returns the changes that will be committed with the next call to [`commit`].
- ///
- /// [`commit`]: Self::commit
- pub fn staged(&self) -> &ChangeSet {
- self.persist.staged()
- }
-
- /// Get a reference to the inner [`TxGraph`].
- pub fn tx_graph(&self) -> &TxGraph<ConfirmationTimeHeightAnchor> {
- self.indexed_graph.graph()
- }
-
- /// Get a reference to the inner [`KeychainTxOutIndex`].
- pub fn spk_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
- &self.indexed_graph.index
- }
-
- /// Get a reference to the inner [`LocalChain`].
- pub fn local_chain(&self) -> &LocalChain {
- &self.chain
- }
-
- /// Introduces a `block` of `height` to the wallet, and tries to connect it to the
- /// `prev_blockhash` of the block's header.
- ///
- /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
- /// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
- ///
- /// [`apply_block_connected_to`]: Self::apply_block_connected_to
- pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> {
- let connected_to = match height.checked_sub(1) {
- Some(prev_height) => BlockId {
- height: prev_height,
- hash: block.header.prev_blockhash,
- },
- None => BlockId {
- height,
- hash: block.block_hash(),
- },
- };
- self.apply_block_connected_to(block, height, connected_to)
- .map_err(|err| match err {
- ApplyHeaderError::InconsistentBlocks => {
- unreachable!("connected_to is derived from the block so must be consistent")
- }
- ApplyHeaderError::CannotConnect(err) => err,
- })
- }
-
- /// Applies relevant transactions from `block` of `height` to the wallet, and connects the
- /// block to the internal chain.
- ///
- /// The `connected_to` parameter informs the wallet how this block connects to the internal
- /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
- /// internal [`TxGraph`].
- pub fn apply_block_connected_to(
- &mut self,
- block: &Block,
- height: u32,
- connected_to: BlockId,
- ) -> Result<(), ApplyHeaderError> {
- let mut changeset = ChangeSet::default();
- changeset.append(
- self.chain
- .apply_header_connected_to(&block.header, height, connected_to)?
- .into(),
- );
- changeset.append(
- self.indexed_graph
- .apply_block_relevant(block, height)
- .into(),
- );
- self.persist.stage(changeset);
- Ok(())
- }
-
- /// Apply relevant unconfirmed transactions to the wallet.
- ///
- /// Transactions that are not relevant are filtered out.
- ///
- /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
- /// when the transaction was last seen in the mempool. This is used for conflict resolution
- /// when there is conflicting unconfirmed transactions. The transaction with the later
- /// `last_seen` is prioritized.
- pub fn apply_unconfirmed_txs<'t>(
- &mut self,
- unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
- ) {
- let indexed_graph_changeset = self
- .indexed_graph
- .batch_insert_relevant_unconfirmed(unconfirmed_txs);
- self.persist.stage(ChangeSet::from(indexed_graph_changeset));
- }
-}
-
-/// Methods to construct sync/full-scan requests for spk-based chain sources.
-impl Wallet {
- /// Create a partial [`SyncRequest`] for this wallet for all revealed spks.
- ///
- /// This is the first step when performing a spk-based wallet partial sync, the returned
- /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to
- /// start a blockchain sync with a spk based blockchain client.
- pub fn start_sync_with_revealed_spks(&self) -> SyncRequest {
- SyncRequest::from_chain_tip(self.chain.tip())
- .cache_graph_txs(self.tx_graph())
- .populate_with_revealed_spks(&self.indexed_graph.index, ..)
- }
-
- /// Create a [`FullScanRequest] for this wallet.
- ///
- /// This is the first step when performing a spk-based wallet full scan, the returned
- /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to
- /// start a blockchain full scan with a spk based blockchain client.
- ///
- /// This operation is generally only used when importing or restoring a previously used wallet
- /// in which the list of used scripts is not known.
- pub fn start_full_scan(&self) -> FullScanRequest<KeychainKind> {
- FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index)
- .cache_graph_txs(self.tx_graph())
- }
-}
-
-impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet {
- fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor> {
- self.indexed_graph.graph()
- }
-}
-
-/// Deterministically generate a unique name given the descriptors defining the wallet
-///
-/// Compatible with [`wallet_name_from_descriptor`]
-pub fn wallet_name_from_descriptor<T>(
- descriptor: T,
- change_descriptor: Option<T>,
- network: Network,
- secp: &SecpCtx,
-) -> Result<String, DescriptorError>
-where
- T: IntoWalletDescriptor,
-{
- //TODO check descriptors contains only public keys
- let descriptor = descriptor
- .into_wallet_descriptor(secp, network)?
- .0
- .to_string();
- let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
- if let Some(change_descriptor) = change_descriptor {
- let change_descriptor = change_descriptor
- .into_wallet_descriptor(secp, network)?
- .0
- .to_string();
- wallet_name.push_str(
- calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
- );
- }
-
- Ok(wallet_name)
-}
-
-fn new_local_utxo(
- keychain: KeychainKind,
- derivation_index: u32,
- full_txo: FullTxOut<ConfirmationTimeHeightAnchor>,
-) -> LocalOutput {
- LocalOutput {
- outpoint: full_txo.outpoint,
- txout: full_txo.txout,
- is_spent: full_txo.spent_by.is_some(),
- confirmation_time: full_txo.chain_position.into(),
- keychain,
- derivation_index,
- }
-}
-
-fn create_signers<E: IntoWalletDescriptor>(
- index: &mut KeychainTxOutIndex<KeychainKind>,
- secp: &Secp256k1<All>,
- descriptor: E,
- change_descriptor: Option<E>,
- network: Network,
-) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), crate::descriptor::error::Error> {
- let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
- let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
- let _ = index.insert_descriptor(KeychainKind::External, descriptor);
-
- let change_signers = match change_descriptor {
- Some(descriptor) => {
- let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
- let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
- let _ = index.insert_descriptor(KeychainKind::Internal, descriptor);
- signers
- }
- None => Arc::new(SignersContainer::new()),
- };
-
- Ok((signers, change_signers))
-}
-
-/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
-#[macro_export]
-#[doc(hidden)]
-macro_rules! floating_rate {
- ($rate:expr) => {{
- use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
- // sat_kwu / 250.0 -> sat_vb
- $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64)
- }};
-}
-
-#[macro_export]
-#[doc(hidden)]
-/// Macro for getting a wallet for use in a doctest
-macro_rules! doctest_wallet {
- () => {{
- use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
- use $crate::chain::{ConfirmationTime, BlockId};
- use $crate::{KeychainKind, wallet::Wallet};
- let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
- let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
-
- let mut wallet = Wallet::new_no_persist(
- descriptor,
- Some(change_descriptor),
- Network::Regtest,
- )
- .unwrap();
- let address = wallet.peek_address(KeychainKind::External, 0).address;
- let tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- value: Amount::from_sat(500_000),
- script_pubkey: address.script_pubkey(),
- }],
- };
- let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
- let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
- height: 500,
- time: 50_000
- });
-
- wallet
- }}
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Generalized signers
-//!
-//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet)
-//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function.
-//!
-//! ```
-//! # use alloc::sync::Arc;
-//! # use core::str::FromStr;
-//! # use bitcoin::secp256k1::{Secp256k1, All};
-//! # use bitcoin::*;
-//! # use bdk::signer::*;
-//! # use bdk::*;
-//! # #[derive(Debug)]
-//! # struct CustomHSM;
-//! # impl CustomHSM {
-//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
-//! # Ok(())
-//! # }
-//! # fn connect() -> Self {
-//! # CustomHSM
-//! # }
-//! # fn get_id(&self) -> SignerId {
-//! # SignerId::Dummy(0)
-//! # }
-//! # }
-//! #[derive(Debug)]
-//! struct CustomSigner {
-//! device: CustomHSM,
-//! }
-//!
-//! impl CustomSigner {
-//! fn connect() -> Self {
-//! CustomSigner { device: CustomHSM::connect() }
-//! }
-//! }
-//!
-//! impl SignerCommon for CustomSigner {
-//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
-//! self.device.get_id()
-//! }
-//! }
-//!
-//! impl InputSigner for CustomSigner {
-//! fn sign_input(
-//! &self,
-//! psbt: &mut Psbt,
-//! input_index: usize,
-//! _sign_options: &SignOptions,
-//! _secp: &Secp256k1<All>,
-//! ) -> Result<(), SignerError> {
-//! self.device.hsm_sign_input(psbt, input_index)?;
-//!
-//! Ok(())
-//! }
-//! }
-//!
-//! let custom_signer = CustomSigner::connect();
-//!
-//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
-//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
-//! wallet.add_signer(
-//! KeychainKind::External,
-//! SignerOrdering(200),
-//! Arc::new(custom_signer)
-//! );
-//!
-//! # Ok::<_, anyhow::Error>(())
-//! ```
-
-use crate::collections::BTreeMap;
-use alloc::string::String;
-use alloc::sync::Arc;
-use alloc::vec::Vec;
-use core::cmp::Ordering;
-use core::fmt;
-use core::ops::{Bound::Included, Deref};
-
-use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
-use bitcoin::hashes::hash160;
-use bitcoin::secp256k1::Message;
-use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
-use bitcoin::{ecdsa, psbt, sighash, taproot};
-use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
-use bitcoin::{PrivateKey, Psbt, PublicKey};
-
-use miniscript::descriptor::{
- Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
- InnerXKey, KeyMap, SinglePriv, SinglePubKey,
-};
-use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
-
-use super::utils::SecpCtx;
-use crate::descriptor::{DescriptorMeta, XKeyUtils};
-use crate::psbt::PsbtUtils;
-use crate::wallet::error::MiniscriptPsbtError;
-
-/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
-/// multiple of them
-#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)]
-pub enum SignerId {
- /// Bitcoin HASH160 (RIPEMD160 after SHA256) hash of an ECDSA public key
- PkHash(hash160::Hash),
- /// The fingerprint of a BIP32 extended key
- Fingerprint(Fingerprint),
- /// Dummy identifier
- Dummy(u64),
-}
-
-impl From<hash160::Hash> for SignerId {
- fn from(hash: hash160::Hash) -> SignerId {
- SignerId::PkHash(hash)
- }
-}
-
-impl From<Fingerprint> for SignerId {
- fn from(fing: Fingerprint) -> SignerId {
- SignerId::Fingerprint(fing)
- }
-}
-
-/// Signing error
-#[derive(Debug)]
-pub enum SignerError {
- /// The private key is missing for the required public key
- MissingKey,
- /// The private key in use has the right fingerprint but derives differently than expected
- InvalidKey,
- /// The user canceled the operation
- UserCanceled,
- /// Input index is out of range
- InputIndexOutOfRange,
- /// The `non_witness_utxo` field of the transaction is required to sign this input
- MissingNonWitnessUtxo,
- /// The `non_witness_utxo` specified is invalid
- InvalidNonWitnessUtxo,
- /// The `witness_utxo` field of the transaction is required to sign this input
- MissingWitnessUtxo,
- /// The `witness_script` field of the transaction is required to sign this input
- MissingWitnessScript,
- /// The fingerprint and derivation path are missing from the psbt input
- MissingHdKeypath,
- /// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
- /// explicitly allowed them
- ///
- /// To enable signing transactions with non-standard sighashes set
- /// [`SignOptions::allow_all_sighashes`] to `true`.
- NonStandardSighash,
- /// Invalid SIGHASH for the signing context in use
- InvalidSighash,
- /// Error while computing the hash to sign
- SighashError(sighash::Error),
- /// Miniscript PSBT error
- MiniscriptPsbt(MiniscriptPsbtError),
- /// To be used only by external libraries implementing [`InputSigner`] or
- /// [`TransactionSigner`], so that they can return their own custom errors, without having to
- /// modify [`SignerError`] in BDK.
- External(String),
-}
-
-impl From<sighash::Error> for SignerError {
- fn from(e: sighash::Error) -> Self {
- SignerError::SighashError(e)
- }
-}
-
-impl fmt::Display for SignerError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::MissingKey => write!(f, "Missing private key"),
- Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"),
- Self::UserCanceled => write!(f, "The user canceled the operation"),
- Self::InputIndexOutOfRange => write!(f, "Input index out of range"),
- Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"),
- Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"),
- Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"),
- Self::MissingWitnessScript => write!(f, "Missing witness script"),
- Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
- Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
- Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
- Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
- Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
- Self::External(err) => write!(f, "{}", err),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for SignerError {}
-
-/// Signing context
-///
-/// Used by our software signers to determine the type of signatures to make
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SignerContext {
- /// Legacy context
- Legacy,
- /// Segwit v0 context (BIP 143)
- Segwitv0,
- /// Taproot context (BIP 340)
- Tap {
- /// Whether the signer can sign for the internal key or not
- is_internal_key: bool,
- },
-}
-
-/// Wrapper to pair a signer with its context
-#[derive(Debug, Clone)]
-pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
- signer: S,
- ctx: SignerContext,
-}
-
-impl<S: Sized + fmt::Debug + Clone> SignerWrapper<S> {
- /// Create a wrapped signer from a signer and a context
- pub fn new(signer: S, ctx: SignerContext) -> Self {
- SignerWrapper { signer, ctx }
- }
-}
-
-impl<S: Sized + fmt::Debug + Clone> Deref for SignerWrapper<S> {
- type Target = S;
-
- fn deref(&self) -> &Self::Target {
- &self.signer
- }
-}
-
-/// Common signer methods
-pub trait SignerCommon: fmt::Debug + Send + Sync {
- /// Return the [`SignerId`] for this signer
- ///
- /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
- /// compare two signers.
- fn id(&self, secp: &SecpCtx) -> SignerId;
-
- /// Return the secret key for the signer
- ///
- /// This is used internally to reconstruct the original descriptor that may contain secrets.
- /// External signers that are meant to keep key isolated should just return `None` here (which
- /// is the default for this method, if not overridden).
- fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
- None
- }
-}
-
-/// PSBT Input signer
-///
-/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing
-/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation
-/// for [`TransactionSigner`].
-pub trait InputSigner: SignerCommon {
- /// Sign a single psbt input
- fn sign_input(
- &self,
- psbt: &mut Psbt,
- input_index: usize,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError>;
-}
-
-/// PSBT signer
-///
-/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction
-/// at once.
-pub trait TransactionSigner: SignerCommon {
- /// Sign all the inputs of the psbt
- fn sign_transaction(
- &self,
- psbt: &mut Psbt,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError>;
-}
-
-impl<T: InputSigner> TransactionSigner for T {
- fn sign_transaction(
- &self,
- psbt: &mut Psbt,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError> {
- for input_index in 0..psbt.inputs.len() {
- self.sign_input(psbt, input_index, sign_options, secp)?;
- }
-
- Ok(())
- }
-}
-
-impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
- fn id(&self, secp: &SecpCtx) -> SignerId {
- SignerId::from(self.root_fingerprint(secp))
- }
-
- fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
- Some(DescriptorSecretKey::XPrv(self.signer.clone()))
- }
-}
-
-impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
- fn sign_input(
- &self,
- psbt: &mut Psbt,
- input_index: usize,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError> {
- if input_index >= psbt.inputs.len() {
- return Err(SignerError::InputIndexOutOfRange);
- }
-
- if psbt.inputs[input_index].final_script_sig.is_some()
- || psbt.inputs[input_index].final_script_witness.is_some()
- {
- return Ok(());
- }
-
- let tap_key_origins = psbt.inputs[input_index]
- .tap_key_origins
- .iter()
- .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource));
- let (public_key, full_path) = match psbt.inputs[input_index]
- .bip32_derivation
- .iter()
- .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
- .chain(tap_key_origins)
- .find_map(|(pk, keysource)| {
- if self.matches(keysource, secp).is_some() {
- Some((pk, keysource.1.clone()))
- } else {
- None
- }
- }) {
- Some((pk, full_path)) => (pk, full_path),
- None => return Ok(()),
- };
-
- let derived_key = match self.origin.clone() {
- Some((_fingerprint, origin_path)) => {
- let deriv_path = DerivationPath::from(
- &full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
- [origin_path.len()..],
- );
- self.xkey.derive_priv(secp, &deriv_path).unwrap()
- }
- None => self.xkey.derive_priv(secp, &full_path).unwrap(),
- };
-
- let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key);
- let valid_key = match public_key {
- SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true,
- SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true,
- _ => false,
- };
- if !valid_key {
- Err(SignerError::InvalidKey)
- } else {
- // HD wallets imply compressed keys
- let priv_key = PrivateKey {
- compressed: true,
- network: self.xkey.network,
- inner: derived_key.private_key,
- };
-
- SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
- }
- }
-}
-
-fn multikey_to_xkeys<K: InnerXKey + Clone>(
- multikey: DescriptorMultiXKey<K>,
-) -> Vec<DescriptorXKey<K>> {
- multikey
- .derivation_paths
- .into_paths()
- .into_iter()
- .map(|derivation_path| DescriptorXKey {
- origin: multikey.origin.clone(),
- xkey: multikey.xkey.clone(),
- derivation_path,
- wildcard: multikey.wildcard,
- })
- .collect()
-}
-
-impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
- fn id(&self, secp: &SecpCtx) -> SignerId {
- SignerId::from(self.root_fingerprint(secp))
- }
-
- fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
- Some(DescriptorSecretKey::MultiXPrv(self.signer.clone()))
- }
-}
-
-impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
- fn sign_input(
- &self,
- psbt: &mut Psbt,
- input_index: usize,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError> {
- let xkeys = multikey_to_xkeys(self.signer.clone());
- for xkey in xkeys {
- SignerWrapper::new(xkey, self.ctx).sign_input(psbt, input_index, sign_options, secp)?
- }
- Ok(())
- }
-}
-
-impl SignerCommon for SignerWrapper<PrivateKey> {
- fn id(&self, secp: &SecpCtx) -> SignerId {
- SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
- }
-
- fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
- Some(DescriptorSecretKey::Single(SinglePriv {
- key: self.signer,
- origin: None,
- }))
- }
-}
-
-impl InputSigner for SignerWrapper<PrivateKey> {
- fn sign_input(
- &self,
- psbt: &mut Psbt,
- input_index: usize,
- sign_options: &SignOptions,
- secp: &SecpCtx,
- ) -> Result<(), SignerError> {
- if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
- return Err(SignerError::InputIndexOutOfRange);
- }
-
- if psbt.inputs[input_index].final_script_sig.is_some()
- || psbt.inputs[input_index].final_script_witness.is_some()
- {
- return Ok(());
- }
-
- let pubkey = PublicKey::from_private_key(secp, self);
- let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
-
- if let SignerContext::Tap { is_internal_key } = self.ctx {
- if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
- if is_internal_key
- && psbt.inputs[input_index].tap_key_sig.is_none()
- && sign_options.sign_with_tap_internal_key
- && x_only_pubkey == psbt_internal_key
- {
- let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
- sign_psbt_schnorr(
- &self.inner,
- x_only_pubkey,
- None,
- &mut psbt.inputs[input_index],
- hash,
- hash_ty,
- secp,
- );
- }
- }
-
- if let Some((leaf_hashes, _)) =
- psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
- {
- let leaf_hashes = leaf_hashes
- .iter()
- .filter(|lh| {
- // Removing the leaves we shouldn't sign for
- let should_sign = match &sign_options.tap_leaves_options {
- TapLeavesOptions::All => true,
- TapLeavesOptions::Include(v) => v.contains(lh),
- TapLeavesOptions::Exclude(v) => !v.contains(lh),
- TapLeavesOptions::None => false,
- };
- // Filtering out the leaves without our key
- should_sign
- && !psbt.inputs[input_index]
- .tap_script_sigs
- .contains_key(&(x_only_pubkey, **lh))
- })
- .cloned()
- .collect::<Vec<_>>();
- for lh in leaf_hashes {
- let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
- sign_psbt_schnorr(
- &self.inner,
- x_only_pubkey,
- Some(lh),
- &mut psbt.inputs[input_index],
- hash,
- hash_ty,
- secp,
- );
- }
- }
-
- return Ok(());
- }
-
- if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
- return Ok(());
- }
-
- let (hash, hash_ty) = match self.ctx {
- SignerContext::Segwitv0 => {
- let (h, t) = Segwitv0::sighash(psbt, input_index, ())?;
- let h = h.to_raw_hash();
- (h, t)
- }
- SignerContext::Legacy => {
- let (h, t) = Legacy::sighash(psbt, input_index, ())?;
- let h = h.to_raw_hash();
- (h, t)
- }
- _ => return Ok(()), // handled above
- };
- sign_psbt_ecdsa(
- &self.inner,
- pubkey,
- &mut psbt.inputs[input_index],
- hash,
- hash_ty,
- secp,
- sign_options.allow_grinding,
- );
-
- Ok(())
- }
-}
-
-fn sign_psbt_ecdsa(
- secret_key: &secp256k1::SecretKey,
- pubkey: PublicKey,
- psbt_input: &mut psbt::Input,
- hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash,
- hash_ty: EcdsaSighashType,
- secp: &SecpCtx,
- allow_grinding: bool,
-) {
- let msg = &Message::from(hash);
- let sig = if allow_grinding {
- secp.sign_ecdsa_low_r(msg, secret_key)
- } else {
- secp.sign_ecdsa(msg, secret_key)
- };
- secp.verify_ecdsa(msg, &sig, &pubkey.inner)
- .expect("invalid or corrupted ecdsa signature");
-
- let final_signature = ecdsa::Signature { sig, hash_ty };
- psbt_input.partial_sigs.insert(pubkey, final_signature);
-}
-
-// Calling this with `leaf_hash` = `None` will sign for key-spend
-fn sign_psbt_schnorr(
- secret_key: &secp256k1::SecretKey,
- pubkey: XOnlyPublicKey,
- leaf_hash: Option<taproot::TapLeafHash>,
- psbt_input: &mut psbt::Input,
- hash: TapSighash,
- hash_ty: TapSighashType,
- secp: &SecpCtx,
-) {
- let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
- let keypair = match leaf_hash {
- None => keypair
- .tap_tweak(secp, psbt_input.tap_merkle_root)
- .to_inner(),
- Some(_) => keypair, // no tweak for script spend
- };
-
- let msg = &Message::from(hash);
- let sig = secp.sign_schnorr(msg, &keypair);
- secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
- .expect("invalid or corrupted schnorr signature");
-
- let final_signature = taproot::Signature { sig, hash_ty };
-
- if let Some(lh) = leaf_hash {
- psbt_input
- .tap_script_sigs
- .insert((pubkey, lh), final_signature);
- } else {
- psbt_input.tap_key_sig = Some(final_signature);
- }
-}
-
-/// Defines the order in which signers are called
-///
-/// The default value is `100`. Signers with an ordering above that will be called later,
-/// and they will thus see the partial signatures added to the transaction once they get to sign
-/// themselves.
-#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
-pub struct SignerOrdering(pub usize);
-
-impl Default for SignerOrdering {
- fn default() -> Self {
- SignerOrdering(100)
- }
-}
-
-#[derive(Debug, Clone)]
-struct SignersContainerKey {
- id: SignerId,
- ordering: SignerOrdering,
-}
-
-impl From<(SignerId, SignerOrdering)> for SignersContainerKey {
- fn from(tuple: (SignerId, SignerOrdering)) -> Self {
- SignersContainerKey {
- id: tuple.0,
- ordering: tuple.1,
- }
- }
-}
-
-/// Container for multiple signers
-#[derive(Debug, Default, Clone)]
-pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn TransactionSigner>>);
-
-impl SignersContainer {
- /// Create a map of public keys to secret keys
- pub fn as_key_map(&self, secp: &SecpCtx) -> KeyMap {
- self.0
- .values()
- .filter_map(|signer| signer.descriptor_secret_key())
- .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
- .collect()
- }
-
- /// Build a new signer container from a [`KeyMap`]
- ///
- /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to
- /// the signers
- pub fn build(
- keymap: KeyMap,
- descriptor: &Descriptor<DescriptorPublicKey>,
- secp: &SecpCtx,
- ) -> SignersContainer {
- let mut container = SignersContainer::new();
-
- for (pubkey, secret) in keymap {
- let ctx = match descriptor {
- Descriptor::Tr(tr) => SignerContext::Tap {
- is_internal_key: tr.internal_key() == &pubkey,
- },
- _ if descriptor.is_witness() => SignerContext::Segwitv0,
- _ => SignerContext::Legacy,
- };
-
- match secret {
- DescriptorSecretKey::Single(private_key) => container.add_external(
- SignerId::from(
- private_key
- .key
- .public_key(secp)
- .to_pubkeyhash(SigType::Ecdsa),
- ),
- SignerOrdering::default(),
- Arc::new(SignerWrapper::new(private_key.key, ctx)),
- ),
- DescriptorSecretKey::XPrv(xprv) => container.add_external(
- SignerId::from(xprv.root_fingerprint(secp)),
- SignerOrdering::default(),
- Arc::new(SignerWrapper::new(xprv, ctx)),
- ),
- DescriptorSecretKey::MultiXPrv(xprv) => container.add_external(
- SignerId::from(xprv.root_fingerprint(secp)),
- SignerOrdering::default(),
- Arc::new(SignerWrapper::new(xprv, ctx)),
- ),
- };
- }
-
- container
- }
-}
-
-impl SignersContainer {
- /// Default constructor
- pub fn new() -> Self {
- SignersContainer(Default::default())
- }
-
- /// Adds an external signer to the container for the specified id. Optionally returns the
- /// signer that was previously in the container, if any
- pub fn add_external(
- &mut self,
- id: SignerId,
- ordering: SignerOrdering,
- signer: Arc<dyn TransactionSigner>,
- ) -> Option<Arc<dyn TransactionSigner>> {
- self.0.insert((id, ordering).into(), signer)
- }
-
- /// Removes a signer from the container and returns it
- pub fn remove(
- &mut self,
- id: SignerId,
- ordering: SignerOrdering,
- ) -> Option<Arc<dyn TransactionSigner>> {
- self.0.remove(&(id, ordering).into())
- }
-
- /// Returns the list of identifiers of all the signers in the container
- pub fn ids(&self) -> Vec<&SignerId> {
- self.0
- .keys()
- .map(|SignersContainerKey { id, .. }| id)
- .collect()
- }
-
- /// Returns the list of signers in the container, sorted by lowest to highest `ordering`
- pub fn signers(&self) -> Vec<&Arc<dyn TransactionSigner>> {
- self.0.values().collect()
- }
-
- /// Finds the signer with lowest ordering for a given id in the container.
- pub fn find(&self, id: SignerId) -> Option<&Arc<dyn TransactionSigner>> {
- self.0
- .range((
- Included(&(id.clone(), SignerOrdering(0)).into()),
- Included(&(id.clone(), SignerOrdering(usize::MAX)).into()),
- ))
- .filter(|(k, _)| k.id == id)
- .map(|(_, v)| v)
- .next()
- }
-}
-
-/// Options for a software signer
-///
-/// Adjust the behavior of our software signers and the way a transaction is finalized
-#[derive(Debug, Clone)]
-pub struct SignOptions {
- /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
- /// provided
- ///
- /// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into
- /// paying a fee larger than expected.
- ///
- /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
- /// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
- /// should correctly produce a signature, at the expense of an increased trust in the creator
- /// of the PSBT.
- ///
- /// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
- pub trust_witness_utxo: bool,
-
- /// Whether the wallet should assume a specific height has been reached when trying to finalize
- /// a transaction
- ///
- /// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
- /// timelock height has already been reached. This option allows overriding the "current height" to let the
- /// wallet use timelocks in the future to spend a coin.
- pub assume_height: Option<u32>,
-
- /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
- /// what its value is
- ///
- /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
- pub allow_all_sighashes: bool,
-
- /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
- ///
- /// Defaults to `true` which will remove partial signatures during finalization.
- pub remove_partial_sigs: bool,
-
- /// Whether to remove taproot specific fields from the PSBT on finalization.
- ///
- /// For inputs this includes the taproot internal key, merkle root, and individual
- /// scripts and signatures. For both inputs and outputs it includes key origin info.
- ///
- /// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
- ///
- /// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
- pub remove_taproot_extras: bool,
-
- /// Whether to try finalizing the PSBT after the inputs are signed.
- ///
- /// Defaults to `true` which will try finalizing PSBT after inputs are signed.
- pub try_finalize: bool,
-
- /// Specifies which Taproot script-spend leaves we should sign for. This option is
- /// ignored if we're signing a non-taproot PSBT.
- ///
- /// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
- pub tap_leaves_options: TapLeavesOptions,
-
- /// Whether we should try to sign a taproot transaction with the taproot internal key
- /// or not. This option is ignored if we're signing a non-taproot PSBT.
- ///
- /// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
- pub sign_with_tap_internal_key: bool,
-
- /// Whether we should grind ECDSA signature to ensure signing with low r
- /// or not.
- /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
- pub allow_grinding: bool,
-}
-
-/// Customize which taproot script-path leaves the signer should sign.
-#[derive(Default, Debug, Clone, PartialEq, Eq)]
-pub enum TapLeavesOptions {
- /// The signer will sign all the leaves it has a key for.
- #[default]
- All,
- /// The signer won't sign leaves other than the ones specified. Note that it could still ignore
- /// some of the specified leaves, if it doesn't have the right key to sign them.
- Include(Vec<taproot::TapLeafHash>),
- /// The signer won't sign the specified leaves.
- Exclude(Vec<taproot::TapLeafHash>),
- /// The signer won't sign any leaf.
- None,
-}
-
-impl Default for SignOptions {
- fn default() -> Self {
- SignOptions {
- trust_witness_utxo: false,
- assume_height: None,
- allow_all_sighashes: false,
- remove_partial_sigs: true,
- remove_taproot_extras: true,
- try_finalize: true,
- tap_leaves_options: TapLeavesOptions::default(),
- sign_with_tap_internal_key: true,
- allow_grinding: true,
- }
- }
-}
-
-pub(crate) trait ComputeSighash {
- type Extra;
- type Sighash;
- type SighashType;
-
- fn sighash(
- psbt: &Psbt,
- input_index: usize,
- extra: Self::Extra,
- ) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
-}
-
-impl ComputeSighash for Legacy {
- type Extra = ();
- type Sighash = sighash::LegacySighash;
- type SighashType = EcdsaSighashType;
-
- fn sighash(
- psbt: &Psbt,
- input_index: usize,
- _extra: (),
- ) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
- if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
- return Err(SignerError::InputIndexOutOfRange);
- }
-
- let psbt_input = &psbt.inputs[input_index];
- let tx_input = &psbt.unsigned_tx.input[input_index];
-
- let sighash = psbt_input
- .sighash_type
- .unwrap_or_else(|| EcdsaSighashType::All.into())
- .ecdsa_hash_ty()
- .map_err(|_| SignerError::InvalidSighash)?;
- let script = match psbt_input.redeem_script {
- Some(ref redeem_script) => redeem_script.clone(),
- None => {
- let non_witness_utxo = psbt_input
- .non_witness_utxo
- .as_ref()
- .ok_or(SignerError::MissingNonWitnessUtxo)?;
- let prev_out = non_witness_utxo
- .output
- .get(tx_input.previous_output.vout as usize)
- .ok_or(SignerError::InvalidNonWitnessUtxo)?;
-
- prev_out.script_pubkey.clone()
- }
- };
-
- Ok((
- sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
- input_index,
- &script,
- sighash.to_u32(),
- )?,
- sighash,
- ))
- }
-}
-
-impl ComputeSighash for Segwitv0 {
- type Extra = ();
- type Sighash = sighash::SegwitV0Sighash;
- type SighashType = EcdsaSighashType;
-
- fn sighash(
- psbt: &Psbt,
- input_index: usize,
- _extra: (),
- ) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
- if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
- return Err(SignerError::InputIndexOutOfRange);
- }
-
- let psbt_input = &psbt.inputs[input_index];
- let tx_input = &psbt.unsigned_tx.input[input_index];
-
- let sighash_type = psbt_input
- .sighash_type
- .unwrap_or_else(|| EcdsaSighashType::All.into())
- .ecdsa_hash_ty()
- .map_err(|_| SignerError::InvalidSighash)?;
-
- // Always try first with the non-witness utxo
- let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
- // Check the provided prev-tx
- if prev_tx.txid() != tx_input.previous_output.txid {
- return Err(SignerError::InvalidNonWitnessUtxo);
- }
-
- // The output should be present, if it's missing the `non_witness_utxo` is invalid
- prev_tx
- .output
- .get(tx_input.previous_output.vout as usize)
- .ok_or(SignerError::InvalidNonWitnessUtxo)?
- } else if let Some(witness_utxo) = &psbt_input.witness_utxo {
- // Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
- // before we get to this point
- witness_utxo
- } else {
- // Nothing has been provided
- return Err(SignerError::MissingNonWitnessUtxo);
- };
- let value = utxo.value;
-
- let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
-
- let sighash = match psbt_input.witness_script {
- Some(ref witness_script) => {
- sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
- }
- None => {
- if utxo.script_pubkey.is_p2wpkh() {
- sighasher.p2wpkh_signature_hash(
- input_index,
- &utxo.script_pubkey,
- value,
- sighash_type,
- )?
- } else if psbt_input
- .redeem_script
- .as_ref()
- .map(|s| s.is_p2wpkh())
- .unwrap_or(false)
- {
- let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
- sighasher.p2wpkh_signature_hash(
- input_index,
- script_pubkey,
- value,
- sighash_type,
- )?
- } else {
- return Err(SignerError::MissingWitnessScript);
- }
- }
- };
- Ok((sighash, sighash_type))
- }
-}
-
-impl ComputeSighash for Tap {
- type Extra = Option<taproot::TapLeafHash>;
- type Sighash = TapSighash;
- type SighashType = TapSighashType;
-
- fn sighash(
- psbt: &Psbt,
- input_index: usize,
- extra: Self::Extra,
- ) -> Result<(Self::Sighash, TapSighashType), SignerError> {
- if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
- return Err(SignerError::InputIndexOutOfRange);
- }
-
- let psbt_input = &psbt.inputs[input_index];
-
- let sighash_type = psbt_input
- .sighash_type
- .unwrap_or_else(|| TapSighashType::Default.into())
- .taproot_hash_ty()
- .map_err(|_| SignerError::InvalidSighash)?;
- let witness_utxos = (0..psbt.inputs.len())
- .map(|i| psbt.get_utxo_for(i))
- .collect::<Vec<_>>();
- let mut all_witness_utxos = vec![];
-
- let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
- let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
- let prevouts = if is_anyone_can_pay {
- sighash::Prevouts::One(
- input_index,
- witness_utxos[input_index]
- .as_ref()
- .ok_or(SignerError::MissingWitnessUtxo)?,
- )
- } else if witness_utxos.iter().all(Option::is_some) {
- all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
- sighash::Prevouts::All(&all_witness_utxos)
- } else {
- return Err(SignerError::MissingWitnessUtxo);
- };
-
- // Assume no OP_CODESEPARATOR
- let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
-
- Ok((
- cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
- sighash_type,
- ))
- }
-}
-
-impl PartialOrd for SignersContainerKey {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for SignersContainerKey {
- fn cmp(&self, other: &Self) -> Ordering {
- self.ordering
- .cmp(&other.ordering)
- .then(self.id.cmp(&other.id))
- }
-}
-
-impl PartialEq for SignersContainerKey {
- fn eq(&self, other: &Self) -> bool {
- self.id == other.id && self.ordering == other.ordering
- }
-}
-
-impl Eq for SignersContainerKey {}
-
-#[cfg(test)]
-mod signers_container_tests {
- use super::*;
- use crate::descriptor;
- use crate::descriptor::IntoWalletDescriptor;
- use crate::keys::{DescriptorKey, IntoDescriptorKey};
- use assert_matches::assert_matches;
- use bitcoin::bip32;
- use bitcoin::secp256k1::{All, Secp256k1};
- use bitcoin::Network;
- use core::str::FromStr;
- use miniscript::ScriptContext;
-
- fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
- let secp = Secp256k1::new();
- this.id(&secp) == that.id(&secp)
- }
-
- // Signers added with the same ordering (like `Ordering::default`) created from `KeyMap`
- // should be preserved and not overwritten.
- // This happens usually when a set of signers is created from a descriptor with private keys.
- #[test]
- fn signers_with_same_ordering() {
- let secp = Secp256k1::new();
-
- let (prvkey1, _, _) = setup_keys(TPRV0_STR);
- let (prvkey2, _, _) = setup_keys(TPRV1_STR);
- let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
- let (wallet_desc, keymap) = desc
- .into_wallet_descriptor(&secp, Network::Testnet)
- .unwrap();
-
- let signers = SignersContainer::build(keymap, &wallet_desc, &secp);
- assert_eq!(signers.ids().len(), 2);
-
- let signers = signers.signers();
- assert_eq!(signers.len(), 2);
- }
-
- #[test]
- fn signers_sorted_by_ordering() {
- let mut signers = SignersContainer::new();
- let signer1 = Arc::new(DummySigner { number: 1 });
- let signer2 = Arc::new(DummySigner { number: 2 });
- let signer3 = Arc::new(DummySigner { number: 3 });
-
- // Mixed order insertions verifies we are not inserting at head or tail.
- signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone());
- signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone());
- signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone());
-
- // Check that signers are sorted from lowest to highest ordering
- let signers = signers.signers();
-
- assert!(is_equal(signers[0], &signer1));
- assert!(is_equal(signers[1], &signer2));
- assert!(is_equal(signers[2], &signer3));
- }
-
- #[test]
- fn find_signer_by_id() {
- let mut signers = SignersContainer::new();
- let signer1 = Arc::new(DummySigner { number: 1 });
- let signer2 = Arc::new(DummySigner { number: 2 });
- let signer3 = Arc::new(DummySigner { number: 3 });
- let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering.
-
- let id1 = SignerId::Dummy(1);
- let id2 = SignerId::Dummy(2);
- let id3 = SignerId::Dummy(3);
- let id_nonexistent = SignerId::Dummy(999);
-
- signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone());
- signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
- signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
-
- assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1));
- assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2));
- assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3));
-
- // The `signer4` has the same ID as `signer3` but lower ordering.
- // It should be found by `id3` instead of `signer3`.
- signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
- assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4));
-
- // Can't find anything with ID that doesn't exist
- assert_matches!(signers.find(id_nonexistent), None);
- }
-
- #[derive(Debug, Clone, Copy)]
- struct DummySigner {
- number: u64,
- }
-
- impl SignerCommon for DummySigner {
- fn id(&self, _secp: &SecpCtx) -> SignerId {
- SignerId::Dummy(self.number)
- }
- }
-
- impl TransactionSigner for DummySigner {
- fn sign_transaction(
- &self,
- _psbt: &mut Psbt,
- _sign_options: &SignOptions,
- _secp: &SecpCtx,
- ) -> Result<(), SignerError> {
- Ok(())
- }
- }
-
- const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
- const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
-
- const PATH: &str = "m/44'/1'/0'/0";
-
- fn setup_keys<Ctx: ScriptContext>(
- tprv: &str,
- ) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
- let secp: Secp256k1<All> = Secp256k1::new();
- let path = bip32::DerivationPath::from_str(PATH).unwrap();
- let tprv = bip32::Xpriv::from_str(tprv).unwrap();
- let tpub = bip32::Xpub::from_priv(&secp, &tprv);
- let fingerprint = tprv.fingerprint(&secp);
- let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
- let pubkey = (tpub, path).into_descriptor_key().unwrap();
-
- (prvkey, pubkey, fingerprint)
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Transaction builder
-//!
-//! ## Example
-//!
-//! ```
-//! # 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!();
-//! // create a TxBuilder from a wallet
-//! let mut tx_builder = wallet.build_tx();
-//!
-//! tx_builder
-//! // Create a transaction with one output to `to_address` of 50_000 satoshi
-//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
-//! // With a custom fee rate of 5.0 satoshi/vbyte
-//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
-//! // Only spend non-change outputs
-//! .do_not_spend_change()
-//! // Turn on RBF signaling
-//! .enable_rbf();
-//! let psbt = tx_builder.finish()?;
-//! # Ok::<(), anyhow::Error>(())
-//! ```
-
-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::{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
-/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and
-/// generate the transaction.
-///
-/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls
-/// as in the following example:
-///
-/// ```
-/// # use bdk::*;
-/// # use bdk::wallet::tx_builder::*;
-/// # use bitcoin::*;
-/// # use core::str::FromStr;
-/// # use bdk::wallet::ChangeSet;
-/// # use bdk::wallet::error::CreateTxError;
-/// # use bdk_persist::PersistBackend;
-/// # use anyhow::Error;
-/// # let mut wallet = doctest_wallet!();
-/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
-/// # let addr2 = addr1.clone();
-/// // chaining
-/// let psbt1 = {
-/// let mut builder = wallet.build_tx();
-/// builder
-/// .ordering(TxOrdering::Untouched)
-/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
-/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
-/// builder.finish()?
-/// };
-///
-/// // non-chaining
-/// let psbt2 = {
-/// let mut builder = wallet.build_tx();
-/// builder.ordering(TxOrdering::Untouched);
-/// for addr in &[addr1, addr2] {
-/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
-/// }
-/// builder.finish()?
-/// };
-///
-/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
-/// # Ok::<(), anyhow::Error>(())
-/// ```
-///
-/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
-/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it.
-///
-/// For further examples see [this module](super::tx_builder)'s documentation;
-///
-/// [`build_tx`]: Wallet::build_tx
-/// [`build_fee_bump`]: Wallet::build_fee_bump
-/// [`finish`]: Self::finish
-/// [`coin_selection`]: Self::coin_selection
-#[derive(Debug)]
-pub struct TxBuilder<'a, Cs, Ctx> {
- 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.
-//TODO: TxParams should eventually be exposed publicly.
-#[derive(Default, Debug, Clone)]
-pub(crate) struct TxParams {
- pub(crate) recipients: Vec<(ScriptBuf, u64)>,
- pub(crate) drain_wallet: bool,
- pub(crate) drain_to: Option<ScriptBuf>,
- pub(crate) fee_policy: Option<FeePolicy>,
- pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
- pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
- pub(crate) utxos: Vec<WeightedUtxo>,
- pub(crate) unspendable: HashSet<OutPoint>,
- pub(crate) manually_selected_only: bool,
- pub(crate) sighash: Option<psbt::PsbtSighashType>,
- pub(crate) ordering: TxOrdering,
- pub(crate) locktime: Option<absolute::LockTime>,
- pub(crate) rbf: Option<RbfValue>,
- pub(crate) version: Option<Version>,
- pub(crate) change_policy: ChangeSpendPolicy,
- pub(crate) only_witness_utxo: bool,
- pub(crate) add_global_xpubs: bool,
- pub(crate) include_output_redeem_witness_script: bool,
- pub(crate) bumping_fee: Option<PreviousFee>,
- pub(crate) current_height: Option<absolute::LockTime>,
- pub(crate) allow_dust: bool,
-}
-
-#[derive(Clone, Copy, Debug)]
-pub(crate) struct PreviousFee {
- pub absolute: u64,
- pub rate: FeeRate,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub(crate) enum FeePolicy {
- FeeRate(FeeRate),
- FeeAmount(u64),
-}
-
-impl Default for FeePolicy {
- fn default() -> Self {
- FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
- }
-}
-
-impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> {
- fn clone(&self) -> Self {
- TxBuilder {
- wallet: self.wallet.clone(),
- params: self.params.clone(),
- coin_selection: self.coin_selection.clone(),
- phantom: PhantomData,
- }
- }
-}
-
-// methods supported by both contexts, for any CoinSelectionAlgorithm
-impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
- /// Set a custom fee rate.
- ///
- /// This method sets the mining fee paid by the transaction as a rate on its size.
- /// This means that the total fee paid is equal to `fee_rate` times the size
- /// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
- /// relay policy.
- ///
- /// Note that this is really a minimum feerate -- it's possible to
- /// overshoot it slightly since adding a change output to drain the remaining
- /// excess might not be viable.
- pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
- self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
- self
- }
-
- /// Set an absolute fee
- /// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
- /// If anyone sets both the fee_absolute method and the fee_rate method,
- /// the FeePolicy enum will be set by whichever method was called last,
- /// as the FeeRate and FeeAmount are mutually exclusive.
- ///
- /// Note that this is really a minimum absolute fee -- it's possible to
- /// overshoot it slightly since adding a change output to drain the remaining
- /// excess might not be viable.
- pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
- self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
- self
- }
-
- /// Set the policy path to use while creating the transaction for a given keychain.
- ///
- /// This method accepts a map where the key is the policy node id (see
- /// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of
- /// the items that are intended to be satisfied from the policy node (see
- /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)).
- ///
- /// ## Example
- ///
- /// An example of when the policy path is needed is the following descriptor:
- /// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`,
- /// derived from the miniscript policy `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`.
- /// It declares three descriptor fragments, and at the top level it uses `thresh()` to
- /// ensure that at least two of them are satisfied. The individual fragments are:
- ///
- /// 1. `pk(A)`
- /// 2. `and(pk(B),older(6))`
- /// 3. `and(pk(C),after(630000))`
- ///
- /// When those conditions are combined in pairs, it's clear that the transaction needs to be created
- /// differently depending on how the user intends to satisfy the policy afterwards:
- ///
- /// * If fragments `1` and `2` are used, the transaction will need to use a specific
- /// `n_sequence` in order to spend an `OP_CSV` branch.
- /// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime`
- /// in order to spend an `OP_CLTV` branch.
- /// * If fragments `2` and `3` are used, the transaction will need both.
- ///
- /// When the spending policy is represented as a tree (see
- /// [`Wallet::policies`](super::Wallet::policies)), every node
- /// is assigned a unique identifier that can be used in the policy path to specify which of
- /// the node's children the user intends to satisfy: for instance, assuming the `thresh()`
- /// root node of this example has an id of `aabbccdd`, the policy path map would look like:
- ///
- /// `{ "aabbccdd" => [0, 1] }`
- ///
- /// where the key is the node's id, and the value is a list of the children that should be
- /// used, in no particular order.
- ///
- /// If a particularly complex descriptor has multiple ambiguous thresholds in its structure,
- /// multiple entries can be added to the map, one for each node that requires an explicit path.
- ///
- /// ```
- /// # use std::str::FromStr;
- /// # use std::collections::BTreeMap;
- /// # use bitcoin::*;
- /// # use bdk::*;
- /// # let to_address =
- /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
- /// .unwrap()
- /// .assume_checked();
- /// # let mut wallet = doctest_wallet!();
- /// let mut path = BTreeMap::new();
- /// path.insert("aabbccdd".to_string(), vec![0, 1]);
- ///
- /// let builder = wallet
- /// .build_tx()
- /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
- /// .policy_path(path, KeychainKind::External);
- ///
- /// # Ok::<(), anyhow::Error>(())
- /// ```
- pub fn policy_path(
- &mut self,
- policy_path: BTreeMap<String, Vec<usize>>,
- keychain: KeychainKind,
- ) -> &mut Self {
- let to_update = match keychain {
- KeychainKind::Internal => &mut self.params.internal_policy_path,
- KeychainKind::External => &mut self.params.external_policy_path,
- };
-
- *to_update = Some(policy_path);
- self
- }
-
- /// Add the list of outpoints to the internal list of UTXOs that **must** be spent.
- ///
- /// If an error occurs while adding any of the UTXOs then none of them are added and the error is returned.
- ///
- /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
- /// the "utxos" and the "unspendable" list, it will be spent.
- pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
- {
- let wallet = self.wallet.borrow();
- let utxos = outpoints
- .iter()
- .map(|outpoint| {
- wallet
- .get_utxo(*outpoint)
- .ok_or(AddUtxoError::UnknownUtxo(*outpoint))
- })
- .collect::<Result<Vec<_>, _>>()?;
-
- for utxo in utxos {
- let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
- let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
- self.params.utxos.push(WeightedUtxo {
- satisfaction_weight,
- utxo: Utxo::Local(utxo),
- });
- }
- }
-
- Ok(self)
- }
-
- /// Add a utxo to the internal list of utxos that **must** be spent
- ///
- /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
- /// the "utxos" and the "unspendable" list, it will be spent.
- pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> {
- self.add_utxos(&[outpoint])
- }
-
- /// Add a foreign UTXO i.e. a UTXO not owned by this wallet.
- ///
- /// At a minimum to add a foreign UTXO we need:
- ///
- /// 1. `outpoint`: To add it to the raw transaction.
- /// 2. `psbt_input`: To know the value.
- /// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation.
- ///
- /// There are several security concerns about adding foreign UTXOs that application
- /// developers should consider. First, how do you know the value of the input is correct? If a
- /// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the
- /// value by checking it against the transaction. If only a `witness_utxo` is provided then this
- /// method doesn't verify the value but just takes it as a given -- it is up to you to check
- /// that whoever sent you the `input_psbt` was not lying!
- ///
- /// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your
- /// application it may be important that this be known precisely. If not, a malicious
- /// counterparty may fool you into putting in a value that is too low, giving the transaction a
- /// lower than expected feerate. They could also fool you into putting a value that is too high
- /// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
- /// of course check the real input weight matches the expected weight prior to broadcasting.
- ///
- /// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
- /// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
- /// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
- ///
- /// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
- ///
- /// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction
- /// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx
- /// graph using the [`Wallet::insert_txout`] function.
- ///
- /// # Errors
- ///
- /// This method returns errors in the following circumstances:
- ///
- /// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
- /// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
- ///
- /// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
- /// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
- /// is called.
- ///
- /// [`only_witness_utxo`]: Self::only_witness_utxo
- /// [`finish`]: Self::finish
- /// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
- pub fn add_foreign_utxo(
- &mut self,
- outpoint: OutPoint,
- psbt_input: psbt::Input,
- satisfaction_weight: usize,
- ) -> Result<&mut Self, AddForeignUtxoError> {
- self.add_foreign_utxo_with_sequence(
- outpoint,
- psbt_input,
- satisfaction_weight,
- Sequence::MAX,
- )
- }
-
- /// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
- pub fn add_foreign_utxo_with_sequence(
- &mut self,
- outpoint: OutPoint,
- psbt_input: psbt::Input,
- satisfaction_weight: usize,
- sequence: Sequence,
- ) -> Result<&mut Self, AddForeignUtxoError> {
- if psbt_input.witness_utxo.is_none() {
- match psbt_input.non_witness_utxo.as_ref() {
- Some(tx) => {
- if tx.txid() != outpoint.txid {
- return Err(AddForeignUtxoError::InvalidTxid {
- input_txid: tx.txid(),
- foreign_utxo: outpoint,
- });
- }
- if tx.output.len() <= outpoint.vout as usize {
- return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
- }
- }
- None => {
- return Err(AddForeignUtxoError::MissingUtxo);
- }
- }
- }
-
- self.params.utxos.push(WeightedUtxo {
- satisfaction_weight,
- utxo: Utxo::Foreign {
- outpoint,
- sequence: Some(sequence),
- psbt_input: Box::new(psbt_input),
- },
- });
-
- Ok(self)
- }
-
- /// Only spend utxos added by [`add_utxo`].
- ///
- /// The wallet will **not** add additional utxos to the transaction even if they are needed to
- /// make the transaction valid.
- ///
- /// [`add_utxo`]: Self::add_utxo
- pub fn manually_selected_only(&mut self) -> &mut Self {
- self.params.manually_selected_only = true;
- self
- }
-
- /// Replace the internal list of unspendable utxos with a new list
- ///
- /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
- /// have priority over these. See the docs of the two linked methods for more details.
- pub fn unspendable(&mut self, unspendable: Vec<OutPoint>) -> &mut Self {
- self.params.unspendable = unspendable.into_iter().collect();
- self
- }
-
- /// Add a utxo to the internal list of unspendable utxos
- ///
- /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
- /// have priority over this. See the docs of the two linked methods for more details.
- pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut Self {
- self.params.unspendable.insert(unspendable);
- self
- }
-
- /// Sign with a specific sig hash
- ///
- /// **Use this option very carefully**
- pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self {
- self.params.sighash = Some(sighash);
- self
- }
-
- /// Choose the ordering for inputs and outputs of the transaction
- pub fn ordering(&mut self, ordering: TxOrdering) -> &mut Self {
- self.params.ordering = ordering;
- self
- }
-
- /// Use a specific nLockTime while creating the transaction
- ///
- /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
- pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self {
- self.params.locktime = Some(locktime);
- self
- }
-
- /// Build a transaction with a specific version
- ///
- /// The `version` should always be greater than `0` and greater than `1` if the wallet's
- /// descriptors contain an "older" (OP_CSV) operator.
- pub fn version(&mut self, version: i32) -> &mut Self {
- self.params.version = Some(Version(version));
- self
- }
-
- /// Do not spend change outputs
- ///
- /// This effectively adds all the change outputs to the "unspendable" list. See
- /// [`TxBuilder::unspendable`].
- pub fn do_not_spend_change(&mut self) -> &mut Self {
- self.params.change_policy = ChangeSpendPolicy::ChangeForbidden;
- self
- }
-
- /// Only spend change outputs
- ///
- /// This effectively adds all the non-change outputs to the "unspendable" list. See
- /// [`TxBuilder::unspendable`].
- pub fn only_spend_change(&mut self) -> &mut Self {
- self.params.change_policy = ChangeSpendPolicy::OnlyChange;
- self
- }
-
- /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and
- /// [`TxBuilder::only_spend_change`] for some shortcuts.
- pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self {
- self.params.change_policy = change_policy;
- self
- }
-
- /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from
- /// SegWit descriptors.
- ///
- /// This reduces the size of the PSBT, but some signers might reject them due to the lack of
- /// the `non_witness_utxo`.
- pub fn only_witness_utxo(&mut self) -> &mut Self {
- self.params.only_witness_utxo = true;
- self
- }
-
- /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::psbt::Output::redeem_script) and
- /// [`psbt::Output::witness_script`](bitcoin::psbt::Output::witness_script) fields.
- ///
- /// This is useful for signers which always require it, like ColdCard hardware wallets.
- pub fn include_output_redeem_witness_script(&mut self) -> &mut Self {
- self.params.include_output_redeem_witness_script = true;
- self
- }
-
- /// Fill-in the `PSBT_GLOBAL_XPUB` field with the extended keys contained in both the external
- /// and internal descriptors
- ///
- /// This is useful for offline signers that take part to a multisig. Some hardware wallets like
- /// BitBox and ColdCard are known to require this.
- pub fn add_global_xpubs(&mut self) -> &mut Self {
- self.params.add_global_xpubs = true;
- self
- }
-
- /// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy.
- pub fn drain_wallet(&mut self) -> &mut Self {
- self.params.drain_wallet = true;
- self
- }
-
- /// Choose the coin selection algorithm
- ///
- /// Overrides the [`DefaultCoinSelectionAlgorithm`].
- ///
- /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
- pub fn coin_selection<P: CoinSelectionAlgorithm>(
- self,
- coin_selection: P,
- ) -> TxBuilder<'a, P, Ctx> {
- TxBuilder {
- wallet: self.wallet,
- params: self.params,
- coin_selection,
- phantom: PhantomData,
- }
- }
-
- /// Enable signaling RBF
- ///
- /// This will use the default nSequence value of `0xFFFFFFFD`.
- pub fn enable_rbf(&mut self) -> &mut Self {
- self.params.rbf = Some(RbfValue::Default);
- self
- }
-
- /// Enable signaling RBF with a specific nSequence value
- ///
- /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
- /// and the given `nsequence` is lower than the CSV value.
- ///
- /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
- /// be a valid nSequence to signal RBF.
- pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
- self.params.rbf = Some(RbfValue::Value(nsequence));
- self
- }
-
- /// Set the current blockchain height.
- ///
- /// This will be used to:
- /// 1. Set the nLockTime for preventing fee sniping.
- /// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
- /// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
- /// mature at `current_height`, we ignore them in the coin selection.
- /// If you want to create a transaction that spends immature coinbase inputs, manually
- /// add them using [`TxBuilder::add_utxos`].
- ///
- /// In both cases, if you don't provide a current height, we use the last sync height.
- pub fn current_height(&mut self, height: u32) -> &mut Self {
- self.params.current_height =
- Some(absolute::LockTime::from_height(height).expect("Invalid height"));
- self
- }
-
- /// Set whether or not the dust limit is checked.
- ///
- /// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
- pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
- self.params.allow_dust = allow_dust;
- self
- }
-}
-
-impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> {
- /// Finish building the transaction.
- ///
- /// Returns a new [`Psbt`] per [`BIP174`].
- ///
- /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
- pub fn finish(self) -> Result<Psbt, CreateTxError> {
- self.wallet
- .borrow_mut()
- .create_tx(self.coin_selection, self.params)
- }
-}
-
-#[derive(Debug)]
-/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
-pub enum AddUtxoError {
- /// Happens when trying to spend an UTXO that is not in the internal database
- UnknownUtxo(OutPoint),
-}
-
-impl fmt::Display for AddUtxoError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::UnknownUtxo(outpoint) => write!(
- f,
- "UTXO not found in the internal database for txid: {} with vout: {}",
- outpoint.txid, outpoint.vout
- ),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for AddUtxoError {}
-
-#[derive(Debug)]
-/// Error returned from [`TxBuilder::add_foreign_utxo`].
-pub enum AddForeignUtxoError {
- /// Foreign utxo outpoint txid does not match PSBT input txid
- InvalidTxid {
- /// PSBT input txid
- input_txid: Txid,
- /// Foreign UTXO outpoint
- foreign_utxo: OutPoint,
- },
- /// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
- InvalidOutpoint(OutPoint),
- /// Foreign utxo missing witness_utxo or non_witness_utxo
- MissingUtxo,
-}
-
-impl fmt::Display for AddForeignUtxoError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidTxid {
- input_txid,
- foreign_utxo,
- } => write!(
- f,
- "Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
- foreign_utxo.txid, input_txid,
- ),
- Self::InvalidOutpoint(outpoint) => write!(
- f,
- "Requested outpoint doesn't exist for txid: {} with vout: {}",
- outpoint.txid, outpoint.vout,
- ),
- Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
- }
- }
-}
-
-#[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 {
- /// Randomized (default)
- #[default]
- Shuffle,
- /// Unchanged
- Untouched,
- /// BIP69 / Lexicographic
- Bip69Lexicographic,
-}
-
-impl TxOrdering {
- /// Sort transaction inputs and outputs by [`TxOrdering`] variant
- pub fn sort_tx(&self, tx: &mut Transaction) {
- match self {
- TxOrdering::Untouched => {}
- TxOrdering::Shuffle => {
- use rand::seq::SliceRandom;
- let mut rng = rand::thread_rng();
- tx.input.shuffle(&mut rng);
- tx.output.shuffle(&mut rng);
- }
- TxOrdering::Bip69Lexicographic => {
- tx.input.sort_unstable_by_key(|txin| {
- (txin.previous_output.txid, txin.previous_output.vout)
- });
- tx.output
- .sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
- }
- }
- }
-}
-
-/// Transaction version
-///
-/// Has a default value of `1`
-#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
-pub(crate) struct Version(pub(crate) i32);
-
-impl Default for Version {
- fn default() -> Self {
- Version(1)
- }
-}
-
-/// RBF nSequence value
-///
-/// Has a default value of `0xFFFFFFFD`
-#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
-pub(crate) enum RbfValue {
- Default,
- Value(Sequence),
-}
-
-impl RbfValue {
- pub(crate) fn get_value(&self) -> Sequence {
- match self {
- RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
- RbfValue::Value(v) => *v,
- }
- }
-}
-
-/// Policy regarding the use of change outputs when creating a transaction
-#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
-pub enum ChangeSpendPolicy {
- /// Use both change and non-change outputs (default)
- #[default]
- ChangeAllowed,
- /// Only use change outputs (see [`TxBuilder::only_spend_change`])
- OnlyChange,
- /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`])
- ChangeForbidden,
-}
-
-impl ChangeSpendPolicy {
- pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
- match self {
- ChangeSpendPolicy::ChangeAllowed => true,
- ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
- ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External,
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\
- 85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\
- 79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\
- dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\
- 03e80300000000000002aaeee80300000000000001aa200300000000000001ff\
- 00000000";
- macro_rules! ordering_test_tx {
- () => {
- deserialize::<bitcoin::Transaction>(&Vec::<u8>::from_hex(ORDERING_TEST_TX).unwrap())
- .unwrap()
- };
- }
-
- use bdk_chain::ConfirmationTime;
- use bitcoin::consensus::deserialize;
- use bitcoin::hex::FromHex;
- use bitcoin::TxOut;
-
- use super::*;
-
- #[test]
- fn test_output_ordering_default_shuffle() {
- assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
- }
-
- #[test]
- fn test_output_ordering_untouched() {
- let original_tx = ordering_test_tx!();
- let mut tx = original_tx.clone();
-
- TxOrdering::Untouched.sort_tx(&mut tx);
-
- assert_eq!(original_tx, tx);
- }
-
- #[test]
- fn test_output_ordering_shuffle() {
- let original_tx = ordering_test_tx!();
- let mut tx = original_tx.clone();
-
- (0..40)
- .find(|_| {
- TxOrdering::Shuffle.sort_tx(&mut tx);
- original_tx.input != tx.input
- })
- .expect("it should have moved the inputs at least once");
-
- let mut tx = original_tx.clone();
- (0..40)
- .find(|_| {
- TxOrdering::Shuffle.sort_tx(&mut tx);
- original_tx.output != tx.output
- })
- .expect("it should have moved the outputs at least once");
- }
-
- #[test]
- fn test_output_ordering_bip69() {
- use core::str::FromStr;
-
- let original_tx = ordering_test_tx!();
- let mut tx = original_tx;
-
- TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
-
- assert_eq!(
- tx.input[0].previous_output,
- bitcoin::OutPoint::from_str(
- "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57:5"
- )
- .unwrap()
- );
- assert_eq!(
- tx.input[1].previous_output,
- bitcoin::OutPoint::from_str(
- "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:0"
- )
- .unwrap()
- );
- assert_eq!(
- tx.input[2].previous_output,
- bitcoin::OutPoint::from_str(
- "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:1"
- )
- .unwrap()
- );
-
- assert_eq!(tx.output[0].value.to_sat(), 800);
- assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
- assert_eq!(
- tx.output[2].script_pubkey,
- ScriptBuf::from(vec![0xAA, 0xEE])
- );
- }
-
- fn get_test_utxos() -> Vec<LocalOutput> {
- use bitcoin::hashes::Hash;
-
- vec![
- LocalOutput {
- outpoint: OutPoint {
- txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
- vout: 0,
- },
- txout: TxOut::NULL,
- keychain: KeychainKind::External,
- is_spent: false,
- confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
- derivation_index: 0,
- },
- LocalOutput {
- outpoint: OutPoint {
- txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
- vout: 1,
- },
- txout: TxOut::NULL,
- keychain: KeychainKind::Internal,
- is_spent: false,
- confirmation_time: ConfirmationTime::Confirmed {
- height: 32,
- time: 42,
- },
- derivation_index: 1,
- },
- ]
- }
-
- #[test]
- fn test_change_spend_policy_default() {
- let change_spend_policy = ChangeSpendPolicy::default();
- let filtered = get_test_utxos()
- .into_iter()
- .filter(|u| change_spend_policy.is_satisfied_by(u))
- .count();
-
- assert_eq!(filtered, 2);
- }
-
- #[test]
- fn test_change_spend_policy_no_internal() {
- let change_spend_policy = ChangeSpendPolicy::ChangeForbidden;
- let filtered = get_test_utxos()
- .into_iter()
- .filter(|u| change_spend_policy.is_satisfied_by(u))
- .collect::<Vec<_>>();
-
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].keychain, KeychainKind::External);
- }
-
- #[test]
- fn test_change_spend_policy_only_internal() {
- let change_spend_policy = ChangeSpendPolicy::OnlyChange;
- let filtered = get_test_utxos()
- .into_iter()
- .filter(|u| change_spend_policy.is_satisfied_by(u))
- .collect::<Vec<_>>();
-
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].keychain, KeychainKind::Internal);
- }
-
- #[test]
- fn test_default_tx_version_1() {
- let version = Version::default();
- assert_eq!(version.0, 1);
- }
-}
+++ /dev/null
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-use bitcoin::secp256k1::{All, Secp256k1};
-use bitcoin::{absolute, Script, Sequence};
-
-use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
-
-/// Trait to check if a value is below the dust limit.
-/// We are performing dust value calculation for a given script public key using rust-bitcoin to
-/// keep it compatible with network dust rate
-// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
-// instead of a <= etc.
-pub trait IsDust {
- /// Check whether or not a value is below dust limit
- fn is_dust(&self, script: &Script) -> bool;
-}
-
-impl IsDust for u64 {
- fn is_dust(&self, script: &Script) -> bool {
- *self < script.dust_value().to_sat()
- }
-}
-
-pub struct After {
- pub current_height: Option<u32>,
- pub assume_height_reached: bool,
-}
-
-impl After {
- pub(crate) fn new(current_height: Option<u32>, assume_height_reached: bool) -> After {
- After {
- current_height,
- assume_height_reached,
- }
- }
-}
-
-pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
- // The RBF value must enable relative timelocks
- if !rbf.is_relative_lock_time() {
- return false;
- }
-
- // Both values should be represented in the same unit (either time-based or
- // block-height based)
- if rbf.is_time_locked() != csv.is_time_locked() {
- return false;
- }
-
- // The value should be at least `csv`
- if rbf < csv {
- return false;
- }
-
- true
-}
-
-impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
- fn check_after(&self, n: absolute::LockTime) -> bool {
- if let Some(current_height) = self.current_height {
- current_height >= n.to_consensus_u32()
- } else {
- self.assume_height_reached
- }
- }
-}
-
-pub struct Older {
- pub current_height: Option<u32>,
- pub create_height: Option<u32>,
- pub assume_height_reached: bool,
-}
-
-impl Older {
- pub(crate) fn new(
- current_height: Option<u32>,
- create_height: Option<u32>,
- assume_height_reached: bool,
- ) -> Older {
- Older {
- current_height,
- create_height,
- assume_height_reached,
- }
- }
-}
-
-impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
- fn check_older(&self, n: Sequence) -> bool {
- if let Some(current_height) = self.current_height {
- // TODO: test >= / >
- current_height
- >= self
- .create_height
- .unwrap_or(0)
- .checked_add(n.to_consensus_u32())
- .expect("Overflowing addition")
- } else {
- self.assume_height_reached
- }
- }
-}
-
-pub(crate) type SecpCtx = Secp256k1<All>;
-
-#[cfg(test)]
-mod test {
- // When nSequence is lower than this flag the timelock is interpreted as block-height-based,
- // otherwise it's time-based
- pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
-
- use super::{check_nsequence_rbf, IsDust};
- use crate::bitcoin::{Address, Network, Sequence};
- use core::str::FromStr;
-
- #[test]
- fn test_is_dust() {
- let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe")
- .unwrap()
- .require_network(Network::Bitcoin)
- .unwrap()
- .script_pubkey();
- assert!(script_p2pkh.is_p2pkh());
- assert!(545.is_dust(&script_p2pkh));
- assert!(!546.is_dust(&script_p2pkh));
-
- let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8")
- .unwrap()
- .require_network(Network::Bitcoin)
- .unwrap()
- .script_pubkey();
- assert!(script_p2wpkh.is_p2wpkh());
- assert!(293.is_dust(&script_p2wpkh));
- assert!(!294.is_dust(&script_p2wpkh));
- }
-
- #[test]
- fn test_check_nsequence_rbf_msb_set() {
- let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
- assert!(!result);
- }
-
- #[test]
- fn test_check_nsequence_rbf_lt_csv() {
- let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
- assert!(!result);
- }
-
- #[test]
- fn test_check_nsequence_rbf_different_unit() {
- let result =
- check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
- assert!(!result);
- }
-
- #[test]
- fn test_check_nsequence_rbf_mask() {
- let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
- assert!(result);
- }
-
- #[test]
- fn test_check_nsequence_rbf_same_unit_blocks() {
- let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
- assert!(result);
- }
-
- #[test]
- fn test_check_nsequence_rbf_same_unit_time() {
- let result = check_nsequence_rbf(
- Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
- Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
- );
- assert!(result);
- }
-}
+++ /dev/null
-#![allow(unused)]
-
-use bdk::{KeychainKind, LocalOutput, Wallet};
-use bdk_chain::indexed_tx_graph::Indexer;
-use bdk_chain::{BlockId, ConfirmationTime};
-use bitcoin::hashes::Hash;
-use bitcoin::{
- transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
- Txid,
-};
-use std::str::FromStr;
-
-// Return a fake wallet that appears to be funded for testing.
-//
-// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
-// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
-// sats are the transaction fee.
-pub fn get_funded_wallet_with_change(
- descriptor: &str,
- change: Option<&str>,
-) -> (Wallet, bitcoin::Txid) {
- let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
- let change_address = wallet.peek_address(KeychainKind::External, 0).address;
- let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
- .expect("address")
- .require_network(Network::Regtest)
- .unwrap();
-
- let tx0 = Transaction {
- version: transaction::Version::ONE,
- lock_time: bitcoin::absolute::LockTime::ZERO,
- input: vec![TxIn {
- previous_output: OutPoint {
- txid: Txid::all_zeros(),
- vout: 0,
- },
- script_sig: Default::default(),
- sequence: Default::default(),
- witness: Default::default(),
- }],
- output: vec![TxOut {
- value: Amount::from_sat(76_000),
- script_pubkey: change_address.script_pubkey(),
- }],
- };
-
- let tx1 = Transaction {
- version: transaction::Version::ONE,
- lock_time: bitcoin::absolute::LockTime::ZERO,
- input: vec![TxIn {
- previous_output: OutPoint {
- txid: tx0.txid(),
- vout: 0,
- },
- script_sig: Default::default(),
- sequence: Default::default(),
- witness: Default::default(),
- }],
- output: vec![
- TxOut {
- value: Amount::from_sat(50_000),
- script_pubkey: change_address.script_pubkey(),
- },
- TxOut {
- value: Amount::from_sat(25_000),
- script_pubkey: sendto_address.script_pubkey(),
- },
- ],
- };
-
- wallet
- .insert_checkpoint(BlockId {
- height: 1_000,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
- wallet
- .insert_checkpoint(BlockId {
- height: 2_000,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
- wallet
- .insert_tx(
- tx0,
- ConfirmationTime::Confirmed {
- height: 1_000,
- time: 100,
- },
- )
- .unwrap();
- wallet
- .insert_tx(
- tx1.clone(),
- ConfirmationTime::Confirmed {
- height: 2_000,
- time: 200,
- },
- )
- .unwrap();
-
- (wallet, tx1.txid())
-}
-
-// Return a fake wallet that appears to be funded for testing.
-//
-// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
-// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
-// sats are the transaction fee.
-pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
- get_funded_wallet_with_change(descriptor, None)
-}
-
-pub fn get_test_wpkh() -> &'static str {
- "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
-}
-
-pub fn get_test_single_sig_csv() -> &'static str {
- // and(pk(Alice),older(6))
- "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
-}
-
-pub fn get_test_a_or_b_plus_csv() -> &'static str {
- // or(pk(Alice),and(pk(Bob),older(144)))
- "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
-}
-
-pub fn get_test_single_sig_cltv() -> &'static str {
- // and(pk(Alice),after(100000))
- "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
-}
-
-pub fn get_test_tr_single_sig() -> &'static str {
- "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
-}
-
-pub fn get_test_tr_with_taptree() -> &'static str {
- "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
-}
-
-pub fn get_test_tr_with_taptree_both_priv() -> &'static str {
- "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
-}
-
-pub fn get_test_tr_repeated_key() -> &'static str {
- "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
-}
-
-pub fn get_test_tr_single_sig_xprv() -> &'static str {
- "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
-}
-
-pub fn get_test_tr_with_taptree_xprv() -> &'static str {
- "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
-}
-
-pub fn get_test_tr_dup_keys() -> &'static str {
- "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
-}
-
-/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
-/// useful in cases where we want to create a feerate from a `f64`, as the
-/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
-///
-/// **Note** this 'quick and dirty' conversion should only be used when the input
-/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
-/// or else the resulting value will be inaccurate.
-pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
- // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
- let sat_kwu = (sat_vb * 250.0).ceil() as u64;
- FeeRate::from_sat_per_kwu(sat_kwu)
-}
+++ /dev/null
-use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
-use bdk::{psbt, KeychainKind, SignOptions};
-use core::str::FromStr;
-mod common;
-use common::*;
-
-// from bip 174
-const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
-
-#[test]
-#[should_panic(expected = "InputIndexOutOfRange")]
-fn test_psbt_malformed_psbt_input_legacy() {
- let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let send_to = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
- let mut psbt = builder.finish().unwrap();
- psbt.inputs.push(psbt_bip.inputs[0].clone());
- let options = SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- };
- let _ = wallet.sign(&mut psbt, options).unwrap();
-}
-
-#[test]
-#[should_panic(expected = "InputIndexOutOfRange")]
-fn test_psbt_malformed_psbt_input_segwit() {
- let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let send_to = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
- let mut psbt = builder.finish().unwrap();
- psbt.inputs.push(psbt_bip.inputs[1].clone());
- let options = SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- };
- let _ = wallet.sign(&mut psbt, options).unwrap();
-}
-
-#[test]
-#[should_panic(expected = "InputIndexOutOfRange")]
-fn test_psbt_malformed_tx_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let send_to = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
- let mut psbt = builder.finish().unwrap();
- psbt.unsigned_tx.input.push(TxIn::default());
- let options = SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- };
- let _ = wallet.sign(&mut psbt, options).unwrap();
-}
-
-#[test]
-fn test_psbt_sign_with_finalized() {
- let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let send_to = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
- let mut psbt = builder.finish().unwrap();
-
- // add a finalized input
- psbt.inputs.push(psbt_bip.inputs[0].clone());
- psbt.unsigned_tx
- .input
- .push(psbt_bip.unsigned_tx.input[0].clone());
-
- let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
-}
-
-#[test]
-fn test_psbt_fee_rate_with_witness_utxo() {
- use psbt::PsbtUtils;
-
- let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
-
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- builder.fee_rate(expected_fee_rate);
- let mut psbt = builder.finish().unwrap();
- let fee_amount = psbt.fee_amount();
- assert!(fee_amount.is_some());
-
- let unfinalized_fee_rate = psbt.fee_rate().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let finalized_fee_rate = psbt.fee_rate().unwrap();
- assert!(finalized_fee_rate >= expected_fee_rate);
- assert!(finalized_fee_rate < unfinalized_fee_rate);
-}
-
-#[test]
-fn test_psbt_fee_rate_with_nonwitness_utxo() {
- use psbt::PsbtUtils;
-
- let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
-
- let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- builder.fee_rate(expected_fee_rate);
- let mut psbt = builder.finish().unwrap();
- let fee_amount = psbt.fee_amount();
- assert!(fee_amount.is_some());
- let unfinalized_fee_rate = psbt.fee_rate().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let finalized_fee_rate = psbt.fee_rate().unwrap();
- assert!(finalized_fee_rate >= expected_fee_rate);
- assert!(finalized_fee_rate < unfinalized_fee_rate);
-}
-
-#[test]
-fn test_psbt_fee_rate_with_missing_txout() {
- use psbt::PsbtUtils;
-
- let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
-
- let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wpkh_wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wpkh_wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- builder.fee_rate(expected_fee_rate);
- let mut wpkh_psbt = builder.finish().unwrap();
-
- wpkh_psbt.inputs[0].witness_utxo = None;
- wpkh_psbt.inputs[0].non_witness_utxo = None;
- assert!(wpkh_psbt.fee_amount().is_none());
- assert!(wpkh_psbt.fee_rate().is_none());
-
- let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = pkh_wallet.peek_address(KeychainKind::External, 0);
- let mut builder = pkh_wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- builder.fee_rate(expected_fee_rate);
- let mut pkh_psbt = builder.finish().unwrap();
-
- pkh_psbt.inputs[0].non_witness_utxo = None;
- assert!(pkh_psbt.fee_amount().is_none());
- assert!(pkh_psbt.fee_rate().is_none());
-}
-
-#[test]
-fn test_psbt_multiple_internalkey_signers() {
- use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
- use bdk::KeychainKind;
- use bitcoin::key::TapTweak;
- use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
- use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
- use bitcoin::{PrivateKey, TxOut};
- use std::sync::Arc;
-
- let secp = Secp256k1::new();
- let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
- let desc = format!("tr({})", wif);
- let prv = PrivateKey::from_wif(wif).unwrap();
- let keypair = Keypair::from_secret_key(&secp, &prv.inner);
-
- let (mut wallet, _) = get_funded_wallet(&desc);
- let to_spend = wallet.get_balance().total();
- let send_to = wallet.peek_address(KeychainKind::External, 0);
- let mut builder = wallet.build_tx();
- builder.drain_to(send_to.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
- let unsigned_tx = psbt.unsigned_tx.clone();
-
- // Adds a signer for the wrong internal key, bdk should not use this key to sign
- wallet.add_signer(
- KeychainKind::External,
- // A signerordering lower than 100, bdk will use this signer first
- SignerOrdering(0),
- Arc::new(SignerWrapper::new(
- PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(),
- SignerContext::Tap {
- is_internal_key: true,
- },
- )),
- );
- let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
- assert!(finalized);
-
- // To verify, we need the signature, message, and pubkey
- let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
- assert!(!witness.is_empty());
- let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
-
- // the prevout we're spending
- let prevouts = &[TxOut {
- script_pubkey: send_to.script_pubkey(),
- value: to_spend,
- }];
- let prevouts = Prevouts::All(prevouts);
- let input_index = 0;
- let mut sighash_cache = SighashCache::new(unsigned_tx);
- let sighash = sighash_cache
- .taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
- .unwrap();
- let message = Message::from(sighash);
-
- // add tweak. this was taken from `signer::sign_psbt_schnorr`
- let keypair = keypair.tap_tweak(&secp, None).to_inner();
- let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
-
- // Must verify if we used the correct key to sign
- let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
- assert!(verify_res.is_ok(), "The wrong internal key was used");
-}
+++ /dev/null
-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 bitcoin::hashes::Hash;
-use bitcoin::key::Secp256k1;
-use bitcoin::psbt;
-use bitcoin::script::PushBytesBuf;
-use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
-use bitcoin::taproot::TapNodeHash;
-use bitcoin::{
- absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf,
- Sequence, Transaction, TxIn, TxOut, Txid, Weight,
-};
-
-mod common;
-use common::*;
-
-fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint {
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- script_pubkey: addr.script_pubkey(),
- value: Amount::from_sat(value),
- }],
- };
-
- wallet.insert_tx(tx.clone(), height).unwrap();
-
- OutPoint {
- txid: tx.txid(),
- vout: 0,
- }
-}
-
-fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
- let latest_cp = wallet.latest_checkpoint();
- let height = latest_cp.height();
- let anchor = if height == 0 {
- ConfirmationTime::Unconfirmed { last_seen: 0 }
- } else {
- ConfirmationTime::Confirmed { height, time: 0 }
- };
- receive_output(wallet, value, anchor)
-}
-
-// The satisfaction size of a P2WPKH is 112 WU =
-// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
-// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
-// a total of 105 WU.
-// Here, we push just once for simplicity, so we have to add an extra byte for the missing
-// OP_PUSH.
-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
- {
- 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
- );
- }
-
- // `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)));
- }
-}
-
-#[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
- {
- 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!(
- 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();
-
- 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!(
- 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;
-
- 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!(
- 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;
-
- 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!(
- err,
- bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
- if got == &got_descriptor && keychain == KeychainKind::Internal
- ),
- "err: {}",
- err,
- );
- }
-
- // 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));
- }
-}
-
-#[test]
-fn test_descriptor_checksum() {
- let (wallet, _) = get_funded_wallet(get_test_wpkh());
- let checksum = wallet.descriptor_checksum(KeychainKind::External);
- assert_eq!(checksum.len(), 8);
-
- let raw_descriptor = wallet
- .keychains()
- .next()
- .unwrap()
- .1
- .to_string()
- .split_once('#')
- .unwrap()
- .0
- .to_string();
- assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum);
-}
-
-#[test]
-fn test_get_funded_wallet_balance() {
- let (wallet, _) = get_funded_wallet(get_test_wpkh());
-
- // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
- // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
- // sats are the transaction fee.
- assert_eq!(wallet.get_balance().confirmed, Amount::from_sat(50_000));
-}
-
-#[test]
-fn test_get_funded_wallet_sent_and_received() {
- let (wallet, txid) = get_funded_wallet(get_test_wpkh());
-
- let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet
- .transactions()
- .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node)))
- .collect();
- tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0));
-
- let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
- let (sent, received) = wallet.sent_and_received(&tx);
-
- // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
- // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
- // sats are the transaction fee.
- assert_eq!(sent.to_sat(), 76_000);
- assert_eq!(received.to_sat(), 50_000);
-}
-
-#[test]
-fn test_get_funded_wallet_tx_fees() {
- let (wallet, txid) = get_funded_wallet(get_test_wpkh());
-
- let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
- let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee");
-
- // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
- // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
- // sats are the transaction fee.
- assert_eq!(tx_fee, 1000)
-}
-
-#[test]
-fn test_get_funded_wallet_tx_fee_rate() {
- let (wallet, txid) = get_funded_wallet(get_test_wpkh());
-
- let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
- let tx_fee_rate = wallet
- .calculate_fee_rate(&tx)
- .expect("transaction fee rate");
-
- // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
- // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
- // sats are the transaction fee.
-
- // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113
- // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212
- // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9
- assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212);
- assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9);
-}
-
-#[test]
-fn test_list_output() {
- let (wallet, txid) = get_funded_wallet(get_test_wpkh());
- let txos = wallet
- .list_output()
- .map(|op| (op.outpoint, op))
- .collect::<std::collections::BTreeMap<_, _>>();
- assert_eq!(txos.len(), 2);
- for (op, txo) in txos {
- if op.txid == txid {
- assert_eq!(txo.txout.value.to_sat(), 50_000);
- assert!(!txo.is_spent);
- } else {
- assert_eq!(txo.txout.value.to_sat(), 76_000);
- assert!(txo.is_spent);
- }
- }
-}
-
-macro_rules! assert_fee_rate {
- ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
- let psbt = $psbt.clone();
- #[allow(unused_mut)]
- let mut tx = $psbt.clone().extract_tx().expect("failed to extract tx");
- $(
- $( $add_signature )*
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
- }
- )*
-
- #[allow(unused_mut)]
- #[allow(unused_assignments)]
- let mut dust_change = false;
- $(
- $( $dust_change )*
- dust_change = true;
- )*
-
- let fee_amount = psbt
- .inputs
- .iter()
- .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value.to_sat())
- - psbt
- .unsigned_tx
- .output
- .iter()
- .fold(0, |acc, o| acc + o.value.to_sat());
-
- assert_eq!(fee_amount, $fees);
-
- let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight())
- .to_sat_per_kwu();
- let fee_rate = $fee_rate.to_sat_per_kwu();
- let half_default = FeeRate::BROADCAST_MIN.checked_div(2)
- .unwrap()
- .to_sat_per_kwu();
-
- if !dust_change {
- assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
- } else {
- assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
- }
- });
-}
-
-macro_rules! from_str {
- ($e:expr, $t:ty) => {{
- use core::str::FromStr;
- <$t>::from_str($e).unwrap()
- }};
-
- ($e:expr) => {
- from_str!($e, _)
- };
-}
-
-#[test]
-#[should_panic(expected = "NoRecipients")]
-fn test_create_tx_empty_recipients() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- wallet.build_tx().finish().unwrap();
-}
-
-#[test]
-#[should_panic(expected = "NoUtxosSelected")]
-fn test_create_tx_manually_selected_empty_utxos() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .manually_selected_only();
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_version_0() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .version(0);
- assert!(matches!(builder.finish(), Err(CreateTxError::Version0)));
-}
-
-#[test]
-fn test_create_tx_version_1_csv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .version(1);
- assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv)));
-}
-
-#[test]
-fn test_create_tx_custom_version() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .version(42);
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.version.0, 42);
-}
-
-#[test]
-fn test_create_tx_default_locktime_is_last_sync_height() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
-
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- // Since we never synced the wallet we don't have a last_sync_height
- // we could use to try to prevent fee sniping. We default to 0.
- assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000);
-}
-
-#[test]
-fn test_create_tx_fee_sniping_locktime_last_sync() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
-
- let psbt = builder.finish().unwrap();
-
- // If there's no current_height we're left with using the last sync height
- assert_eq!(
- psbt.unsigned_tx.lock_time.to_consensus_u32(),
- wallet.latest_checkpoint().height()
- );
-}
-
-#[test]
-fn test_create_tx_default_locktime_cltv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000);
-}
-
-#[test]
-fn test_create_tx_custom_locktime() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .current_height(630_001)
- .nlocktime(absolute::LockTime::from_height(630_000).unwrap());
- let psbt = builder.finish().unwrap();
-
- // When we explicitly specify a nlocktime
- // we don't try any fee sniping prevention trick
- // (we ignore the current_height)
- assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000);
-}
-
-#[test]
-fn test_create_tx_custom_locktime_compatible_with_cltv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .nlocktime(absolute::LockTime::from_height(630_000).unwrap());
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000);
-}
-
-#[test]
-fn test_create_tx_custom_locktime_incompatible_with_cltv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .nlocktime(absolute::LockTime::from_height(50000).unwrap());
- assert!(matches!(builder.finish(),
- Err(CreateTxError::LockTime { requested, required })
- if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000));
-}
-
-#[test]
-fn test_create_tx_no_rbf_csv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
-}
-
-#[test]
-fn test_create_tx_with_default_rbf_csv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- // When CSV is enabled it takes precedence over the rbf value (unless forced by the user).
- // It will be set to the OP_CSV value, in this case 6
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
-}
-
-#[test]
-fn test_create_tx_with_custom_rbf_csv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf_with_sequence(Sequence(3));
- assert!(matches!(builder.finish(),
- Err(CreateTxError::RbfSequenceCsv { rbf, csv })
- if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6));
-}
-
-#[test]
-fn test_create_tx_no_rbf_cltv() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
-}
-
-#[test]
-fn test_create_tx_invalid_rbf_sequence() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
- assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence)));
-}
-
-#[test]
-fn test_create_tx_custom_rbf_sequence() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf_with_sequence(Sequence(0xDEADBEEF));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF));
-}
-
-#[test]
-fn test_create_tx_default_sequence() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
-}
-
-#[test]
-fn test_create_tx_change_policy_no_internal() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .do_not_spend_change();
- assert!(matches!(
- builder.finish(),
- Err(CreateTxError::ChangePolicyDescriptor)
- ));
-}
-
-macro_rules! check_fee {
- ($wallet:expr, $psbt: expr) => {{
- let tx = $psbt.clone().extract_tx().expect("failed to extract tx");
- let tx_fee = $wallet.calculate_fee(&tx).ok();
- assert_eq!(tx_fee, $psbt.fee_amount());
- tx_fee
- }};
-}
-
-#[test]
-fn test_create_tx_drain_wallet_and_drain_to() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.unsigned_tx.output.len(), 1);
- assert_eq!(
- psbt.unsigned_tx.output[0].value.to_sat(),
- 50_000 - fee.unwrap_or(0)
- );
-}
-
-#[test]
-fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
- .unwrap()
- .assume_checked();
- let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000))
- .drain_to(drain_addr.script_pubkey())
- .drain_wallet();
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
- let outputs = psbt.unsigned_tx.output;
-
- assert_eq!(outputs.len(), 2);
- let main_output = outputs
- .iter()
- .find(|x| x.script_pubkey == addr.script_pubkey())
- .unwrap();
- let drain_output = outputs
- .iter()
- .find(|x| x.script_pubkey == drain_addr.script_pubkey())
- .unwrap();
- assert_eq!(main_output.value.to_sat(), 20_000,);
- assert_eq!(drain_output.value.to_sat(), 30_000 - fee.unwrap_or(0));
-}
-
-#[test]
-fn test_create_tx_drain_to_and_utxos() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .add_utxos(&utxos)
- .unwrap();
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.unsigned_tx.output.len(), 1);
- assert_eq!(
- psbt.unsigned_tx.output[0].value.to_sat(),
- 50_000 - fee.unwrap_or(0)
- );
-}
-
-#[test]
-#[should_panic(expected = "NoRecipients")]
-fn test_create_tx_drain_to_no_drain_wallet_no_utxos() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(drain_addr.script_pubkey());
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_default_fee_rate() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature);
-}
-
-#[test]
-fn test_create_tx_custom_fee_rate() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
-}
-
-#[test]
-fn test_create_tx_absolute_fee() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_absolute(100);
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(fee.unwrap_or(0), 100);
- assert_eq!(psbt.unsigned_tx.output.len(), 1);
- assert_eq!(
- psbt.unsigned_tx.output[0].value.to_sat(),
- 50_000 - fee.unwrap_or(0)
- );
-}
-
-#[test]
-fn test_create_tx_absolute_zero_fee() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_absolute(0);
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(fee.unwrap_or(0), 0);
- assert_eq!(psbt.unsigned_tx.output.len(), 1);
- assert_eq!(
- psbt.unsigned_tx.output[0].value.to_sat(),
- 50_000 - fee.unwrap_or(0)
- );
-}
-
-#[test]
-#[should_panic(expected = "InsufficientFunds")]
-fn test_create_tx_absolute_high_fee() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_absolute(60_000);
- let _ = builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_add_change() {
- use bdk::wallet::tx_builder::TxOrdering;
-
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .ordering(TxOrdering::Untouched);
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.unsigned_tx.output.len(), 2);
- assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 25_000);
- assert_eq!(
- psbt.unsigned_tx.output[1].value.to_sat(),
- 25_000 - fee.unwrap_or(0)
- );
-}
-
-#[test]
-fn test_create_tx_skip_change_dust() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(49_800));
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.unsigned_tx.output.len(), 1);
- assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 49_800);
- assert_eq!(fee.unwrap_or(0), 200);
-}
-
-#[test]
-#[should_panic(expected = "InsufficientFunds")]
-fn test_create_tx_drain_to_dust_amount() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- // very high fee rate, so that the only output would be below dust
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_rate(FeeRate::from_sat_per_vb_unchecked(454));
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_ordering_respected() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- 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);
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.unsigned_tx.output.len(), 3);
- assert_eq!(
- psbt.unsigned_tx.output[0].value.to_sat(),
- 10_000 - fee.unwrap_or(0)
- );
- assert_eq!(psbt.unsigned_tx.output[1].value.to_sat(), 10_000);
- assert_eq!(psbt.unsigned_tx.output[2].value.to_sat(), 30_000);
-}
-
-#[test]
-fn test_create_tx_default_sighash() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.inputs[0].sighash_type, None);
-}
-
-#[test]
-fn test_create_tx_custom_sighash() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .sighash(EcdsaSighashType::Single.into());
- let psbt = builder.finish().unwrap();
-
- assert_eq!(
- psbt.inputs[0].sighash_type,
- Some(EcdsaSighashType::Single.into())
- );
-}
-
-#[test]
-fn test_create_tx_input_hd_keypaths() {
- use bitcoin::bip32::{DerivationPath, Fingerprint};
- use core::str::FromStr;
-
- let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
- assert_eq!(
- psbt.inputs[0].bip32_derivation.values().next().unwrap(),
- &(
- Fingerprint::from_str("d34db33f").unwrap(),
- DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap()
- )
- );
-}
-
-#[test]
-fn test_create_tx_output_hd_keypaths() {
- use bitcoin::bip32::{DerivationPath, Fingerprint};
- use core::str::FromStr;
-
- let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
-
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
- let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index);
- assert_eq!(
- psbt.outputs[0].bip32_derivation.values().next().unwrap(),
- &(
- Fingerprint::from_str("d34db33f").unwrap(),
- DerivationPath::from_str(&expected_derivation_path).unwrap()
- )
- );
-}
-
-#[test]
-fn test_create_tx_set_redeem_script_p2sh() {
- use bitcoin::hex::FromHex;
-
- let (mut wallet, _) =
- get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert_eq!(
- psbt.inputs[0].redeem_script,
- Some(ScriptBuf::from(
- Vec::<u8>::from_hex(
- "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
- )
- .unwrap()
- ))
- );
- assert_eq!(psbt.inputs[0].witness_script, None);
-}
-
-#[test]
-fn test_create_tx_set_witness_script_p2wsh() {
- use bitcoin::hex::FromHex;
-
- let (mut wallet, _) =
- get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.inputs[0].redeem_script, None);
- assert_eq!(
- psbt.inputs[0].witness_script,
- Some(ScriptBuf::from(
- Vec::<u8>::from_hex(
- "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
- )
- .unwrap()
- ))
- );
-}
-
-#[test]
-fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() {
- let (mut wallet, _) =
- get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- let script = ScriptBuf::from_hex(
- "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac",
- )
- .unwrap();
-
- assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_p2wsh()));
- assert_eq!(psbt.inputs[0].witness_script, Some(script));
-}
-
-#[test]
-fn test_create_tx_non_witness_utxo() {
- let (mut wallet, _) =
- get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert!(psbt.inputs[0].non_witness_utxo.is_some());
- assert!(psbt.inputs[0].witness_utxo.is_none());
-}
-
-#[test]
-fn test_create_tx_only_witness_utxo() {
- let (mut wallet, _) =
- get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .only_witness_utxo()
- .drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert!(psbt.inputs[0].non_witness_utxo.is_none());
- assert!(psbt.inputs[0].witness_utxo.is_some());
-}
-
-#[test]
-fn test_create_tx_shwpkh_has_witness_utxo() {
- let (mut wallet, _) =
- get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert!(psbt.inputs[0].witness_utxo.is_some());
-}
-
-#[test]
-fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() {
- let (mut wallet, _) =
- get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert!(psbt.inputs[0].non_witness_utxo.is_some());
- assert!(psbt.inputs[0].witness_utxo.is_some());
-}
-
-#[test]
-fn test_create_tx_add_utxo() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let small_output_tx = Transaction {
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- version: transaction::Version::non_standard(0),
- lock_time: absolute::LockTime::ZERO,
- };
- wallet
- .insert_tx(
- small_output_tx.clone(),
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- )
- .unwrap();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .add_utxo(OutPoint {
- txid: small_output_tx.txid(),
- vout: 0,
- })
- .unwrap();
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
-
- assert_eq!(
- psbt.unsigned_tx.input.len(),
- 2,
- "should add an additional input since 25_000 < 30_000"
- );
- assert_eq!(
- sent_received.0,
- Amount::from_sat(75_000),
- "total should be sum of both inputs"
- );
-}
-
-#[test]
-#[should_panic(expected = "InsufficientFunds")]
-fn test_create_tx_manually_selected_insufficient() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let small_output_tx = Transaction {
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- version: transaction::Version::non_standard(0),
- lock_time: absolute::LockTime::ZERO,
- };
-
- wallet
- .insert_tx(
- small_output_tx.clone(),
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- )
- .unwrap();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .add_utxo(OutPoint {
- txid: small_output_tx.txid(),
- vout: 0,
- })
- .unwrap()
- .manually_selected_only();
- builder.finish().unwrap();
-}
-
-#[test]
-#[should_panic(expected = "SpendingPolicyRequired(External)")]
-fn test_create_tx_policy_path_required() {
- let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000));
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_policy_path_no_csv() {
- let descriptors = get_test_wpkh();
- let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap();
-
- let tx = Transaction {
- version: transaction::Version::non_standard(0),
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(50_000),
- }],
- };
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
- let root_id = external_policy.id;
- // child #0 is just the key "A"
- let path = vec![(root_id, vec![0])].into_iter().collect();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .policy_path(path, KeychainKind::External);
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
-}
-
-#[test]
-fn test_create_tx_policy_path_use_csv() {
- let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
-
- let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
- let root_id = external_policy.id;
- // child #1 is or(pk(B),older(144))
- let path = vec![(root_id, vec![1])].into_iter().collect();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .policy_path(path, KeychainKind::External);
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144));
-}
-
-#[test]
-fn test_create_tx_policy_path_ignored_subtree_with_csv() {
- let (mut wallet, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))");
-
- let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
- let root_id = external_policy.id;
- // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)
- let path = vec![(root_id, vec![0])].into_iter().collect();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
- .policy_path(path, KeychainKind::External);
- let psbt = builder.finish().unwrap();
-
- assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
-}
-
-#[test]
-fn test_create_tx_global_xpubs_with_origin() {
- use bitcoin::bip32;
- use bitcoin::hex::FromHex;
-
- let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .add_global_xpubs();
- let psbt = builder.finish().unwrap();
-
- let key = bip32::Xpub::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap();
- let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap();
- let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
-
- assert_eq!(psbt.xpub.len(), 1);
- assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path)));
-}
-
-#[test]
-fn test_add_foreign_utxo() {
- let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
- let (wallet2, _) =
- get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let utxo = wallet2.list_unspent().next().expect("must take!");
- let foreign_utxo_satisfaction = wallet2
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- let psbt_input = psbt::Input {
- witness_utxo: Some(utxo.txout.clone()),
- ..Default::default()
- };
-
- let mut builder = wallet1.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
- .only_witness_utxo()
- .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
- .unwrap();
- let mut psbt = builder.finish().unwrap();
- wallet1.insert_txout(utxo.outpoint, utxo.txout);
- let fee = check_fee!(wallet1, psbt);
- let sent_received =
- wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
-
- assert_eq!(
- (sent_received.0 - sent_received.1).to_sat(),
- 10_000 + fee.unwrap_or(0),
- "we should have only net spent ~10_000"
- );
-
- assert!(
- psbt.unsigned_tx
- .input
- .iter()
- .any(|input| input.previous_output == utxo.outpoint),
- "foreign_utxo should be in there"
- );
-
- let finished = wallet1
- .sign(
- &mut psbt,
- SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- },
- )
- .unwrap();
-
- assert!(
- !finished,
- "only one of the inputs should have been signed so far"
- );
-
- let finished = wallet2
- .sign(
- &mut psbt,
- SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- },
- )
- .unwrap();
- assert!(finished, "all the inputs should have been signed now");
-}
-
-#[test]
-#[should_panic(
- expected = "MissingTxOut([OutPoint { txid: 0x21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])"
-)]
-fn test_calculate_fee_with_missing_foreign_utxo() {
- let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
- let (wallet2, _) =
- get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let utxo = wallet2.list_unspent().next().expect("must take!");
- let foreign_utxo_satisfaction = wallet2
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- let psbt_input = psbt::Input {
- witness_utxo: Some(utxo.txout.clone()),
- ..Default::default()
- };
-
- let mut builder = wallet1.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
- .only_witness_utxo()
- .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
- .unwrap();
- let psbt = builder.finish().unwrap();
- let tx = psbt.extract_tx().expect("failed to extract tx");
- wallet1.calculate_fee(&tx).unwrap();
-}
-
-#[test]
-fn test_add_foreign_utxo_invalid_psbt_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
- let foreign_utxo_satisfaction = wallet
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- let mut builder = wallet.build_tx();
- let result =
- builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
- assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
-}
-
-#[test]
-fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
- let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh());
- let (wallet2, txid2) =
- get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
-
- let utxo2 = wallet2.list_unspent().next().unwrap();
- let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone();
- let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone();
-
- let satisfaction_weight = wallet2
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- let mut builder = wallet1.build_tx();
- assert!(
- builder
- .add_foreign_utxo(
- utxo2.outpoint,
- psbt::Input {
- non_witness_utxo: Some(tx1.as_ref().clone()),
- ..Default::default()
- },
- satisfaction_weight
- )
- .is_err(),
- "should fail when outpoint doesn't match psbt_input"
- );
- assert!(
- builder
- .add_foreign_utxo(
- utxo2.outpoint,
- psbt::Input {
- non_witness_utxo: Some(tx2.as_ref().clone()),
- ..Default::default()
- },
- satisfaction_weight
- )
- .is_ok(),
- "should be ok when outpoint does match psbt_input"
- );
-}
-
-#[test]
-fn test_add_foreign_utxo_only_witness_utxo() {
- let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
- let (wallet2, txid2) =
- get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let utxo2 = wallet2.list_unspent().next().unwrap();
-
- let satisfaction_weight = wallet2
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- let mut builder = wallet1.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000));
-
- {
- let mut builder = builder.clone();
- let psbt_input = psbt::Input {
- witness_utxo: Some(utxo2.txout.clone()),
- ..Default::default()
- };
- builder
- .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
- .unwrap();
- assert!(
- builder.finish().is_err(),
- "psbt_input with witness_utxo should fail with only witness_utxo"
- );
- }
-
- {
- let mut builder = builder.clone();
- let psbt_input = psbt::Input {
- witness_utxo: Some(utxo2.txout.clone()),
- ..Default::default()
- };
- builder
- .only_witness_utxo()
- .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
- .unwrap();
- assert!(
- builder.finish().is_ok(),
- "psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled"
- );
- }
-
- {
- let mut builder = builder.clone();
- let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx;
- let psbt_input = psbt::Input {
- non_witness_utxo: Some(tx2.as_ref().clone()),
- ..Default::default()
- };
- builder
- .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
- .unwrap();
- assert!(
- builder.finish().is_ok(),
- "psbt_input with non_witness_utxo should succeed by default"
- );
- }
-}
-
-#[test]
-fn test_get_psbt_input() {
- // this should grab a known good utxo and set the input
- let (wallet, _) = get_funded_wallet(get_test_wpkh());
- for utxo in wallet.list_unspent() {
- let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap();
- assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some());
- }
-}
-
-#[test]
-#[should_panic(
- expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
-)]
-fn test_create_tx_global_xpubs_origin_missing() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .add_global_xpubs();
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_create_tx_global_xpubs_master_without_origin() {
- use bitcoin::bip32;
- use bitcoin::hex::FromHex;
-
- let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .add_global_xpubs();
- let psbt = builder.finish().unwrap();
-
- let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap();
- let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap();
-
- assert_eq!(psbt.xpub.len(), 1);
- assert_eq!(
- psbt.xpub.get(&key),
- Some(&(fingerprint, bip32::DerivationPath::default()))
- );
-}
-
-#[test]
-#[should_panic(expected = "IrreplaceableTransaction")]
-fn test_bump_fee_irreplaceable_tx() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- wallet.build_fee_bump(txid).unwrap().finish().unwrap();
-}
-
-#[test]
-#[should_panic(expected = "TransactionConfirmed")]
-fn test_bump_fee_confirmed_tx() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
-
- wallet
- .insert_tx(
- tx,
- ConfirmationTime::Confirmed {
- height: 42,
- time: 42_000,
- },
- )
- .unwrap();
-
- wallet.build_fee_bump(txid).unwrap().finish().unwrap();
-}
-
-#[test]
-fn test_bump_fee_low_fee_rate() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let feerate = psbt.fee_rate().unwrap();
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
-
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_rate(FeeRate::BROADCAST_MIN);
- let res = builder.finish();
- assert_matches!(
- res,
- Err(CreateTxError::FeeRateTooLow { .. }),
- "expected FeeRateTooLow error"
- );
-
- let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb
- let sat_vb = required as f64 / 250.0;
- let expect = format!("Fee rate too low: required {} sat/vb", sat_vb);
- assert_eq!(res.unwrap_err().to_string(), expect);
-}
-
-#[test]
-#[should_panic(expected = "FeeTooLow")]
-fn test_bump_fee_low_abs() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
-
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_absolute(10);
- builder.finish().unwrap();
-}
-
-#[test]
-#[should_panic(expected = "FeeTooLow")]
-fn test_bump_fee_zero_abs() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_absolute(0);
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_bump_fee_reduce_change() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let original_sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let original_fee = check_fee!(wallet, psbt);
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_rate(feerate).enable_rbf();
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(sent_received.0, original_sent_received.0);
- assert_eq!(
- sent_received.1 + Amount::from_sat(fee.unwrap_or(0)),
- original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0))
- );
- assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(25_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- 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.enable_rbf();
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(sent_received.0, original_sent_received.0);
- assert_eq!(
- sent_received.1 + Amount::from_sat(fee.unwrap_or(0)),
- original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0))
- );
- assert!(
- fee.unwrap_or(0) > original_fee.unwrap_or(0),
- "{} > {}",
- fee.unwrap_or(0),
- original_fee.unwrap_or(0)
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(25_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- assert_eq!(fee.unwrap_or(0), 200);
-}
-
-#[test]
-fn test_bump_fee_reduce_single_recipient() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let tx = psbt.clone().extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let original_fee = check_fee!(wallet, psbt);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder
- .fee_rate(feerate)
- .allow_shrinking(addr.script_pubkey())
- .unwrap();
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(sent_received.0, original_sent_received.0);
- assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.output.len(), 1);
- assert_eq!(
- tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)),
- sent_received.0
- );
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
-}
-
-#[test]
-fn test_bump_fee_absolute_reduce_single_recipient() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let original_fee = check_fee!(wallet, psbt);
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder
- .allow_shrinking(addr.script_pubkey())
- .unwrap()
- .fee_absolute(300);
- let psbt = builder.finish().unwrap();
- let tx = &psbt.unsigned_tx;
- let sent_received = wallet.sent_and_received(tx);
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(sent_received.0, original_sent_received.0);
- assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
-
- assert_eq!(tx.output.len(), 1);
- assert_eq!(
- tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)),
- sent_received.0
- );
-
- assert_eq!(fee.unwrap_or(0), 300);
-}
-
-#[test]
-fn test_bump_fee_drain_wallet() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- // receive an extra tx so that our wallet has two utxos.
- let tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- };
- wallet
- .insert_tx(
- tx.clone(),
- ConfirmationTime::Confirmed {
- height: wallet.latest_checkpoint().height(),
- time: 42_000,
- },
- )
- .unwrap();
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
-
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .add_utxo(OutPoint {
- txid: tx.txid(),
- vout: 0,
- })
- .unwrap()
- .manually_selected_only()
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
-
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
-
- // for the new feerate, it should be enough to reduce the output, but since we specify
- // `drain_wallet` we expect to spend everything
- 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"));
-
- assert_eq!(sent_received.0, Amount::from_sat(75_000));
-}
-
-#[test]
-#[should_panic(expected = "InsufficientFunds")]
-fn test_bump_fee_remove_output_manually_selected_only() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- // receive an extra tx so that our wallet has two utxos. then we manually pick only one of
- // 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.
- let init_tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- };
- wallet
- .insert_tx(
- init_tx.clone(),
- wallet
- .transactions()
- .last()
- .unwrap()
- .chain_position
- .cloned()
- .into(),
- )
- .unwrap();
- let outpoint = OutPoint {
- txid: init_tx.txid(),
- vout: 0,
- };
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .add_utxo(outpoint)
- .unwrap()
- .manually_selected_only()
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder
- .manually_selected_only()
- .fee_rate(FeeRate::from_sat_per_vb_unchecked(255));
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_bump_fee_add_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let init_tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- };
- let pos = wallet
- .transactions()
- .last()
- .unwrap()
- .chain_position
- .cloned()
- .into();
- wallet.insert_tx(init_tx, pos).unwrap();
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let original_details = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
- assert_eq!(
- sent_received.0,
- original_details.0 + Amount::from_sat(25_000)
- );
- assert_eq!(
- Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
- Amount::from_sat(30_000)
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(45_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
-}
-
-#[test]
-fn test_bump_fee_absolute_add_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- receive_output_in_latest_block(&mut wallet, 25_000);
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_absolute(6_000);
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(
- sent_received.0,
- original_sent_received.0 + Amount::from_sat(25_000)
- );
- assert_eq!(
- Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
- Amount::from_sat(30_000)
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(45_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- assert_eq!(fee.unwrap_or(0), 6_000);
-}
-
-#[test]
-fn test_bump_fee_no_change_add_input_and_change() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let op = receive_output_in_latest_block(&mut wallet, 25_000);
-
- // initially make a tx without change by using `drain_to`
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .add_utxo(op)
- .unwrap()
- .manually_selected_only()
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let original_sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let original_fee = check_fee!(wallet, psbt);
-
- let tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- wallet
- .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
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- let original_send_all_amount =
- original_sent_received.0 - Amount::from_sat(original_fee.unwrap_or(0));
- assert_eq!(
- sent_received.0,
- original_sent_received.0 + Amount::from_sat(50_000)
- );
- assert_eq!(
- sent_received.1,
- Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0))
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- original_send_all_amount
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0))
- );
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
-}
-
-#[test]
-fn test_bump_fee_add_input_change_dust() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- receive_output_in_latest_block(&mut wallet, 25_000);
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let original_sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let original_fee = check_fee!(wallet, psbt);
-
- let mut tx = psbt.extract_tx().expect("failed to extract tx");
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight
- }
- let original_tx_weight = tx.weight();
- assert_eq!(tx.input.len(), 1);
- assert_eq!(tx.output.len(), 2);
- let txid = tx.txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- // We set a fee high enough that during rbf we are forced to add
- // a new input and also that we have to remove the change
- // that we had previously
-
- // We calculate the new weight as:
- // original weight
- // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4
- // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4
- // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4
- let new_tx_weight =
- original_tx_weight + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(124);
- // two inputs (50k, 25k) and one output (45k) - epsilon
- // We use epsilon here to avoid asking for a slightly too high feerate
- let fee_abs = 50_000 + 25_000 - 45_000 - 10;
- builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight);
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(
- original_sent_received.1,
- Amount::from_sat(5_000 - original_fee.unwrap_or(0))
- );
-
- assert_eq!(
- sent_received.0,
- original_sent_received.0 + Amount::from_sat(25_000)
- );
- assert_eq!(fee.unwrap_or(0), 30_000);
- assert_eq!(sent_received.1, Amount::ZERO);
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 1);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(45_000)
- );
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature);
-}
-
-#[test]
-fn test_bump_fee_force_add_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000);
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let mut tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
- }
- wallet
- .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- // 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_rate(FeeRate::from_sat_per_vb_unchecked(5));
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(
- sent_received.0,
- original_sent_received.0 + Amount::from_sat(25_000)
- );
- assert_eq!(
- Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
- Amount::from_sat(30_000)
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(45_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
-}
-
-#[test]
-fn test_bump_fee_absolute_force_add_input() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000);
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let mut tx = psbt.extract_tx().expect("failed to extract tx");
- let original_sent_received = wallet.sent_and_received(&tx);
- let txid = tx.txid();
- // skip saving the new utxos, we know they can't be used anyways
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
- }
- wallet
- .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- // 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);
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(
- sent_received.0,
- original_sent_received.0 + Amount::from_sat(25_000)
- );
- assert_eq!(
- Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
- Amount::from_sat(30_000)
- );
-
- let tx = &psbt.unsigned_tx;
- assert_eq!(tx.input.len(), 2);
- assert_eq!(tx.output.len(), 2);
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey == addr.script_pubkey())
- .unwrap()
- .value,
- Amount::from_sat(45_000)
- );
- assert_eq!(
- tx.output
- .iter()
- .find(|txout| txout.script_pubkey != addr.script_pubkey())
- .unwrap()
- .value,
- sent_received.1
- );
-
- assert_eq!(fee.unwrap_or(0), 250);
-}
-
-#[test]
-#[should_panic(expected = "InsufficientFunds")]
-fn test_bump_fee_unconfirmed_inputs_only() {
- // We try to bump the fee, but:
- // - We can't reduce the change, as we have no change
- // - All our UTXOs are unconfirmed
- // So, we fail with "InsufficientFunds", as per RBF rule 2:
- // The replacement transaction may only include an unconfirmed input
- // if that input was included in one of the original transactions.
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .drain_wallet()
- .drain_to(addr.script_pubkey())
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- // Now we receive one transaction with 0 confirmations. We won't be able to use that for
- // fee bumping, as it's still unconfirmed!
- receive_output(
- &mut wallet,
- 25_000,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- );
- let mut tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
- }
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_bump_fee_unconfirmed_input() {
- // We create a tx draining the wallet and spending one confirmed
- // and one unconfirmed UTXO. We check that we can fee bump normally
- // (BIP125 rule 2 only apply to newly added unconfirmed input, you can
- // always fee bump with an unconfirmed input if it was included in the
- // original transaction)
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- // We receive a tx with 0 confirmations, which will be used as an input
- // in the drain tx.
- receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0));
- let mut builder = wallet.build_tx();
- builder
- .drain_wallet()
- .drain_to(addr.script_pubkey())
- .enable_rbf();
- let psbt = builder.finish().unwrap();
- let mut tx = psbt.extract_tx().expect("failed to extract tx");
- let txid = tx.txid();
- for txin in &mut tx.input {
- txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
- }
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
-
- let mut builder = wallet.build_fee_bump(txid).unwrap();
- builder
- .fee_rate(FeeRate::from_sat_per_vb_unchecked(15))
- .allow_shrinking(addr.script_pubkey())
- .unwrap();
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_fee_amount_negative_drain_val() {
- // While building the transaction, bdk would calculate the drain_value
- // as
- // current_delta - fee_amount - drain_fee
- // using saturating_sub, meaning that if the result would end up negative,
- // it'll remain to zero instead.
- // This caused a bug in master where we would calculate the wrong fee
- // for a transaction.
- // See https://github.com/bitcoindevkit/bdk/issues/660
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
- .unwrap()
- .assume_checked();
- let fee_rate = FeeRate::from_sat_per_kwu(500);
- let incoming_op = receive_output_in_latest_block(&mut wallet, 8859);
-
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630))
- .add_utxo(incoming_op)
- .unwrap()
- .enable_rbf()
- .fee_rate(fee_rate);
- let psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- assert_eq!(psbt.inputs.len(), 1);
- assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature);
-}
-
-#[test]
-fn test_sign_single_xprv() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_sign_single_xprv_with_master_fingerprint_and_path() {
- let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_sign_single_xprv_bip44_path() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_sign_single_xprv_sh_wpkh() {
- let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_sign_single_wif() {
- let (mut wallet, _) =
- get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_sign_single_xprv_no_hd_keypaths() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- psbt.inputs[0].bip32_derivation.clear();
- assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0);
-
- let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
- assert!(finalized);
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(extracted.input[0].witness.len(), 2);
-}
-
-#[test]
-fn test_include_output_redeem_witness_script() {
- let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))");
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .include_output_redeem_witness_script();
- let psbt = builder.finish().unwrap();
-
- // p2sh-p2wsh transaction should contain both witness and redeem scripts
- assert!(psbt
- .outputs
- .iter()
- .any(|output| output.redeem_script.is_some() && output.witness_script.is_some()));
-}
-
-#[test]
-fn test_signing_only_one_of_multiple_inputs() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
- .include_output_redeem_witness_script();
- let mut psbt = builder.finish().unwrap();
-
- // add another input to the psbt that is at least passable.
- let dud_input = bitcoin::psbt::Input {
- witness_utxo: Some(TxOut {
- value: Amount::from_sat(100_000),
- script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str(
- "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)",
- )
- .unwrap()
- .script_pubkey(),
- }),
- ..Default::default()
- };
-
- psbt.inputs.push(dud_input);
- psbt.unsigned_tx.input.push(bitcoin::TxIn::default());
- let is_final = wallet
- .sign(
- &mut psbt,
- SignOptions {
- trust_witness_utxo: true,
- ..Default::default()
- },
- )
- .unwrap();
- assert!(
- !is_final,
- "shouldn't be final since we can't sign one of the inputs"
- );
- assert!(
- psbt.inputs[0].final_script_witness.is_some(),
- "should finalized input it signed"
- )
-}
-
-#[test]
-fn test_remove_partial_sigs_after_finalize_sign_option() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
-
- for remove_partial_sigs in &[true, false] {
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- assert!(wallet
- .sign(
- &mut psbt,
- SignOptions {
- remove_partial_sigs: *remove_partial_sigs,
- ..Default::default()
- },
- )
- .unwrap());
-
- psbt.inputs.iter().for_each(|input| {
- if *remove_partial_sigs {
- assert!(input.partial_sigs.is_empty())
- } else {
- assert!(!input.partial_sigs.is_empty())
- }
- });
- }
-}
-
-#[test]
-fn test_try_finalize_sign_option() {
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
-
- for try_finalize in &[true, false] {
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let finalized = wallet
- .sign(
- &mut psbt,
- SignOptions {
- try_finalize: *try_finalize,
- ..Default::default()
- },
- )
- .unwrap();
-
- psbt.inputs.iter().for_each(|input| {
- if *try_finalize {
- assert!(finalized);
- assert!(input.final_script_sig.is_some());
- assert!(input.final_script_witness.is_some());
- } else {
- assert!(!finalized);
- assert!(input.final_script_sig.is_none());
- assert!(input.final_script_witness.is_none());
- }
- });
- }
-}
-
-#[test]
-fn test_sign_nonstandard_sighash() {
- let sighash = EcdsaSighashType::NonePlusAnyoneCanPay;
-
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .sighash(sighash.into())
- .drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let result = wallet.sign(&mut psbt, Default::default());
- assert!(
- result.is_err(),
- "Signing should have failed because the TX uses non-standard sighashes"
- );
- assert_matches!(
- result,
- Err(SignerError::NonStandardSighash),
- "Signing failed with the wrong error type"
- );
-
- // try again after opting-in
- let result = wallet.sign(
- &mut psbt,
- SignOptions {
- allow_all_sighashes: true,
- ..Default::default()
- },
- );
- assert!(result.is_ok(), "Signing should have worked");
- assert!(
- result.unwrap(),
- "Should finalize the input since we can produce signatures"
- );
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(
- *extracted.input[0].witness.to_vec()[0].last().unwrap(),
- sighash.to_u32() as u8,
- "The signature should have been made with the right sighash"
- );
-}
-
-#[test]
-fn test_unused_address() {
- let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
- None, Network::Testnet).unwrap();
-
- // `list_unused_addresses` should be empty if we haven't revealed any
- assert!(wallet
- .list_unused_addresses(KeychainKind::External)
- .next()
- .is_none());
-
- assert_eq!(
- wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
- assert_eq!(
- wallet
- .list_unused_addresses(KeychainKind::External)
- .next()
- .unwrap()
- .to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
-}
-
-#[test]
-fn test_next_unused_address() {
- let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
- let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap();
- assert_eq!(wallet.derivation_index(KeychainKind::External), None);
-
- assert_eq!(
- wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
- assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0));
- // calling next_unused again gives same address
- assert_eq!(
- wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
- assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0));
-
- // test mark used / unused
- assert!(wallet.mark_used(KeychainKind::External, 0));
- let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- assert_eq!(next_unused_addr.index, 1);
-
- assert!(wallet.unmark_used(KeychainKind::External, 0));
- let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- assert_eq!(next_unused_addr.index, 0);
-
- // use the above address
- receive_output_in_latest_block(&mut wallet, 25_000);
-
- assert_eq!(
- wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
- assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1));
-
- // trying to mark index 0 unused should return false
- assert!(!wallet.unmark_used(KeychainKind::External, 0));
-}
-
-#[test]
-fn test_peek_address_at_index() {
- let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
- None, Network::Testnet).unwrap();
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 1).to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 0).to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 2).to_string(),
- "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
- );
-
- // current new address is not affected
- assert_eq!(
- wallet
- .reveal_next_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
- );
-
- assert_eq!(
- wallet
- .reveal_next_address(KeychainKind::External)
- .unwrap()
- .to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
-}
-
-#[test]
-fn test_peek_address_at_index_not_derivable() {
- let wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
- None, Network::Testnet).unwrap();
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 1).to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 0).to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 2).to_string(),
- "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
- );
-}
-
-#[test]
-fn test_returns_index_and_address() {
- let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
- None, Network::Testnet).unwrap();
-
- // new index 0
- assert_eq!(
- wallet.reveal_next_address(KeychainKind::External).unwrap(),
- AddressInfo {
- index: 0,
- address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- }
- );
-
- // new index 1
- assert_eq!(
- wallet.reveal_next_address(KeychainKind::External).unwrap(),
- AddressInfo {
- index: 1,
- address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- }
- );
-
- // peek index 25
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 25),
- AddressInfo {
- index: 25,
- address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- }
- );
-
- // new index 2
- assert_eq!(
- wallet.reveal_next_address(KeychainKind::External).unwrap(),
- AddressInfo {
- index: 2,
- address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- }
- );
-}
-
-#[test]
-fn test_sending_to_bip350_bech32m_address() {
- let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
- let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000));
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_get_address() {
- use bdk::descriptor::template::Bip84;
- let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let wallet = Wallet::new_no_persist(
- Bip84(key, KeychainKind::External),
- Some(Bip84(key, KeychainKind::Internal)),
- Network::Regtest,
- )
- .unwrap();
-
- assert_eq!(
- wallet.peek_address(KeychainKind::External, 0),
- AddressInfo {
- index: 0,
- address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- }
- );
-
- assert_eq!(
- wallet.peek_address(KeychainKind::Internal, 0),
- AddressInfo {
- index: 0,
- address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::Internal,
- }
- );
-
- let wallet =
- Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap();
-
- assert_eq!(
- wallet.peek_address(KeychainKind::Internal, 0),
- AddressInfo {
- index: 0,
- address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
- .unwrap()
- .assume_checked(),
- keychain: KeychainKind::External,
- },
- "when there's no internal descriptor it should just use external"
- );
-}
-
-#[test]
-fn test_reveal_addresses() {
- let desc = get_test_tr_single_sig_xprv();
- let mut wallet = Wallet::new_no_persist(desc, None, Network::Signet).unwrap();
- let keychain = KeychainKind::External;
-
- let last_revealed_addr = wallet
- .reveal_addresses_to(keychain, 9)
- .unwrap()
- .last()
- .unwrap();
- assert_eq!(wallet.derivation_index(keychain), Some(9));
-
- let unused_addrs = wallet.list_unused_addresses(keychain).collect::<Vec<_>>();
- assert_eq!(unused_addrs.len(), 10);
- assert_eq!(unused_addrs.last().unwrap(), &last_revealed_addr);
-
- // revealing to an already revealed index returns nothing
- let mut already_revealed = wallet.reveal_addresses_to(keychain, 9).unwrap();
- assert!(already_revealed.next().is_none());
-}
-
-#[test]
-fn test_get_address_no_reuse_single_descriptor() {
- use bdk::descriptor::template::Bip84;
- use std::collections::HashSet;
-
- let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
- let mut wallet =
- Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap();
-
- let mut used_set = HashSet::new();
-
- (0..3).for_each(|_| {
- let external_addr = wallet
- .reveal_next_address(KeychainKind::External)
- .unwrap()
- .address;
- assert!(used_set.insert(external_addr));
-
- let internal_addr = wallet
- .reveal_next_address(KeychainKind::Internal)
- .unwrap()
- .address;
- assert!(used_set.insert(internal_addr));
- });
-}
-
-#[test]
-fn test_taproot_remove_tapfields_after_finalize_sign_option() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
-
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
- let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
- assert!(finalized);
-
- // removes tap_* from inputs
- for input in &psbt.inputs {
- assert!(input.tap_key_sig.is_none());
- assert!(input.tap_script_sigs.is_empty());
- assert!(input.tap_scripts.is_empty());
- assert!(input.tap_key_origins.is_empty());
- assert!(input.tap_internal_key.is_none());
- assert!(input.tap_merkle_root.is_none());
- }
- // removes key origins from outputs
- for output in &psbt.outputs {
- assert!(output.tap_key_origins.is_empty());
- }
-}
-
-#[test]
-fn test_taproot_psbt_populate_tap_key_origins() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
- let addr = wallet.reveal_next_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let psbt = builder.finish().unwrap();
-
- assert_eq!(
- psbt.inputs[0]
- .tap_key_origins
- .clone()
- .into_iter()
- .collect::<Vec<_>>(),
- vec![(
- from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"),
- (vec![], (from_str!("f6a5cb8b"), from_str!("m/0")))
- )],
- "Wrong input tap_key_origins"
- );
- assert_eq!(
- psbt.outputs[0]
- .tap_key_origins
- .clone()
- .into_iter()
- .collect::<Vec<_>>(),
- vec![(
- from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"),
- (vec![], (from_str!("f6a5cb8b"), from_str!("m/1")))
- )],
- "Wrong output tap_key_origins"
- );
-}
-
-#[test]
-fn test_taproot_psbt_populate_tap_key_origins_repeated_key() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key());
- let addr = wallet.reveal_next_address(KeychainKind::External).unwrap();
-
- let path = vec![("rn4nre9c".to_string(), vec![0])]
- .into_iter()
- .collect();
-
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
- .policy_path(path, KeychainKind::External);
- let psbt = builder.finish().unwrap();
-
- let mut input_key_origins = psbt.inputs[0]
- .tap_key_origins
- .clone()
- .into_iter()
- .collect::<Vec<_>>();
- input_key_origins.sort();
-
- assert_eq!(
- input_key_origins,
- vec![
- (
- from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
- (
- vec![
- from_str!(
- "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
- ),
- from_str!(
- "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
- ),
- ],
- (FromStr::from_str("ece52657").unwrap(), vec![].into())
- )
- ),
- (
- from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
- (
- vec![],
- (FromStr::from_str("871fd295").unwrap(), vec![].into())
- )
- )
- ],
- "Wrong input tap_key_origins"
- );
-
- let mut output_key_origins = psbt.outputs[0]
- .tap_key_origins
- .clone()
- .into_iter()
- .collect::<Vec<_>>();
- output_key_origins.sort();
-
- assert_eq!(
- input_key_origins, output_key_origins,
- "Wrong output tap_key_origins"
- );
-}
-
-#[test]
-fn test_taproot_psbt_input_tap_tree() {
- use bitcoin::hex::FromHex;
- use bitcoin::taproot;
-
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let psbt = builder.finish().unwrap();
-
- assert_eq!(
- psbt.inputs[0].tap_merkle_root,
- Some(
- TapNodeHash::from_str(
- "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986"
- )
- .unwrap()
- ),
- );
- assert_eq!(
- psbt.inputs[0].tap_scripts.clone().into_iter().collect::<Vec<_>>(),
- vec![
- (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)),
- (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)),
- ],
- );
- assert_eq!(
- psbt.inputs[0].tap_internal_key,
- Some(from_str!(
- "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"
- ))
- );
-
- // Since we are creating an output to the same address as the input, assert that the
- // internal_key is the same
- assert_eq!(
- psbt.inputs[0].tap_internal_key,
- psbt.outputs[0].tap_internal_key
- );
-
- let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap();
- assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree));
-}
-
-#[test]
-fn test_taproot_sign_missing_witness_utxo() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
- let witness_utxo = psbt.inputs[0].witness_utxo.take();
-
- let result = wallet.sign(
- &mut psbt,
- SignOptions {
- allow_all_sighashes: true,
- ..Default::default()
- },
- );
- assert_matches!(
- result,
- Err(SignerError::MissingWitnessUtxo),
- "Signing should have failed with the correct error because the witness_utxo is missing"
- );
-
- // restore the witness_utxo
- psbt.inputs[0].witness_utxo = witness_utxo;
-
- let result = wallet.sign(
- &mut psbt,
- SignOptions {
- allow_all_sighashes: true,
- ..Default::default()
- },
- );
-
- assert_matches!(
- result,
- Ok(true),
- "Should finalize the input since we can produce signatures"
- );
-}
-
-#[test]
-fn test_taproot_sign_using_non_witness_utxo() {
- let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder.drain_to(addr.script_pubkey()).drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- psbt.inputs[0].witness_utxo = None;
- psbt.inputs[0].non_witness_utxo =
- Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone());
- assert!(
- psbt.inputs[0].non_witness_utxo.is_some(),
- "Previous tx should be present in the database"
- );
-
- let result = wallet.sign(&mut psbt, Default::default());
- assert!(result.is_ok(), "Signing should have worked");
- assert!(
- result.unwrap(),
- "Should finalize the input since we can produce signatures"
- );
-}
-
-#[test]
-fn test_taproot_foreign_utxo() {
- let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
- let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig());
-
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let utxo = wallet2.list_unspent().next().unwrap();
- let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap();
- let foreign_utxo_satisfaction = wallet2
- .get_descriptor_for_keychain(KeychainKind::External)
- .max_weight_to_satisfy()
- .unwrap();
-
- assert!(
- psbt_input.non_witness_utxo.is_none(),
- "`non_witness_utxo` should never be populated for taproot"
- );
-
- let mut builder = wallet1.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
- .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
- .unwrap();
- let psbt = builder.finish().unwrap();
- let sent_received =
- wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
- wallet1.insert_txout(utxo.outpoint, utxo.txout);
- let fee = check_fee!(wallet1, psbt);
-
- assert_eq!(
- sent_received.0 - sent_received.1,
- Amount::from_sat(10_000 + fee.unwrap_or(0)),
- "we should have only net spent ~10_000"
- );
-
- assert!(
- psbt.unsigned_tx
- .input
- .iter()
- .any(|input| input.previous_output == utxo.outpoint),
- "foreign_utxo should be in there"
- );
-}
-
-fn test_spend_from_wallet(mut wallet: Wallet) {
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
-
- assert!(
- wallet.sign(&mut psbt, Default::default()).unwrap(),
- "Unable to finalize tx"
- );
-}
-
-// #[test]
-// fn test_taproot_key_spend() {
-// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
-// test_spend_from_wallet(wallet);
-
-// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
-// test_spend_from_wallet(wallet);
-// }
-
-#[test]
-fn test_taproot_no_key_spend() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
-
- assert!(
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- sign_with_tap_internal_key: false,
- ..Default::default()
- },
- )
- .unwrap(),
- "Unable to finalize tx"
- );
-
- assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none()));
-}
-
-#[test]
-fn test_taproot_script_spend() {
- let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
- test_spend_from_wallet(wallet);
-
- let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv());
- test_spend_from_wallet(wallet);
-}
-
-#[test]
-fn test_taproot_script_spend_sign_all_leaves() {
- use bdk::signer::TapLeavesOptions;
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
-
- assert!(
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- tap_leaves_options: TapLeavesOptions::All,
- ..Default::default()
- },
- )
- .unwrap(),
- "Unable to finalize tx"
- );
-
- assert!(psbt
- .inputs
- .iter()
- .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len()));
-}
-
-#[test]
-fn test_taproot_script_spend_sign_include_some_leaves() {
- use bdk::signer::TapLeavesOptions;
- use bitcoin::taproot::TapLeafHash;
-
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
- let mut script_leaves: Vec<_> = psbt.inputs[0]
- .tap_scripts
- .clone()
- .values()
- .map(|(script, version)| TapLeafHash::from_script(script, *version))
- .collect();
- let included_script_leaves = vec![script_leaves.pop().unwrap()];
- let excluded_script_leaves = script_leaves;
-
- assert!(
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()),
- ..Default::default()
- },
- )
- .unwrap(),
- "Unable to finalize tx"
- );
-
- assert!(psbt.inputs[0]
- .tap_script_sigs
- .iter()
- .all(|s| included_script_leaves.contains(&s.0 .1)
- && !excluded_script_leaves.contains(&s.0 .1)));
-}
-
-#[test]
-fn test_taproot_script_spend_sign_exclude_some_leaves() {
- use bdk::signer::TapLeavesOptions;
- use bitcoin::taproot::TapLeafHash;
-
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
- let mut script_leaves: Vec<_> = psbt.inputs[0]
- .tap_scripts
- .clone()
- .values()
- .map(|(script, version)| TapLeafHash::from_script(script, *version))
- .collect();
- let included_script_leaves = [script_leaves.pop().unwrap()];
- let excluded_script_leaves = script_leaves;
-
- assert!(
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()),
- ..Default::default()
- },
- )
- .unwrap(),
- "Unable to finalize tx"
- );
-
- assert!(psbt.inputs[0]
- .tap_script_sigs
- .iter()
- .all(|s| included_script_leaves.contains(&s.0 .1)
- && !excluded_script_leaves.contains(&s.0 .1)));
-}
-
-#[test]
-fn test_taproot_script_spend_sign_no_leaves() {
- use bdk::signer::TapLeavesOptions;
- let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
-
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- tap_leaves_options: TapLeavesOptions::None,
- ..Default::default()
- },
- )
- .unwrap();
-
- assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty()));
-}
-
-#[test]
-fn test_taproot_sign_derive_index_from_psbt() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
-
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
- let mut psbt = builder.finish().unwrap();
-
- // re-create the wallet with an empty db
- let wallet_empty =
- Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap();
-
- // signing with an empty db means that we will only look at the psbt to infer the
- // derivation index
- assert!(
- wallet_empty.sign(&mut psbt, Default::default()).unwrap(),
- "Unable to finalize tx"
- );
-}
-
-#[test]
-fn test_taproot_sign_explicit_sighash_all() {
- let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .sighash(TapSighashType::All.into())
- .drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let result = wallet.sign(&mut psbt, Default::default());
- assert!(
- result.is_ok(),
- "Signing should work because SIGHASH_ALL is safe"
- )
-}
-
-#[test]
-fn test_taproot_sign_non_default_sighash() {
- let sighash = TapSighashType::NonePlusAnyoneCanPay;
-
- let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .sighash(sighash.into())
- .drain_wallet();
- let mut psbt = builder.finish().unwrap();
-
- let witness_utxo = psbt.inputs[0].witness_utxo.take();
-
- let result = wallet.sign(&mut psbt, Default::default());
- assert!(
- result.is_err(),
- "Signing should have failed because the TX uses non-standard sighashes"
- );
- assert_matches!(
- result,
- Err(SignerError::NonStandardSighash),
- "Signing failed with the wrong error type"
- );
-
- // try again after opting-in
- let result = wallet.sign(
- &mut psbt,
- SignOptions {
- allow_all_sighashes: true,
- ..Default::default()
- },
- );
- assert!(
- result.is_err(),
- "Signing should have failed because the witness_utxo is missing"
- );
- assert_matches!(
- result,
- Err(SignerError::MissingWitnessUtxo),
- "Signing failed with the wrong error type"
- );
-
- // restore the witness_utxo
- psbt.inputs[0].witness_utxo = witness_utxo;
-
- let result = wallet.sign(
- &mut psbt,
- SignOptions {
- allow_all_sighashes: true,
- ..Default::default()
- },
- );
-
- assert!(result.is_ok(), "Signing should have worked");
- assert!(
- result.unwrap(),
- "Should finalize the input since we can produce signatures"
- );
-
- let extracted = psbt.extract_tx().expect("failed to extract tx");
- assert_eq!(
- *extracted.input[0].witness.to_vec()[0].last().unwrap(),
- sighash as u8,
- "The signature should have been made with the right sighash"
- );
-}
-
-#[test]
-fn test_spend_coinbase() {
- let descriptor = get_test_wpkh();
- let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap();
-
- let confirmation_height = 5;
- wallet
- .insert_checkpoint(BlockId {
- height: confirmation_height,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
- let coinbase_tx = Transaction {
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- input: vec![TxIn {
- previous_output: OutPoint::null(),
- ..Default::default()
- }],
- output: vec![TxOut {
- script_pubkey: wallet
- .next_unused_address(KeychainKind::External)
- .unwrap()
- .script_pubkey(),
- value: Amount::from_sat(25_000),
- }],
- };
- wallet
- .insert_tx(
- coinbase_tx,
- ConfirmationTime::Confirmed {
- height: confirmation_height,
- time: 30_000,
- },
- )
- .unwrap();
-
- let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
- let maturity_time = confirmation_height + COINBASE_MATURITY;
-
- let balance = wallet.get_balance();
- assert_eq!(
- balance,
- Balance {
- immature: Amount::from_sat(25_000),
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO
- }
- );
-
- // We try to create a transaction, only to notice that all
- // our funds are unspendable
- let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
- .unwrap()
- .assume_checked();
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), balance.immature / 2)
- .current_height(confirmation_height);
- assert!(matches!(
- builder.finish(),
- Err(CreateTxError::CoinSelection(
- coin_selection::Error::InsufficientFunds {
- needed: _,
- available: 0
- }
- ))
- ));
-
- // Still unspendable...
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), balance.immature / 2)
- .current_height(not_yet_mature_time);
- assert_matches!(
- builder.finish(),
- Err(CreateTxError::CoinSelection(
- coin_selection::Error::InsufficientFunds {
- needed: _,
- available: 0
- }
- ))
- );
-
- wallet
- .insert_checkpoint(BlockId {
- height: maturity_time,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
- let balance = wallet.get_balance();
- assert_eq!(
- balance,
- Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::from_sat(25_000)
- }
- );
- let mut builder = wallet.build_tx();
- builder
- .add_recipient(addr.script_pubkey(), balance.confirmed / 2)
- .current_height(maturity_time);
- builder.finish().unwrap();
-}
-
-#[test]
-fn test_allow_dust_limit() {
- let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
-
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
-
- let mut builder = wallet.build_tx();
-
- builder.add_recipient(addr.script_pubkey(), Amount::ZERO);
-
- assert_matches!(
- builder.finish(),
- Err(CreateTxError::OutputBelowDustLimit(0))
- );
-
- let mut builder = wallet.build_tx();
-
- builder
- .allow_dust(true)
- .add_recipient(addr.script_pubkey(), Amount::ZERO);
-
- assert!(builder.finish().is_ok());
-}
-
-#[test]
-fn test_fee_rate_sign_no_grinding_high_r() {
- // Our goal is to obtain a transaction with a signature with high-R (71 bytes
- // instead of 70). We then check that our fee rate and fee calculation is
- // alright.
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
- let mut builder = wallet.build_tx();
- let mut data = PushBytesBuf::try_from(vec![0]).unwrap();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_rate(fee_rate)
- .add_data(&data);
- let mut psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
- let (op_return_vout, _) = psbt
- .unsigned_tx
- .output
- .iter()
- .enumerate()
- .find(|(_n, i)| i.script_pubkey.is_op_return())
- .unwrap();
-
- let mut sig_len: usize = 0;
- // We try to sign many different times until we find a longer signature (71 bytes)
- while sig_len < 71 {
- // Changing the OP_RETURN data will make the signature change (but not the fee, until
- // data[0] is small enough)
- data.as_mut_bytes()[0] += 1;
- psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data);
- // Clearing the previous signature
- psbt.inputs[0].partial_sigs.clear();
- // Signing
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- remove_partial_sigs: false,
- try_finalize: false,
- allow_grinding: false,
- ..Default::default()
- },
- )
- .unwrap();
- // We only have one key in the partial_sigs map, this is a trick to retrieve it
- let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
- sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
- }
- // Actually finalizing the transaction...
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- remove_partial_sigs: false,
- allow_grinding: false,
- ..Default::default()
- },
- )
- .unwrap();
- // ...and checking that everything is fine
- assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
-}
-
-#[test]
-fn test_fee_rate_sign_grinding_low_r() {
- // Our goal is to obtain a transaction with a signature with low-R (70 bytes)
- // by setting the `allow_grinding` signing option as true.
- // We then check that our fee rate and fee calculation is alright and that our
- // signature is 70 bytes.
- let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
- let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
- let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
- let mut builder = wallet.build_tx();
- builder
- .drain_to(addr.script_pubkey())
- .drain_wallet()
- .fee_rate(fee_rate);
- let mut psbt = builder.finish().unwrap();
- let fee = check_fee!(wallet, psbt);
-
- wallet
- .sign(
- &mut psbt,
- SignOptions {
- remove_partial_sigs: false,
- allow_grinding: true,
- ..Default::default()
- },
- )
- .unwrap();
-
- let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
- let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
- assert_eq!(sig_len, 70);
- assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
-}
-
-#[test]
-fn test_taproot_load_descriptor_duplicated_keys() {
- // Added after issue https://github.com/bitcoindevkit/bdk/issues/760
- //
- // Having the same key in multiple taproot leaves is safe and should be accepted by BDK
-
- let (wallet, _) = get_funded_wallet(get_test_tr_dup_keys());
- let addr = wallet.peek_address(KeychainKind::External, 0);
-
- assert_eq!(
- addr.to_string(),
- "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5"
- );
-}
-
-#[test]
-/// The wallet should re-use previously allocated change addresses when the tx using them is cancelled
-fn test_tx_cancellation() {
- macro_rules! new_tx {
- ($wallet:expr) => {{
- let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
- .unwrap()
- .assume_checked();
- let mut builder = $wallet.build_tx();
- builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000));
-
- let psbt = builder.finish().unwrap();
-
- psbt
- }};
- }
-
- let (mut wallet, _) =
- get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv()));
-
- let psbt1 = new_tx!(wallet);
- let change_derivation_1 = psbt1
- .unsigned_tx
- .output
- .iter()
- .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
- .unwrap();
- assert_eq!(change_derivation_1, (KeychainKind::Internal, 0));
-
- let psbt2 = new_tx!(wallet);
-
- let change_derivation_2 = psbt2
- .unsigned_tx
- .output
- .iter()
- .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
- .unwrap();
- assert_eq!(change_derivation_2, (KeychainKind::Internal, 1));
-
- wallet.cancel_tx(&psbt1.extract_tx().expect("failed to extract tx"));
-
- let psbt3 = new_tx!(wallet);
- let change_derivation_3 = psbt3
- .unsigned_tx
- .output
- .iter()
- .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
- .unwrap();
- assert_eq!(change_derivation_3, (KeychainKind::Internal, 0));
-
- let psbt3 = new_tx!(wallet);
- let change_derivation_3 = psbt3
- .unsigned_tx
- .output
- .iter()
- .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
- .unwrap();
- assert_eq!(change_derivation_3, (KeychainKind::Internal, 2));
-
- wallet.cancel_tx(&psbt3.extract_tx().expect("failed to extract tx"));
-
- let psbt3 = new_tx!(wallet);
- let change_derivation_4 = psbt3
- .unsigned_tx
- .output
- .iter()
- .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
- .unwrap();
- assert_eq!(change_derivation_4, (KeychainKind::Internal, 2));
-}
-
-#[test]
-fn test_thread_safety() {
- fn thread_safe<T: Send + Sync>() {}
- thread_safe::<Wallet>(); // compiles only if true
-}
readme = "README.md"
[dependencies]
-bdk = { path = "../bdk" }
+bdk_wallet = { path = "../wallet" }
hwi = { version = "0.8.0", features = [ "miniscript"] }
//! 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;
//! #
//! # }
//! ```
//!
-//! [`TransactionSigner`]: bdk::wallet::signer::TransactionSigner
+//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
mod signer;
pub use signer::*;
-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
fn sign_transaction(
&self,
psbt: &mut Psbt,
- _sign_options: &bdk::SignOptions,
+ _sign_options: &bdk_wallet::SignOptions,
_secp: &Secp256k1<All>,
) -> Result<(), SignerError> {
psbt.combine(
// 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;
//
//
// 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();
--- /dev/null
+[package]
+name = "bdk_wallet"
+homepage = "https://bitcoindevkit.org"
+version = "1.0.0-alpha.11"
+repository = "https://github.com/bitcoindevkit/bdk"
+documentation = "https://docs.rs/bdk"
+description = "A modern, lightweight, descriptor-based wallet library"
+keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
+readme = "README.md"
+license = "MIT OR Apache-2.0"
+authors = ["Bitcoin Dev Kit Developers"]
+edition = "2021"
+rust-version = "1.63"
+
+[dependencies]
+anyhow = { version = "1", default-features = false }
+rand = "^0.8"
+miniscript = { version = "11.0.0", features = ["serde"], default-features = false }
+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" }
+
+# Optional dependencies
+bip39 = { version = "2.0", optional = true }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+getrandom = "0.2"
+js-sys = "0.3"
+
+[features]
+default = ["std"]
+std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
+compiler = ["miniscript/compiler"]
+all-keys = ["keys-bip39"]
+keys-bip39 = ["bip39"]
+
+# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
+# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
+# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
+dev-getrandom-wasm = ["getrandom/js"]
+
+[dev-dependencies]
+lazy_static = "1.4"
+assert_matches = "1.5.0"
+tempfile = "3"
+bdk_file_store = { path = "../file_store" }
+anyhow = "1"
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[[example]]
+name = "mnemonic_to_descriptors"
+path = "examples/mnemonic_to_descriptors.rs"
+required-features = ["all-keys"]
+
+[[example]]
+name = "miniscriptc"
+path = "examples/compiler.rs"
+required-features = ["compiler"]
--- /dev/null
+<div align="center">
+ <h1>BDK</h1>
+
+ <img src="https://raw.githubusercontent.com/bitcoindevkit/bdk/master/static/bdk.png" width="220" />
+
+ <p>
+ <strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
+ </p>
+
+ <p>
+ <a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
+ <a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
+ <a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
+ <a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
+ <a href="https://docs.rs/bdk_wallet"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
+ <a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
+ <a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
+ </p>
+
+ <h4>
+ <a href="https://bitcoindevkit.org">Project Homepage</a>
+ <span> | </span>
+ <a href="https://docs.rs/bdk_wallet">Documentation</a>
+ </h4>
+</div>
+
+# BDK Wallet
+
+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
+[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it
+chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
+can create and sign transactions.
+
+For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
+
+## Blockchain data
+
+In order to get blockchain data for `Wallet` to consume, you should configure a client from
+an available chain source. Typically you make a request to the chain source and get a response
+that the `Wallet` can use to update its view of the chain.
+
+**Blockchain Data Sources**
+
+* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
+* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
+* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
+
+**Examples**
+
+* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
+* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
+* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
+* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
+
+## Persistence
+
+To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
+
+**Implementations**
+
+* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
+
+**Example**
+
+<!-- compile_fail because outpoint and txout are fake variables -->
+```rust,compile_fail
+use bdk_wallet::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
+
+fn main() {
+ // Create a new file `Store`.
+ let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
+
+ let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
+ let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
+
+ // Insert a single `TxOut` at `OutPoint` into the wallet.
+ let _ = wallet.insert_txout(outpoint, txout);
+ wallet.commit().expect("must write to database");
+}
+```
+
+<!-- ### Sync the balance of a descriptor -->
+
+<!-- ```rust,no_run -->
+<!-- use bdk_wallet::Wallet; -->
+<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
+<!-- use bdk_wallet::SyncOptions; -->
+<!-- use bdk_wallet::electrum_client::Client; -->
+<!-- use bdk_wallet::bitcoin::Network; -->
+
+<!-- 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/*)", -->
+<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
+<!-- Network::Testnet, -->
+<!-- )?; -->
+
+<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
+
+<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
+
+<!-- Ok(()) -->
+<!-- } -->
+<!-- ``` -->
+<!-- ### Generate a few addresses -->
+
+<!-- ```rust -->
+<!-- use bdk_wallet::Wallet; -->
+<!-- use bdk_wallet::wallet::AddressIndex::New; -->
+<!-- use bdk_wallet::bitcoin::Network; -->
+
+<!-- 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/*)"), -->
+<!-- Network::Testnet, -->
+<!-- )?; -->
+
+<!-- println!("Address #0: {}", wallet.get_address(New)); -->
+<!-- println!("Address #1: {}", wallet.get_address(New)); -->
+<!-- println!("Address #2: {}", wallet.get_address(New)); -->
+
+<!-- Ok(()) -->
+<!-- } -->
+<!-- ``` -->
+
+<!-- ### Create a transaction -->
+
+<!-- ```rust,no_run -->
+<!-- use bdk_wallet::{FeeRate, Wallet, SyncOptions}; -->
+<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
+
+<!-- use bdk_wallet::electrum_client::Client; -->
+<!-- use bdk_wallet::wallet::AddressIndex::New; -->
+
+<!-- use bitcoin::base64; -->
+<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
+<!-- use bdk_wallet::bitcoin::Network; -->
+
+<!-- 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/*)", -->
+<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
+<!-- Network::Testnet, -->
+<!-- )?; -->
+
+<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
+
+<!-- let send_to = wallet.get_address(New); -->
+<!-- let (psbt, details) = { -->
+<!-- let mut builder = wallet.build_tx(); -->
+<!-- builder -->
+<!-- .add_recipient(send_to.script_pubkey(), 50_000) -->
+<!-- .enable_rbf() -->
+<!-- .do_not_spend_change() -->
+<!-- .fee_rate(FeeRate::from_sat_per_vb(5.0)); -->
+<!-- builder.finish()? -->
+<!-- }; -->
+
+<!-- println!("Transaction details: {:#?}", details); -->
+<!-- println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); -->
+
+<!-- Ok(()) -->
+<!-- } -->
+<!-- ``` -->
+
+<!-- ### Sign a transaction -->
+
+<!-- ```rust,no_run -->
+<!-- use bdk_wallet::{Wallet, SignOptions}; -->
+
+<!-- use bitcoin::base64; -->
+<!-- use bdk_wallet::bitcoin::consensus::deserialize; -->
+<!-- use bdk_wallet::bitcoin::Network; -->
+
+<!-- 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/*)"), -->
+<!-- Network::Testnet, -->
+<!-- )?; -->
+
+<!-- let psbt = "..."; -->
+<!-- let mut psbt = deserialize(&base64::decode(psbt).unwrap())?; -->
+
+<!-- let _finalized = wallet.sign(&mut psbt, SignOptions::default())?; -->
+
+<!-- Ok(()) -->
+<!-- } -->
+<!-- ``` -->
+
+## Testing
+
+### Unit testing
+
+```bash
+cargo test
+```
+
+# License
+
+Licensed under either of
+
+* Apache License, Version 2.0, ([LICENSE-APACHE](../../LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
+* MIT license ([LICENSE-MIT](../../LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
+
+at your option.
+
+# Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally
+submitted for inclusion in the work by you, as defined in the Apache-2.0
+license, shall be dual licensed as above, without any additional terms or
+conditions.
+
+[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
+[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
+[`bdk_chain`]: https://docs.rs/bdk_chain/latest
+[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
+[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
+[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
+[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
+[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+extern crate bdk_wallet;
+extern crate bitcoin;
+extern crate miniscript;
+extern crate serde_json;
+
+use std::error::Error;
+use std::str::FromStr;
+
+use bitcoin::Network;
+use miniscript::policy::Concrete;
+use miniscript::Descriptor;
+
+use bdk_wallet::{KeychainKind, Wallet};
+
+/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
+/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
+/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
+/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
+/// can be derived from the policy.
+///
+/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
+
+fn main() -> Result<(), Box<dyn Error>> {
+ // We start with a generic miniscript policy string
+ let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
+ println!("Compiling policy: \n{}", policy_str);
+
+ // Parse the string as a [`Concrete`] type miniscript policy.
+ let policy = Concrete::<String>::from_str(policy_str)?;
+
+ // Create a `wsh` type descriptor from the policy.
+ // `policy.compile()` returns the resulting miniscript from the policy.
+ let descriptor = Descriptor::new_wsh(policy.compile()?)?;
+
+ println!("Compiled into following Descriptor: \n{}", descriptor);
+
+ // Create a new wallet from this descriptor
+ let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
+
+ println!(
+ "First derived address from the descriptor: \n{}",
+ wallet.next_unused_address(KeychainKind::External)?,
+ );
+
+ // BDK also has it's own `Policy` structure to represent the spending condition in a more
+ // human readable json format.
+ let spending_policy = wallet.policies(KeychainKind::External)?;
+ println!(
+ "The BDK spending policy: \n{}",
+ serde_json::to_string_pretty(&spending_policy)?
+ );
+
+ Ok(())
+}
--- /dev/null
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+use anyhow::anyhow;
+use bdk_wallet::bitcoin::bip32::DerivationPath;
+use bdk_wallet::bitcoin::secp256k1::Secp256k1;
+use bdk_wallet::bitcoin::Network;
+use bdk_wallet::descriptor;
+use bdk_wallet::descriptor::IntoWalletDescriptor;
+use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount};
+use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
+use bdk_wallet::miniscript::Tap;
+use std::str::FromStr;
+
+/// This example demonstrates how to generate a mnemonic phrase
+/// using BDK and use that to generate a descriptor string.
+fn main() -> Result<(), anyhow::Error> {
+ let secp = Secp256k1::new();
+
+ // In this example we are generating a 12 words mnemonic phrase
+ // but it is also possible generate 15, 18, 21 and 24 words
+ // using their respective `WordCount` variant.
+ let mnemonic: GeneratedKey<_, Tap> =
+ Mnemonic::generate((WordCount::Words12, Language::English))
+ .map_err(|_| anyhow!("Mnemonic generation error"))?;
+
+ println!("Mnemonic phrase: {}", *mnemonic);
+ let mnemonic_with_passphrase = (mnemonic, None);
+
+ // define external and internal derivation key path
+ let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
+ let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
+
+ // generate external and internal descriptor from mnemonic
+ let (external_descriptor, ext_keymap) =
+ descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
+ .into_wallet_descriptor(&secp, Network::Testnet)?;
+ let (internal_descriptor, int_keymap) =
+ descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
+ .into_wallet_descriptor(&secp, Network::Testnet)?;
+
+ println!("tpub external descriptor: {}", external_descriptor);
+ println!("tpub internal descriptor: {}", internal_descriptor);
+ println!(
+ "tprv external descriptor: {}",
+ external_descriptor.to_string_with_secret(&ext_keymap)
+ );
+ println!(
+ "tprv internal descriptor: {}",
+ internal_descriptor.to_string_with_secret(&int_keymap)
+ );
+
+ Ok(())
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+extern crate bdk_wallet;
+use std::error::Error;
+
+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_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.
+/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
+/// to spend from.
+///
+/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
+/// one of the Extend Private key.
+
+fn main() -> Result<(), Box<dyn Error>> {
+ let secp = bitcoin::secp256k1::Secp256k1::new();
+
+ // The descriptor used in the example
+ // The form is "wsh(multi(2, <privkey>, <pubkey>))"
+ let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
+
+ // Use the descriptor string to derive the full descriptor and a keymap.
+ // The wallet descriptor can be used to create a new bdk_wallet::wallet.
+ // While the `keymap` can be used to create a `SignerContainer`.
+ //
+ // The `SignerContainer` can sign for `PSBT`s.
+ // a bdk_wallet::wallet internally uses these to handle transaction signing.
+ // But they can be used as independent tools also.
+ let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
+
+ println!("Example Descriptor for policy analysis : {}", wallet_desc);
+
+ // Create the signer with the keymap and descriptor.
+ let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
+
+ // Extract the Policy from the given descriptor and signer.
+ // Note that Policy is a wallet specific structure. It depends on the the descriptor, and
+ // what the concerned wallet with a given signer can sign for.
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
+ .expect("We expect a policy");
+
+ println!("Derived Policy for the descriptor {:#?}", policy);
+
+ Ok(())
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptor checksum
+//!
+//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
+//! checksum of a descriptor
+
+use crate::descriptor::DescriptorError;
+use alloc::string::String;
+
+const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
+const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
+
+fn poly_mod(mut c: u64, val: u64) -> u64 {
+ let c0 = c >> 35;
+ c = ((c & 0x7ffffffff) << 5) ^ val;
+ if c0 & 1 > 0 {
+ c ^= 0xf5dee51989
+ };
+ if c0 & 2 > 0 {
+ c ^= 0xa9fdca3312
+ };
+ if c0 & 4 > 0 {
+ c ^= 0x1bab10e32d
+ };
+ if c0 & 8 > 0 {
+ c ^= 0x3706b1677a
+ };
+ if c0 & 16 > 0 {
+ c ^= 0x644d626ffd
+ };
+
+ c
+}
+
+/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
+pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
+ let mut c = 1;
+ let mut cls = 0;
+ let mut clscount = 0;
+
+ let mut original_checksum = None;
+ if let Some(split) = desc.split_once('#') {
+ desc = split.0;
+ original_checksum = Some(split.1);
+ }
+
+ for ch in desc.as_bytes() {
+ let pos = INPUT_CHARSET
+ .iter()
+ .position(|b| b == ch)
+ .ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
+ c = poly_mod(c, pos & 31);
+ cls = cls * 3 + (pos >> 5);
+ clscount += 1;
+ if clscount == 3 {
+ c = poly_mod(c, cls);
+ cls = 0;
+ clscount = 0;
+ }
+ }
+ if clscount > 0 {
+ c = poly_mod(c, cls);
+ }
+ (0..8).for_each(|_| c = poly_mod(c, 0));
+ c ^= 1;
+
+ let mut checksum = [0_u8; 8];
+ for j in 0..8 {
+ checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
+ }
+
+ // if input data already had a checksum, check calculated checksum against original checksum
+ if let Some(original_checksum) = original_checksum {
+ if original_checksum.as_bytes() != checksum {
+ return Err(DescriptorError::InvalidDescriptorChecksum);
+ }
+ }
+
+ Ok(checksum)
+}
+
+/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
+pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
+ // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
+ calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::descriptor::calc_checksum;
+ use assert_matches::assert_matches;
+
+ // test calc_checksum() function; it should return the same value as Bitcoin Core
+ #[test]
+ fn test_calc_checksum() {
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
+ assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
+
+ let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
+ assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
+ }
+
+ // test calc_checksum() function; it should return the same value as Bitcoin Core even if the
+ // descriptor string includes a checksum hash
+ #[test]
+ fn test_calc_checksum_with_checksum_hash() {
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
+ assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
+
+ let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
+ assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
+ assert_matches!(
+ calc_checksum(desc),
+ Err(DescriptorError::InvalidDescriptorChecksum)
+ );
+
+ let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
+ assert_matches!(
+ calc_checksum(desc),
+ Err(DescriptorError::InvalidDescriptorChecksum)
+ );
+ }
+
+ #[test]
+ fn test_calc_checksum_invalid_character() {
+ let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
+ let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
+
+ assert_matches!(
+ calc_checksum(&invalid_desc),
+ Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
+ );
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptors DSL
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_top_level_sh {
+ // disallow `sortedmulti` in `bare()`
+ ( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => {
+ compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands");
+ };
+ ( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => {
+ compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands");
+ };
+
+ ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{
+ use core::marker::PhantomData;
+
+ use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
+ use $crate::miniscript::$ctx;
+
+ let build_desc = |k, pks| {
+ Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
+ };
+
+ $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*)
+ }};
+ ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{
+ use core::marker::PhantomData;
+
+ use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
+ use $crate::miniscript::$ctx;
+
+ let build_desc = |k, pks| {
+ Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
+ };
+
+ $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*)
+ }};
+
+ ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{
+ use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
+
+ $crate::fragment!($( $minisc )*)
+ .and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks)))
+ .and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::<DescriptorPublicKey>::$inner_struct(inner), key_map, valid_networks)))
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_top_level_pk {
+ ( $inner_type:ident, $ctx:ty, $key:expr ) => {{
+ use $crate::miniscript::descriptor::$inner_type;
+
+ #[allow(unused_imports)]
+ use $crate::keys::{DescriptorKey, IntoDescriptorKey};
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+
+ $key.into_descriptor_key()
+ .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp))
+ .map_err($crate::descriptor::DescriptorError::Key)
+ .map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks))
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_top_level_tr {
+ ( $internal_key:expr, $tap_tree:expr ) => {{
+ use $crate::miniscript::descriptor::{
+ Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr,
+ };
+ use $crate::miniscript::Tap;
+
+ #[allow(unused_imports)]
+ use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
+
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+
+ $internal_key
+ .into_descriptor_key()
+ .and_then(|key: DescriptorKey<Tap>| key.extract(&secp))
+ .map_err($crate::descriptor::DescriptorError::Key)
+ .and_then(|(pk, mut key_map, mut valid_networks)| {
+ let tap_tree = $tap_tree.map(
+ |(tap_tree, tree_keymap, tree_networks): (
+ TapTree<DescriptorPublicKey>,
+ KeyMap,
+ ValidNetworks,
+ )| {
+ key_map.extend(tree_keymap.into_iter());
+ valid_networks =
+ $crate::keys::merge_networks(&valid_networks, &tree_networks);
+
+ tap_tree
+ },
+ );
+
+ Ok((
+ Descriptor::<DescriptorPublicKey>::Tr(Tr::new(pk, tap_tree)?),
+ key_map,
+ valid_networks,
+ ))
+ })
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_leaf_opcode {
+ ( $terminal_variant:ident ) => {{
+ use $crate::descriptor::CheckMiniscript;
+
+ $crate::miniscript::Miniscript::from_ast(
+ $crate::miniscript::miniscript::decode::Terminal::$terminal_variant,
+ )
+ .map_err($crate::descriptor::DescriptorError::Miniscript)
+ .and_then(|minisc| {
+ minisc.check_miniscript()?;
+ Ok(minisc)
+ })
+ .map(|minisc| {
+ (
+ minisc,
+ $crate::miniscript::descriptor::KeyMap::default(),
+ $crate::keys::any_network(),
+ )
+ })
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_leaf_opcode_value {
+ ( $terminal_variant:ident, $value:expr ) => {{
+ use $crate::descriptor::CheckMiniscript;
+
+ $crate::miniscript::Miniscript::from_ast(
+ $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value),
+ )
+ .map_err($crate::descriptor::DescriptorError::Miniscript)
+ .and_then(|minisc| {
+ minisc.check_miniscript()?;
+ Ok(minisc)
+ })
+ .map(|minisc| {
+ (
+ minisc,
+ $crate::miniscript::descriptor::KeyMap::default(),
+ $crate::keys::any_network(),
+ )
+ })
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_leaf_opcode_value_two {
+ ( $terminal_variant:ident, $one:expr, $two:expr ) => {{
+ use $crate::descriptor::CheckMiniscript;
+
+ $crate::miniscript::Miniscript::from_ast(
+ $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two),
+ )
+ .map_err($crate::descriptor::DescriptorError::Miniscript)
+ .and_then(|minisc| {
+ minisc.check_miniscript()?;
+ Ok(minisc)
+ })
+ .map(|minisc| {
+ (
+ minisc,
+ $crate::miniscript::descriptor::KeyMap::default(),
+ $crate::keys::any_network(),
+ )
+ })
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_node_opcode_two {
+ ( $terminal_variant:ident, $( $inner:tt )* ) => ({
+ use $crate::descriptor::CheckMiniscript;
+
+ let inner = $crate::fragment_internal!( @t $( $inner )* );
+ let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened();
+
+ a
+ .and_then(|a| Ok((a, b?)))
+ .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| {
+ // join key_maps
+ a_keymap.extend(b_keymap.into_iter());
+
+ let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
+ $crate::alloc::sync::Arc::new(a_minisc),
+ $crate::alloc::sync::Arc::new(b_minisc),
+ ))?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
+ })
+ });
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_node_opcode_three {
+ ( $terminal_variant:ident, $( $inner:tt )* ) => ({
+ use $crate::descriptor::CheckMiniscript;
+
+ let inner = $crate::fragment_internal!( @t $( $inner )* );
+ let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened();
+
+ a
+ .and_then(|a| Ok((a, b?, c?)))
+ .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| {
+ // join key_maps
+ a_keymap.extend(b_keymap.into_iter());
+ a_keymap.extend(c_keymap.into_iter());
+
+ let networks = $crate::keys::merge_networks(&a_networks, &b_networks);
+ let networks = $crate::keys::merge_networks(&networks, &c_networks);
+
+ let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
+ $crate::alloc::sync::Arc::new(a_minisc),
+ $crate::alloc::sync::Arc::new(b_minisc),
+ $crate::alloc::sync::Arc::new(c_minisc),
+ ))?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, a_keymap, networks))
+ })
+ });
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_sortedmulti {
+ ( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+ $crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp)
+ });
+ ( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({
+ use $crate::keys::IntoDescriptorKey;
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+
+ let keys = vec![
+ $(
+ $key.into_descriptor_key(),
+ )*
+ ];
+
+ keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
+ .map_err($crate::descriptor::DescriptorError::Key)
+ .and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp))
+ });
+
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! parse_tap_tree {
+ ( @merge $tree_a:expr, $tree_b:expr) => {{
+ use $crate::miniscript::descriptor::TapTree;
+
+ $tree_a
+ .and_then(|tree_a| Ok((tree_a, $tree_b?)))
+ .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
+ a_keymap.extend(b_keymap.into_iter());
+ Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
+ })
+
+ }};
+
+ // Two sub-trees
+ ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{
+ let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
+ let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
+
+ $crate::parse_tap_tree!(@merge tree_a, tree_b)
+ }};
+
+ // One leaf and a sub-tree
+ ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{
+ let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
+ let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
+
+ $crate::parse_tap_tree!(@merge tree_a, tree_b)
+ }};
+ ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
+ let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
+ let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
+
+ $crate::parse_tap_tree!(@merge tree_a, tree_b)
+ }};
+
+ // Two leaves
+ ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
+ let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
+ let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
+
+ $crate::parse_tap_tree!(@merge tree_a, tree_b)
+ }};
+
+ // Single leaf
+ ( $op:ident ( $( $minisc:tt )* ) ) => {{
+ use $crate::alloc::sync::Arc;
+ use $crate::miniscript::descriptor::TapTree;
+
+ $crate::fragment!( $op ( $( $minisc )* ) )
+ .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks))
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! apply_modifier {
+ ( $terminal_variant:ident, $inner:expr ) => {{
+ use $crate::descriptor::CheckMiniscript;
+
+ $inner
+ .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() })
+ .and_then(|(minisc, keymap, networks)| {
+ let minisc = $crate::miniscript::Miniscript::from_ast(
+ $crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
+ $crate::alloc::sync::Arc::new(minisc),
+ ),
+ )?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, keymap, networks))
+ })
+ }};
+
+ ( a: $inner:expr ) => {{
+ $crate::apply_modifier!(Alt, $inner)
+ }};
+ ( s: $inner:expr ) => {{
+ $crate::apply_modifier!(Swap, $inner)
+ }};
+ ( c: $inner:expr ) => {{
+ $crate::apply_modifier!(Check, $inner)
+ }};
+ ( d: $inner:expr ) => {{
+ $crate::apply_modifier!(DupIf, $inner)
+ }};
+ ( v: $inner:expr ) => {{
+ $crate::apply_modifier!(Verify, $inner)
+ }};
+ ( j: $inner:expr ) => {{
+ $crate::apply_modifier!(NonZero, $inner)
+ }};
+ ( n: $inner:expr ) => {{
+ $crate::apply_modifier!(ZeroNotEqual, $inner)
+ }};
+
+ // Modifiers expanded to other operators
+ ( t: $inner:expr ) => {{
+ $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
+ $crate::impl_leaf_opcode_value_two!(
+ AndV,
+ $crate::alloc::sync::Arc::new(a_minisc),
+ $crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0)
+ )
+ .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
+ })
+ }};
+ ( l: $inner:expr ) => {{
+ $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
+ $crate::impl_leaf_opcode_value_two!(
+ OrI,
+ $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0),
+ $crate::alloc::sync::Arc::new(a_minisc)
+ )
+ .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
+ })
+ }};
+ ( u: $inner:expr ) => {{
+ $inner.and_then(|(a_minisc, a_keymap, a_networks)| {
+ $crate::impl_leaf_opcode_value_two!(
+ OrI,
+ $crate::alloc::sync::Arc::new(a_minisc),
+ $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0)
+ )
+ .map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
+ })
+ }};
+}
+
+/// Macro to write full descriptors with code
+///
+/// This macro expands to a `Result` of
+/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError)
+///
+/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers
+/// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be
+/// broken up to `s:d:v:older(144)`.
+///
+/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements
+/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that
+/// case they must be wrapped in quotes, which is another difference compared to the standard
+/// descriptor syntax.
+///
+/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey
+///
+/// ## Example
+///
+/// Signature plus timelock descriptor:
+///
+/// ```
+/// # use std::str::FromStr;
+/// 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>>(())
+/// ```
+///
+/// -------
+///
+/// 2-of-3 that becomes a 1-of-3 after a timelock has expired. Both `descriptor_a` and `descriptor_b` are equivalent: the first
+/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
+/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
+///
+/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))`
+///
+/// ```
+/// # use std::str::FromStr;
+/// let my_key_1 = bitcoin::PublicKey::from_str(
+/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c",
+/// )?;
+/// let my_key_2 =
+/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
+/// let my_timelock = 50;
+///
+/// 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))
+/// )
+/// }?;
+///
+/// #[rustfmt::skip]
+/// let b_items = vec![
+/// bdk_wallet::fragment!(pk(my_key_1))?,
+/// bdk_wallet::fragment!(s:pk(my_key_2))?,
+/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?,
+/// ];
+/// 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());
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// ------
+///
+/// Simple 2-of-2 multi-signature, equivalent to: `wsh(multi(2, ...))`
+///
+/// ```
+/// # use std::str::FromStr;
+/// let my_key_1 = bitcoin::PublicKey::from_str(
+/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c",
+/// )?;
+/// let my_key_2 =
+/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
+///
+/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! {
+/// wsh (
+/// multi(2, my_key_1, my_key_2)
+/// )
+/// }?;
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// ------
+///
+/// Native-Segwit single-sig, equivalent to: `wpkh(...)`
+///
+/// ```
+/// let my_key =
+/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
+///
+/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?;
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// [`Vec`]: alloc::vec::Vec
+#[macro_export]
+macro_rules! descriptor {
+ ( bare ( $( $minisc:tt )* ) ) => ({
+ $crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*)
+ });
+ ( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({
+ $crate::descriptor!(shwsh ($( $minisc )*))
+ });
+ ( shwsh ( $( $minisc:tt )* ) ) => ({
+ $crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*)
+ });
+ ( pk ( $key:expr ) ) => ({
+ // `pk()` is actually implemented as `bare(pk())`
+ $crate::descriptor!( bare ( pk ( $key ) ) )
+ });
+ ( pkh ( $key:expr ) ) => ({
+ use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
+
+ $crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key)
+ .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
+ .map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Pkh(a), b, c))
+ });
+ ( wpkh ( $key:expr ) ) => ({
+ use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
+
+ $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
+ .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
+ .map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Wpkh(a), b, c))
+ });
+ ( sh ( wpkh ( $key:expr ) ) ) => ({
+ $crate::descriptor!(shwpkh ( $key ))
+ });
+ ( shwpkh ( $key:expr ) ) => ({
+ use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh};
+
+ $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
+ .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
+ .and_then(|(a, b, c)| Ok((Descriptor::<DescriptorPublicKey>::Sh(Sh::new_wpkh(a.into_inner())?), b, c)))
+ });
+ ( sh ( $( $minisc:tt )* ) ) => ({
+ $crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*)
+ });
+ ( wsh ( $( $minisc:tt )* ) ) => ({
+ $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
+ });
+
+ ( tr ( $internal_key:expr ) ) => ({
+ $crate::impl_top_level_tr!($internal_key, None)
+ });
+ ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({
+ let tap_tree = $crate::parse_tap_tree!( $( $taptree )* );
+ tap_tree
+ .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree)))
+ });
+}
+
+#[doc(hidden)]
+pub struct TupleTwo<A, B> {
+ pub a: A,
+ pub b: B,
+}
+
+impl<A, B> TupleTwo<A, B> {
+ pub fn flattened(self) -> (A, B) {
+ (self.a, self.b)
+ }
+}
+
+impl<A, B> From<(A, (B, ()))> for TupleTwo<A, B> {
+ fn from((a, (b, _)): (A, (B, ()))) -> Self {
+ TupleTwo { a, b }
+ }
+}
+
+#[doc(hidden)]
+pub struct TupleThree<A, B, C> {
+ pub a: A,
+ pub b: B,
+ pub c: C,
+}
+
+impl<A, B, C> TupleThree<A, B, C> {
+ pub fn flattened(self) -> (A, B, C) {
+ (self.a, self.b, self.c)
+ }
+}
+
+impl<A, B, C> From<(A, (B, (C, ())))> for TupleThree<A, B, C> {
+ fn from((a, (b, (c, _))): (A, (B, (C, ())))) -> Self {
+ TupleThree { a, b, c }
+ }
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! group_multi_keys {
+ ( $( $key:expr ),+ ) => {{
+ use $crate::keys::IntoDescriptorKey;
+
+ let keys = vec![
+ $(
+ $key.into_descriptor_key(),
+ )*
+ ];
+
+ keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
+ .map_err($crate::descriptor::DescriptorError::Key)
+ }};
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! fragment_internal {
+ // The @v prefix is used to parse a sequence of operands and return them in a vector. This is
+ // used by operands that take a variable number of arguments, like `thresh()` and `multi()`.
+ ( @v $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({
+ let mut v = vec![$crate::fragment!( $op ( $( $args )* ) )];
+ v.append(&mut $crate::fragment_internal!( @v $( $tail )* ));
+
+ v
+ });
+ // Match modifiers
+ ( @v $modif:tt : $( $tail:tt )* ) => ({
+ let mut v = $crate::fragment_internal!( @v $( $tail )* );
+ let first = v.drain(..1).next().unwrap();
+
+ let first = $crate::apply_modifier!($modif:first);
+
+ let mut v_final = vec![first];
+ v_final.append(&mut v);
+
+ v_final
+ });
+ // Remove commas between operands
+ ( @v , $( $tail:tt )* ) => ({
+ $crate::fragment_internal!( @v $( $tail )* )
+ });
+ ( @v ) => ({
+ vec![]
+ });
+
+ // The @t prefix is used to parse a sequence of operands and return them in a tuple. This
+ // allows checking at compile-time the number of arguments passed to an operand. For this
+ // reason it's used by `and_*()`, `or_*()`, etc.
+ //
+ // Unfortunately, due to the fact that concatenating tuples is pretty hard, the final result
+ // adds in the first spot the parsed operand and in the second spot the result of parsing
+ // all the following ones. For two operands the type then corresponds to: (X, (X, ())). For
+ // three operands it's (X, (X, (X, ()))), etc.
+ //
+ // To check that the right number of arguments has been passed we can "cast" those tuples to
+ // more convenient structures like `TupleTwo`. If the conversion succeeds, the right number of
+ // args was passed. Otherwise the compilation fails entirely.
+ ( @t $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({
+ ($crate::fragment!( $op ( $( $args )* ) ), $crate::fragment_internal!( @t $( $tail )* ))
+ });
+ // Match modifiers
+ ( @t $modif:tt : $( $tail:tt )* ) => ({
+ let (first, tail) = $crate::fragment_internal!( @t $( $tail )* );
+ ($crate::apply_modifier!($modif:first), tail)
+ });
+ // Remove commas between operands
+ ( @t , $( $tail:tt )* ) => ({
+ $crate::fragment_internal!( @t $( $tail )* )
+ });
+ ( @t ) => ({});
+
+ // Fallback to calling `fragment!()`
+ ( $( $tokens:tt )* ) => ({
+ $crate::fragment!($( $tokens )*)
+ });
+}
+
+/// Macro to write descriptor fragments with code
+///
+/// This macro will be expanded to an object of type `Result<(Miniscript<DescriptorPublicKey, _>, KeyMap, ValidNetworks), DescriptorError>`. It allows writing
+/// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec(m, ...))`.
+///
+/// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro.
+#[macro_export]
+macro_rules! fragment {
+ // Modifiers
+ ( $modif:tt : $( $tail:tt )* ) => ({
+ let op = $crate::fragment!( $( $tail )* );
+ $crate::apply_modifier!($modif:op)
+ });
+
+ // Miniscript
+ ( true ) => ({
+ $crate::impl_leaf_opcode!(True)
+ });
+ ( false ) => ({
+ $crate::impl_leaf_opcode!(False)
+ });
+ ( pk_k ( $key:expr ) ) => ({
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+ $crate::keys::make_pk($key, &secp)
+ });
+ ( pk ( $key:expr ) ) => ({
+ $crate::fragment!(c:pk_k ( $key ))
+ });
+ ( pk_h ( $key:expr ) ) => ({
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+ $crate::keys::make_pkh($key, &secp)
+ });
+ ( after ( $value:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
+ });
+ ( older ( $value:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
+ });
+ ( sha256 ( $hash:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(Sha256, $hash)
+ });
+ ( hash256 ( $hash:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(Hash256, $hash)
+ });
+ ( ripemd160 ( $hash:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(Ripemd160, $hash)
+ });
+ ( hash160 ( $hash:expr ) ) => ({
+ $crate::impl_leaf_opcode_value!(Hash160, $hash)
+ });
+ ( and_v ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(AndV, $( $inner )*)
+ });
+ ( and_b ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(AndB, $( $inner )*)
+ });
+ ( and_or ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_three!(AndOr, $( $inner )*)
+ });
+ ( andor ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_three!(AndOr, $( $inner )*)
+ });
+ ( or_b ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(OrB, $( $inner )*)
+ });
+ ( or_d ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(OrD, $( $inner )*)
+ });
+ ( or_c ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(OrC, $( $inner )*)
+ });
+ ( or_i ( $( $inner:tt )* ) ) => ({
+ $crate::impl_node_opcode_two!(OrI, $( $inner )*)
+ });
+ ( thresh_vec ( $thresh:expr, $items:expr ) ) => ({
+ use $crate::miniscript::descriptor::KeyMap;
+
+ let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
+ let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect();
+
+ let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| {
+ keys_acc.extend(key.into_iter());
+ let net_acc = $crate::keys::merge_networks(&net_acc, &net);
+
+ (keys_acc, net_acc)
+ });
+
+ $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items)
+ .map(|(minisc, _, _)| (minisc, key_maps, valid_networks))
+ });
+ ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
+ let items = $crate::fragment_internal!( @v $( $inner )* );
+
+ items.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
+ .and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
+ });
+ ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+
+ $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
+ });
+ ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
+ $crate::group_multi_keys!( $( $key ),* )
+ .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) ))
+ });
+ ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
+ let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
+
+ $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
+ });
+ ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
+ $crate::group_multi_keys!( $( $key ),* )
+ .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) ))
+ });
+
+ // `sortedmulti()` is handled separately
+ ( sortedmulti ( $( $inner:tt )* ) ) => ({
+ compile_error!("`sortedmulti` can only be used as the root operand of a descriptor");
+ });
+ ( sortedmulti_vec ( $( $inner:tt )* ) ) => ({
+ compile_error!("`sortedmulti_vec` can only be used as the root operand of a descriptor");
+ });
+}
+
+#[cfg(test)]
+mod test {
+ use alloc::string::ToString;
+ use bitcoin::secp256k1::Secp256k1;
+ use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
+ use miniscript::{Descriptor, Legacy, Segwitv0};
+
+ use core::str::FromStr;
+
+ use crate::descriptor::{DescriptorError, DescriptorMeta};
+ use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
+ use bitcoin::bip32;
+ use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
+ use bitcoin::PrivateKey;
+
+ // test the descriptor!() macro
+
+ // verify descriptor generates expected script(s) (if bare or pk) or address(es)
+ fn check(
+ desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
+ is_witness: bool,
+ is_fixed: bool,
+ expected: &[&str],
+ ) {
+ let (desc, _key_map, _networks) = desc.unwrap();
+ assert_eq!(desc.is_witness(), is_witness);
+ assert_eq!(!desc.has_wildcard(), is_fixed);
+ for i in 0..expected.len() {
+ let child_desc = desc
+ .at_derivation_index(i as u32)
+ .expect("i is not hardened");
+ let address = child_desc.address(Regtest);
+ if let Ok(address) = address {
+ assert_eq!(address.to_string(), *expected.get(i).unwrap());
+ } else {
+ let script = child_desc.script_pubkey();
+ assert_eq!(script.to_hex_string(), *expected.get(i).unwrap());
+ }
+ }
+ }
+
+ // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
+ // - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
+
+ // expected script for pk and bare manually created
+ // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses`
+
+ #[test]
+ fn test_fixed_legacy_descriptors() {
+ let pubkey1 = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ let pubkey2 = bitcoin::PublicKey::from_str(
+ "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
+ )
+ .unwrap();
+
+ check(
+ descriptor!(bare(multi(1,pubkey1,pubkey2))),
+ false,
+ true,
+ &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"],
+ );
+ check(
+ descriptor!(pk(pubkey1)),
+ false,
+ true,
+ &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"],
+ );
+ check(
+ descriptor!(pkh(pubkey1)),
+ false,
+ true,
+ &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
+ );
+ check(
+ descriptor!(sh(multi(1, pubkey1, pubkey2))),
+ false,
+ true,
+ &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"],
+ );
+ }
+
+ #[test]
+ fn test_fixed_segwitv0_descriptors() {
+ let pubkey1 = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ let pubkey2 = bitcoin::PublicKey::from_str(
+ "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
+ )
+ .unwrap();
+
+ check(
+ descriptor!(wpkh(pubkey1)),
+ true,
+ true,
+ &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
+ );
+ check(
+ descriptor!(sh(wpkh(pubkey1))),
+ true,
+ true,
+ &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
+ );
+ check(
+ descriptor!(wsh(multi(1, pubkey1, pubkey2))),
+ true,
+ true,
+ &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"],
+ );
+ check(
+ descriptor!(sh(wsh(multi(1, pubkey1, pubkey2)))),
+ true,
+ true,
+ &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"],
+ );
+ }
+
+ #[test]
+ fn test_fixed_threeop_descriptors() {
+ let redeem_key = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ let move_key = bitcoin::PublicKey::from_str(
+ "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
+ )
+ .unwrap();
+
+ check(
+ descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))),
+ true,
+ true,
+ &["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"],
+ );
+ }
+
+ #[test]
+ fn test_bip32_legacy_descriptors() {
+ let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+
+ let path = bip32::DerivationPath::from_str("m/0").unwrap();
+ let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
+ check(
+ descriptor!(pk(desc_key)),
+ false,
+ false,
+ &[
+ "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac",
+ "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac",
+ "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac",
+ ],
+ );
+
+ let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
+ check(
+ descriptor!(pkh(desc_key)),
+ false,
+ false,
+ &[
+ "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP",
+ "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj",
+ "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn",
+ ],
+ );
+
+ let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
+ let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
+ let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
+
+ check(
+ descriptor!(sh(multi(1, desc_key1, desc_key2))),
+ false,
+ false,
+ &[
+ "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8",
+ "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At",
+ "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr",
+ ],
+ );
+ }
+
+ #[test]
+ fn test_bip32_segwitv0_descriptors() {
+ let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+
+ let path = bip32::DerivationPath::from_str("m/0").unwrap();
+ let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
+ check(
+ descriptor!(wpkh(desc_key)),
+ true,
+ false,
+ &[
+ "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0",
+ "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad",
+ "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r",
+ ],
+ );
+
+ let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
+ check(
+ descriptor!(sh(wpkh(desc_key))),
+ true,
+ false,
+ &[
+ "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763",
+ "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn",
+ "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx",
+ ],
+ );
+
+ let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
+ let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap();
+ let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap();
+ check(
+ descriptor!(wsh(multi(1, desc_key1, desc_key2))),
+ true,
+ false,
+ &[
+ "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49",
+ "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k",
+ "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7",
+ ],
+ );
+
+ let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
+ let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
+ check(
+ descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))),
+ true,
+ false,
+ &[
+ "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz",
+ "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw",
+ "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC",
+ ],
+ );
+ }
+
+ #[test]
+ fn test_dsl_sortedmulti() {
+ let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
+
+ let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
+ let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
+
+ let desc_key1 = (key_1, path_1);
+ let desc_key2 = (key_2, path_2);
+
+ check(
+ descriptor!(sh(sortedmulti(1, desc_key1.clone(), desc_key2.clone()))),
+ false,
+ false,
+ &[
+ "2MsxzPEJDBzpGffJXPaDpfXZAUNnZhaMh2N",
+ "2My3x3DLPK3UbGWGpxrXr1RnbD8MNC4FpgS",
+ "2NByEuiQT7YLqHCTNxL5KwYjvtuCYcXNBSC",
+ "2N1TGbP81kj2VUKTSWgrwxoMfuWjvfUdyu7",
+ "2N3Bomq2fpAcLRNfZnD3bCWK9quan28CxCR",
+ "2N9nrZaEzEFDqEAU9RPvDnXGT6AVwBDKAQb",
+ ],
+ );
+
+ check(
+ descriptor!(sh(wsh(sortedmulti(
+ 1,
+ desc_key1.clone(),
+ desc_key2.clone()
+ )))),
+ true,
+ false,
+ &[
+ "2NCogc5YyM4N6ruv1hUa7WLMW1BPeCK7N9B",
+ "2N6mkSAKi1V2oaBXby7XHdvBMKEDRQcFpNe",
+ "2NFmTSttm9v6bXeoWaBvpMcgfPQcZhNn3Eh",
+ "2Mvib87RBPUHXNEpX5S5Kv1qqrhBfgBGsJM",
+ "2MtMv5mcK2EjcLsH8Txpx2JxLLzHr4ttczL",
+ "2MsWCB56rb4T6yPv8QudZGHERTwNgesE4f6",
+ ],
+ );
+
+ check(
+ descriptor!(wsh(sortedmulti_vec(1, vec![desc_key1, desc_key2]))),
+ true,
+ false,
+ &[
+ "bcrt1qcvq0lg8q7a47ytrd7zk5y7uls7mulrenjgvflwylpppgwf8029es4vhpnj",
+ "bcrt1q80yn8sdt6l7pjvkz25lglyaqctlmsq9ugk80rmxt8yu0npdsj97sc7l4de",
+ "bcrt1qrvf6024v9s50qhffe3t2fr2q9ckdhx2g6jz32chm2pp24ymgtr5qfrdmct",
+ "bcrt1q6srfmra0ynypym35c7jvsxt2u4yrugeajq95kg2ps7lk6h2gaunsq9lzxn",
+ "bcrt1qhl8rrzzcdpu7tcup3lcg7tge52sqvwy5fcv4k78v6kxtwmqf3v6qpvyjza",
+ "bcrt1ql2elz9mhm9ll27ddpewhxs732xyl2fk2kpkqz9gdyh33wgcun4vstrd49k",
+ ],
+ );
+ }
+
+ // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
+ #[test]
+ fn test_valid_networks() {
+ let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ let path = bip32::DerivationPath::from_str("m/0").unwrap();
+ let desc_key = (xprv, path).into_descriptor_key().unwrap();
+
+ let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap();
+ assert_eq!(
+ valid_networks,
+ [Testnet, Regtest, Signet].iter().cloned().collect()
+ );
+
+ let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
+ let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
+ let desc_key = (xprv, path).into_descriptor_key().unwrap();
+
+ let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap();
+ assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect());
+ }
+
+ // - verify the key_maps are correctly merged together
+ #[test]
+ fn test_key_maps_merged() {
+ let secp = Secp256k1::new();
+
+ let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
+ let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
+
+ let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
+ let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
+ let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
+
+ let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
+ let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
+ let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
+
+ let (_desc, key_map, _valid_networks) =
+ descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap();
+ assert_eq!(key_map.len(), 3);
+
+ let desc_key1: DescriptorKey<Segwitv0> = (xprv1, path1).into_descriptor_key().unwrap();
+ let desc_key2: DescriptorKey<Segwitv0> = (xprv2, path2).into_descriptor_key().unwrap();
+ let desc_key3: DescriptorKey<Segwitv0> = (xprv3, path3).into_descriptor_key().unwrap();
+
+ let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap();
+ let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap();
+ let (key3, _key_map, _valid_networks) = desc_key3.extract(&secp).unwrap();
+ assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*");
+ assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*");
+ assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*");
+ }
+
+ // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey<Segwitv0> to a pkh() descriptor should throw a compilation error
+ #[test]
+ fn test_script_context_validation() {
+ // this compiles
+ let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ let path = bip32::DerivationPath::from_str("m/0").unwrap();
+ let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
+
+ let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
+ assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2");
+
+ // as expected this does not compile due to invalid context
+ //let desc_key:DescriptorKey<Segwitv0> = (xprv, path.clone()).into_descriptor_key().unwrap();
+ //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
+ }
+
+ #[test]
+ fn test_dsl_modifiers() {
+ let private_key =
+ PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
+ let (descriptor, _, _) =
+ descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
+
+ assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
+ }
+
+ #[test]
+ #[should_panic(expected = "Miniscript(ContextError(UncompressedKeysNotAllowed))")]
+ fn test_dsl_miniscript_checks() {
+ let mut uncompressed_pk =
+ PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
+ uncompressed_pk.compressed = false;
+
+ descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
+ }
+
+ #[test]
+ fn test_dsl_tr_only_key() {
+ let private_key =
+ PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
+ let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap();
+
+ assert_eq!(
+ descriptor.to_string(),
+ "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v"
+ )
+ }
+
+ #[test]
+ fn test_dsl_tr_simple_tree() {
+ let private_key =
+ PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
+ let (descriptor, _, _) =
+ descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap();
+
+ assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d")
+ }
+
+ #[test]
+ fn test_dsl_tr_single_leaf() {
+ let private_key =
+ PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
+ let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap();
+
+ assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7")
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptor errors
+use core::fmt;
+
+/// Errors related to the parsing and usage of descriptors
+#[derive(Debug)]
+pub enum Error {
+ /// Invalid HD Key path, such as having a wildcard but a length != 1
+ InvalidHdKeyPath,
+ /// The provided descriptor doesn't match its checksum
+ InvalidDescriptorChecksum,
+ /// The descriptor contains hardened derivation steps on public extended keys
+ HardenedDerivationXpub,
+ /// The descriptor contains multipath keys
+ MultiPath,
+
+ /// Error thrown while working with [`keys`](crate::keys)
+ Key(crate::keys::KeyError),
+ /// Error while extracting and manipulating policies
+ Policy(crate::descriptor::policy::PolicyError),
+
+ /// Invalid byte found in the descriptor checksum
+ InvalidDescriptorCharacter(u8),
+
+ /// BIP32 error
+ Bip32(bitcoin::bip32::Error),
+ /// Error during base58 decoding
+ Base58(bitcoin::base58::Error),
+ /// Key-related error
+ Pk(bitcoin::key::Error),
+ /// Miniscript error
+ Miniscript(miniscript::Error),
+ /// Hex decoding error
+ Hex(bitcoin::hex::HexToBytesError),
+}
+
+impl From<crate::keys::KeyError> for Error {
+ fn from(key_error: crate::keys::KeyError) -> Error {
+ match key_error {
+ crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
+ crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
+ e => Error::Key(e),
+ }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"),
+ Self::InvalidDescriptorChecksum => {
+ write!(f, "The provided descriptor doesn't match its checksum")
+ }
+ Self::HardenedDerivationXpub => write!(
+ f,
+ "The descriptor contains hardened derivation steps on public extended keys"
+ ),
+ Self::MultiPath => write!(
+ f,
+ "The descriptor contains multipath keys, which are not supported yet"
+ ),
+ Self::Key(err) => write!(f, "Key error: {}", err),
+ Self::Policy(err) => write!(f, "Policy error: {}", err),
+ Self::InvalidDescriptorCharacter(char) => {
+ write!(f, "Invalid descriptor character: {}", char)
+ }
+ Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
+ Self::Base58(err) => write!(f, "Base58 error: {}", err),
+ Self::Pk(err) => write!(f, "Key-related error: {}", err),
+ Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
+ Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for Error {}
+
+impl From<bitcoin::bip32::Error> for Error {
+ fn from(err: bitcoin::bip32::Error) -> Self {
+ Error::Bip32(err)
+ }
+}
+
+impl From<bitcoin::base58::Error> for Error {
+ fn from(err: bitcoin::base58::Error) -> Self {
+ Error::Base58(err)
+ }
+}
+
+impl From<bitcoin::key::Error> for Error {
+ fn from(err: bitcoin::key::Error) -> Self {
+ Error::Pk(err)
+ }
+}
+
+impl From<miniscript::Error> for Error {
+ fn from(err: miniscript::Error) -> Self {
+ Error::Miniscript(err)
+ }
+}
+
+impl From<bitcoin::hex::HexToBytesError> for Error {
+ fn from(err: bitcoin::hex::HexToBytesError) -> Self {
+ Error::Hex(err)
+ }
+}
+
+impl From<crate::descriptor::policy::PolicyError> for Error {
+ fn from(err: crate::descriptor::policy::PolicyError) -> Self {
+ Error::Policy(err)
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptors
+//!
+//! This module contains generic utilities to work with descriptors, plus some re-exported types
+//! from [`miniscript`].
+
+use crate::collections::BTreeMap;
+use alloc::string::String;
+use alloc::vec::Vec;
+
+use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
+use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
+use bitcoin::{psbt, taproot};
+use bitcoin::{Network, TxOut};
+
+use miniscript::descriptor::{
+ DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType,
+ DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard,
+};
+pub use miniscript::{
+ Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
+};
+use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
+
+use crate::descriptor::policy::BuildSatisfaction;
+
+pub mod checksum;
+#[doc(hidden)]
+pub mod dsl;
+pub mod error;
+pub mod policy;
+pub mod template;
+
+pub use self::checksum::calc_checksum;
+use self::checksum::calc_checksum_bytes;
+pub use self::error::Error as DescriptorError;
+pub use self::policy::Policy;
+use self::template::DescriptorTemplateOut;
+use crate::keys::{IntoDescriptorKey, KeyError};
+use crate::wallet::signer::SignersContainer;
+use crate::wallet::utils::SecpCtx;
+
+/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`]
+pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
+
+/// Alias for a [`Descriptor`] that contains extended **derived** keys
+pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
+
+/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
+/// [`psbt::Output`]
+///
+/// [`psbt::Input`]: bitcoin::psbt::Input
+/// [`psbt::Output`]: bitcoin::psbt::Output
+pub type HdKeyPaths = BTreeMap<secp256k1::PublicKey, KeySource>;
+
+/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or
+/// [`psbt::Output`]
+///
+/// [`psbt::Input`]: bitcoin::psbt::Input
+/// [`psbt::Output`]: bitcoin::psbt::Output
+pub type TapKeyOrigins = BTreeMap<XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
+
+/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
+pub trait IntoWalletDescriptor {
+ /// Convert to wallet descriptor
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>;
+}
+
+impl IntoWalletDescriptor for &str {
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ let descriptor = match self.split_once('#') {
+ Some((desc, original_checksum)) => {
+ let checksum = calc_checksum_bytes(desc)?;
+ if original_checksum.as_bytes() != checksum {
+ return Err(DescriptorError::InvalidDescriptorChecksum);
+ }
+ desc
+ }
+ None => self,
+ };
+
+ ExtendedDescriptor::parse_descriptor(secp, descriptor)?
+ .into_wallet_descriptor(secp, network)
+ }
+}
+
+impl IntoWalletDescriptor for &String {
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ self.as_str().into_wallet_descriptor(secp, network)
+ }
+}
+
+impl IntoWalletDescriptor for ExtendedDescriptor {
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ (self, KeyMap::default()).into_wallet_descriptor(secp, network)
+ }
+}
+
+impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ use crate::keys::DescriptorKey;
+
+ struct Translator<'s, 'd> {
+ secp: &'s SecpCtx,
+ descriptor: &'d ExtendedDescriptor,
+ network: Network,
+ }
+
+ impl<'s, 'd> miniscript::Translator<DescriptorPublicKey, String, DescriptorError>
+ for Translator<'s, 'd>
+ {
+ fn pk(&mut self, pk: &DescriptorPublicKey) -> Result<String, DescriptorError> {
+ let secp = &self.secp;
+
+ let (_, _, networks) = if self.descriptor.is_taproot() {
+ let descriptor_key: DescriptorKey<miniscript::Tap> =
+ pk.clone().into_descriptor_key()?;
+ descriptor_key.extract(secp)?
+ } else if self.descriptor.is_witness() {
+ let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
+ pk.clone().into_descriptor_key()?;
+ descriptor_key.extract(secp)?
+ } else {
+ let descriptor_key: DescriptorKey<miniscript::Legacy> =
+ pk.clone().into_descriptor_key()?;
+ descriptor_key.extract(secp)?
+ };
+
+ if networks.contains(&self.network) {
+ Ok(Default::default())
+ } else {
+ Err(DescriptorError::Key(KeyError::InvalidNetwork))
+ }
+ }
+ fn sha256(
+ &mut self,
+ _sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
+ ) -> Result<String, DescriptorError> {
+ Ok(Default::default())
+ }
+ fn hash256(
+ &mut self,
+ _hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
+ ) -> Result<String, DescriptorError> {
+ Ok(Default::default())
+ }
+ fn ripemd160(
+ &mut self,
+ _ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
+ ) -> Result<String, DescriptorError> {
+ Ok(Default::default())
+ }
+ fn hash160(
+ &mut self,
+ _hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
+ ) -> Result<String, DescriptorError> {
+ Ok(Default::default())
+ }
+ }
+
+ // check the network for the keys
+ use miniscript::TranslateErr;
+ match self.0.translate_pk(&mut Translator {
+ secp,
+ network,
+ descriptor: &self.0,
+ }) {
+ Ok(_) => {}
+ Err(TranslateErr::TranslatorErr(e)) => return Err(e),
+ Err(TranslateErr::OuterError(e)) => return Err(e.into()),
+ }
+
+ Ok(self)
+ }
+}
+
+impl IntoWalletDescriptor for DescriptorTemplateOut {
+ fn into_wallet_descriptor(
+ self,
+ _secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ struct Translator {
+ network: Network,
+ }
+
+ impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
+ for Translator
+ {
+ fn pk(
+ &mut self,
+ pk: &DescriptorPublicKey,
+ ) -> Result<DescriptorPublicKey, DescriptorError> {
+ // workaround for xpubs generated by other key types, like bip39: since when the
+ // conversion is made one network has to be chosen, what we generally choose
+ // "mainnet", but then override the set of valid networks to specify that all of
+ // them are valid. here we reset the network to make sure the wallet struct gets a
+ // descriptor with the right network everywhere.
+ let pk = match pk {
+ DescriptorPublicKey::XPub(ref xpub) => {
+ let mut xpub = xpub.clone();
+ xpub.xkey.network = self.network;
+
+ DescriptorPublicKey::XPub(xpub)
+ }
+ other => other.clone(),
+ };
+
+ Ok(pk)
+ }
+ miniscript::translate_hash_clone!(
+ DescriptorPublicKey,
+ DescriptorPublicKey,
+ DescriptorError
+ );
+ }
+
+ let (desc, keymap, networks) = self;
+
+ if !networks.contains(&network) {
+ return Err(DescriptorError::Key(KeyError::InvalidNetwork));
+ }
+
+ // fixup the network for keys that need it in the descriptor
+ use miniscript::TranslateErr;
+ let translated = match desc.translate_pk(&mut Translator { network }) {
+ Ok(descriptor) => descriptor,
+ Err(TranslateErr::TranslatorErr(e)) => return Err(e),
+ Err(TranslateErr::OuterError(e)) => return Err(e.into()),
+ };
+ // ...and in the key map
+ let fixed_keymap = keymap
+ .into_iter()
+ .map(|(mut k, mut v)| {
+ match (&mut k, &mut v) {
+ (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
+ xpub.xkey.network = network;
+ xprv.xkey.network = network;
+ }
+ (_, DescriptorSecretKey::Single(key)) => {
+ key.key.network = network;
+ }
+ _ => {}
+ }
+
+ (k, v)
+ })
+ .collect();
+
+ Ok((translated, fixed_keymap))
+ }
+}
+
+/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
+/// descriptor
+pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
+ inner: T,
+ secp: &SecpCtx,
+ network: Network,
+) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
+
+ // Ensure the keys don't contain any hardened derivation steps or hardened wildcards
+ let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
+ if let DescriptorPublicKey::XPub(DescriptorXKey {
+ derivation_path,
+ wildcard,
+ ..
+ }) = k
+ {
+ return *wildcard == Wildcard::Hardened
+ || derivation_path.into_iter().any(ChildNumber::is_hardened);
+ }
+
+ false
+ });
+ if descriptor_contains_hardened_steps {
+ return Err(DescriptorError::HardenedDerivationXpub);
+ }
+
+ if descriptor.is_multipath() {
+ return Err(DescriptorError::MultiPath);
+ }
+
+ // Run miniscript's sanity check, which will look for duplicated keys and other potential
+ // issues
+ descriptor.sanity_check()?;
+
+ Ok((descriptor, keymap))
+}
+
+#[doc(hidden)]
+/// Used internally mainly by the `descriptor!()` and `fragment!()` macros
+pub trait CheckMiniscript<Ctx: miniscript::ScriptContext> {
+ fn check_miniscript(&self) -> Result<(), miniscript::Error>;
+}
+
+impl<Ctx: miniscript::ScriptContext, Pk: miniscript::MiniscriptKey> CheckMiniscript<Ctx>
+ for miniscript::Miniscript<Pk, Ctx>
+{
+ fn check_miniscript(&self) -> Result<(), miniscript::Error> {
+ Ctx::check_global_validity(self)?;
+
+ Ok(())
+ }
+}
+
+/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`]
+pub trait ExtractPolicy {
+ /// Extract the spending [`policy`]
+ fn extract_policy(
+ &self,
+ signers: &SignersContainer,
+ psbt: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Result<Option<Policy>, DescriptorError>;
+}
+
+pub(crate) trait XKeyUtils {
+ fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
+}
+
+impl<T> XKeyUtils for DescriptorMultiXKey<T>
+where
+ T: InnerXKey,
+{
+ fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
+ match self.origin {
+ Some((fingerprint, _)) => fingerprint,
+ None => self.xkey.xkey_fingerprint(secp),
+ }
+ }
+}
+
+impl<T> XKeyUtils for DescriptorXKey<T>
+where
+ T: InnerXKey,
+{
+ fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
+ match self.origin {
+ Some((fingerprint, _)) => fingerprint,
+ None => self.xkey.xkey_fingerprint(secp),
+ }
+ }
+}
+
+pub(crate) trait DescriptorMeta {
+ fn is_witness(&self) -> bool;
+ fn is_taproot(&self) -> bool;
+ fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>>;
+ fn derive_from_hd_keypaths(
+ &self,
+ hd_keypaths: &HdKeyPaths,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor>;
+ fn derive_from_tap_key_origins(
+ &self,
+ tap_key_origins: &TapKeyOrigins,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor>;
+ fn derive_from_psbt_key_origins(
+ &self,
+ key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor>;
+ fn derive_from_psbt_input(
+ &self,
+ psbt_input: &psbt::Input,
+ utxo: Option<TxOut>,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor>;
+}
+
+impl DescriptorMeta for ExtendedDescriptor {
+ fn is_witness(&self) -> bool {
+ matches!(
+ self.desc_type(),
+ DescriptorType::Wpkh
+ | DescriptorType::ShWpkh
+ | DescriptorType::Wsh
+ | DescriptorType::ShWsh
+ | DescriptorType::ShWshSortedMulti
+ | DescriptorType::WshSortedMulti
+ )
+ }
+
+ fn is_taproot(&self) -> bool {
+ self.desc_type() == DescriptorType::Tr
+ }
+
+ fn get_extended_keys(&self) -> Vec<DescriptorXKey<Xpub>> {
+ let mut answer = Vec::new();
+
+ self.for_each_key(|pk| {
+ if let DescriptorPublicKey::XPub(xpub) = pk {
+ answer.push(xpub.clone());
+ }
+
+ true
+ });
+
+ answer
+ }
+
+ fn derive_from_psbt_key_origins(
+ &self,
+ key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor> {
+ // Ensure that deriving `xpub` with `path` yields `expected`
+ let verify_key =
+ |xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
+ let derived = xpub
+ .xkey
+ .derive_pub(secp, path)
+ .expect("The path should never contain hardened derivation steps")
+ .public_key;
+
+ match expected {
+ SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
+ SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
+ _ => false,
+ }
+ };
+
+ let mut path_found = None;
+
+ // using `for_any_key` should make this stop as soon as we return `true`
+ self.for_any_key(|key| {
+ if let DescriptorPublicKey::XPub(xpub) = key {
+ // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
+ // return the "prefix" that matched, so we remove that prefix from the full path
+ // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
+ // path of length 1 if the key is `wildcard` and an empty path otherwise.
+ let root_fingerprint = xpub.root_fingerprint(secp);
+ let derive_path = key_origins
+ .get_key_value(&root_fingerprint)
+ .and_then(|(fingerprint, (path, expected))| {
+ xpub.matches(&(*fingerprint, (*path).clone()), secp)
+ .zip(Some((path, expected)))
+ })
+ .and_then(|(prefix, (full_path, expected))| {
+ let derive_path = full_path
+ .into_iter()
+ .skip(prefix.into_iter().count())
+ .cloned()
+ .collect::<DerivationPath>();
+
+ // `derive_path` only contains the replacement index for the wildcard, if present, or
+ // an empty path for fixed descriptors. To verify the key we also need the normal steps
+ // that come before the wildcard, so we take them directly from `xpub` and then append
+ // the final index
+ if verify_key(
+ xpub,
+ &xpub.derivation_path.extend(derive_path.clone()),
+ expected,
+ ) {
+ Some(derive_path)
+ } else {
+ None
+ }
+ });
+
+ match derive_path {
+ Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
+ // Ignore hardened wildcards
+ if let ChildNumber::Normal { index } = path[0] {
+ path_found = Some(index);
+ return true;
+ }
+ }
+ Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
+ path_found = Some(0);
+ return true;
+ }
+ _ => {}
+ }
+ }
+
+ false
+ });
+
+ path_found.map(|path| {
+ self.at_derivation_index(path)
+ .expect("We ignore hardened wildcards")
+ })
+ }
+
+ fn derive_from_hd_keypaths(
+ &self,
+ hd_keypaths: &HdKeyPaths,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor> {
+ // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
+ let key_origins = hd_keypaths
+ .iter()
+ .map(|(pk, (fingerprint, path))| {
+ (
+ *fingerprint,
+ (path, SinglePubKey::FullKey(PublicKey::new(*pk))),
+ )
+ })
+ .collect();
+ self.derive_from_psbt_key_origins(key_origins, secp)
+ }
+
+ fn derive_from_tap_key_origins(
+ &self,
+ tap_key_origins: &TapKeyOrigins,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor> {
+ // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
+ let key_origins = tap_key_origins
+ .iter()
+ .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk))))
+ .collect();
+ self.derive_from_psbt_key_origins(key_origins, secp)
+ }
+
+ fn derive_from_psbt_input(
+ &self,
+ psbt_input: &psbt::Input,
+ utxo: Option<TxOut>,
+ secp: &SecpCtx,
+ ) -> Option<DerivedDescriptor> {
+ if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
+ return Some(derived);
+ }
+ if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
+ return Some(derived);
+ }
+ if self.has_wildcard() {
+ // We can't try to bruteforce the derivation index, exit here
+ return None;
+ }
+
+ let descriptor = self.at_derivation_index(0).expect("0 is not hardened");
+ match descriptor.desc_type() {
+ // TODO: add pk() here
+ DescriptorType::Pkh
+ | DescriptorType::Wpkh
+ | DescriptorType::ShWpkh
+ | DescriptorType::Tr
+ if utxo.is_some()
+ && descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
+ {
+ Some(descriptor)
+ }
+ DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
+ if psbt_input.redeem_script.is_some()
+ && &descriptor.explicit_script().unwrap()
+ == psbt_input.redeem_script.as_ref().unwrap() =>
+ {
+ Some(descriptor)
+ }
+ DescriptorType::Wsh
+ | DescriptorType::ShWsh
+ | DescriptorType::ShWshSortedMulti
+ | DescriptorType::WshSortedMulti
+ if psbt_input.witness_script.is_some()
+ && &descriptor.explicit_script().unwrap()
+ == psbt_input.witness_script.as_ref().unwrap() =>
+ {
+ Some(descriptor)
+ }
+ _ => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use alloc::string::ToString;
+ use core::str::FromStr;
+
+ use assert_matches::assert_matches;
+ use bitcoin::hex::FromHex;
+ use bitcoin::secp256k1::Secp256k1;
+ use bitcoin::ScriptBuf;
+ use bitcoin::{bip32, Psbt};
+
+ use super::*;
+ use crate::psbt::PsbtUtils;
+
+ #[test]
+ fn test_derive_from_psbt_input_wpkh_wif() {
+ let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
+ "wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)",
+ )
+ .unwrap();
+ let psbt = Psbt::deserialize(
+ &Vec::<u8>::from_hex(
+ "70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\
+ 11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\
+ 001493ce48570b55c42c2af816aeaba06cfee1224fae000000000001011fa086\
+ 01000000000016001493ce48570b55c42c2af816aeaba06cfee1224fae010304\
+ 010000000000",
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ assert!(descriptor
+ .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
+ .is_some());
+ }
+
+ #[test]
+ fn test_derive_from_psbt_input_pkh_tpub() {
+ let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
+ "pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)",
+ )
+ .unwrap();
+ let psbt = Psbt::deserialize(
+ &Vec::<u8>::from_hex(
+ "70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\
+ e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\
+ a91432bb94283282f72b2e034709e348c44d5a4db0ef8700000000000100f902\
+ 0000000001010167e99c0eb67640f3a1b6805f2d8be8238c947f8aaf49eb0a9c\
+ bee6a42c984200000000171600142b29a22019cca05b9c2b2d283a4c4489e1cf\
+ 9f8ffeffffff02a01dced06100000017a914e2abf033cadbd74f0f4c74946201\
+ decd20d5c43c8780969800000000001976a9148b0fce5fb1264e599a65387313\
+ 3c95478b902eb288ac02473044022015d9211576163fa5b001e84dfa3d44efd9\
+ 86b8f3a0d3d2174369288b2b750906022048dacc0e5d73ae42512fd2b97e2071\
+ a8d0bce443b390b1fe0b8128fe70ec919e01210232dad1c5a67dcb0116d407e2\
+ 52584228ab7ec00e8b9779d0c3ffe8114fc1a7d2c80600000103040100000022\
+ 0603433b83583f8c4879b329dd08bbc7da935e4cc02f637ff746e05f0466ffb2\
+ a6a2180f0569432c00008000000080000000800a000000000000000000",
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ assert!(descriptor
+ .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
+ .is_some());
+ }
+
+ #[test]
+ fn test_derive_from_psbt_input_wsh() {
+ let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
+ "wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))",
+ )
+ .unwrap();
+ let psbt = Psbt::deserialize(
+ &Vec::<u8>::from_hex(
+ "70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\
+ 67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\
+ a914ad105f61102e0d01d7af40d06d6a5c3ae2f7fde387000000000001012b80\
+ 969800000000002200203ca72f106a72234754890ca7640c43f65d2174e44d33\
+ 336030f9059345091044010304010000000105252103b6633fef2397a0a9de9d\
+ 7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14ad56b20000",
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ assert!(descriptor
+ .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
+ .is_some());
+ }
+
+ #[test]
+ fn test_derive_from_psbt_input_sh() {
+ let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
+ "sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))",
+ )
+ .unwrap();
+ let psbt = Psbt::deserialize(
+ &Vec::<u8>::from_hex(
+ "70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\
+ a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\
+ a91457b148ba4d3e5fa8608a8657875124e3d1c9390887f09c0900000100e002\
+ 0000000001016ba1bbe05cc93574a0d611ec7d93ad0ab6685b28d0cd80e8a82d\
+ debb326643c90100000000feffffff02809698000000000017a914d9a6e8c455\
+ 8e16c8253afe53ce37ad61cf4c38c487403504cf6100000017a9144044fb6e0b\
+ 757dfc1b34886b6a95aef4d3db137e870247304402202a9b72d939bcde8ba2a1\
+ e0980597e47af4f5c152a78499143c3d0a78ac2286a602207a45b1df9e93b8c9\
+ 6f09f5c025fe3e413ca4b905fe65ee55d32a3276439a9b8f012102dc1fcc2636\
+ 4da1aa718f03d8d9bd6f2ff410ed2cf1245a168aa3bcc995ac18e0a806000001\
+ 03040100000001042821021403881a5587297818fcaf17d239cefca22fce84a4\
+ 5b3b1d23e836c4af671dbbad03f09c09b10000",
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ assert!(descriptor
+ .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new())
+ .is_some());
+ }
+
+ #[test]
+ fn test_to_wallet_descriptor_fixup_networks() {
+ use crate::keys::{any_network, IntoDescriptorKey};
+
+ let secp = Secp256k1::new();
+
+ let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
+ let path = bip32::DerivationPath::from_str("m/0").unwrap();
+
+ // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
+ // we are using an "xpub"
+ let key = (xprv, path.clone()).into_descriptor_key().unwrap();
+ // override it with any. this happens in some key conversions, like bip39
+ let key = key.override_valid_networks(any_network());
+
+ // make a descriptor out of it
+ let desc = crate::descriptor!(wpkh(key)).unwrap();
+ // this should convert the key that supports "any_network" to the right network (testnet)
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+
+ let mut xprv_testnet = xprv;
+ xprv_testnet.network = Network::Testnet;
+
+ let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet);
+ let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
+ xkey: xpub_testnet,
+ origin: None,
+ derivation_path: path,
+ wildcard: Wildcard::Unhardened,
+ });
+
+ assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
+ assert_eq!(
+ keymap
+ .get(&desc_pubkey)
+ .map(|key| key.to_public(&secp).unwrap()),
+ Some(desc_pubkey)
+ );
+ }
+
+ // test IntoWalletDescriptor trait from &str with and without checksum appended
+ #[test]
+ fn test_descriptor_from_str_with_checksum() {
+ let secp = Secp256k1::new();
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
+ }
+
+ // test IntoWalletDescriptor trait from &str with keys from right and wrong network
+ #[test]
+ fn test_descriptor_from_str_with_keys_network() {
+ let secp = Secp256k1::new();
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Regtest);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Regtest);
+ assert!(desc.is_ok());
+
+ let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
+ .into_wallet_descriptor(&secp, Network::Testnet);
+ assert!(desc.is_ok());
+
+ let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
+ .into_wallet_descriptor(&secp, Network::Bitcoin);
+ assert!(desc.is_ok());
+
+ let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Bitcoin);
+ assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
+
+ let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
+ .into_wallet_descriptor(&secp, Network::Bitcoin);
+ assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
+ }
+
+ // test IntoWalletDescriptor trait from the output of the descriptor!() macro
+ #[test]
+ fn test_descriptor_from_str_from_output_of_macro() {
+ let secp = Secp256k1::new();
+
+ let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
+ let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
+ let key = (tpub, path).into_descriptor_key().unwrap();
+
+ // make a descriptor out of it
+ let desc = crate::descriptor!(wpkh(key)).unwrap();
+
+ let (wallet_desc, _) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let wallet_desc_str = wallet_desc.to_string();
+ assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw");
+
+ let (wallet_desc2, _) = wallet_desc_str
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ assert_eq!(wallet_desc, wallet_desc2)
+ }
+
+ #[test]
+ fn test_into_wallet_descriptor_checked() {
+ let secp = Secp256k1::new();
+
+ let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
+ let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+
+ assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
+
+ let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
+ let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+
+ assert_matches!(result, Err(DescriptorError::MultiPath));
+
+ // repeated pubkeys
+ let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
+ let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_sh_wsh_sortedmulti_redeemscript() {
+ use miniscript::psbt::PsbtInputExt;
+
+ let secp = Secp256k1::new();
+
+ let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
+ let (descriptor, _) =
+ into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
+
+ let descriptor = descriptor.at_derivation_index(0).unwrap();
+
+ let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
+
+ let mut psbt_input = psbt::Input::default();
+ psbt_input
+ .update_with_descriptor_unchecked(&descriptor)
+ .unwrap();
+
+ assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh()));
+ assert_eq!(psbt_input.witness_script, Some(script));
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptor policy
+//!
+//! This module implements the logic to extract and represent the spending policies of a descriptor
+//! in a more human-readable format.
+//!
+//! This is an **EXPERIMENTAL** feature, API and other major changes are expected.
+//!
+//! ## Example
+//!
+//! ```
+//! # use std::sync::Arc;
+//! # use bdk_wallet::descriptor::*;
+//! # use bdk_wallet::wallet::signer::*;
+//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
+//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
+//! let secp = Secp256k1::new();
+//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
+//!
+//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?;
+//! println!("{:?}", extended_desc);
+//!
+//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
+//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
+//! println!("policy: {}", serde_json::to_string(&policy).unwrap());
+//! # Ok::<(), anyhow::Error>(())
+//! ```
+
+use crate::collections::{BTreeMap, HashSet, VecDeque};
+use alloc::string::String;
+use alloc::vec::Vec;
+use core::cmp::max;
+
+use core::fmt;
+
+use serde::ser::SerializeMap;
+use serde::{Serialize, Serializer};
+
+use bitcoin::bip32::Fingerprint;
+use bitcoin::hashes::{hash160, ripemd160, sha256};
+use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
+
+use miniscript::descriptor::{
+ DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
+};
+use miniscript::hash256;
+use miniscript::{
+ Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
+};
+
+use crate::descriptor::ExtractPolicy;
+use crate::keys::ExtScriptContext;
+use crate::wallet::signer::{SignerId, SignersContainer};
+use crate::wallet::utils::{After, Older, SecpCtx};
+
+use super::checksum::calc_checksum;
+use super::error::Error;
+use super::XKeyUtils;
+use bitcoin::psbt::{self, Psbt};
+use miniscript::psbt::PsbtInputSatisfier;
+
+/// A unique identifier for a key
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PkOrF {
+ /// A legacy public key
+ Pubkey(PublicKey),
+ /// A x-only public key
+ XOnlyPubkey(XOnlyPublicKey),
+ /// An extended key fingerprint
+ Fingerprint(Fingerprint),
+}
+
+impl PkOrF {
+ fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
+ match k {
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::FullKey(pk),
+ ..
+ }) => PkOrF::Pubkey(*pk),
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::XOnly(pk),
+ ..
+ }) => PkOrF::XOnlyPubkey(*pk),
+ DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
+ DescriptorPublicKey::MultiXPub(multi) => {
+ PkOrF::Fingerprint(multi.root_fingerprint(secp))
+ }
+ }
+ }
+}
+
+/// An item that needs to be satisfied
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(tag = "type", rename_all = "UPPERCASE")]
+pub enum SatisfiableItem {
+ // Leaves
+ /// ECDSA Signature for a raw public key
+ EcdsaSignature(PkOrF),
+ /// Schnorr Signature for a raw public key
+ SchnorrSignature(PkOrF),
+ /// SHA256 preimage hash
+ Sha256Preimage {
+ /// The digest value
+ hash: sha256::Hash,
+ },
+ /// Double SHA256 preimage hash
+ Hash256Preimage {
+ /// The digest value
+ hash: hash256::Hash,
+ },
+ /// RIPEMD160 preimage hash
+ Ripemd160Preimage {
+ /// The digest value
+ hash: ripemd160::Hash,
+ },
+ /// SHA256 then RIPEMD160 preimage hash
+ Hash160Preimage {
+ /// The digest value
+ hash: hash160::Hash,
+ },
+ /// Absolute timeclock timestamp
+ AbsoluteTimelock {
+ /// The timelock value
+ value: absolute::LockTime,
+ },
+ /// Relative timelock locktime
+ RelativeTimelock {
+ /// The timelock value
+ value: Sequence,
+ },
+ /// Multi-signature public keys with threshold count
+ Multisig {
+ /// The raw public key or extended key fingerprint
+ keys: Vec<PkOrF>,
+ /// The required threshold count
+ threshold: usize,
+ },
+
+ // Complex item
+ /// Threshold items with threshold count
+ Thresh {
+ /// The policy items
+ items: Vec<Policy>,
+ /// The required threshold count
+ threshold: usize,
+ },
+}
+
+impl SatisfiableItem {
+ /// Returns whether the [`SatisfiableItem`] is a leaf item
+ pub fn is_leaf(&self) -> bool {
+ !matches!(
+ self,
+ SatisfiableItem::Thresh {
+ items: _,
+ threshold: _,
+ }
+ )
+ }
+
+ /// Returns a unique id for the [`SatisfiableItem`]
+ pub fn id(&self) -> String {
+ calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
+ .expect("Failed to compute a SatisfiableItem id")
+ }
+}
+
+fn combinations(vec: &[usize], size: usize) -> Vec<Vec<usize>> {
+ assert!(vec.len() >= size);
+
+ let mut answer = Vec::new();
+
+ let mut queue = VecDeque::new();
+ for (index, val) in vec.iter().enumerate() {
+ let mut new_vec = Vec::with_capacity(size);
+ new_vec.push(*val);
+ queue.push_back((index, new_vec));
+ }
+
+ while let Some((index, vals)) = queue.pop_front() {
+ if vals.len() >= size {
+ answer.push(vals);
+ } else {
+ for (new_index, val) in vec.iter().skip(index + 1).enumerate() {
+ let mut cloned = vals.clone();
+ cloned.push(*val);
+ queue.push_front((new_index, cloned));
+ }
+ }
+ }
+
+ answer
+}
+
+fn mix<T: Clone>(vec: Vec<Vec<T>>) -> Vec<Vec<T>> {
+ if vec.is_empty() || vec.iter().any(Vec::is_empty) {
+ return vec![];
+ }
+
+ let mut answer = Vec::new();
+ let size = vec.len();
+
+ let mut queue = VecDeque::new();
+ for i in &vec[0] {
+ let mut new_vec = Vec::with_capacity(size);
+ new_vec.push(i.clone());
+ queue.push_back(new_vec);
+ }
+
+ while let Some(vals) = queue.pop_front() {
+ if vals.len() >= size {
+ answer.push(vals);
+ } else {
+ let level = vals.len();
+ for i in &vec[level] {
+ let mut cloned = vals.clone();
+ cloned.push(i.clone());
+ queue.push_front(cloned);
+ }
+ }
+ }
+
+ answer
+}
+
+/// Type for a map of sets of [`Condition`] items keyed by each set's index
+pub type ConditionMap = BTreeMap<usize, HashSet<Condition>>;
+/// Type for a map of folded sets of [`Condition`] items keyed by a vector of the combined set's indexes
+pub type FoldedConditionMap = BTreeMap<Vec<usize>, HashSet<Condition>>;
+
+fn serialize_folded_cond_map<S>(
+ input_map: &FoldedConditionMap,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ let mut map = serializer.serialize_map(Some(input_map.len()))?;
+ for (k, v) in input_map {
+ let k_string = format!("{:?}", k);
+ map.serialize_entry(&k_string, v)?;
+ }
+ map.end()
+}
+
+/// Represent if and how much a policy item is satisfied by the wallet's descriptor
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(tag = "type", rename_all = "UPPERCASE")]
+pub enum Satisfaction {
+ /// Only a partial satisfaction of some kind of threshold policy
+ Partial {
+ /// Total number of items
+ n: usize,
+ /// Threshold
+ m: usize,
+ /// The items that can be satisfied by the descriptor or are satisfied in the PSBT
+ items: Vec<usize>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ /// Whether the items are sorted in lexicographic order (used by `sortedmulti`)
+ sorted: Option<bool>,
+ #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+ /// Extra conditions that also need to be satisfied
+ conditions: ConditionMap,
+ },
+ /// Can reach the threshold of some kind of threshold policy
+ PartialComplete {
+ /// Total number of items
+ n: usize,
+ /// Threshold
+ m: usize,
+ /// The items that can be satisfied by the descriptor
+ items: Vec<usize>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ /// Whether the items are sorted in lexicographic order (used by `sortedmulti`)
+ sorted: Option<bool>,
+ #[serde(
+ serialize_with = "serialize_folded_cond_map",
+ skip_serializing_if = "BTreeMap::is_empty"
+ )]
+ /// Extra conditions that also need to be satisfied
+ conditions: FoldedConditionMap,
+ },
+
+ /// Can satisfy the policy item
+ Complete {
+ /// Extra conditions that also need to be satisfied
+ condition: Condition,
+ },
+ /// Cannot satisfy or contribute to the policy item
+ None,
+}
+
+impl Satisfaction {
+ /// Returns whether the [`Satisfaction`] is a leaf item
+ pub fn is_leaf(&self) -> bool {
+ match self {
+ Satisfaction::None | Satisfaction::Complete { .. } => true,
+ Satisfaction::PartialComplete { .. } | Satisfaction::Partial { .. } => false,
+ }
+ }
+
+ // add `inner` as one of self's partial items. this only makes sense on partials
+ fn add(&mut self, inner: &Satisfaction, inner_index: usize) -> Result<(), PolicyError> {
+ match self {
+ Satisfaction::None | Satisfaction::Complete { .. } => Err(PolicyError::AddOnLeaf),
+ Satisfaction::PartialComplete { .. } => Err(PolicyError::AddOnPartialComplete),
+ Satisfaction::Partial {
+ n,
+ ref mut conditions,
+ ref mut items,
+ ..
+ } => {
+ if inner_index >= *n || items.contains(&inner_index) {
+ return Err(PolicyError::IndexOutOfRange(inner_index));
+ }
+
+ match inner {
+ // not relevant if not completed yet
+ Satisfaction::None | Satisfaction::Partial { .. } => return Ok(()),
+ Satisfaction::Complete { condition } => {
+ items.push(inner_index);
+ conditions.insert(inner_index, vec![*condition].into_iter().collect());
+ }
+ Satisfaction::PartialComplete {
+ conditions: other_conditions,
+ ..
+ } => {
+ items.push(inner_index);
+ let conditions_set = other_conditions
+ .values()
+ .fold(HashSet::new(), |set, i| set.union(i).cloned().collect());
+ conditions.insert(inner_index, conditions_set);
+ }
+ }
+
+ Ok(())
+ }
+ }
+ }
+
+ fn finalize(&mut self) {
+ // if partial try to bump it to a partialcomplete
+ if let Satisfaction::Partial {
+ n,
+ m,
+ items,
+ conditions,
+ sorted,
+ } = self
+ {
+ if items.len() >= *m {
+ let mut map = BTreeMap::new();
+ let indexes = combinations(items, *m);
+ // `indexes` at this point is a Vec<Vec<usize>>, with the "n choose k" of items (m of n)
+ indexes
+ .into_iter()
+ // .inspect(|x| println!("--- orig --- {:?}", x))
+ // we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one
+ // condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we
+ // consider every possible options and check whether or not they are compatible.
+ // since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out
+ .flat_map(|i_vec| {
+ mix(i_vec
+ .iter()
+ .map(|i| {
+ conditions
+ .get(i)
+ .map(|set| set.clone().into_iter().collect())
+ .unwrap_or_default()
+ })
+ .collect())
+ .into_iter()
+ .map(|x| (i_vec.clone(), x))
+ .collect::<Vec<(Vec<usize>, Vec<Condition>)>>()
+ })
+ // .inspect(|x| println!("flat {:?}", x))
+ // try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err
+ .map(|(key, val)| {
+ (
+ key,
+ val.into_iter()
+ .try_fold(Condition::default(), |acc, v| acc.merge(&v)),
+ )
+ })
+ // .inspect(|x| println!("try_fold {:?}", x))
+ // filter out all the incompatible combinations
+ .filter(|(_, val)| val.is_ok())
+ // .inspect(|x| println!("filter {:?}", x))
+ // push them into the map
+ .for_each(|(key, val)| {
+ map.entry(key)
+ .or_insert_with(HashSet::new)
+ .insert(val.unwrap());
+ });
+ // TODO: if the map is empty, the conditions are not compatible, return an error?
+ *self = Satisfaction::PartialComplete {
+ n: *n,
+ m: *m,
+ items: items.clone(),
+ conditions: map,
+ sorted: *sorted,
+ };
+ }
+ }
+ }
+}
+
+impl From<bool> for Satisfaction {
+ fn from(other: bool) -> Self {
+ if other {
+ Satisfaction::Complete {
+ condition: Default::default(),
+ }
+ } else {
+ Satisfaction::None
+ }
+ }
+}
+
+/// Descriptor spending policy
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct Policy {
+ /// Identifier for this policy node
+ pub id: String,
+
+ /// Type of this policy node
+ #[serde(flatten)]
+ pub item: SatisfiableItem,
+ /// How much a given PSBT already satisfies this policy node in terms of signatures
+ pub satisfaction: Satisfaction,
+ /// How the wallet's descriptor can satisfy this policy node
+ pub contribution: Satisfaction,
+}
+
+/// An extra condition that must be satisfied but that is out of control of the user
+/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
+#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
+pub struct Condition {
+ /// Optional CheckSequenceVerify condition
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub csv: Option<Sequence>,
+ /// Optional timelock condition
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub timelock: Option<absolute::LockTime>,
+}
+
+impl Condition {
+ fn merge_nlocktime(
+ a: absolute::LockTime,
+ b: absolute::LockTime,
+ ) -> Result<absolute::LockTime, PolicyError> {
+ if !a.is_same_unit(b) {
+ Err(PolicyError::MixedTimelockUnits)
+ } else if a > b {
+ Ok(a)
+ } else {
+ Ok(b)
+ }
+ }
+
+ fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
+ if a.is_time_locked() != b.is_time_locked() {
+ Err(PolicyError::MixedTimelockUnits)
+ } else {
+ Ok(max(a, b))
+ }
+ }
+
+ pub(crate) fn merge(mut self, other: &Condition) -> Result<Self, PolicyError> {
+ match (self.csv, other.csv) {
+ (Some(a), Some(b)) => self.csv = Some(Self::merge_nsequence(a, b)?),
+ (None, any) => self.csv = any,
+ _ => {}
+ }
+
+ match (self.timelock, other.timelock) {
+ (Some(a), Some(b)) => self.timelock = Some(Self::merge_nlocktime(a, b)?),
+ (None, any) => self.timelock = any,
+ _ => {}
+ }
+
+ Ok(self)
+ }
+
+ /// Returns `true` if there are no extra conditions to verify
+ pub fn is_null(&self) -> bool {
+ self.csv.is_none() && self.timelock.is_none()
+ }
+}
+
+/// Errors that can happen while extracting and manipulating policies
+#[derive(Debug, PartialEq, Eq)]
+pub enum PolicyError {
+ /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`]
+ NotEnoughItemsSelected(String),
+ /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`]
+ IndexOutOfRange(usize),
+ /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`]
+ AddOnLeaf,
+ /// Can not add to an item that is [`Satisfaction::PartialComplete`]
+ AddOnPartialComplete,
+ /// Can not merge CSV or timelock values unless both are less than or both are equal or greater than 500_000_000
+ MixedTimelockUnits,
+ /// Incompatible conditions (not currently used)
+ IncompatibleConditions,
+}
+
+impl fmt::Display for PolicyError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err),
+ Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index),
+ Self::AddOnLeaf => write!(f, "Add on leaf"),
+ Self::AddOnPartialComplete => write!(f, "Add on partial complete"),
+ Self::MixedTimelockUnits => write!(f, "Mixed timelock units"),
+ Self::IncompatibleConditions => write!(f, "Incompatible conditions"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for PolicyError {}
+
+impl Policy {
+ fn new(item: SatisfiableItem) -> Self {
+ Policy {
+ id: item.id(),
+ item,
+ satisfaction: Satisfaction::None,
+ contribution: Satisfaction::None,
+ }
+ }
+
+ fn make_and(a: Option<Policy>, b: Option<Policy>) -> Result<Option<Policy>, PolicyError> {
+ match (a, b) {
+ (None, None) => Ok(None),
+ (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
+ (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2),
+ }
+ }
+
+ fn make_or(a: Option<Policy>, b: Option<Policy>) -> Result<Option<Policy>, PolicyError> {
+ match (a, b) {
+ (None, None) => Ok(None),
+ (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
+ (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1),
+ }
+ }
+
+ fn make_thresh(items: Vec<Policy>, threshold: usize) -> Result<Option<Policy>, PolicyError> {
+ if threshold == 0 {
+ return Ok(None);
+ }
+
+ let mut contribution = Satisfaction::Partial {
+ n: items.len(),
+ m: threshold,
+ items: vec![],
+ conditions: Default::default(),
+ sorted: None,
+ };
+ let mut satisfaction = contribution.clone();
+ for (index, item) in items.iter().enumerate() {
+ contribution.add(&item.contribution, index)?;
+ satisfaction.add(&item.satisfaction, index)?;
+ }
+
+ contribution.finalize();
+ satisfaction.finalize();
+
+ let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
+ policy.contribution = contribution;
+ policy.satisfaction = satisfaction;
+
+ Ok(Some(policy))
+ }
+
+ fn make_multisig<Ctx: ScriptContext + 'static>(
+ keys: &[DescriptorPublicKey],
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ threshold: usize,
+ sorted: bool,
+ secp: &SecpCtx,
+ ) -> Result<Option<Policy>, PolicyError> {
+ if threshold == 0 {
+ return Ok(None);
+ }
+
+ let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect();
+
+ let mut contribution = Satisfaction::Partial {
+ n: keys.len(),
+ m: threshold,
+ items: vec![],
+ conditions: Default::default(),
+ sorted: Some(sorted),
+ };
+ let mut satisfaction = contribution.clone();
+
+ for (index, key) in keys.iter().enumerate() {
+ if signers.find(signer_id(key, secp)).is_some() {
+ contribution.add(
+ &Satisfaction::Complete {
+ condition: Default::default(),
+ },
+ index,
+ )?;
+ }
+
+ if let Some(psbt) = build_sat.psbt() {
+ if Ctx::find_signature(psbt, key, secp) {
+ satisfaction.add(
+ &Satisfaction::Complete {
+ condition: Default::default(),
+ },
+ index,
+ )?;
+ }
+ }
+ }
+ satisfaction.finalize();
+ contribution.finalize();
+
+ let mut policy: Policy = SatisfiableItem::Multisig {
+ keys: parsed_keys,
+ threshold,
+ }
+ .into();
+ policy.contribution = contribution;
+ policy.satisfaction = satisfaction;
+
+ Ok(Some(policy))
+ }
+
+ /// Return whether or not a specific path in the policy tree is required to unambiguously
+ /// create a transaction
+ ///
+ /// What this means is that for some spending policies the user should select which paths in
+ /// the tree it intends to satisfy while signing, because the transaction must be created differently based
+ /// on that.
+ pub fn requires_path(&self) -> bool {
+ self.get_condition(&BTreeMap::new()).is_err()
+ }
+
+ /// Return the conditions that are set by the spending policy for a given path in the
+ /// policy tree
+ pub fn get_condition(
+ &self,
+ path: &BTreeMap<String, Vec<usize>>,
+ ) -> Result<Condition, PolicyError> {
+ // if items.len() == threshold, selected can be omitted and we take all of them by default
+ let default = match &self.item {
+ SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => {
+ (0..*threshold).collect()
+ }
+ SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(),
+ _ => HashSet::new(),
+ };
+ let selected: HashSet<_> = match path.get(&self.id) {
+ Some(arr) => arr.iter().copied().collect(),
+ _ => default,
+ };
+
+ match &self.item {
+ SatisfiableItem::Thresh { items, threshold } => {
+ let mapped_req = items
+ .iter()
+ .map(|i| i.get_condition(path))
+ .collect::<Vec<_>>();
+
+ // if all the requirements are null we don't care about `selected` because there
+ // are no requirements
+ if mapped_req
+ .iter()
+ .all(|cond| matches!(cond, Ok(c) if c.is_null()))
+ {
+ return Ok(Condition::default());
+ }
+
+ // make sure all the indexes in the `selected` list are within range
+ for index in &selected {
+ if *index >= items.len() {
+ return Err(PolicyError::IndexOutOfRange(*index));
+ }
+ }
+
+ // if we have something, make sure we have enough items. note that the user can set
+ // an empty value for this step in case of n-of-n, because `selected` is set to all
+ // the elements above
+ if selected.len() < *threshold {
+ return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
+ }
+
+ // check the selected items, see if there are conflicting requirements
+ mapped_req
+ .into_iter()
+ .enumerate()
+ .filter(|(index, _)| selected.contains(index))
+ .try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?))
+ }
+ SatisfiableItem::Multisig { keys, threshold } => {
+ if selected.len() < *threshold {
+ return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
+ }
+ if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) {
+ return Err(PolicyError::IndexOutOfRange(item));
+ }
+
+ Ok(Condition::default())
+ }
+ SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition {
+ csv: None,
+ timelock: Some(*value),
+ }),
+ SatisfiableItem::RelativeTimelock { value } => Ok(Condition {
+ csv: Some(*value),
+ timelock: None,
+ }),
+ _ => Ok(Condition::default()),
+ }
+ }
+}
+
+impl From<SatisfiableItem> for Policy {
+ fn from(other: SatisfiableItem) -> Self {
+ Self::new(other)
+ }
+}
+
+fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
+ // For consistency we always compute the key hash in "ecdsa" form (with the leading sign
+ // prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
+ // for a key, so it doesn't really matter how the identifier is computed.
+ match key {
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::FullKey(pk),
+ ..
+ }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::XOnly(pk),
+ ..
+ }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
+ DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
+ DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(),
+ }
+}
+
+fn make_generic_signature<M: Fn() -> SatisfiableItem, F: Fn(&Psbt) -> bool>(
+ key: &DescriptorPublicKey,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ make_policy: M,
+ find_sig: F,
+) -> Policy {
+ let mut policy: Policy = make_policy().into();
+
+ policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
+ Satisfaction::Complete {
+ condition: Default::default(),
+ }
+ } else {
+ Satisfaction::None
+ };
+
+ if let Some(psbt) = build_sat.psbt() {
+ policy.satisfaction = if find_sig(psbt) {
+ Satisfaction::Complete {
+ condition: Default::default(),
+ }
+ } else {
+ Satisfaction::None
+ };
+ }
+
+ policy
+}
+
+fn generic_sig_in_psbt<
+ // C is for "check", it's a closure we use to *check* if a psbt input contains the signature
+ // for a specific key
+ C: Fn(&psbt::Input, &SinglePubKey) -> bool,
+ // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input
+ E: Fn(&psbt::Input, Fingerprint) -> Option<SinglePubKey>,
+>(
+ psbt: &Psbt,
+ key: &DescriptorPublicKey,
+ secp: &SecpCtx,
+ check: C,
+ extract: E,
+) -> bool {
+ //TODO check signature validity
+ psbt.inputs.iter().all(|input| match key {
+ DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
+ DescriptorPublicKey::XPub(xpub) => {
+ //TODO check actual derivation matches
+ match extract(input, xpub.root_fingerprint(secp)) {
+ Some(pubkey) => check(input, &pubkey),
+ None => false,
+ }
+ }
+ DescriptorPublicKey::MultiXPub(xpub) => {
+ //TODO check actual derivation matches
+ match extract(input, xpub.root_fingerprint(secp)) {
+ Some(pubkey) => check(input, &pubkey),
+ None => false,
+ }
+ }
+ })
+}
+
+trait SigExt: ScriptContext {
+ fn make_signature(
+ key: &DescriptorPublicKey,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Policy;
+
+ fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool;
+}
+
+impl<T: ScriptContext + 'static> SigExt for T {
+ fn make_signature(
+ key: &DescriptorPublicKey,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Policy {
+ if T::as_enum().is_taproot() {
+ make_generic_signature(
+ key,
+ signers,
+ build_sat,
+ secp,
+ || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)),
+ |psbt| Self::find_signature(psbt, key, secp),
+ )
+ } else {
+ make_generic_signature(
+ key,
+ signers,
+ build_sat,
+ secp,
+ || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)),
+ |psbt| Self::find_signature(psbt, key, secp),
+ )
+ }
+ }
+
+ fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
+ if T::as_enum().is_taproot() {
+ generic_sig_in_psbt(
+ psbt,
+ key,
+ secp,
+ |input, pk| {
+ let pk = match pk {
+ SinglePubKey::XOnly(pk) => pk,
+ _ => return false,
+ };
+
+ if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() {
+ true
+ } else {
+ input.tap_script_sigs.keys().any(|(sk, _)| sk == pk)
+ }
+ },
+ |input, fing| {
+ input
+ .tap_key_origins
+ .iter()
+ .find(|(_, (_, (f, _)))| f == &fing)
+ .map(|(pk, _)| SinglePubKey::XOnly(*pk))
+ },
+ )
+ } else {
+ generic_sig_in_psbt(
+ psbt,
+ key,
+ secp,
+ |input, pk| match pk {
+ SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk),
+ _ => false,
+ },
+ |input, fing| {
+ input
+ .bip32_derivation
+ .iter()
+ .find(|(_, (f, _))| f == &fing)
+ .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk)))
+ },
+ )
+ }
+ }
+}
+
+impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
+ fn extract_policy(
+ &self,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Result<Option<Policy>, Error> {
+ Ok(match &self.node {
+ // Leaves
+ Terminal::True | Terminal::False => None,
+ Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)),
+ Terminal::PkH(pubkey_hash) => {
+ Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
+ }
+ Terminal::After(value) => {
+ let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
+ value: (*value).into(),
+ }
+ .into();
+ policy.contribution = Satisfaction::Complete {
+ condition: Condition {
+ timelock: Some((*value).into()),
+ csv: None,
+ },
+ };
+ if let BuildSatisfaction::PsbtTimelocks {
+ current_height,
+ psbt,
+ ..
+ } = build_sat
+ {
+ let after = After::new(Some(current_height), false);
+ let after_sat =
+ Satisfier::<bitcoin::PublicKey>::check_after(&after, (*value).into());
+ let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
+ Satisfier::<bitcoin::PublicKey>::check_after(&sat, (*value).into())
+ });
+ if after_sat && inputs_sat {
+ policy.satisfaction = policy.contribution.clone();
+ }
+ }
+
+ Some(policy)
+ }
+ Terminal::Older(value) => {
+ let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into();
+ policy.contribution = Satisfaction::Complete {
+ condition: Condition {
+ timelock: None,
+ csv: Some(*value),
+ },
+ };
+ if let BuildSatisfaction::PsbtTimelocks {
+ current_height,
+ input_max_height,
+ psbt,
+ } = build_sat
+ {
+ let older = Older::new(Some(current_height), Some(input_max_height), false);
+ let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
+ let inputs_sat = psbt_inputs_sat(psbt)
+ .all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
+ if older_sat && inputs_sat {
+ policy.satisfaction = policy.contribution.clone();
+ }
+ }
+
+ Some(policy)
+ }
+ Terminal::Sha256(hash) => Some(SatisfiableItem::Sha256Preimage { hash: *hash }.into()),
+ Terminal::Hash256(hash) => {
+ Some(SatisfiableItem::Hash256Preimage { hash: *hash }.into())
+ }
+ Terminal::Ripemd160(hash) => {
+ Some(SatisfiableItem::Ripemd160Preimage { hash: *hash }.into())
+ }
+ Terminal::Hash160(hash) => {
+ Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
+ }
+ Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
+ Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
+ }
+ // Identities
+ Terminal::Alt(inner)
+ | Terminal::Swap(inner)
+ | Terminal::Check(inner)
+ | Terminal::DupIf(inner)
+ | Terminal::Verify(inner)
+ | Terminal::NonZero(inner)
+ | Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?,
+ // Complex policies
+ Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and(
+ a.extract_policy(signers, build_sat, secp)?,
+ b.extract_policy(signers, build_sat, secp)?,
+ )?,
+ Terminal::AndOr(x, y, z) => Policy::make_or(
+ Policy::make_and(
+ x.extract_policy(signers, build_sat, secp)?,
+ y.extract_policy(signers, build_sat, secp)?,
+ )?,
+ z.extract_policy(signers, build_sat, secp)?,
+ )?,
+ Terminal::OrB(a, b)
+ | Terminal::OrD(a, b)
+ | Terminal::OrC(a, b)
+ | Terminal::OrI(a, b) => Policy::make_or(
+ a.extract_policy(signers, build_sat, secp)?,
+ b.extract_policy(signers, build_sat, secp)?,
+ )?,
+ Terminal::Thresh(k, nodes) => {
+ let mut threshold = *k;
+ let mapped: Vec<_> = nodes
+ .iter()
+ .map(|n| n.extract_policy(signers, build_sat, secp))
+ .collect::<Result<Vec<_>, _>>()?
+ .into_iter()
+ .flatten()
+ .collect();
+
+ if mapped.len() < nodes.len() {
+ threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
+ None => return Ok(None),
+ Some(x) => x,
+ };
+ }
+
+ Policy::make_thresh(mapped, threshold)?
+ }
+
+ // Unsupported
+ Terminal::RawPkH(_) => None,
+ })
+ }
+}
+
+fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator<Item = PsbtInputSatisfier> {
+ (0..psbt.inputs.len()).map(move |i| PsbtInputSatisfier::new(psbt, i))
+}
+
+/// Options to build the satisfaction field in the policy
+#[derive(Debug, Clone, Copy)]
+pub enum BuildSatisfaction<'a> {
+ /// Don't generate `satisfaction` field
+ None,
+ /// Analyze the given PSBT to check for existing signatures
+ Psbt(&'a Psbt),
+ /// Like `Psbt` variant and also check for expired timelocks
+ PsbtTimelocks {
+ /// Given PSBT
+ psbt: &'a Psbt,
+ /// Current blockchain height
+ current_height: u32,
+ /// The highest confirmation height between the inputs
+ /// CSV should consider different inputs, but we consider the worst condition for the tx as whole
+ input_max_height: u32,
+ },
+}
+impl<'a> BuildSatisfaction<'a> {
+ fn psbt(&self) -> Option<&'a Psbt> {
+ match self {
+ BuildSatisfaction::None => None,
+ BuildSatisfaction::Psbt(psbt) => Some(psbt),
+ BuildSatisfaction::PsbtTimelocks { psbt, .. } => Some(psbt),
+ }
+ }
+}
+
+impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
+ fn extract_policy(
+ &self,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Result<Option<Policy>, Error> {
+ fn make_sortedmulti<Ctx: ScriptContext + 'static>(
+ keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
+ signers: &SignersContainer,
+ build_sat: BuildSatisfaction,
+ secp: &SecpCtx,
+ ) -> Result<Option<Policy>, Error> {
+ Ok(Policy::make_multisig::<Ctx>(
+ keys.pks.as_ref(),
+ signers,
+ build_sat,
+ keys.k,
+ true,
+ secp,
+ )?)
+ }
+
+ match self {
+ Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature(
+ pk.as_inner(),
+ signers,
+ build_sat,
+ secp,
+ ))),
+ Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
+ pk.as_inner(),
+ signers,
+ build_sat,
+ secp,
+ ))),
+ Descriptor::Sh(sh) => match sh.as_inner() {
+ ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
+ pk.as_inner(),
+ signers,
+ build_sat,
+ secp,
+ ))),
+ ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
+ ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
+ ShInner::Wsh(wsh) => match wsh.as_inner() {
+ WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
+ WshInner::SortedMulti(ref keys) => {
+ make_sortedmulti(keys, signers, build_sat, secp)
+ }
+ },
+ },
+ Descriptor::Wsh(wsh) => match wsh.as_inner() {
+ WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
+ WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
+ },
+ Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
+ Descriptor::Tr(tr) => {
+ // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh`
+ // node with threshold = 1 and the key spend signature plus all the tree leaves
+ let key_spend_sig =
+ miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
+
+ if tr.tap_tree().is_none() {
+ Ok(Some(key_spend_sig))
+ } else {
+ let mut items = vec![key_spend_sig];
+ items.append(
+ &mut tr
+ .iter_scripts()
+ .filter_map(|(_, ms)| {
+ ms.extract_policy(signers, build_sat, secp).transpose()
+ })
+ .collect::<Result<Vec<_>, _>>()?,
+ );
+
+ Ok(Policy::make_thresh(items, 1)?)
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::descriptor;
+ use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
+
+ use super::*;
+ use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
+ use crate::keys::{DescriptorKey, IntoDescriptorKey};
+ use crate::wallet::signer::SignersContainer;
+ use alloc::{string::ToString, sync::Arc};
+ use assert_matches::assert_matches;
+ use bitcoin::bip32;
+ use bitcoin::secp256k1::Secp256k1;
+ use bitcoin::Network;
+ use core::str::FromStr;
+
+ const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
+ const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
+
+ const PATH: &str = "m/44'/1'/0'/0";
+
+ fn setup_keys<Ctx: ScriptContext>(
+ tprv: &str,
+ path: &str,
+ secp: &SecpCtx,
+ ) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
+ let path = bip32::DerivationPath::from_str(path).unwrap();
+ let tprv = bip32::Xpriv::from_str(tprv).unwrap();
+ let tpub = bip32::Xpub::from_priv(secp, &tprv);
+ let fingerprint = tprv.fingerprint(secp);
+ let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
+ let pubkey = (tpub, path).into_descriptor_key().unwrap();
+
+ (prvkey, pubkey, fingerprint)
+ }
+
+ // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi())
+
+ #[test]
+ fn test_extract_policy_for_wpkh() {
+ let secp = Secp256k1::new();
+
+ let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp);
+ let desc = descriptor!(wpkh(pubkey)).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
+ assert_matches!(&policy.contribution, Satisfaction::None);
+
+ let desc = descriptor!(wpkh(prvkey)).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
+ assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
+ }
+
+ // 2 pub keys descriptor, required 2 prv keys
+ #[test]
+ fn test_extract_policy_for_sh_multi_partial_0of2() {
+ let secp = Secp256k1::new();
+ let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let desc = descriptor!(sh(multi(2, pubkey0, pubkey1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
+ && keys[0] == PkOrF::Fingerprint(fingerprint0)
+ && keys[1] == PkOrF::Fingerprint(fingerprint1)
+ );
+ // TODO should this be "Satisfaction::None" since we have no prv keys?
+ // TODO should items and conditions not be empty?
+ assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
+ && m == &2usize
+ && items.is_empty()
+ && conditions.is_empty()
+ );
+ }
+
+ // 1 prv and 1 pub key descriptor, required 2 prv keys
+ #[test]
+ fn test_extract_policy_for_sh_multi_partial_1of2() {
+ let secp = Secp256k1::new();
+ let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let desc = descriptor!(sh(multi(2, prvkey0, pubkey1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+ assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
+ && keys[0] == PkOrF::Fingerprint(fingerprint0)
+ && keys[1] == PkOrF::Fingerprint(fingerprint1)
+ );
+
+ assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
+ && m == &2usize
+ && items.len() == 1
+ && conditions.contains_key(&0)
+ );
+ }
+
+ // 1 prv and 1 pub key descriptor, required 1 prv keys
+ #[test]
+ #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
+ fn test_extract_policy_for_sh_multi_complete_1of2() {
+ let secp = Secp256k1::new();
+
+ let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
+ && keys[0] == PkOrF::Fingerprint(fingerprint0)
+ && keys[1] == PkOrF::Fingerprint(fingerprint1)
+ );
+ assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
+ && m == &1
+ && items.len() == 2
+ && conditions.contains_key(&vec![0])
+ && conditions.contains_key(&vec![1])
+ );
+ }
+
+ // 2 prv keys descriptor, required 2 prv keys
+ #[test]
+ fn test_extract_policy_for_sh_multi_complete_2of2() {
+ let secp = Secp256k1::new();
+
+ let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let desc = descriptor!(sh(multi(2, prvkey0, prvkey1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
+ && keys[0] == PkOrF::Fingerprint(fingerprint0)
+ && keys[1] == PkOrF::Fingerprint(fingerprint1)
+ );
+
+ assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
+ && m == &2
+ && items.len() == 2
+ && conditions.contains_key(&vec![0,1])
+ );
+ }
+
+ // test ExtractPolicy trait with extended and single keys
+
+ #[test]
+ fn test_extract_policy_for_single_wpkh() {
+ let secp = Secp256k1::new();
+
+ let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp);
+ let desc = descriptor!(wpkh(pubkey)).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
+ assert_matches!(&policy.contribution, Satisfaction::None);
+
+ let desc = descriptor!(wpkh(prvkey)).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint);
+ assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
+ }
+
+ // single key, 1 prv and 1 pub key descriptor, required 1 prv keys
+ #[test]
+ #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
+ fn test_extract_policy_for_single_wsh_multi_complete_1of2() {
+ let secp = Secp256k1::new();
+
+ let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1
+ && keys[0] == PkOrF::Fingerprint(fingerprint0)
+ && keys[1] == PkOrF::Fingerprint(fingerprint1)
+ );
+ assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
+ && m == 1
+ && items.len() == 2
+ && conditions.contains_key(&vec![0])
+ && conditions.contains_key(&vec![1])
+ );
+ }
+
+ // test ExtractPolicy trait with descriptors containing timelocks in a thresh()
+
+ #[test]
+ #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225
+ fn test_extract_policy_for_wsh_multi_timelock() {
+ let secp = Secp256k1::new();
+
+ let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp);
+ let sequence = 50;
+ #[rustfmt::skip]
+ let desc = descriptor!(wsh(thresh(
+ 2,
+ pk(prvkey0),
+ s:pk(pubkey1),
+ s:d:v:older(sequence)
+ )))
+ .unwrap();
+
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2);
+
+ assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
+ && m == &2
+ && items.len() == 3
+ && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
+ && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
+ && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
+ );
+ }
+
+ // - mixed timelocks should fail
+
+ #[test]
+ #[ignore]
+ fn test_extract_policy_for_wsh_mixed_timelocks() {
+ let secp = Secp256k1::new();
+ let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds
+ let locktime_blocks = 100;
+ let locktime_seconds = locktime_blocks + locktime_threshold;
+ let desc = descriptor!(sh(and_v(
+ v: pk(prvkey0),
+ and_v(v: after(locktime_seconds), after(locktime_blocks))
+ )))
+ .unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let _policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+ // println!("desc policy = {:?}", policy); // TODO remove
+ // TODO how should this fail with mixed timelocks?
+ }
+
+ // - multiple timelocks of the same type should be correctly merged together
+ #[test]
+ #[ignore]
+ fn test_extract_policy_for_multiple_same_timelocks() {
+ let secp = Secp256k1::new();
+ let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp);
+ let locktime_blocks0 = 100;
+ let locktime_blocks1 = 200;
+ let desc = descriptor!(sh(and_v(
+ v: pk(prvkey0),
+ and_v(v: after(locktime_blocks0), after(locktime_blocks1))
+ )))
+ .unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let _policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+ // println!("desc policy = {:?}", policy); // TODO remove
+ // TODO how should this merge timelocks?
+ let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp);
+ let locktime_seconds0 = 500000100;
+ let locktime_seconds1 = 500000200;
+ let desc = descriptor!(sh(and_v(
+ v: pk(prvkey1),
+ and_v(v: after(locktime_seconds0), after(locktime_seconds1))
+ )))
+ .unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+ let _policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ // println!("desc policy = {:?}", policy); // TODO remove
+
+ // TODO how should this merge timelocks?
+ }
+
+ #[test]
+ fn test_get_condition_multisig() {
+ let secp = Secp256k1::new();
+
+ let (_, pk0, _) = setup_keys(TPRV0_STR, PATH, &secp);
+ let (_, pk1, _) = setup_keys(TPRV1_STR, PATH, &secp);
+
+ let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ // no args, choose the default
+ let no_args = policy.get_condition(&vec![].into_iter().collect());
+ assert_eq!(no_args, Ok(Condition::default()));
+
+ // enough args
+ let eq_thresh =
+ policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect());
+ assert_eq!(eq_thresh, Ok(Condition::default()));
+
+ // more args, it doesn't really change anything
+ let gt_thresh =
+ policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect());
+ assert_eq!(gt_thresh, Ok(Condition::default()));
+
+ // not enough args, error
+ let lt_thresh =
+ policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect());
+ assert_eq!(
+ lt_thresh,
+ Err(PolicyError::NotEnoughItemsSelected(policy.id.clone()))
+ );
+
+ // index out of range
+ let out_of_range =
+ policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect());
+ assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5)));
+ }
+
+ const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
+ const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
+ const CAROL_TPRV_STR:&str = "tprv8ZgxMBicQKsPdC3CicFifuLCEyVVdXVUNYorxUWj3iGZ6nimnLAYAY9SYB7ib8rKzRxrCKFcEytCt6szwd2GHnGPRCBLAEAoSVDefSNk4Bt";
+ const ALICE_BOB_PATH: &str = "m/0'";
+
+ #[test]
+ fn test_extract_satisfaction() {
+ const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
+ const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA==";
+ const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA";
+
+ let secp = Secp256k1::new();
+
+ let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(wsh(multi(2, prvkey_alice, prvkey_bob))).unwrap();
+
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+
+ let addr = wallet_desc
+ .at_derivation_index(0)
+ .unwrap()
+ .address(Network::Testnet)
+ .unwrap();
+ assert_eq!(
+ "tb1qg3cwv3xt50gdg875qvjjpfgaps86gtk4rz0ejvp6ttc5ldnlxuvqlcn0xk",
+ addr.to_string()
+ );
+
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
+
+ let policy_alice_psbt = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
+ .unwrap()
+ .unwrap();
+ //println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
+
+ assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
+ && m == &2
+ && items == &vec![0]
+ );
+
+ let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
+ let policy_bob_psbt = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
+ .unwrap()
+ .unwrap();
+ //println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
+
+ assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
+ && m == &2
+ && items == &vec![1]
+ );
+
+ let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
+ let policy_alice_bob_psbt = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
+ .unwrap()
+ .unwrap();
+ assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
+ && m == &2
+ && items == &vec![0, 1]
+ );
+ }
+
+ #[test]
+ fn test_extract_satisfaction_timelock() {
+ //const PSBT_POLICY_CONSIDER_TIMELOCK_NOT_EXPIRED: &str = "cHNidP8BAFMBAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAD/////ATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
+ const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED: &str = "cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
+ const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED: &str ="cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstIMEUCIQCtZxNm6H3Ux3pnc64DSpgohMdBj+57xhFHcURYt2BpPAIgG3OnI7bcj/3GtWX1HHyYGSI7QGa/zq5YnsmK1Cw29NABAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEIoAQASDBFAiEArWcTZuh91Md6Z3OuA0qYKITHQY/ue8YRR3FEWLdgaTwCIBtzpyO23I/9xrVl9Rx8mBkiO0Bmv86uWJ7JitQsNvTQAQEBUnZjUrJpaHwhA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLrJN8IQL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiKyTUocAAA==";
+
+ let secp = Secp256k1::new();
+
+ let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc =
+ descriptor!(wsh(thresh(2,n:d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
+
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let addr = wallet_desc
+ .at_derivation_index(0)
+ .unwrap()
+ .address(Network::Testnet)
+ .unwrap();
+ assert_eq!(
+ "tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58",
+ addr.to_string()
+ );
+
+ let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap();
+
+ let build_sat = BuildSatisfaction::PsbtTimelocks {
+ psbt: &psbt,
+ current_height: 10,
+ input_max_height: 9,
+ };
+
+ let policy = wallet_desc
+ .extract_policy(&signers_container, build_sat, &secp)
+ .unwrap()
+ .unwrap();
+ assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
+ && m == &2
+ && items.is_empty()
+ );
+ //println!("{}", serde_json::to_string(&policy).unwrap());
+
+ let build_sat_expired = BuildSatisfaction::PsbtTimelocks {
+ psbt: &psbt,
+ current_height: 12,
+ input_max_height: 9,
+ };
+
+ let policy_expired = wallet_desc
+ .extract_policy(&signers_container, build_sat_expired, &secp)
+ .unwrap()
+ .unwrap();
+ assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
+ && m == &2
+ && items == &vec![0]
+ );
+ //println!("{}", serde_json::to_string(&policy_expired).unwrap());
+
+ let psbt_signed = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap();
+
+ let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks {
+ psbt: &psbt_signed,
+ current_height: 12,
+ input_max_height: 9,
+ };
+
+ let policy_expired_signed = wallet_desc
+ .extract_policy(&signers_container, build_sat_expired_signed, &secp)
+ .unwrap()
+ .unwrap();
+ assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
+ && m == &2
+ && items == &vec![0, 1]
+ );
+ //println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
+ }
+
+ #[test]
+ fn test_extract_pkh() {
+ let secp = Secp256k1::new();
+
+ let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (prvkey_carol, _, _) = setup_keys(CAROL_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(wsh(c: andor(
+ pk(prvkey_alice),
+ pk_k(prvkey_bob),
+ pk_h(prvkey_carol),
+ )))
+ .unwrap();
+
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp);
+ assert!(policy.is_ok());
+ }
+
+ #[test]
+ fn test_extract_tr_key_spend() {
+ let secp = Secp256k1::new();
+
+ let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(tr(prvkey)).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap();
+ assert_eq!(
+ policy,
+ Some(Policy {
+ id: "48u0tz0n".to_string(),
+ item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)),
+ satisfaction: Satisfaction::None,
+ contribution: Satisfaction::Complete {
+ condition: Condition::default()
+ }
+ })
+ );
+ }
+
+ #[test]
+ fn test_extract_tr_script_spend() {
+ let secp = Secp256k1::new();
+
+ let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+ let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
+
+ let policy = wallet_desc
+ .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
+ assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]);
+
+ let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
+ let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
+
+ let thresh_items = match policy.item {
+ SatisfiableItem::Thresh { items, .. } => items,
+ _ => unreachable!(),
+ };
+
+ assert_eq!(thresh_items[0].item, bob_sig);
+ assert_eq!(thresh_items[1].item, alice_sig);
+ }
+
+ #[test]
+ fn test_extract_tr_satisfaction_key_spend() {
+ const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA==";
+ const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA=";
+
+ let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
+ let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
+
+ let secp = Secp256k1::new();
+
+ let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(tr(pubkey)).unwrap();
+ let (wallet_desc, _) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+
+ let policy_unsigned = wallet_desc
+ .extract_policy(
+ &SignersContainer::default(),
+ BuildSatisfaction::Psbt(&unsigned_psbt),
+ &secp,
+ )
+ .unwrap()
+ .unwrap();
+ let policy_signed = wallet_desc
+ .extract_policy(
+ &SignersContainer::default(),
+ BuildSatisfaction::Psbt(&signed_psbt),
+ &secp,
+ )
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(policy_unsigned.satisfaction, Satisfaction::None);
+ assert_eq!(
+ policy_signed.satisfaction,
+ Satisfaction::Complete {
+ condition: Default::default()
+ }
+ );
+ }
+
+ #[test]
+ fn test_extract_tr_satisfaction_script_spend() {
+ const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
+ const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
+
+ let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
+ let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
+
+ let secp = Secp256k1::new();
+
+ let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
+ let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
+
+ let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap();
+ let (wallet_desc, _) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+
+ let policy_unsigned = wallet_desc
+ .extract_policy(
+ &SignersContainer::default(),
+ BuildSatisfaction::Psbt(&unsigned_psbt),
+ &secp,
+ )
+ .unwrap()
+ .unwrap();
+ let policy_signed = wallet_desc
+ .extract_policy(
+ &SignersContainer::default(),
+ BuildSatisfaction::Psbt(&signed_psbt),
+ &secp,
+ )
+ .unwrap()
+ .unwrap();
+
+ assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
+ assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty());
+
+ assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
+ assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]);
+
+ let satisfied_items = match policy_signed.item {
+ SatisfiableItem::Thresh { items, .. } => items,
+ _ => unreachable!(),
+ };
+
+ assert_eq!(
+ satisfied_items[0].satisfaction,
+ Satisfaction::Complete {
+ condition: Default::default()
+ }
+ );
+ assert_eq!(
+ satisfied_items[1].satisfaction,
+ Satisfaction::Complete {
+ condition: Default::default()
+ }
+ );
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Descriptor templates
+//!
+//! This module contains the definition of various common script templates that are ready to be
+//! used. See the documentation of each template for an example.
+
+use bitcoin::bip32;
+use bitcoin::Network;
+
+use miniscript::{Legacy, Segwitv0, Tap};
+
+use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap};
+use crate::descriptor::DescriptorError;
+use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks};
+use crate::wallet::utils::SecpCtx;
+use crate::{descriptor, KeychainKind};
+
+/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others
+pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
+
+/// Trait for descriptor templates that can be built into a full descriptor
+///
+/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be
+/// passed directly to the [`Wallet`](crate::Wallet) constructor.
+///
+/// ## Example
+///
+/// ```
+/// use bdk_wallet::descriptor::error::Error as DescriptorError;
+/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError};
+/// use bdk_wallet::miniscript::Legacy;
+/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut};
+/// use bitcoin::Network;
+///
+/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
+///
+/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
+/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+/// Ok(bdk_wallet::descriptor!(pkh(self.0))?)
+/// }
+/// }
+/// ```
+pub trait DescriptorTemplate {
+ /// Build the complete descriptor
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError>;
+}
+
+/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
+/// [`build`](DescriptorTemplate::build) method
+impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
+ fn into_wallet_descriptor(
+ self,
+ secp: &SecpCtx,
+ network: Network,
+ ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
+ self.build(network)?.into_wallet_descriptor(secp, network)
+ }
+}
+
+/// P2PKH template. Expands to a descriptor `pkh(key)`
+///
+/// ## Example
+///
+/// ```
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
+///
+/// assert_eq!(
+/// wallet
+/// .next_unused_address(KeychainKind::External)?
+/// .to_string(),
+/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
+/// );
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
+
+impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
+ fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ descriptor!(pkh(self.0))
+ }
+}
+
+/// P2WPKH-P2SH template. Expands to a descriptor `sh(wpkh(key))`
+///
+/// ## Example
+///
+/// ```
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
+///
+/// assert_eq!(
+/// wallet
+/// .next_unused_address(KeychainKind::External)?
+/// .to_string(),
+/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
+/// );
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+#[allow(non_camel_case_types)]
+pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
+
+impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
+ fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ descriptor!(sh(wpkh(self.0)))
+ }
+}
+
+/// P2WPKH template. Expands to a descriptor `wpkh(key)`
+///
+/// ## Example
+///
+/// ```
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
+///
+/// assert_eq!(
+/// wallet
+/// .next_unused_address(KeychainKind::External)?
+/// .to_string(),
+/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
+/// );
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
+
+impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
+ fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ descriptor!(wpkh(self.0))
+ }
+}
+
+/// P2TR template. Expands to a descriptor `tr(key)`
+///
+/// ## Example
+///
+/// ```
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
+///
+/// assert_eq!(
+/// wallet
+/// .next_unused_address(KeychainKind::External)?
+/// .to_string(),
+/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
+/// );
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
+
+impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
+ fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ descriptor!(tr(self.0))
+ }
+}
+
+/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
+///
+/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
+///
+/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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(
+/// Bip44(key.clone(), KeychainKind::External),
+/// Some(Bip44(key, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
+
+impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
+ }
+}
+
+/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
+///
+/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet.
+///
+/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
+///
+/// See [`Bip44`] for a template that does the full derivation, but requires private data
+/// for the key.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(
+/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
+/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
+
+impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Pkh(legacy::make_bipxx_public(
+ 44, self.0, self.1, self.2, network,
+ )?)
+ .build(network)
+ }
+}
+
+/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))`
+///
+/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
+///
+/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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(
+/// Bip49(key.clone(), KeychainKind::External),
+/// Some(Bip49(key, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
+
+impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
+ }
+}
+
+/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))`
+///
+/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet.
+///
+/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
+///
+/// See [`Bip49`] for a template that does the full derivation, but requires private data
+/// for the key.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(
+/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
+/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
+
+impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(
+ 49, self.0, self.1, self.2, network,
+ )?)
+ .build(network)
+ }
+}
+
+/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)`
+///
+/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
+///
+/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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(
+/// Bip84(key.clone(), KeychainKind::External),
+/// Some(Bip84(key, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
+
+impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
+ }
+}
+
+/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)`
+///
+/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet.
+///
+/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
+///
+/// See [`Bip84`] for a template that does the full derivation, but requires private data
+/// for the key.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(
+/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
+/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
+
+impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2Wpkh(segwit_v0::make_bipxx_public(
+ 84, self.0, self.1, self.2, network,
+ )?)
+ .build(network)
+ }
+}
+
+/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)`
+///
+/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
+///
+/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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(
+/// Bip86(key.clone(), KeychainKind::External),
+/// Some(Bip86(key, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
+
+impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network)
+ }
+}
+
+/// BIP86 public template. Expands to `tr(key/{0,1}/*)`
+///
+/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet.
+///
+/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
+///
+/// See [`Bip86`] for a template that does the full derivation, but requires private data
+/// for the key.
+///
+/// ## Example
+///
+/// ```
+/// # use std::str::FromStr;
+/// # 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")?;
+/// let mut wallet = Wallet::new_no_persist(
+/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
+/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
+/// Network::Testnet,
+/// )?;
+///
+/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
+/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
+
+impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {
+ fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
+ P2TR(segwit_v1::make_bipxx_public(
+ 86, self.0, self.1, self.2, network,
+ )?)
+ .build(network)
+ }
+}
+
+macro_rules! expand_make_bipxx {
+ ( $mod_name:ident, $ctx:ty ) => {
+ mod $mod_name {
+ use super::*;
+
+ pub(super) fn make_bipxx_private<K: DerivableKey<$ctx>>(
+ bip: u32,
+ key: K,
+ keychain: KeychainKind,
+ network: Network,
+ ) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
+ let mut derivation_path = alloc::vec::Vec::with_capacity(4);
+ derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
+
+ match network {
+ Network::Bitcoin => {
+ derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
+ }
+ _ => {
+ derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?);
+ }
+ }
+ derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
+
+ match keychain {
+ KeychainKind::External => {
+ derivation_path.push(bip32::ChildNumber::from_normal_idx(0)?)
+ }
+ KeychainKind::Internal => {
+ derivation_path.push(bip32::ChildNumber::from_normal_idx(1)?)
+ }
+ };
+
+ let derivation_path: bip32::DerivationPath = derivation_path.into();
+
+ Ok((key, derivation_path))
+ }
+ pub(super) fn make_bipxx_public<K: DerivableKey<$ctx>>(
+ bip: u32,
+ key: K,
+ parent_fingerprint: bip32::Fingerprint,
+ keychain: KeychainKind,
+ network: Network,
+ ) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
+ let derivation_path: bip32::DerivationPath = match keychain {
+ KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
+ KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(),
+ };
+
+ let source_path = bip32::DerivationPath::from(vec![
+ bip32::ChildNumber::from_hardened_idx(bip)?,
+ match network {
+ Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?,
+ _ => bip32::ChildNumber::from_hardened_idx(1)?,
+ },
+ bip32::ChildNumber::from_hardened_idx(0)?,
+ ]);
+
+ Ok((key, (parent_fingerprint, source_path), derivation_path))
+ }
+ }
+ };
+}
+
+expand_make_bipxx!(legacy, Legacy);
+expand_make_bipxx!(segwit_v0, Segwitv0);
+expand_make_bipxx!(segwit_v1, Tap);
+
+#[cfg(test)]
+mod test {
+ // test existing descriptor templates, make sure they are expanded to the right descriptors
+
+ use alloc::{string::ToString, vec::Vec};
+ use core::str::FromStr;
+
+ use super::*;
+ use crate::descriptor::{DescriptorError, DescriptorMeta};
+ use crate::keys::ValidNetworks;
+ use assert_matches::assert_matches;
+ use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
+ use miniscript::Descriptor;
+
+ // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
+ #[test]
+ fn test_bip44_template_cointype() {
+ use bitcoin::bip32::ChildNumber::{self, Hardened};
+
+ let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
+ assert_eq!(Network::Bitcoin, xprvkey.network);
+ let xdesc = Bip44(xprvkey, KeychainKind::Internal)
+ .build(Network::Bitcoin)
+ .unwrap();
+
+ if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
+ let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
+ let purpose = path.first().unwrap();
+ assert_matches!(purpose, Hardened { index: 44 });
+ let coin_type = path.get(1).unwrap();
+ assert_matches!(coin_type, Hardened { index: 0 });
+ }
+
+ let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ assert_eq!(Network::Testnet, tprvkey.network);
+ let tdesc = Bip44(tprvkey, KeychainKind::Internal)
+ .build(Network::Testnet)
+ .unwrap();
+
+ if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
+ let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
+ let purpose = path.first().unwrap();
+ assert_matches!(purpose, Hardened { index: 44 });
+ let coin_type = path.get(1).unwrap();
+ assert_matches!(coin_type, Hardened { index: 1 });
+ }
+ }
+
+ // verify template descriptor generates expected address(es)
+ fn check(
+ desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
+ is_witness: bool,
+ is_taproot: bool,
+ is_fixed: bool,
+ network: Network,
+ expected: &[&str],
+ ) {
+ let (desc, _key_map, _networks) = desc.unwrap();
+ assert_eq!(desc.is_witness(), is_witness);
+ assert_eq!(desc.is_taproot(), is_taproot);
+ assert_eq!(!desc.has_wildcard(), is_fixed);
+ for i in 0..expected.len() {
+ let index = i as u32;
+ let child_desc = if !desc.has_wildcard() {
+ desc.at_derivation_index(0).unwrap()
+ } else {
+ desc.at_derivation_index(index).unwrap()
+ };
+ let address = child_desc.address(network).unwrap();
+ assert_eq!(address.to_string(), *expected.get(i).unwrap());
+ }
+ }
+
+ // P2PKH
+ #[test]
+ fn test_p2ph_template() {
+ let prvkey =
+ bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
+ .unwrap();
+ check(
+ P2Pkh(prvkey).build(Network::Bitcoin),
+ false,
+ false,
+ true,
+ Network::Regtest,
+ &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
+ );
+
+ let pubkey = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ check(
+ P2Pkh(pubkey).build(Network::Bitcoin),
+ false,
+ false,
+ true,
+ Network::Regtest,
+ &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
+ );
+ }
+
+ // P2WPKH-P2SH `sh(wpkh(key))`
+ #[test]
+ fn test_p2wphp2sh_template() {
+ let prvkey =
+ bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
+ .unwrap();
+ check(
+ P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
+ true,
+ false,
+ true,
+ Network::Regtest,
+ &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
+ );
+
+ let pubkey = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ check(
+ P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
+ true,
+ false,
+ true,
+ Network::Regtest,
+ &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
+ );
+ }
+
+ // P2WPKH `wpkh(key)`
+ #[test]
+ fn test_p2wph_template() {
+ let prvkey =
+ bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
+ .unwrap();
+ check(
+ P2Wpkh(prvkey).build(Network::Bitcoin),
+ true,
+ false,
+ true,
+ Network::Regtest,
+ &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
+ );
+
+ let pubkey = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ check(
+ P2Wpkh(pubkey).build(Network::Bitcoin),
+ true,
+ false,
+ true,
+ Network::Regtest,
+ &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
+ );
+ }
+
+ // P2TR `tr(key)`
+ #[test]
+ fn test_p2tr_template() {
+ let prvkey =
+ bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
+ .unwrap();
+ check(
+ P2TR(prvkey).build(Network::Bitcoin),
+ false,
+ true,
+ true,
+ Network::Regtest,
+ &["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"],
+ );
+
+ let pubkey = bitcoin::PublicKey::from_str(
+ "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
+ )
+ .unwrap();
+ check(
+ P2TR(pubkey).build(Network::Bitcoin),
+ false,
+ true,
+ true,
+ Network::Regtest,
+ &["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"],
+ );
+ }
+
+ // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
+ #[test]
+ fn test_bip44_template() {
+ let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ check(
+ Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
+ false,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5",
+ "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP",
+ "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ",
+ ],
+ );
+ check(
+ Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
+ false,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "muHF98X9KxEzdKrnFAX85KeHv96eXopaip",
+ "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR",
+ "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA",
+ ],
+ );
+ }
+
+ // BIP44 public `pkh(key/{0,1}/*)`
+ #[test]
+ fn test_bip44_public_template() {
+ let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
+ let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
+ check(
+ Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
+ false,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "miNG7dJTzJqNbFS19svRdTCisC65dsubtR",
+ "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg",
+ "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g",
+ ],
+ );
+ check(
+ Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
+ false,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H",
+ "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG",
+ "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo",
+ ],
+ );
+ }
+
+ // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
+ #[test]
+ fn test_bip49_template() {
+ let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ check(
+ Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV",
+ "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS",
+ "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4",
+ ],
+ );
+ check(
+ Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG",
+ "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p",
+ "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn",
+ ],
+ );
+ }
+
+ // BIP49 public `sh(wpkh(key/{0,1}/*))`
+ #[test]
+ fn test_bip49_public_template() {
+ let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
+ let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
+ check(
+ Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt",
+ "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX",
+ "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7",
+ ],
+ );
+ check(
+ Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ",
+ "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH",
+ "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp",
+ ],
+ );
+ }
+
+ // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
+ #[test]
+ fn test_bip84_template() {
+ let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ check(
+ Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s",
+ "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp",
+ "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp",
+ ],
+ );
+ check(
+ Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa",
+ "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45",
+ "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce",
+ ],
+ );
+ }
+
+ // BIP84 public `wpkh(key/{0,1}/*)`
+ #[test]
+ fn test_bip84_public_template() {
+ let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
+ let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
+ check(
+ Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h",
+ "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana",
+ "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9",
+ ],
+ );
+ check(
+ Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
+ true,
+ false,
+ false,
+ Network::Regtest,
+ &[
+ "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2",
+ "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp",
+ "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp",
+ ],
+ );
+ }
+
+ // BIP86 `tr(key/86'/0'/0'/{0,1}/*)`
+ // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
+ #[test]
+ fn test_bip86_template() {
+ let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
+ check(
+ Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
+ false,
+ true,
+ false,
+ Network::Bitcoin,
+ &[
+ "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
+ "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
+ "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
+ ],
+ );
+ check(
+ Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
+ false,
+ true,
+ false,
+ Network::Bitcoin,
+ &[
+ "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
+ "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
+ "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
+ ],
+ );
+ }
+
+ // BIP86 public `tr(key/{0,1}/*)`
+ // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
+ #[test]
+ fn test_bip86_public_template() {
+ let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
+ let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
+ check(
+ Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
+ false,
+ true,
+ false,
+ Network::Bitcoin,
+ &[
+ "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
+ "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
+ "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
+ ],
+ );
+ check(
+ Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
+ false,
+ true,
+ false,
+ Network::Bitcoin,
+ &[
+ "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
+ "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
+ "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
+ ],
+ );
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! BIP-0039
+
+// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for
+// something that should be fairly simple to re-implement.
+
+use alloc::string::String;
+use bitcoin::bip32;
+use bitcoin::Network;
+
+use miniscript::ScriptContext;
+
+pub use bip39::{Error, Language, Mnemonic};
+
+type Seed = [u8; 64];
+
+/// Type describing entropy length (aka word count) in the mnemonic
+pub enum WordCount {
+ /// 12 words mnemonic (128 bits entropy)
+ Words12 = 128,
+ /// 15 words mnemonic (160 bits entropy)
+ Words15 = 160,
+ /// 18 words mnemonic (192 bits entropy)
+ Words18 = 192,
+ /// 21 words mnemonic (224 bits entropy)
+ Words21 = 224,
+ /// 24 words mnemonic (256 bits entropy)
+ Words24 = 256,
+}
+
+use super::{
+ any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError,
+};
+
+fn set_valid_on_any_network<Ctx: ScriptContext>(
+ descriptor_key: DescriptorKey<Ctx>,
+) -> DescriptorKey<Ctx> {
+ // We have to pick one network to build the xprv, but since the bip39 standard doesn't
+ // encode the network, the xprv we create is actually valid everywhere. So we override the
+ // valid networks with `any_network()`.
+ descriptor_key.override_valid_networks(any_network())
+}
+
+/// Type for a BIP39 mnemonic with an optional passphrase
+pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
+
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
+ }
+
+ fn into_descriptor_key(
+ self,
+ source: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let descriptor_key = self
+ .into_extended_key()?
+ .into_descriptor_key(source, derivation_path)?;
+
+ Ok(set_valid_on_any_network(descriptor_key))
+ }
+}
+
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ let (mnemonic, passphrase) = self;
+ let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or(""));
+
+ seed.into_extended_key()
+ }
+
+ fn into_descriptor_key(
+ self,
+ source: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let descriptor_key = self
+ .into_extended_key()?
+ .into_descriptor_key(source, derivation_path)?;
+
+ Ok(set_valid_on_any_network(descriptor_key))
+ }
+}
+
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>) {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ let (mnemonic, passphrase) = self;
+ (mnemonic.into_key(), passphrase).into_extended_key()
+ }
+
+ fn into_descriptor_key(
+ self,
+ source: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let (mnemonic, passphrase) = self;
+ (mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path)
+ }
+}
+
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ (self, None).into_extended_key()
+ }
+
+ fn into_descriptor_key(
+ self,
+ source: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let descriptor_key = self
+ .into_extended_key()?
+ .into_descriptor_key(source, derivation_path)?;
+
+ Ok(set_valid_on_any_network(descriptor_key))
+ }
+}
+
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
+ type Entropy = [u8; 32];
+
+ type Options = (WordCount, Language);
+ type Error = Option<bip39::Error>;
+
+ fn generate_with_entropy(
+ (word_count, language): Self::Options,
+ entropy: Self::Entropy,
+ ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ let entropy = &entropy[..(word_count as usize / 8)];
+ let mnemonic = Mnemonic::from_entropy_in(language, entropy)?;
+
+ Ok(GeneratedKey::new(mnemonic, any_network()))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use alloc::string::ToString;
+ use core::str::FromStr;
+
+ use bitcoin::bip32;
+
+ use bip39::{Language, Mnemonic};
+
+ use crate::keys::{any_network, GeneratableKey, GeneratedKey};
+
+ use super::WordCount;
+
+ #[test]
+ fn test_keys_bip39_mnemonic() {
+ let mnemonic =
+ "aim bunker wash balance finish force paper analyst cabin spoon stable organ";
+ let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
+ let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
+
+ let key = (mnemonic, path);
+ let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
+ assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv");
+ assert_eq!(keys.len(), 1);
+ assert_eq!(networks.len(), 4);
+ }
+
+ #[test]
+ fn test_keys_bip39_mnemonic_passphrase() {
+ let mnemonic =
+ "aim bunker wash balance finish force paper analyst cabin spoon stable organ";
+ let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap();
+ let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap();
+
+ let key = ((mnemonic, Some("passphrase".into())), path);
+ let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
+ assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m");
+ assert_eq!(keys.len(), 1);
+ assert_eq!(networks.len(), 4);
+ }
+
+ #[test]
+ fn test_keys_generate_bip39() {
+ let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
+ Mnemonic::generate_with_entropy(
+ (WordCount::Words12, Language::English),
+ crate::keys::test::TEST_ENTROPY,
+ )
+ .unwrap();
+ assert_eq!(generated_mnemonic.valid_networks, any_network());
+ assert_eq!(
+ generated_mnemonic.to_string(),
+ "primary fetch primary fetch primary fetch primary fetch primary fetch primary fever"
+ );
+
+ let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
+ Mnemonic::generate_with_entropy(
+ (WordCount::Words24, Language::English),
+ crate::keys::test::TEST_ENTROPY,
+ )
+ .unwrap();
+ assert_eq!(generated_mnemonic.valid_networks, any_network());
+ assert_eq!(generated_mnemonic.to_string(), "primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary foster");
+ }
+
+ #[test]
+ fn test_keys_generate_bip39_random() {
+ let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
+ Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
+ assert_eq!(generated_mnemonic.valid_networks, any_network());
+
+ let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
+ Mnemonic::generate((WordCount::Words24, Language::English)).unwrap();
+ assert_eq!(generated_mnemonic.valid_networks, any_network());
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Key formats
+
+use crate::collections::HashSet;
+use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+use core::any::TypeId;
+use core::fmt;
+use core::marker::PhantomData;
+use core::ops::Deref;
+use core::str::FromStr;
+
+use bitcoin::secp256k1::{self, Secp256k1, Signing};
+
+use bitcoin::bip32;
+use bitcoin::{key::XOnlyPublicKey, Network, PrivateKey, PublicKey};
+
+use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
+pub use miniscript::descriptor::{
+ DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
+ SortedMultiVec,
+};
+pub use miniscript::ScriptContext;
+use miniscript::{Miniscript, Terminal};
+
+use crate::descriptor::{CheckMiniscript, DescriptorError};
+use crate::wallet::utils::SecpCtx;
+
+#[cfg(feature = "keys-bip39")]
+#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
+pub mod bip39;
+
+/// Set of valid networks for a key
+pub type ValidNetworks = HashSet<Network>;
+
+/// Create a set containing mainnet, testnet, signet, and regtest
+pub fn any_network() -> ValidNetworks {
+ vec![
+ Network::Bitcoin,
+ Network::Testnet,
+ Network::Regtest,
+ Network::Signet,
+ ]
+ .into_iter()
+ .collect()
+}
+/// Create a set only containing mainnet
+pub fn mainnet_network() -> ValidNetworks {
+ vec![Network::Bitcoin].into_iter().collect()
+}
+/// Create a set containing testnet and regtest
+pub fn test_networks() -> ValidNetworks {
+ vec![Network::Testnet, Network::Regtest, Network::Signet]
+ .into_iter()
+ .collect()
+}
+/// Compute the intersection of two sets
+pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks {
+ a.intersection(b).cloned().collect()
+}
+
+/// Container for public or secret keys
+#[derive(Debug)]
+pub enum DescriptorKey<Ctx: ScriptContext> {
+ #[doc(hidden)]
+ Public(DescriptorPublicKey, ValidNetworks, PhantomData<Ctx>),
+ #[doc(hidden)]
+ Secret(DescriptorSecretKey, ValidNetworks, PhantomData<Ctx>),
+}
+
+impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
+ /// Create an instance given a public key and a set of valid networks
+ pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self {
+ DescriptorKey::Public(public, networks, PhantomData)
+ }
+
+ /// Create an instance given a secret key and a set of valid networks
+ pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self {
+ DescriptorKey::Secret(secret, networks, PhantomData)
+ }
+
+ /// Override the computed set of valid networks
+ pub fn override_valid_networks(self, networks: ValidNetworks) -> Self {
+ match self {
+ DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData),
+ DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData),
+ }
+ }
+
+ // 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)]
+ pub fn extract(
+ self,
+ secp: &SecpCtx,
+ ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> {
+ match self {
+ DescriptorKey::Public(public, valid_networks, _) => {
+ Ok((public, KeyMap::default(), valid_networks))
+ }
+ DescriptorKey::Secret(secret, valid_networks, _) => {
+ let mut key_map = KeyMap::new();
+
+ let public = secret
+ .to_public(secp)
+ .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
+ key_map.insert(public.clone(), secret);
+
+ Ok((public, key_map, valid_networks))
+ }
+ }
+ }
+}
+
+/// Enum representation of the known valid [`ScriptContext`]s
+#[derive(Debug, Eq, PartialEq, Copy, Clone)]
+pub enum ScriptContextEnum {
+ /// Legacy scripts
+ Legacy,
+ /// Segwitv0 scripts
+ Segwitv0,
+ /// Taproot scripts
+ Tap,
+}
+
+impl ScriptContextEnum {
+ /// Returns whether the script context is [`ScriptContextEnum::Legacy`]
+ pub fn is_legacy(&self) -> bool {
+ self == &ScriptContextEnum::Legacy
+ }
+
+ /// Returns whether the script context is [`ScriptContextEnum::Segwitv0`]
+ pub fn is_segwit_v0(&self) -> bool {
+ self == &ScriptContextEnum::Segwitv0
+ }
+
+ /// Returns whether the script context is [`ScriptContextEnum::Tap`]
+ pub fn is_taproot(&self) -> bool {
+ self == &ScriptContextEnum::Tap
+ }
+}
+
+/// Trait that adds extra useful methods to [`ScriptContext`]s
+pub trait ExtScriptContext: ScriptContext {
+ /// Returns the [`ScriptContext`] as a [`ScriptContextEnum`]
+ fn as_enum() -> ScriptContextEnum;
+
+ /// Returns whether the script context is [`Legacy`](miniscript::Legacy)
+ fn is_legacy() -> bool {
+ Self::as_enum().is_legacy()
+ }
+
+ /// Returns whether the script context is [`Segwitv0`](miniscript::Segwitv0)
+ fn is_segwit_v0() -> bool {
+ Self::as_enum().is_segwit_v0()
+ }
+
+ /// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1
+ fn is_taproot() -> bool {
+ Self::as_enum().is_taproot()
+ }
+}
+
+impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
+ fn as_enum() -> ScriptContextEnum {
+ match TypeId::of::<Ctx>() {
+ t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
+ t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
+ t if t == TypeId::of::<miniscript::Tap>() => ScriptContextEnum::Tap,
+ _ => unimplemented!("Unknown ScriptContext type"),
+ }
+ }
+}
+
+/// Trait for objects that can be turned into a public or secret [`DescriptorKey`]
+///
+/// The generic type `Ctx` is used to define the context in which the key is valid: some key
+/// formats, like the mnemonics used by Electrum wallets, encode internally whether the wallet is
+/// legacy or segwit. Thus, trying to turn a valid legacy mnemonic into a `DescriptorKey`
+/// that would become part of a segwit descriptor should fail.
+///
+/// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful
+/// methods that can be used to check at runtime which `Ctx` is being used.
+///
+/// For key types that can do this check statically (because they can only work within a
+/// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type
+/// checking.
+///
+/// Keys also have control over the networks they support: constructing the return object with
+/// [`DescriptorKey::from_public`] or [`DescriptorKey::from_secret`] allows to specify a set of
+/// [`ValidNetworks`].
+///
+/// ## Examples
+///
+/// Key type valid in any context:
+///
+/// ```
+/// use bdk_wallet::bitcoin::PublicKey;
+///
+/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
+///
+/// pub struct MyKeyType {
+/// pubkey: PublicKey,
+/// }
+///
+/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
+/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+/// self.pubkey.into_descriptor_key()
+/// }
+/// }
+/// ```
+///
+/// Key type that is only valid on mainnet:
+///
+/// ```
+/// use bdk_wallet::bitcoin::PublicKey;
+///
+/// use bdk_wallet::keys::{
+/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
+/// ScriptContext, SinglePub, SinglePubKey,
+/// };
+///
+/// pub struct MyKeyType {
+/// pubkey: PublicKey,
+/// }
+///
+/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
+/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+/// Ok(DescriptorKey::from_public(
+/// DescriptorPublicKey::Single(SinglePub {
+/// origin: None,
+/// key: SinglePubKey::FullKey(self.pubkey),
+/// }),
+/// mainnet_network(),
+/// ))
+/// }
+/// }
+/// ```
+///
+/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
+///
+/// ```
+/// use bdk_wallet::bitcoin::PublicKey;
+///
+/// use bdk_wallet::keys::{
+/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext,
+/// };
+///
+/// pub struct MyKeyType {
+/// is_legacy: bool,
+/// pubkey: PublicKey,
+/// }
+///
+/// impl<Ctx: ScriptContext + 'static> IntoDescriptorKey<Ctx> for MyKeyType {
+/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+/// if Ctx::is_legacy() == self.is_legacy {
+/// self.pubkey.into_descriptor_key()
+/// } else {
+/// Err(KeyError::InvalidScriptContext)
+/// }
+/// }
+/// }
+/// ```
+///
+/// Key type that can only work within [`miniscript::Segwitv0`] context. Only the specialized version
+/// of the trait is implemented.
+///
+/// This example deliberately fails to compile, to demonstrate how the compiler can catch when keys
+/// are misused. In this case, the "segwit-only" key is used to build a `pkh()` descriptor, which
+/// makes the compiler (correctly) fail.
+///
+/// ```compile_fail
+/// use bdk_wallet::bitcoin::PublicKey;
+/// use core::str::FromStr;
+///
+/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
+///
+/// pub struct MySegwitOnlyKeyType {
+/// pubkey: PublicKey,
+/// }
+///
+/// impl IntoDescriptorKey<bdk_wallet::miniscript::Segwitv0> for MySegwitOnlyKeyType {
+/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk_wallet::miniscript::Segwitv0>, KeyError> {
+/// self.pubkey.into_descriptor_key()
+/// }
+/// }
+///
+/// let key = MySegwitOnlyKeyType {
+/// pubkey: PublicKey::from_str("...")?,
+/// };
+/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?;
+/// // ^^^^^ changing this to `wpkh` would make it compile
+///
+/// # Ok::<_, Box<dyn std::error::Error>>(())
+/// ```
+pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
+ /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`]
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError>;
+}
+
+/// Enum for extended keys that can be either `xprv` or `xpub`
+///
+/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
+/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
+///
+/// Defaults to the [`Legacy`](miniscript::Legacy) context.
+pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
+ /// A private extended key, aka an `xprv`
+ Private((bip32::Xpriv, PhantomData<Ctx>)),
+ /// A public extended key, aka an `xpub`
+ Public((bip32::Xpub, PhantomData<Ctx>)),
+}
+
+impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
+ /// Return whether or not the key contains the private data
+ pub fn has_secret(&self) -> bool {
+ match self {
+ ExtendedKey::Private(_) => true,
+ ExtendedKey::Public(_) => false,
+ }
+ }
+
+ /// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
+ /// given [`Network`], if the key contains the private data
+ pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
+ match self {
+ ExtendedKey::Private((mut xprv, _)) => {
+ xprv.network = network;
+ Some(xprv)
+ }
+ ExtendedKey::Public(_) => None,
+ }
+ }
+
+ /// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
+ /// given [`Network`]
+ pub fn into_xpub<C: Signing>(
+ self,
+ network: bitcoin::Network,
+ secp: &Secp256k1<C>,
+ ) -> bip32::Xpub {
+ let mut xpub = match self {
+ ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
+ ExtendedKey::Public((xpub, _)) => xpub,
+ };
+
+ xpub.network = network;
+ xpub
+ }
+}
+
+impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
+ fn from(xpub: bip32::Xpub) -> Self {
+ ExtendedKey::Public((xpub, PhantomData))
+ }
+}
+
+impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
+ fn from(xprv: bip32::Xpriv) -> Self {
+ ExtendedKey::Private((xprv, PhantomData))
+ }
+}
+
+/// Trait for keys that can be derived.
+///
+/// When extra metadata are provided, a [`DerivableKey`] can be transformed into a
+/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented
+/// for `(DerivableKey, DerivationPath)` and
+/// `(DerivableKey, KeySource, DerivationPath)` tuples.
+///
+/// For key types that don't encode any indication about the path to use (like bip39), it's
+/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same
+/// rules regarding script context and valid networks apply.
+///
+/// ## Examples
+///
+/// Key types that can be directly converted into an [`Xpriv`] or
+/// an [`Xpub`] can implement only the required `into_extended_key()` method.
+///
+/// ```
+/// use bdk_wallet::bitcoin;
+/// use bdk_wallet::bitcoin::bip32;
+/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
+///
+/// struct MyCustomKeyType {
+/// key_data: bitcoin::PrivateKey,
+/// chain_code: [u8; 32],
+/// network: bitcoin::Network,
+/// }
+///
+/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
+/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+/// let xprv = bip32::Xpriv {
+/// network: self.network,
+/// depth: 0,
+/// parent_fingerprint: bip32::Fingerprint::default(),
+/// private_key: self.key_data.inner,
+/// chain_code: bip32::ChainCode::from(&self.chain_code),
+/// child_number: bip32::ChildNumber::Normal { index: 0 },
+/// };
+///
+/// xprv.into_extended_key()
+/// }
+/// }
+/// ```
+///
+/// Types that don't internally encode the [`Network`] in which they are valid need some extra
+/// steps to override the set of valid networks, otherwise only the network specified in the
+/// [`Xpriv`] or [`Xpub`] will be considered valid.
+///
+/// ```
+/// use bdk_wallet::bitcoin;
+/// use bdk_wallet::bitcoin::bip32;
+/// use bdk_wallet::keys::{
+/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
+/// };
+///
+/// struct MyCustomKeyType {
+/// key_data: bitcoin::PrivateKey,
+/// chain_code: [u8; 32],
+/// }
+///
+/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
+/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+/// let xprv = bip32::Xpriv {
+/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
+/// depth: 0,
+/// parent_fingerprint: bip32::Fingerprint::default(),
+/// private_key: self.key_data.inner,
+/// chain_code: bip32::ChainCode::from(&self.chain_code),
+/// child_number: bip32::ChildNumber::Normal { index: 0 },
+/// };
+///
+/// xprv.into_extended_key()
+/// }
+///
+/// fn into_descriptor_key(
+/// self,
+/// source: Option<bip32::KeySource>,
+/// derivation_path: bip32::DerivationPath,
+/// ) -> Result<DescriptorKey<Ctx>, KeyError> {
+/// let descriptor_key = self
+/// .into_extended_key()?
+/// .into_descriptor_key(source, derivation_path)?;
+///
+/// // Override the set of valid networks here
+/// Ok(descriptor_key.override_valid_networks(any_network()))
+/// }
+/// }
+/// ```
+///
+/// [`DerivationPath`]: (bip32::DerivationPath)
+/// [`Xpriv`]: (bip32::Xpriv)
+/// [`Xpub`]: (bip32::Xpub)
+pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
+ /// Consume `self` and turn it into an [`ExtendedKey`]
+ #[cfg_attr(
+ feature = "keys-bip39",
+ doc = r##"
+This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
+like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
+```rust
+use bdk_wallet::bitcoin::Network;
+use bdk_wallet::keys::{DerivableKey, ExtendedKey};
+use bdk_wallet::keys::bip39::{Mnemonic, Language};
+
+# fn main() -> Result<(), Box<dyn std::error::Error>> {
+let xkey: ExtendedKey =
+ Mnemonic::parse_in(
+ Language::English,
+ "jelly crash boy whisper mouse ecology tuna soccer memory million news short",
+ )?
+ .into_extended_key()?;
+let xprv = xkey.into_xprv(Network::Bitcoin).unwrap();
+# Ok(()) }
+```
+"##
+ )]
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError>;
+
+ /// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as
+ /// key origin and derivation path
+ fn into_descriptor_key(
+ self,
+ origin: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ match self.into_extended_key()? {
+ ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey {
+ origin,
+ xkey: xprv,
+ derivation_path,
+ wildcard: Wildcard::Unhardened,
+ })
+ .into_descriptor_key(),
+ ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey {
+ origin,
+ xkey: xpub,
+ derivation_path,
+ wildcard: Wildcard::Unhardened,
+ })
+ .into_descriptor_key(),
+ }
+ }
+}
+
+/// Identity conversion
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ Ok(self)
+ }
+}
+
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ Ok(self.into())
+ }
+}
+
+impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ Ok(self.into())
+ }
+}
+
+/// Output of a [`GeneratableKey`] key generation
+pub struct GeneratedKey<K, Ctx: ScriptContext> {
+ key: K,
+ valid_networks: ValidNetworks,
+ phantom: PhantomData<Ctx>,
+}
+
+impl<K, Ctx: ScriptContext> GeneratedKey<K, Ctx> {
+ fn new(key: K, valid_networks: ValidNetworks) -> Self {
+ GeneratedKey {
+ key,
+ valid_networks,
+ phantom: PhantomData,
+ }
+ }
+
+ /// Consumes `self` and returns the key
+ pub fn into_key(self) -> K {
+ self.key
+ }
+}
+
+impl<K, Ctx: ScriptContext> Deref for GeneratedKey<K, Ctx> {
+ type Target = K;
+
+ fn deref(&self) -> &Self::Target {
+ &self.key
+ }
+}
+
+impl<K: Clone, Ctx: ScriptContext> Clone for GeneratedKey<K, Ctx> {
+ fn clone(&self) -> GeneratedKey<K, Ctx> {
+ GeneratedKey {
+ key: self.key.clone(),
+ valid_networks: self.valid_networks.clone(),
+ phantom: self.phantom,
+ }
+ }
+}
+
+// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
+// right `valid_networks`.
+impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
+where
+ Ctx: ScriptContext,
+ K: DerivableKey<Ctx>,
+{
+ fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
+ self.key.into_extended_key()
+ }
+
+ fn into_descriptor_key(
+ self,
+ origin: Option<bip32::KeySource>,
+ derivation_path: bip32::DerivationPath,
+ ) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?;
+ Ok(descriptor_key.override_valid_networks(self.valid_networks))
+ }
+}
+
+// Make generated keys directly usable in descriptors, and make sure they get assigned the right
+// `valid_networks`.
+impl<Ctx, K> IntoDescriptorKey<Ctx> for GeneratedKey<K, Ctx>
+where
+ Ctx: ScriptContext,
+ K: IntoDescriptorKey<Ctx>,
+{
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let desc_key = self.key.into_descriptor_key()?;
+ Ok(desc_key.override_valid_networks(self.valid_networks))
+ }
+}
+
+/// Trait for keys that can be generated
+///
+/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply.
+///
+/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self`
+/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for
+/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also
+/// [`IntoDescriptorKey`].
+pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
+ /// Type specifying the amount of entropy required e.g. `[u8;32]`
+ type Entropy: AsMut<[u8]> + Default;
+
+ /// Extra options required by the `generate_with_entropy`
+ type Options;
+ /// Returned error in case of failure
+ type Error: core::fmt::Debug;
+
+ /// Generate a key given the extra options and the entropy
+ fn generate_with_entropy(
+ options: Self::Options,
+ entropy: Self::Entropy,
+ ) -> Result<GeneratedKey<Self, Ctx>, Self::Error>;
+
+ /// Generate a key given the options with a random entropy
+ fn generate(options: Self::Options) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ use rand::{thread_rng, Rng};
+
+ let mut entropy = Self::Entropy::default();
+ thread_rng().fill(entropy.as_mut());
+ Self::generate_with_entropy(options, entropy)
+ }
+}
+
+/// Trait that allows generating a key with the default options
+///
+/// This trait is automatically implemented if the [`GeneratableKey::Options`] implements [`Default`].
+pub trait GeneratableDefaultOptions<Ctx>: GeneratableKey<Ctx>
+where
+ Ctx: ScriptContext,
+ <Self as GeneratableKey<Ctx>>::Options: Default,
+{
+ /// Generate a key with the default options and a given entropy
+ fn generate_with_entropy_default(
+ entropy: Self::Entropy,
+ ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ Self::generate_with_entropy(Default::default(), entropy)
+ }
+
+ /// Generate a key with the default options and a random entropy
+ fn generate_default() -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ Self::generate(Default::default())
+ }
+}
+
+/// Automatic implementation of [`GeneratableDefaultOptions`] for [`GeneratableKey`]s where
+/// `Options` implements `Default`
+impl<Ctx, K> GeneratableDefaultOptions<Ctx> for K
+where
+ Ctx: ScriptContext,
+ K: GeneratableKey<Ctx>,
+ <K as GeneratableKey<Ctx>>::Options: Default,
+{
+}
+
+impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
+ type Entropy = [u8; 32];
+
+ type Options = ();
+ type Error = bip32::Error;
+
+ fn generate_with_entropy(
+ _: Self::Options,
+ entropy: Self::Entropy,
+ ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ // pick a arbitrary network here, but say that we support all of them
+ let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
+ Ok(GeneratedKey::new(xprv, any_network()))
+ }
+}
+
+/// Options for generating a [`PrivateKey`]
+///
+/// Defaults to creating compressed keys, which save on-chain bytes and fees
+#[derive(Debug, Copy, Clone)]
+pub struct PrivateKeyGenerateOptions {
+ /// Whether the generated key should be "compressed" or not
+ pub compressed: bool,
+}
+
+impl Default for PrivateKeyGenerateOptions {
+ fn default() -> Self {
+ PrivateKeyGenerateOptions { compressed: true }
+ }
+}
+
+impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
+ type Entropy = [u8; secp256k1::constants::SECRET_KEY_SIZE];
+
+ type Options = PrivateKeyGenerateOptions;
+ type Error = bip32::Error;
+
+ fn generate_with_entropy(
+ options: Self::Options,
+ entropy: Self::Entropy,
+ ) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
+ // pick a arbitrary network here, but say that we support all of them
+ let inner = secp256k1::SecretKey::from_slice(&entropy)?;
+ let private_key = PrivateKey {
+ compressed: options.compressed,
+ network: Network::Bitcoin,
+ inner,
+ };
+
+ Ok(GeneratedKey::new(private_key, any_network()))
+ }
+}
+
+impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
+ for (T, bip32::DerivationPath)
+{
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ self.0.into_descriptor_key(None, self.1)
+ }
+}
+
+impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
+ for (T, bip32::KeySource, bip32::DerivationPath)
+{
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ self.0.into_descriptor_key(Some(self.1), self.2)
+ }
+}
+
+fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
+ pks: Vec<Pk>,
+ secp: &SecpCtx,
+) -> Result<(Vec<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError> {
+ let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks
+ .into_iter()
+ .map(|key| key.into_descriptor_key()?.extract(secp))
+ .collect::<Result<Vec<_>, _>>()?
+ .into_iter()
+ .map(|(a, b, c)| (a, (b, c)))
+ .unzip();
+
+ let (key_map, valid_networks) = key_maps_networks.into_iter().fold(
+ (KeyMap::default(), any_network()),
+ |(mut keys_acc, net_acc), (key, net)| {
+ keys_acc.extend(key);
+ let net_acc = merge_networks(&net_acc, &net);
+
+ (keys_acc, net_acc)
+ },
+ );
+
+ Ok((pks, key_map, valid_networks))
+}
+
+// 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,
+ secp: &SecpCtx,
+) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
+ let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?;
+ let minisc = Miniscript::from_ast(Terminal::PkK(key))?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, key_map, valid_networks))
+}
+
+// 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,
+ secp: &SecpCtx,
+) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
+ let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?;
+ let minisc = Miniscript::from_ast(Terminal::PkH(key))?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, key_map, valid_networks))
+}
+
+// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments
+#[doc(hidden)]
+pub fn make_multi<
+ Pk: IntoDescriptorKey<Ctx>,
+ Ctx: ScriptContext,
+ V: Fn(usize, Vec<DescriptorPublicKey>) -> Terminal<DescriptorPublicKey, Ctx>,
+>(
+ thresh: usize,
+ variant: V,
+ pks: Vec<Pk>,
+ secp: &SecpCtx,
+) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
+ let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
+ let minisc = Miniscript::from_ast(variant(thresh, pks))?;
+
+ minisc.check_miniscript()?;
+
+ Ok((minisc, key_map, valid_networks))
+}
+
+// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments
+#[doc(hidden)]
+pub fn make_sortedmulti<Pk, Ctx, F>(
+ thresh: usize,
+ pks: Vec<Pk>,
+ build_desc: F,
+ secp: &SecpCtx,
+) -> Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>
+where
+ Pk: IntoDescriptorKey<Ctx>,
+ Ctx: ScriptContext,
+ F: Fn(
+ usize,
+ Vec<DescriptorPublicKey>,
+ ) -> Result<(Descriptor<DescriptorPublicKey>, PhantomData<Ctx>), DescriptorError>,
+{
+ let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
+ let descriptor = build_desc(thresh, pks)?.0;
+
+ Ok((descriptor, key_map, valid_networks))
+}
+
+/// 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)
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let networks = match self {
+ DescriptorPublicKey::Single(_) => any_network(),
+ DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
+ if xkey.network == Network::Bitcoin =>
+ {
+ mainnet_network()
+ }
+ _ => test_networks(),
+ };
+
+ Ok(DescriptorKey::from_public(self, networks))
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::FullKey(self),
+ origin: None,
+ })
+ .into_descriptor_key()
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ DescriptorPublicKey::Single(SinglePub {
+ key: SinglePubKey::XOnly(self),
+ origin: None,
+ })
+ .into_descriptor_key()
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ let networks = match &self {
+ DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
+ mainnet_network()
+ }
+ DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
+ if xkey.network == Network::Bitcoin =>
+ {
+ mainnet_network()
+ }
+ _ => test_networks(),
+ };
+
+ Ok(DescriptorKey::from_secret(self, networks))
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ DescriptorSecretKey::from_str(self)
+ .map_err(|e| KeyError::Message(e.to_string()))?
+ .into_descriptor_key()
+ }
+}
+
+impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
+ fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
+ DescriptorSecretKey::Single(SinglePriv {
+ key: self,
+ origin: None,
+ })
+ .into_descriptor_key()
+ }
+}
+
+/// Errors thrown while working with [`keys`](crate::keys)
+#[derive(Debug)]
+pub enum KeyError {
+ /// The key cannot exist in the given script context
+ InvalidScriptContext,
+ /// The key is not valid for the given network
+ InvalidNetwork,
+ /// The key has an invalid checksum
+ InvalidChecksum,
+
+ /// Custom error message
+ Message(String),
+
+ /// BIP32 error
+ Bip32(bitcoin::bip32::Error),
+ /// Miniscript error
+ Miniscript(miniscript::Error),
+}
+
+impl From<miniscript::Error> for KeyError {
+ fn from(err: miniscript::Error) -> Self {
+ KeyError::Miniscript(err)
+ }
+}
+
+impl From<bip32::Error> for KeyError {
+ fn from(err: bip32::Error) -> Self {
+ KeyError::Bip32(err)
+ }
+}
+
+impl fmt::Display for KeyError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidScriptContext => write!(f, "Invalid script context"),
+ Self::InvalidNetwork => write!(f, "Invalid network"),
+ Self::InvalidChecksum => write!(f, "Invalid checksum"),
+ Self::Message(err) => write!(f, "{}", err),
+ Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
+ Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for KeyError {}
+
+#[cfg(test)]
+pub mod test {
+ use bitcoin::bip32;
+
+ use super::*;
+
+ pub const TEST_ENTROPY: [u8; 32] = [0xAA; 32];
+
+ #[test]
+ fn test_keys_generate_xprv() {
+ let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
+ bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
+
+ assert_eq!(generated_xprv.valid_networks, any_network());
+ assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
+ }
+
+ #[test]
+ fn test_keys_generate_wif() {
+ let generated_wif: GeneratedKey<_, miniscript::Segwitv0> =
+ bitcoin::PrivateKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
+
+ assert_eq!(generated_wif.valid_networks, any_network());
+ assert_eq!(
+ generated_wif.to_string(),
+ "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch"
+ );
+ }
+
+ #[cfg(feature = "keys-bip39")]
+ #[test]
+ fn test_keys_wif_network_bip39() {
+ let xkey: ExtendedKey = bip39::Mnemonic::parse_in(
+ bip39::Language::English,
+ "jelly crash boy whisper mouse ecology tuna soccer memory million news short",
+ )
+ .unwrap()
+ .into_extended_key()
+ .unwrap();
+ let xprv = xkey.into_xprv(Network::Testnet).unwrap();
+
+ assert_eq!(xprv.network, Network::Testnet);
+ }
+}
--- /dev/null
+#![doc = include_str!("../README.md")]
+// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(
+ docsrs,
+ doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
+)]
+#![no_std]
+#![warn(missing_docs)]
+
+#[cfg(feature = "std")]
+#[macro_use]
+extern crate std;
+
+#[doc(hidden)]
+#[macro_use]
+pub extern crate alloc;
+
+pub extern crate bitcoin;
+pub extern crate miniscript;
+extern crate serde;
+extern crate serde_json;
+
+#[cfg(feature = "keys-bip39")]
+extern crate bip39;
+
+pub mod descriptor;
+pub mod keys;
+pub mod psbt;
+pub(crate) mod types;
+pub mod wallet;
+
+pub use descriptor::template;
+pub use descriptor::HdKeyPaths;
+pub use types::*;
+pub use wallet::signer;
+pub use wallet::signer::SignOptions;
+pub use wallet::tx_builder::TxBuilder;
+pub use wallet::Wallet;
+
+/// Get the version of BDK at runtime
+pub fn version() -> &'static str {
+ env!("CARGO_PKG_VERSION", "unknown")
+}
+
+pub use bdk_chain as chain;
+pub(crate) use bdk_chain::collections;
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Additional functions on the `rust-bitcoin` `Psbt` structure.
+
+use alloc::vec::Vec;
+use bitcoin::Amount;
+use bitcoin::FeeRate;
+use bitcoin::Psbt;
+use bitcoin::TxOut;
+
+// TODO upstream the functions here to `rust-bitcoin`?
+
+/// Trait to add functions to extract utxos and calculate fees.
+pub trait PsbtUtils {
+ /// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
+ fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
+
+ /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
+ /// If the PSBT is missing a TxOut for an input returns None.
+ fn fee_amount(&self) -> Option<u64>;
+
+ /// The transaction's fee rate. This value will only be accurate if calculated AFTER the
+ /// `Psbt` is finalized and all witness/signature data is added to the
+ /// transaction.
+ /// If the PSBT is missing a TxOut for an input returns None.
+ fn fee_rate(&self) -> Option<FeeRate>;
+}
+
+impl PsbtUtils for Psbt {
+ fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
+ let tx = &self.unsigned_tx;
+ let input = self.inputs.get(input_index)?;
+
+ match (&input.witness_utxo, &input.non_witness_utxo) {
+ (Some(_), _) => input.witness_utxo.clone(),
+ (_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
+ in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
+ }),
+ _ => None,
+ }
+ }
+
+ fn fee_amount(&self) -> Option<u64> {
+ let tx = &self.unsigned_tx;
+ let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
+
+ utxos.map(|inputs| {
+ let input_amount: u64 = inputs.iter().map(|i| i.value.to_sat()).sum();
+ let output_amount: u64 = self
+ .unsigned_tx
+ .output
+ .iter()
+ .map(|o| o.value.to_sat())
+ .sum();
+ input_amount
+ .checked_sub(output_amount)
+ .expect("input amount must be greater than output amount")
+ })
+ }
+
+ fn fee_rate(&self) -> Option<FeeRate> {
+ let fee_amount = self.fee_amount();
+ let weight = self.clone().extract_tx().ok()?.weight();
+ fee_amount.map(|fee| Amount::from_sat(fee) / weight)
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+use alloc::boxed::Box;
+use core::convert::AsRef;
+
+use bdk_chain::ConfirmationTime;
+use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
+use bitcoin::psbt;
+
+use serde::{Deserialize, Serialize};
+
+/// Types of keychains
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
+pub enum KeychainKind {
+ /// External keychain, used for deriving recipient addresses.
+ External = 0,
+ /// Internal keychain, used for deriving change addresses.
+ Internal = 1,
+}
+
+impl KeychainKind {
+ /// Return [`KeychainKind`] as a byte
+ pub fn as_byte(&self) -> u8 {
+ match self {
+ KeychainKind::External => b'e',
+ KeychainKind::Internal => b'i',
+ }
+ }
+}
+
+impl AsRef<[u8]> for KeychainKind {
+ fn as_ref(&self) -> &[u8] {
+ match self {
+ KeychainKind::External => b"e",
+ KeychainKind::Internal => b"i",
+ }
+ }
+}
+
+/// An unspent output owned by a [`Wallet`].
+///
+/// [`Wallet`]: crate::Wallet
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
+pub struct LocalOutput {
+ /// Reference to a transaction output
+ pub outpoint: OutPoint,
+ /// Transaction output
+ pub txout: TxOut,
+ /// Type of keychain
+ pub keychain: KeychainKind,
+ /// Whether this UTXO is spent or not
+ pub is_spent: bool,
+ /// The derivation index for the script pubkey in the wallet
+ pub derivation_index: u32,
+ /// The confirmation time for transaction containing this utxo
+ pub confirmation_time: ConfirmationTime,
+}
+
+/// A [`Utxo`] with its `satisfaction_weight`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct WeightedUtxo {
+ /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
+ /// properly maintain the feerate when adding this input to a transaction during coin selection.
+ ///
+ /// [weight units]: https://en.bitcoin.it/wiki/Weight_units
+ pub satisfaction_weight: usize,
+ /// The UTXO
+ pub utxo: Utxo,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+/// An unspent transaction output (UTXO).
+pub enum Utxo {
+ /// A UTXO owned by the local wallet.
+ Local(LocalOutput),
+ /// A UTXO owned by another wallet.
+ Foreign {
+ /// The location of the output.
+ outpoint: OutPoint,
+ /// The nSequence value to set for this input.
+ sequence: Option<Sequence>,
+ /// The information about the input we require to add it to a PSBT.
+ // Box it to stop the type being too big.
+ psbt_input: Box<psbt::Input>,
+ },
+}
+
+impl Utxo {
+ /// Get the location of the UTXO
+ pub fn outpoint(&self) -> OutPoint {
+ match &self {
+ Utxo::Local(local) => local.outpoint,
+ Utxo::Foreign { outpoint, .. } => *outpoint,
+ }
+ }
+
+ /// Get the `TxOut` of the UTXO
+ pub fn txout(&self) -> &TxOut {
+ match &self {
+ Utxo::Local(local) => &local.txout,
+ Utxo::Foreign {
+ outpoint,
+ psbt_input,
+ ..
+ } => {
+ if let Some(prev_tx) = &psbt_input.non_witness_utxo {
+ return &prev_tx.output[outpoint.vout as usize];
+ }
+
+ if let Some(txout) = &psbt_input.witness_utxo {
+ return txout;
+ }
+
+ unreachable!("Foreign UTXOs will always have one of these set")
+ }
+ }
+ }
+
+ /// Get the sequence number if an explicit sequence number has to be set for this input.
+ pub fn sequence(&self) -> Option<Sequence> {
+ match self {
+ Utxo::Local(_) => None,
+ Utxo::Foreign { sequence, .. } => *sequence,
+ }
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Coin selection
+//!
+//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to
+//! define custom coin selection algorithms.
+//!
+//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on
+//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will
+//! be used if it is not explicitly set.
+//!
+//! [`TxBuilder`]: super::tx_builder::TxBuilder
+//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection
+//!
+//! ## Example
+//!
+//! ```
+//! # use std::str::FromStr;
+//! # use bitcoin::*;
+//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
+//! # use bdk_wallet::wallet::error::CreateTxError;
+//! # use bdk_persist::PersistBackend;
+//! # use bdk_wallet::*;
+//! # use bdk_wallet::wallet::coin_selection::decide_change;
+//! # use anyhow::Error;
+//! #[derive(Debug)]
+//! struct AlwaysSpendEverything;
+//!
+//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
+//! fn coin_select(
+//! &self,
+//! required_utxos: Vec<WeightedUtxo>,
+//! optional_utxos: Vec<WeightedUtxo>,
+//! fee_rate: FeeRate,
+//! target_amount: u64,
+//! drain_script: &Script,
+//! ) -> Result<CoinSelectionResult, coin_selection::Error> {
+//! let mut selected_amount = 0;
+//! let mut additional_weight = Weight::ZERO;
+//! let all_utxos_selected = required_utxos
+//! .into_iter()
+//! .chain(optional_utxos)
+//! .scan(
+//! (&mut selected_amount, &mut additional_weight),
+//! |(selected_amount, additional_weight), weighted_utxo| {
+//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
+//! **additional_weight += Weight::from_wu(
+//! (TxIn::default().segwit_weight().to_wu()
+//! + weighted_utxo.satisfaction_weight as u64)
+//! as u64,
+//! );
+//! Some(weighted_utxo.utxo)
+//! },
+//! )
+//! .collect::<Vec<_>>();
+//! let additional_fees = (fee_rate * additional_weight).to_sat();
+//! let amount_needed_with_fees = additional_fees + target_amount;
+//! if selected_amount < amount_needed_with_fees {
+//! return Err(coin_selection::Error::InsufficientFunds {
+//! needed: amount_needed_with_fees,
+//! available: selected_amount,
+//! });
+//! }
+//!
+//! let remaining_amount = selected_amount - amount_needed_with_fees;
+//!
+//! let excess = decide_change(remaining_amount, fee_rate, drain_script);
+//!
+//! Ok(CoinSelectionResult {
+//! selected: all_utxos_selected,
+//! fee_amount: additional_fees,
+//! excess,
+//! })
+//! }
+//! }
+//!
+//! # let mut wallet = doctest_wallet!();
+//! // create wallet, sync, ...
+//!
+//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
+//! .unwrap()
+//! .require_network(Network::Testnet)
+//! .unwrap();
+//! let psbt = {
+//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
+//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
+//! builder.finish()?
+//! };
+//!
+//! // inspect, sign, broadcast, ...
+//!
+//! # Ok::<(), anyhow::Error>(())
+//! ```
+
+use crate::chain::collections::HashSet;
+use crate::wallet::utils::IsDust;
+use crate::Utxo;
+use crate::WeightedUtxo;
+use bitcoin::FeeRate;
+
+use alloc::vec::Vec;
+use bitcoin::consensus::encode::serialize;
+use bitcoin::OutPoint;
+use bitcoin::TxIn;
+use bitcoin::{Script, Weight};
+
+use core::convert::TryInto;
+use core::fmt::{self, Formatter};
+use rand::seq::SliceRandom;
+
+/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
+/// overridden
+pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
+
+/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module
+#[derive(Debug)]
+pub enum Error {
+ /// Wallet's UTXO set is not enough to cover recipient's requested plus fee
+ InsufficientFunds {
+ /// Sats needed for some transaction
+ needed: u64,
+ /// Sats available for spending
+ available: u64,
+ },
+ /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for
+ /// the desired outputs plus fee, if there is not such combination this error is thrown
+ BnBNoExactMatch,
+ /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow
+ /// exponentially, thus a limit is set, and when hit, this error is thrown
+ BnBTotalTriesExceeded,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InsufficientFunds { needed, available } => write!(
+ f,
+ "Insufficient funds: {} sat available of {} sat needed",
+ available, needed
+ ),
+ Self::BnBTotalTriesExceeded => {
+ write!(f, "Branch and bound coin selection: total tries exceeded")
+ }
+ Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for Error {}
+
+#[derive(Debug)]
+/// Remaining amount after performing coin selection
+pub enum Excess {
+ /// It's not possible to create spendable output from excess using the current drain output
+ NoChange {
+ /// Threshold to consider amount as dust for this particular change script_pubkey
+ dust_threshold: u64,
+ /// Exceeding amount of current selection over outgoing value and fee costs
+ remaining_amount: u64,
+ /// The calculated fee for the drain TxOut with the selected script_pubkey
+ change_fee: u64,
+ },
+ /// It's possible to create spendable output from excess using the current drain output
+ Change {
+ /// Effective amount available to create change after deducting the change output fee
+ amount: u64,
+ /// The deducted change output fee
+ fee: u64,
+ },
+}
+
+/// Result of a successful coin selection
+#[derive(Debug)]
+pub struct CoinSelectionResult {
+ /// List of outputs selected for use as inputs
+ pub selected: Vec<Utxo>,
+ /// Total fee amount for the selected utxos in satoshis
+ pub fee_amount: u64,
+ /// Remaining amount after deducing fees and outgoing outputs
+ pub excess: Excess,
+}
+
+impl CoinSelectionResult {
+ /// The total value of the inputs selected.
+ pub fn selected_amount(&self) -> u64 {
+ self.selected.iter().map(|u| u.txout().value.to_sat()).sum()
+ }
+
+ /// The total value of the inputs selected from the local wallet.
+ pub fn local_selected_amount(&self) -> u64 {
+ self.selected
+ .iter()
+ .filter_map(|u| match u {
+ Utxo::Local(_) => Some(u.txout().value.to_sat()),
+ _ => None,
+ })
+ .sum()
+ }
+}
+
+/// Trait for generalized coin selection algorithms
+///
+/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
+/// selection algorithm when it creates transactions.
+///
+/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
+pub trait CoinSelectionAlgorithm: core::fmt::Debug {
+ /// Perform the coin selection
+ ///
+ /// - `database`: a reference to the wallet's database that can be used to lookup additional
+ /// details for a specific UTXO
+ /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their
+ /// weight cost
+ /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their
+ /// weight cost
+ /// - `fee_rate`: fee rate to use
+ /// - `target_amount`: the outgoing amount in satoshis and the fees already
+ /// accumulated from added outputs and transaction’s header.
+ /// - `drain_script`: the script to use in case of change
+ #[allow(clippy::too_many_arguments)]
+ fn coin_select(
+ &self,
+ required_utxos: Vec<WeightedUtxo>,
+ optional_utxos: Vec<WeightedUtxo>,
+ fee_rate: FeeRate,
+ target_amount: u64,
+ drain_script: &Script,
+ ) -> Result<CoinSelectionResult, Error>;
+}
+
+/// Simple and dumb coin selection
+///
+/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
+/// from the largest ones until the required amount is reached.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct LargestFirstCoinSelection;
+
+impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
+ fn coin_select(
+ &self,
+ required_utxos: Vec<WeightedUtxo>,
+ mut optional_utxos: Vec<WeightedUtxo>,
+ fee_rate: FeeRate,
+ target_amount: u64,
+ drain_script: &Script,
+ ) -> Result<CoinSelectionResult, Error> {
+ // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
+ // initially smallest to largest, before being reversed with `.rev()`.
+ let utxos = {
+ optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
+ required_utxos
+ .into_iter()
+ .map(|utxo| (true, utxo))
+ .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
+ };
+
+ select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
+ }
+}
+
+/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next
+///
+/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting
+/// from the oldest ones until the required amount is reached.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct OldestFirstCoinSelection;
+
+impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
+ fn coin_select(
+ &self,
+ required_utxos: Vec<WeightedUtxo>,
+ mut optional_utxos: Vec<WeightedUtxo>,
+ fee_rate: FeeRate,
+ target_amount: u64,
+ drain_script: &Script,
+ ) -> Result<CoinSelectionResult, Error> {
+ // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
+ // oldest to newest according to blocktime
+ // For utxo that doesn't exist in DB, they will have lowest priority to be selected
+ let utxos = {
+ optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
+ Utxo::Local(local) => Some(local.confirmation_time),
+ Utxo::Foreign { .. } => None,
+ });
+
+ required_utxos
+ .into_iter()
+ .map(|utxo| (true, utxo))
+ .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
+ };
+
+ select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
+ }
+}
+
+/// Decide if change can be created
+///
+/// - `remaining_amount`: the amount in which the selected coins exceed the target amount
+/// - `fee_rate`: required fee rate for the current selection
+/// - `drain_script`: script to consider change creation
+pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
+ // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
+ let drain_output_len = serialize(drain_script).len() + 8usize;
+ let change_fee =
+ (fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat();
+ let drain_val = remaining_amount.saturating_sub(change_fee);
+
+ if drain_val.is_dust(drain_script) {
+ let dust_threshold = drain_script.dust_value().to_sat();
+ Excess::NoChange {
+ dust_threshold,
+ change_fee,
+ remaining_amount,
+ }
+ } else {
+ Excess::Change {
+ amount: drain_val,
+ fee: change_fee,
+ }
+ }
+}
+
+fn select_sorted_utxos(
+ utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
+ fee_rate: FeeRate,
+ target_amount: u64,
+ drain_script: &Script,
+) -> Result<CoinSelectionResult, Error> {
+ let mut selected_amount = 0;
+ let mut fee_amount = 0;
+ let selected = utxos
+ .scan(
+ (&mut selected_amount, &mut fee_amount),
+ |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
+ if must_use || **selected_amount < target_amount + **fee_amount {
+ **fee_amount += (fee_rate
+ * Weight::from_wu(
+ TxIn::default().segwit_weight().to_wu()
+ + weighted_utxo.satisfaction_weight as u64,
+ ))
+ .to_sat();
+ **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
+ Some(weighted_utxo.utxo)
+ } else {
+ None
+ }
+ },
+ )
+ .collect::<Vec<_>>();
+
+ let amount_needed_with_fees = target_amount + fee_amount;
+ if selected_amount < amount_needed_with_fees {
+ return Err(Error::InsufficientFunds {
+ needed: amount_needed_with_fees,
+ available: selected_amount,
+ });
+ }
+
+ let remaining_amount = selected_amount - amount_needed_with_fees;
+
+ let excess = decide_change(remaining_amount, fee_rate, drain_script);
+
+ Ok(CoinSelectionResult {
+ selected,
+ fee_amount,
+ excess,
+ })
+}
+
+#[derive(Debug, Clone)]
+// Adds fee information to an UTXO.
+struct OutputGroup {
+ weighted_utxo: WeightedUtxo,
+ // Amount of fees for spending a certain utxo, calculated using a certain FeeRate
+ fee: u64,
+ // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
+ effective_value: i64,
+}
+
+impl OutputGroup {
+ fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
+ let fee = (fee_rate
+ * Weight::from_wu(
+ TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
+ ))
+ .to_sat();
+ let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
+ OutputGroup {
+ weighted_utxo,
+ fee,
+ effective_value,
+ }
+ }
+}
+
+/// Branch and bound coin selection
+///
+/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: <http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
+#[derive(Debug, Clone)]
+pub struct BranchAndBoundCoinSelection {
+ size_of_change: u64,
+}
+
+impl Default for BranchAndBoundCoinSelection {
+ fn default() -> Self {
+ Self {
+ // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes)
+ size_of_change: 8 + 1 + 22,
+ }
+ }
+}
+
+impl BranchAndBoundCoinSelection {
+ /// Create new instance with target size for change output
+ pub fn new(size_of_change: u64) -> Self {
+ Self { size_of_change }
+ }
+}
+
+const BNB_TOTAL_TRIES: usize = 100_000;
+
+impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
+ fn coin_select(
+ &self,
+ required_utxos: Vec<WeightedUtxo>,
+ optional_utxos: Vec<WeightedUtxo>,
+ fee_rate: FeeRate,
+ target_amount: u64,
+ drain_script: &Script,
+ ) -> Result<CoinSelectionResult, Error> {
+ // Mapping every (UTXO, usize) to an output group
+ let required_utxos: Vec<OutputGroup> = required_utxos
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative
+ // effective value
+ let optional_utxos: Vec<OutputGroup> = optional_utxos
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .filter(|u| u.effective_value.is_positive())
+ .collect();
+
+ let curr_value = required_utxos
+ .iter()
+ .fold(0, |acc, x| acc + x.effective_value);
+
+ let curr_available_value = optional_utxos
+ .iter()
+ .fold(0, |acc, x| acc + x.effective_value);
+
+ let cost_of_change =
+ (Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat();
+
+ // `curr_value` and `curr_available_value` are both the sum of *effective_values* of
+ // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
+ // negative effective value, so it will always be positive.
+ //
+ // Since we are required to spend the required UTXOs (curr_value) we have to consider
+ // all their effective values, even when negative, which means that curr_value could
+ // be negative as well.
+ //
+ // If the sum of curr_value and curr_available_value is negative or lower than our target,
+ // we can immediately exit with an error, as it's guaranteed we will never find a solution
+ // if we actually run the BnB.
+ let total_value: Result<u64, _> = (curr_available_value + curr_value).try_into();
+ match total_value {
+ Ok(v) if v >= target_amount => {}
+ _ => {
+ // Assume we spend all the UTXOs we can (all the required + all the optional with
+ // positive effective value), sum their value and their fee cost.
+ let (utxo_fees, utxo_value) = required_utxos
+ .iter()
+ .chain(optional_utxos.iter())
+ .fold((0, 0), |(mut fees, mut value), utxo| {
+ fees += utxo.fee;
+ value += utxo.weighted_utxo.utxo.txout().value.to_sat();
+
+ (fees, value)
+ });
+
+ // Add to the target the fee cost of the UTXOs
+ return Err(Error::InsufficientFunds {
+ needed: target_amount + utxo_fees,
+ available: utxo_value,
+ });
+ }
+ }
+
+ let target_amount = target_amount
+ .try_into()
+ .expect("Bitcoin amount to fit into i64");
+
+ if curr_value > target_amount {
+ // remaining_amount can't be negative as that would mean the
+ // selection wasn't successful
+ // target_amount = amount_needed + (fee_amount - vin_fees)
+ let remaining_amount = (curr_value - target_amount) as u64;
+
+ let excess = decide_change(remaining_amount, fee_rate, drain_script);
+
+ return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
+ vec![],
+ required_utxos,
+ excess,
+ ));
+ }
+
+ Ok(self
+ .bnb(
+ required_utxos.clone(),
+ optional_utxos.clone(),
+ curr_value,
+ curr_available_value,
+ target_amount,
+ cost_of_change,
+ drain_script,
+ fee_rate,
+ )
+ .unwrap_or_else(|_| {
+ self.single_random_draw(
+ required_utxos,
+ optional_utxos,
+ curr_value,
+ target_amount,
+ drain_script,
+ fee_rate,
+ )
+ }))
+ }
+}
+
+impl BranchAndBoundCoinSelection {
+ // TODO: make this more Rust-onic :)
+ // (And perhaps refactor with less arguments?)
+ #[allow(clippy::too_many_arguments)]
+ fn bnb(
+ &self,
+ required_utxos: Vec<OutputGroup>,
+ mut optional_utxos: Vec<OutputGroup>,
+ mut curr_value: i64,
+ mut curr_available_value: i64,
+ target_amount: i64,
+ cost_of_change: u64,
+ drain_script: &Script,
+ fee_rate: FeeRate,
+ ) -> Result<CoinSelectionResult, Error> {
+ // current_selection[i] will contain true if we are using optional_utxos[i],
+ // false otherwise. Note that current_selection.len() could be less than
+ // optional_utxos.len(), it just means that we still haven't decided if we should keep
+ // certain optional_utxos or not.
+ let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
+
+ // Sort the utxo_pool
+ optional_utxos.sort_unstable_by_key(|a| a.effective_value);
+ optional_utxos.reverse();
+
+ // Contains the best selection we found
+ let mut best_selection = Vec::new();
+ let mut best_selection_value = None;
+
+ // Depth First search loop for choosing the UTXOs
+ for _ in 0..BNB_TOTAL_TRIES {
+ // Conditions for starting a backtrack
+ let mut backtrack = false;
+ // Cannot possibly reach target with the amount remaining in the curr_available_value,
+ // or the selected value is out of range.
+ // Go back and try other branch
+ if curr_value + curr_available_value < target_amount
+ || curr_value > target_amount + cost_of_change as i64
+ {
+ backtrack = true;
+ } else if curr_value >= target_amount {
+ // Selected value is within range, there's no point in going forward. Start
+ // backtracking
+ backtrack = true;
+
+ // If we found a solution better than the previous one, or if there wasn't previous
+ // solution, update the best solution
+ if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
+ best_selection.clone_from(¤t_selection);
+ best_selection_value = Some(curr_value);
+ }
+
+ // If we found a perfect match, break here
+ if curr_value == target_amount {
+ break;
+ }
+ }
+
+ // Backtracking, moving backwards
+ if backtrack {
+ // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
+ while let Some(false) = current_selection.last() {
+ current_selection.pop();
+ curr_available_value += optional_utxos[current_selection.len()].effective_value;
+ }
+
+ if current_selection.last_mut().is_none() {
+ // We have walked back to the first utxo and no branch is untraversed. All solutions searched
+ // If best selection is empty, then there's no exact match
+ if best_selection.is_empty() {
+ return Err(Error::BnBNoExactMatch);
+ }
+ break;
+ }
+
+ if let Some(c) = current_selection.last_mut() {
+ // Output was included on previous iterations, try excluding now.
+ *c = false;
+ }
+
+ let utxo = &optional_utxos[current_selection.len() - 1];
+ curr_value -= utxo.effective_value;
+ } else {
+ // Moving forwards, continuing down this branch
+ let utxo = &optional_utxos[current_selection.len()];
+
+ // Remove this utxo from the curr_available_value utxo amount
+ curr_available_value -= utxo.effective_value;
+
+ // Inclusion branch first (Largest First Exploration)
+ current_selection.push(true);
+ curr_value += utxo.effective_value;
+ }
+ }
+
+ // Check for solution
+ if best_selection.is_empty() {
+ return Err(Error::BnBTotalTriesExceeded);
+ }
+
+ // Set output set
+ let selected_utxos = optional_utxos
+ .into_iter()
+ .zip(best_selection)
+ .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
+ .collect::<Vec<OutputGroup>>();
+
+ let selected_amount = best_selection_value.unwrap();
+
+ // remaining_amount can't be negative as that would mean the
+ // selection wasn't successful
+ // target_amount = amount_needed + (fee_amount - vin_fees)
+ let remaining_amount = (selected_amount - target_amount) as u64;
+
+ let excess = decide_change(remaining_amount, fee_rate, drain_script);
+
+ Ok(BranchAndBoundCoinSelection::calculate_cs_result(
+ selected_utxos,
+ required_utxos,
+ excess,
+ ))
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn single_random_draw(
+ &self,
+ required_utxos: Vec<OutputGroup>,
+ mut optional_utxos: Vec<OutputGroup>,
+ curr_value: i64,
+ target_amount: i64,
+ drain_script: &Script,
+ fee_rate: FeeRate,
+ ) -> CoinSelectionResult {
+ optional_utxos.shuffle(&mut rand::thread_rng());
+ let selected_utxos = optional_utxos.into_iter().fold(
+ (curr_value, vec![]),
+ |(mut amount, mut utxos), utxo| {
+ if amount >= target_amount {
+ (amount, utxos)
+ } else {
+ amount += utxo.effective_value;
+ utxos.push(utxo);
+ (amount, utxos)
+ }
+ },
+ );
+
+ // remaining_amount can't be negative as that would mean the
+ // selection wasn't successful
+ // target_amount = amount_needed + (fee_amount - vin_fees)
+ let remaining_amount = (selected_utxos.0 - target_amount) as u64;
+
+ let excess = decide_change(remaining_amount, fee_rate, drain_script);
+
+ BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
+ }
+
+ fn calculate_cs_result(
+ mut selected_utxos: Vec<OutputGroup>,
+ mut required_utxos: Vec<OutputGroup>,
+ excess: Excess,
+ ) -> CoinSelectionResult {
+ selected_utxos.append(&mut required_utxos);
+ let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::<u64>();
+ let selected = selected_utxos
+ .into_iter()
+ .map(|u| u.weighted_utxo.utxo)
+ .collect::<Vec<_>>();
+
+ CoinSelectionResult {
+ selected,
+ fee_amount,
+ excess,
+ }
+ }
+}
+
+/// Remove duplicate UTXOs.
+///
+/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
+pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
+where
+ I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
+{
+ let mut visited = HashSet::<OutPoint>::new();
+ let required = required
+ .into_iter()
+ .filter(|utxo| visited.insert(utxo.utxo.outpoint()))
+ .collect::<I>();
+ let optional = optional
+ .into_iter()
+ .filter(|utxo| visited.insert(utxo.utxo.outpoint()))
+ .collect::<I>();
+ (required, optional)
+}
+
+#[cfg(test)]
+mod test {
+ use assert_matches::assert_matches;
+ use core::str::FromStr;
+
+ use bdk_chain::ConfirmationTime;
+ use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
+
+ use super::*;
+ use crate::types::*;
+ use crate::wallet::coin_selection::filter_duplicates;
+
+ use rand::rngs::StdRng;
+ use rand::seq::SliceRandom;
+ use rand::{Rng, RngCore, SeedableRng};
+
+ // signature len (1WU) + signature and sighash (72WU)
+ // + pubkey len (1WU) + pubkey (33WU)
+ const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
+
+ const FEE_AMOUNT: u64 = 50;
+
+ fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo {
+ assert!(index < 10);
+ let outpoint = OutPoint::from_str(&format!(
+ "000000000000000000000000000000000000000000000000000000000000000{}:0",
+ index
+ ))
+ .unwrap();
+ WeightedUtxo {
+ satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
+ utxo: Utxo::Local(LocalOutput {
+ outpoint,
+ txout: TxOut {
+ value: Amount::from_sat(value),
+ script_pubkey: ScriptBuf::new(),
+ },
+ keychain: KeychainKind::External,
+ is_spent: false,
+ derivation_index: 42,
+ confirmation_time,
+ }),
+ }
+ }
+
+ fn get_test_utxos() -> Vec<WeightedUtxo> {
+ vec![
+ utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }),
+ utxo(
+ FEE_AMOUNT - 40,
+ 1,
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ ),
+ utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }),
+ ]
+ }
+
+ fn get_oldest_first_test_utxos() -> Vec<WeightedUtxo> {
+ // ensure utxos are from different tx
+ let utxo1 = utxo(
+ 120_000,
+ 1,
+ ConfirmationTime::Confirmed {
+ height: 1,
+ time: 1231006505,
+ },
+ );
+ let utxo2 = utxo(
+ 80_000,
+ 2,
+ ConfirmationTime::Confirmed {
+ height: 2,
+ time: 1231006505,
+ },
+ );
+ let utxo3 = utxo(
+ 300_000,
+ 3,
+ ConfirmationTime::Confirmed {
+ height: 3,
+ time: 1231006505,
+ },
+ );
+ vec![utxo1, utxo2, utxo3]
+ }
+
+ fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
+ let mut res = Vec::new();
+ for i in 0..utxos_number {
+ res.push(WeightedUtxo {
+ satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
+ utxo: Utxo::Local(LocalOutput {
+ outpoint: OutPoint::from_str(&format!(
+ "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
+ i
+ ))
+ .unwrap(),
+ txout: TxOut {
+ value: Amount::from_sat(rng.gen_range(0..200000000)),
+ script_pubkey: ScriptBuf::new(),
+ },
+ keychain: KeychainKind::External,
+ is_spent: false,
+ derivation_index: rng.next_u32(),
+ confirmation_time: if rng.gen_bool(0.5) {
+ ConfirmationTime::Confirmed {
+ height: rng.next_u32(),
+ time: rng.next_u64(),
+ }
+ } else {
+ ConfirmationTime::Unconfirmed { last_seen: 0 }
+ },
+ }),
+ });
+ }
+ res
+ }
+
+ fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
+ (0..utxos_number)
+ .map(|i| WeightedUtxo {
+ satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
+ utxo: Utxo::Local(LocalOutput {
+ outpoint: OutPoint::from_str(&format!(
+ "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
+ i
+ ))
+ .unwrap(),
+ txout: TxOut {
+ value: Amount::from_sat(utxos_value),
+ script_pubkey: ScriptBuf::new(),
+ },
+ keychain: KeychainKind::External,
+ is_spent: false,
+ derivation_index: 42,
+ confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
+ }),
+ })
+ .collect()
+ }
+
+ fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
+ let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
+ utxos.shuffle(&mut rng);
+ utxos[..utxos_picked_len]
+ .iter()
+ .map(|u| u.utxo.txout().value.to_sat())
+ .sum()
+ }
+
+ #[test]
+ fn test_largest_first_coin_selection_success() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 250_000 + FEE_AMOUNT;
+
+ let result = LargestFirstCoinSelection
+ .coin_select(
+ utxos,
+ vec![],
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 3);
+ assert_eq!(result.selected_amount(), 300_010);
+ assert_eq!(result.fee_amount, 204)
+ }
+
+ #[test]
+ fn test_largest_first_coin_selection_use_all() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let result = LargestFirstCoinSelection
+ .coin_select(
+ utxos,
+ vec![],
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 3);
+ assert_eq!(result.selected_amount(), 300_010);
+ assert_eq!(result.fee_amount, 204);
+ }
+
+ #[test]
+ fn test_largest_first_coin_selection_use_only_necessary() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let result = LargestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 1);
+ assert_eq!(result.selected_amount(), 200_000);
+ assert_eq!(result.fee_amount, 68);
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_largest_first_coin_selection_insufficient_funds() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 500_000 + FEE_AMOUNT;
+
+ LargestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 250_000 + FEE_AMOUNT;
+
+ LargestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1000),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ fn test_oldest_first_coin_selection_success() {
+ let utxos = get_oldest_first_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 180_000 + FEE_AMOUNT;
+
+ let result = OldestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 2);
+ assert_eq!(result.selected_amount(), 200_000);
+ assert_eq!(result.fee_amount, 136)
+ }
+
+ #[test]
+ fn test_oldest_first_coin_selection_use_all() {
+ let utxos = get_oldest_first_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let result = OldestFirstCoinSelection
+ .coin_select(
+ utxos,
+ vec![],
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 3);
+ assert_eq!(result.selected_amount(), 500_000);
+ assert_eq!(result.fee_amount, 204);
+ }
+
+ #[test]
+ fn test_oldest_first_coin_selection_use_only_necessary() {
+ let utxos = get_oldest_first_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let result = OldestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 1);
+ assert_eq!(result.selected_amount(), 120_000);
+ assert_eq!(result.fee_amount, 68);
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_oldest_first_coin_selection_insufficient_funds() {
+ let utxos = get_oldest_first_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 600_000 + FEE_AMOUNT;
+
+ OldestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
+ let utxos = get_oldest_first_test_utxos();
+
+ let target_amount: u64 = utxos
+ .iter()
+ .map(|wu| wu.utxo.txout().value.to_sat())
+ .sum::<u64>()
+ - 50;
+ let drain_script = ScriptBuf::default();
+
+ OldestFirstCoinSelection
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1000),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ fn test_bnb_coin_selection_success() {
+ // In this case bnb won't find a suitable match and single random draw will
+ // select three outputs
+ let utxos = generate_same_value_utxos(100_000, 20);
+
+ let drain_script = ScriptBuf::default();
+
+ let target_amount = 250_000 + FEE_AMOUNT;
+
+ let result = BranchAndBoundCoinSelection::default()
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 3);
+ assert_eq!(result.selected_amount(), 300_000);
+ assert_eq!(result.fee_amount, 204);
+ }
+
+ #[test]
+ fn test_bnb_coin_selection_required_are_enough() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let result = BranchAndBoundCoinSelection::default()
+ .coin_select(
+ utxos.clone(),
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 3);
+ assert_eq!(result.selected_amount(), 300_010);
+ assert_eq!(result.fee_amount, 204);
+ }
+
+ #[test]
+ fn test_bnb_coin_selection_optional_are_enough() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 299756 + FEE_AMOUNT;
+
+ let result = BranchAndBoundCoinSelection::default()
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 2);
+ assert_eq!(result.selected_amount(), 300000);
+ assert_eq!(result.fee_amount, 136);
+ }
+
+ #[test]
+ #[ignore]
+ fn test_bnb_coin_selection_required_not_enough() {
+ let utxos = get_test_utxos();
+
+ let required = vec![utxos[0].clone()];
+ let mut optional = utxos[1..].to_vec();
+ optional.push(utxo(
+ 500_000,
+ 3,
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ ));
+
+ // Defensive assertions, for sanity and in case someone changes the test utxos vector.
+ let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
+ assert_eq!(amount, 100_000);
+ let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum();
+ assert!(amount > 150_000);
+ let drain_script = ScriptBuf::default();
+
+ let target_amount = 150_000 + FEE_AMOUNT;
+
+ let result = BranchAndBoundCoinSelection::default()
+ .coin_select(
+ required,
+ optional,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 2);
+ assert_eq!(result.selected_amount(), 300_000);
+ assert_eq!(result.fee_amount, 136);
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_bnb_coin_selection_insufficient_funds() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 500_000 + FEE_AMOUNT;
+
+ BranchAndBoundCoinSelection::default()
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "InsufficientFunds")]
+ fn test_bnb_coin_selection_insufficient_funds_high_fees() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 250_000 + FEE_AMOUNT;
+
+ BranchAndBoundCoinSelection::default()
+ .coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(1000),
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ fn test_bnb_coin_selection_check_fee_rate() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+ let target_amount = 99932; // first utxo's effective value
+ let feerate = FeeRate::BROADCAST_MIN;
+
+ let result = BranchAndBoundCoinSelection::new(0)
+ .coin_select(vec![], utxos, feerate, target_amount, &drain_script)
+ .unwrap();
+
+ assert_eq!(result.selected.len(), 1);
+ assert_eq!(result.selected_amount(), 100_000);
+ let input_weight =
+ TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64;
+ // the final fee rate should be exactly the same as the fee rate given
+ let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight);
+ assert_eq!(result_feerate, feerate);
+ }
+
+ #[test]
+ fn test_bnb_coin_selection_exact_match() {
+ let seed = [0; 32];
+ let mut rng: StdRng = SeedableRng::from_seed(seed);
+
+ for _i in 0..200 {
+ let mut optional_utxos = generate_random_utxos(&mut rng, 16);
+ let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
+ let drain_script = ScriptBuf::default();
+ let result = BranchAndBoundCoinSelection::new(0)
+ .coin_select(
+ vec![],
+ optional_utxos,
+ FeeRate::ZERO,
+ target_amount,
+ &drain_script,
+ )
+ .unwrap();
+ assert_eq!(result.selected_amount(), target_amount);
+ }
+ }
+
+ #[test]
+ #[should_panic(expected = "BnBNoExactMatch")]
+ fn test_bnb_function_no_exact_match() {
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
+ let utxos: Vec<OutputGroup> = get_test_utxos()
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
+
+ let size_of_change = 31;
+ let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
+
+ let drain_script = ScriptBuf::default();
+ let target_amount = 20_000 + FEE_AMOUNT;
+ BranchAndBoundCoinSelection::new(size_of_change)
+ .bnb(
+ vec![],
+ utxos,
+ 0,
+ curr_available_value,
+ target_amount as i64,
+ cost_of_change,
+ &drain_script,
+ fee_rate,
+ )
+ .unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "BnBTotalTriesExceeded")]
+ fn test_bnb_function_tries_exceeded() {
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
+ let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
+
+ let size_of_change = 31;
+ let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
+ let target_amount = 20_000 + FEE_AMOUNT;
+
+ let drain_script = ScriptBuf::default();
+
+ BranchAndBoundCoinSelection::new(size_of_change)
+ .bnb(
+ vec![],
+ utxos,
+ 0,
+ curr_available_value,
+ target_amount as i64,
+ cost_of_change,
+ &drain_script,
+ fee_rate,
+ )
+ .unwrap();
+ }
+
+ // The match won't be exact but still in the range
+ #[test]
+ fn test_bnb_function_almost_exact_match_with_fees() {
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
+ let size_of_change = 31;
+ let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat();
+
+ let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ let curr_value = 0;
+
+ let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
+
+ // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
+ // cost_of_change + 5.
+ let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5;
+
+ let drain_script = ScriptBuf::default();
+
+ let result = BranchAndBoundCoinSelection::new(size_of_change)
+ .bnb(
+ vec![],
+ utxos,
+ curr_value,
+ curr_available_value,
+ target_amount,
+ cost_of_change,
+ &drain_script,
+ fee_rate,
+ )
+ .unwrap();
+ assert_eq!(result.selected_amount(), 100_000);
+ assert_eq!(result.fee_amount, 136);
+ }
+
+ // TODO: bnb() function should be optimized, and this test should be done with more utxos
+ #[test]
+ fn test_bnb_function_exact_match_more_utxos() {
+ let seed = [0; 32];
+ let mut rng: StdRng = SeedableRng::from_seed(seed);
+ let fee_rate = FeeRate::ZERO;
+
+ for _ in 0..200 {
+ let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ let curr_value = 0;
+
+ let curr_available_value = optional_utxos
+ .iter()
+ .fold(0, |acc, x| acc + x.effective_value);
+
+ let target_amount =
+ optional_utxos[3].effective_value + optional_utxos[23].effective_value;
+
+ let drain_script = ScriptBuf::default();
+
+ let result = BranchAndBoundCoinSelection::new(0)
+ .bnb(
+ vec![],
+ optional_utxos,
+ curr_value,
+ curr_available_value,
+ target_amount,
+ 0,
+ &drain_script,
+ fee_rate,
+ )
+ .unwrap();
+ assert_eq!(result.selected_amount(), target_amount as u64);
+ }
+ }
+
+ #[test]
+ fn test_single_random_draw_function_success() {
+ let seed = [0; 32];
+ let mut rng: StdRng = SeedableRng::from_seed(seed);
+ let mut utxos = generate_random_utxos(&mut rng, 300);
+ let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
+
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
+ let utxos: Vec<OutputGroup> = utxos
+ .into_iter()
+ .map(|u| OutputGroup::new(u, fee_rate))
+ .collect();
+
+ let drain_script = ScriptBuf::default();
+
+ let result = BranchAndBoundCoinSelection::default().single_random_draw(
+ vec![],
+ utxos,
+ 0,
+ target_amount as i64,
+ &drain_script,
+ fee_rate,
+ );
+
+ assert!(result.selected_amount() > target_amount);
+ assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
+ }
+
+ #[test]
+ fn test_bnb_exclude_negative_effective_value() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+
+ let selection = BranchAndBoundCoinSelection::default().coin_select(
+ vec![],
+ utxos,
+ FeeRate::from_sat_per_vb_unchecked(10),
+ 500_000,
+ &drain_script,
+ );
+
+ assert_matches!(
+ selection,
+ Err(Error::InsufficientFunds {
+ available: 300_000,
+ ..
+ })
+ );
+ }
+
+ #[test]
+ fn test_bnb_include_negative_effective_value_when_required() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+
+ let (required, optional) = utxos.into_iter().partition(
+ |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
+ );
+
+ let selection = BranchAndBoundCoinSelection::default().coin_select(
+ required,
+ optional,
+ FeeRate::from_sat_per_vb_unchecked(10),
+ 500_000,
+ &drain_script,
+ );
+
+ assert_matches!(
+ selection,
+ Err(Error::InsufficientFunds {
+ available: 300_010,
+ ..
+ })
+ );
+ }
+
+ #[test]
+ fn test_bnb_sum_of_effective_value_negative() {
+ let utxos = get_test_utxos();
+ let drain_script = ScriptBuf::default();
+
+ let selection = BranchAndBoundCoinSelection::default().coin_select(
+ utxos,
+ vec![],
+ FeeRate::from_sat_per_vb_unchecked(10_000),
+ 500_000,
+ &drain_script,
+ );
+
+ assert_matches!(
+ selection,
+ Err(Error::InsufficientFunds {
+ available: 300_010,
+ ..
+ })
+ );
+ }
+
+ #[test]
+ fn test_filter_duplicates() {
+ fn utxo(txid: &str, value: u64) -> WeightedUtxo {
+ WeightedUtxo {
+ satisfaction_weight: 0,
+ utxo: Utxo::Local(LocalOutput {
+ outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
+ txout: TxOut {
+ value: Amount::from_sat(value),
+ script_pubkey: ScriptBuf::new(),
+ },
+ keychain: KeychainKind::External,
+ is_spent: false,
+ derivation_index: 0,
+ confirmation_time: ConfirmationTime::Confirmed {
+ height: 12345,
+ time: 12345,
+ },
+ }),
+ }
+ }
+
+ fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
+ let mut v = utxos
+ .iter()
+ .map(|&(txid, value)| utxo(txid, value))
+ .collect::<Vec<_>>();
+ v.sort_by_key(|u| u.utxo.outpoint());
+ v
+ }
+
+ struct TestCase<'a> {
+ name: &'a str,
+ required: &'a [(&'a str, u64)],
+ optional: &'a [(&'a str, u64)],
+ exp_required: &'a [(&'a str, u64)],
+ exp_optional: &'a [(&'a str, u64)],
+ }
+
+ let test_cases = [
+ TestCase {
+ name: "no_duplicates",
+ required: &[("A", 1000), ("B", 2100)],
+ optional: &[("C", 1000)],
+ exp_required: &[("A", 1000), ("B", 2100)],
+ exp_optional: &[("C", 1000)],
+ },
+ TestCase {
+ name: "duplicate_required_utxos",
+ required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
+ optional: &[("D", 2100)],
+ exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
+ exp_optional: &[("D", 2100)],
+ },
+ TestCase {
+ name: "duplicate_optional_utxos",
+ required: &[("A", 3000), ("B", 1200)],
+ optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
+ exp_required: &[("A", 3000), ("B", 1200)],
+ exp_optional: &[("C", 5000), ("D", 1300)],
+ },
+ TestCase {
+ name: "duplicate_across_required_and_optional_utxos",
+ required: &[("A", 3000), ("B", 1200), ("C", 2100)],
+ optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
+ exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
+ exp_optional: &[("D", 1200), ("E", 5000)],
+ },
+ ];
+
+ for (i, t) in test_cases.into_iter().enumerate() {
+ println!("Case {}: {}", i, t.name);
+ let (required, optional) =
+ filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
+ assert_eq!(
+ required,
+ to_utxo_vec(t.exp_required),
+ "[{}:{}] unexpected `required` result",
+ i,
+ t.name
+ );
+ assert_eq!(
+ optional,
+ to_utxo_vec(t.exp_optional),
+ "[{}:{}] unexpected `optional` result",
+ i,
+ t.name
+ );
+ }
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
+
+use crate::descriptor::policy::PolicyError;
+use crate::descriptor::DescriptorError;
+use crate::wallet::coin_selection;
+use crate::{descriptor, KeychainKind};
+use alloc::string::String;
+use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
+use core::fmt;
+
+/// Errors returned by miniscript when updating inconsistent PSBTs
+#[derive(Debug, Clone)]
+pub enum MiniscriptPsbtError {
+ /// Descriptor key conversion error
+ Conversion(miniscript::descriptor::ConversionError),
+ /// Return error type for PsbtExt::update_input_with_descriptor
+ UtxoUpdate(miniscript::psbt::UtxoUpdateError),
+ /// Return error type for PsbtExt::update_output_with_descriptor
+ OutputUpdate(miniscript::psbt::OutputUpdateError),
+}
+
+impl fmt::Display for MiniscriptPsbtError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Conversion(err) => write!(f, "Conversion error: {}", err),
+ Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
+ Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for MiniscriptPsbtError {}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::finish`]
+///
+/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
+pub enum CreateTxError {
+ /// There was a problem with the descriptors passed in
+ Descriptor(DescriptorError),
+ /// We were unable to load wallet data from or write wallet data to the persistence backend
+ Persist(anyhow::Error),
+ /// There was a problem while extracting and manipulating policies
+ Policy(PolicyError),
+ /// Spending policy is not compatible with this [`KeychainKind`]
+ SpendingPolicyRequired(KeychainKind),
+ /// Requested invalid transaction version '0'
+ Version0,
+ /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV
+ Version1Csv,
+ /// Requested `LockTime` is less than is required to spend from this script
+ LockTime {
+ /// Requested `LockTime`
+ requested: absolute::LockTime,
+ /// Required `LockTime`
+ required: absolute::LockTime,
+ },
+ /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE
+ RbfSequence,
+ /// Cannot enable RBF with `Sequence` given a required OP_CSV
+ RbfSequenceCsv {
+ /// Given RBF `Sequence`
+ rbf: Sequence,
+ /// Required OP_CSV `Sequence`
+ csv: Sequence,
+ },
+ /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
+ FeeTooLow {
+ /// Required fee absolute value (satoshi)
+ required: u64,
+ },
+ /// When bumping a tx the fee rate requested is lower than required
+ FeeRateTooLow {
+ /// Required fee rate
+ required: bitcoin::FeeRate,
+ },
+ /// `manually_selected_only` option is selected but no utxo has been passed
+ NoUtxosSelected,
+ /// Output created is under the dust limit, 546 satoshis
+ OutputBelowDustLimit(usize),
+ /// The `change_policy` was set but the wallet does not have a change_descriptor
+ ChangePolicyDescriptor,
+ /// There was an error with coin selection
+ CoinSelection(coin_selection::Error),
+ /// Wallet's UTXO set is not enough to cover recipient's requested plus fee
+ InsufficientFunds {
+ /// Sats needed for some transaction
+ needed: u64,
+ /// Sats available for spending
+ available: u64,
+ },
+ /// Cannot build a tx without recipients
+ NoRecipients,
+ /// Partially signed bitcoin transaction error
+ Psbt(psbt::Error),
+ /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
+ /// key in the descriptor must either be a master key itself (having depth = 0) or have an
+ /// explicit origin provided
+ ///
+ /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
+ MissingKeyOrigin(String),
+ /// Happens when trying to spend an UTXO that is not in the internal database
+ UnknownUtxo,
+ /// Missing non_witness_utxo on foreign utxo for given `OutPoint`
+ MissingNonWitnessUtxo(OutPoint),
+ /// Miniscript PSBT error
+ MiniscriptPsbt(MiniscriptPsbtError),
+}
+
+impl fmt::Display for CreateTxError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Descriptor(e) => e.fmt(f),
+ Self::Persist(e) => {
+ write!(
+ f,
+ "failed to load wallet data from or write wallet data to persistence backend: {}",
+ e
+ )
+ }
+ Self::Policy(e) => e.fmt(f),
+ CreateTxError::SpendingPolicyRequired(keychain_kind) => {
+ write!(f, "Spending policy required: {:?}", keychain_kind)
+ }
+ CreateTxError::Version0 => {
+ write!(f, "Invalid version `0`")
+ }
+ CreateTxError::Version1Csv => {
+ write!(
+ f,
+ "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
+ )
+ }
+ CreateTxError::LockTime {
+ requested,
+ required,
+ } => {
+ write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested)
+ }
+ CreateTxError::RbfSequence => {
+ write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")
+ }
+ CreateTxError::RbfSequenceCsv { rbf, csv } => {
+ write!(
+ f,
+ "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
+ rbf, csv
+ )
+ }
+ CreateTxError::FeeTooLow { required } => {
+ write!(f, "Fee to low: required {} sat", required)
+ }
+ CreateTxError::FeeRateTooLow { required } => {
+ write!(
+ f,
+ // Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
+ //"Fee rate too low: required {required:#}"
+ "Fee rate too low: required {} sat/vb",
+ crate::floating_rate!(required)
+ )
+ }
+ CreateTxError::NoUtxosSelected => {
+ write!(f, "No UTXO selected")
+ }
+ CreateTxError::OutputBelowDustLimit(limit) => {
+ write!(f, "Output below the dust limit: {}", limit)
+ }
+ CreateTxError::ChangePolicyDescriptor => {
+ write!(
+ f,
+ "The `change_policy` can be set only if the wallet has a change_descriptor"
+ )
+ }
+ CreateTxError::CoinSelection(e) => e.fmt(f),
+ CreateTxError::InsufficientFunds { needed, available } => {
+ write!(
+ f,
+ "Insufficient funds: {} sat available of {} sat needed",
+ available, needed
+ )
+ }
+ CreateTxError::NoRecipients => {
+ write!(f, "Cannot build tx without recipients")
+ }
+ CreateTxError::Psbt(e) => e.fmt(f),
+ CreateTxError::MissingKeyOrigin(err) => {
+ write!(f, "Missing key origin: {}", err)
+ }
+ CreateTxError::UnknownUtxo => {
+ write!(f, "UTXO not found in the internal database")
+ }
+ CreateTxError::MissingNonWitnessUtxo(outpoint) => {
+ write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint)
+ }
+ CreateTxError::MiniscriptPsbt(err) => {
+ write!(f, "Miniscript PSBT error: {}", err)
+ }
+ }
+ }
+}
+
+impl From<descriptor::error::Error> for CreateTxError {
+ fn from(err: descriptor::error::Error) -> Self {
+ CreateTxError::Descriptor(err)
+ }
+}
+
+impl From<PolicyError> for CreateTxError {
+ fn from(err: PolicyError) -> Self {
+ CreateTxError::Policy(err)
+ }
+}
+
+impl From<MiniscriptPsbtError> for CreateTxError {
+ fn from(err: MiniscriptPsbtError) -> Self {
+ CreateTxError::MiniscriptPsbt(err)
+ }
+}
+
+impl From<psbt::Error> for CreateTxError {
+ fn from(err: psbt::Error) -> Self {
+ CreateTxError::Psbt(err)
+ }
+}
+
+impl From<coin_selection::Error> for CreateTxError {
+ fn from(err: coin_selection::Error) -> Self {
+ CreateTxError::CoinSelection(err)
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for CreateTxError {}
+
+#[derive(Debug)]
+/// Error returned from [`Wallet::build_fee_bump`]
+///
+/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
+pub enum BuildFeeBumpError {
+ /// Happens when trying to spend an UTXO that is not in the internal database
+ UnknownUtxo(OutPoint),
+ /// Thrown when a tx is not found in the internal database
+ TransactionNotFound(Txid),
+ /// Happens when trying to bump a transaction that is already confirmed
+ TransactionConfirmed(Txid),
+ /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
+ IrreplaceableTransaction(Txid),
+ /// Node doesn't have data to estimate a fee rate
+ FeeRateUnavailable,
+}
+
+impl fmt::Display for BuildFeeBumpError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnknownUtxo(outpoint) => write!(
+ f,
+ "UTXO not found in the internal database with txid: {}, vout: {}",
+ outpoint.txid, outpoint.vout
+ ),
+ Self::TransactionNotFound(txid) => {
+ write!(
+ f,
+ "Transaction not found in the internal database with txid: {}",
+ txid
+ )
+ }
+ Self::TransactionConfirmed(txid) => {
+ write!(f, "Transaction already confirmed with txid: {}", txid)
+ }
+ Self::IrreplaceableTransaction(txid) => {
+ write!(f, "Transaction can't be replaced with txid: {}", txid)
+ }
+ Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for BuildFeeBumpError {}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Wallet export
+//!
+//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md).
+//!
+//! ## Examples
+//!
+//! ### Import from JSON
+//!
+//! ```
+//! # use std::str::FromStr;
+//! # use bitcoin::*;
+//! # use bdk_wallet::wallet::export::*;
+//! # use bdk_wallet::*;
+//! let import = r#"{
+//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
+//! "blockheight":1782088,
+//! "label":"testnet"
+//! }"#;
+//!
+//! let import = FullyNodedExport::from_str(import)?;
+//! let wallet = Wallet::new_no_persist(
+//! &import.descriptor(),
+//! import.change_descriptor().as_ref(),
+//! Network::Testnet,
+//! )?;
+//! # Ok::<_, Box<dyn std::error::Error>>(())
+//! ```
+//!
+//! ### Export a `Wallet`
+//! ```
+//! # use bitcoin::*;
+//! # 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/*)"),
+//! Network::Testnet,
+//! )?;
+//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
+//!
+//! println!("Exported: {}", export.to_string());
+//! # Ok::<_, Box<dyn std::error::Error>>(())
+//! ```
+
+use alloc::string::String;
+use core::fmt;
+use core::str::FromStr;
+use serde::{Deserialize, Serialize};
+
+use miniscript::descriptor::{ShInner, WshInner};
+use miniscript::{Descriptor, ScriptContext, Terminal};
+
+use crate::types::KeychainKind;
+use crate::wallet::Wallet;
+
+/// Alias for [`FullyNodedExport`]
+#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")]
+pub type WalletExport = FullyNodedExport;
+
+/// Structure that contains the export of a wallet
+///
+/// For a usage example see [this module](crate::wallet::export)'s documentation.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct FullyNodedExport {
+ descriptor: String,
+ /// Earliest block to rescan when looking for the wallet's transactions
+ pub blockheight: u32,
+ /// Arbitrary label for the wallet
+ pub label: String,
+}
+
+impl fmt::Display for FullyNodedExport {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", serde_json::to_string(self).unwrap())
+ }
+}
+
+impl FromStr for FullyNodedExport {
+ type Err = serde_json::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ serde_json::from_str(s)
+ }
+}
+
+fn remove_checksum(s: String) -> String {
+ s.split_once('#').map(|(a, _)| String::from(a)).unwrap()
+}
+
+impl FullyNodedExport {
+ /// Export a wallet
+ ///
+ /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
+ /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44
+ /// and others.
+ ///
+ /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database
+ /// for the oldest transaction it knows and use that as the earliest block to rescan.
+ ///
+ /// If the database is empty or `include_blockheight` is false, the `blockheight` field
+ /// returned will be `0`.
+ pub fn export_wallet(
+ wallet: &Wallet,
+ label: &str,
+ include_blockheight: bool,
+ ) -> Result<Self, &'static str> {
+ let descriptor = wallet
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .to_string_with_secret(
+ &wallet
+ .get_signers(KeychainKind::External)
+ .as_key_map(wallet.secp_ctx()),
+ );
+ let descriptor = remove_checksum(descriptor);
+ Self::is_compatible_with_core(&descriptor)?;
+
+ let blockheight = if include_blockheight {
+ wallet.transactions().next().map_or(0, |canonical_tx| {
+ match canonical_tx.chain_position {
+ bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
+ bdk_chain::ChainPosition::Unconfirmed(_) => 0,
+ }
+ })
+ } else {
+ 0
+ };
+
+ let export = FullyNodedExport {
+ descriptor,
+ label: label.into(),
+ blockheight,
+ };
+
+ let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
+ false => None,
+ true => {
+ let descriptor = wallet
+ .get_descriptor_for_keychain(KeychainKind::Internal)
+ .to_string_with_secret(
+ &wallet
+ .get_signers(KeychainKind::Internal)
+ .as_key_map(wallet.secp_ctx()),
+ );
+ Some(remove_checksum(descriptor))
+ }
+ };
+ if export.change_descriptor() != change_descriptor {
+ return Err("Incompatible change descriptor");
+ }
+
+ Ok(export)
+ }
+
+ fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> {
+ fn check_ms<Ctx: ScriptContext>(
+ terminal: &Terminal<String, Ctx>,
+ ) -> Result<(), &'static str> {
+ if let Terminal::Multi(_, _) = terminal {
+ Ok(())
+ } else {
+ Err("The descriptor contains operators not supported by Bitcoin Core")
+ }
+ }
+
+ // pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti()
+ match Descriptor::<String>::from_str(descriptor).map_err(|_| "Invalid descriptor")? {
+ Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()),
+ Descriptor::Sh(sh) => match sh.as_inner() {
+ ShInner::Wpkh(_) => Ok(()),
+ ShInner::SortedMulti(_) => Ok(()),
+ ShInner::Wsh(wsh) => match wsh.as_inner() {
+ WshInner::SortedMulti(_) => Ok(()),
+ WshInner::Ms(ms) => check_ms(&ms.node),
+ },
+ ShInner::Ms(ms) => check_ms(&ms.node),
+ },
+ Descriptor::Wsh(wsh) => match wsh.as_inner() {
+ WshInner::SortedMulti(_) => Ok(()),
+ WshInner::Ms(ms) => check_ms(&ms.node),
+ },
+ _ => Err("The descriptor is not compatible with Bitcoin Core"),
+ }
+ }
+
+ /// Return the external descriptor
+ pub fn descriptor(&self) -> String {
+ self.descriptor.clone()
+ }
+
+ /// Return the internal descriptor, if present
+ pub fn change_descriptor(&self) -> Option<String> {
+ let replaced = self.descriptor.replace("/0/*", "/1/*");
+
+ if replaced != self.descriptor {
+ Some(replaced)
+ } else {
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use core::str::FromStr;
+
+ use crate::std::string::ToString;
+ use bdk_chain::{BlockId, ConfirmationTime};
+ use bitcoin::hashes::Hash;
+ use bitcoin::{transaction, BlockHash, Network, Transaction};
+
+ use super::*;
+ use crate::wallet::Wallet;
+
+ fn get_test_wallet(
+ descriptor: &str,
+ change_descriptor: Option<&str>,
+ network: Network,
+ ) -> Wallet {
+ let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
+ let transaction = Transaction {
+ input: vec![],
+ output: vec![],
+ version: transaction::Version::non_standard(0),
+ lock_time: bitcoin::absolute::LockTime::ZERO,
+ };
+ wallet
+ .insert_checkpoint(BlockId {
+ height: 5001,
+ hash: BlockHash::all_zeros(),
+ })
+ .unwrap();
+ wallet
+ .insert_tx(
+ transaction,
+ ConfirmationTime::Confirmed {
+ height: 5000,
+ time: 0,
+ },
+ )
+ .unwrap();
+ wallet
+ }
+
+ #[test]
+ fn test_export_bip44() {
+ let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
+ let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
+
+ let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
+ let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
+
+ assert_eq!(export.descriptor(), descriptor);
+ assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
+ assert_eq!(export.blockheight, 5000);
+ assert_eq!(export.label, "Test Label");
+ }
+
+ #[test]
+ #[should_panic(expected = "Incompatible change descriptor")]
+ fn test_export_no_change() {
+ // This wallet explicitly doesn't have a change descriptor. It should be impossible to
+ // export, because exporting this kind of external descriptor normally implies the
+ // existence of an internal descriptor
+
+ let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
+
+ let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
+ FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
+ }
+
+ #[test]
+ #[should_panic(expected = "Incompatible change descriptor")]
+ fn test_export_incompatible_change() {
+ // This wallet has a change descriptor, but the derivation path is not in the "standard"
+ // bip44/49/etc format
+
+ let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
+ let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
+
+ let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
+ FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
+ }
+
+ #[test]
+ fn test_export_multi() {
+ let descriptor = "wsh(multi(2,\
+ [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\
+ [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\
+ [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\
+ ))";
+ let change_descriptor = "wsh(multi(2,\
+ [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\
+ [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\
+ [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/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/*)";
+ let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
+
+ let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
+ let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
+
+ assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
+ }
+
+ #[test]
+ fn test_export_from_json() {
+ let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
+ let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
+
+ let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
+ let export = FullyNodedExport::from_str(import_str).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");
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! HWI Signer
+//!
+//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
+//! used with hardware wallets.
+//! ```no_run
+//! # use bdk_wallet::bitcoin::Network;
+//! # use bdk_wallet::signer::SignerOrdering;
+//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
+//! # use bdk_wallet::wallet::AddressIndex::New;
+//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+//! # use hwi::HWIClient;
+//! # use std::sync::Arc;
+//! #
+//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
+//! let mut devices = HWIClient::enumerate()?;
+//! if devices.is_empty() {
+//! panic!("No devices found!");
+//! }
+//! let first_device = devices.remove(0)?;
+//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
+//!
+//! # let mut wallet = Wallet::new_no_persist(
+//! # "",
+//! # None,
+//! # Network::Testnet,
+//! # )?;
+//! #
+//! // Adding the hardware signer to the BDK wallet
+//! wallet.add_signer(
+//! KeychainKind::External,
+//! SignerOrdering(200),
+//! Arc::new(custom_signer),
+//! );
+//!
+//! # Ok(())
+//! # }
+//! ```
+
+use bitcoin::bip32::Fingerprint;
+use bitcoin::secp256k1::{All, Secp256k1};
+use bitcoin::Psbt;
+
+use hwi::error::Error;
+use hwi::types::{HWIChain, HWIDevice};
+use hwi::HWIClient;
+
+use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
+
+#[derive(Debug)]
+/// Custom signer for Hardware Wallets
+///
+/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
+pub struct HWISigner {
+ fingerprint: Fingerprint,
+ client: HWIClient,
+}
+
+impl HWISigner {
+ /// Create a instance from the specified device and chain
+ pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
+ let client = HWIClient::get_client(device, false, chain)?;
+ Ok(HWISigner {
+ fingerprint: device.fingerprint,
+ client,
+ })
+ }
+}
+
+impl SignerCommon for HWISigner {
+ fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
+ SignerId::Fingerprint(self.fingerprint)
+ }
+}
+
+/// This implementation ignores `sign_options`
+impl TransactionSigner for HWISigner {
+ fn sign_transaction(
+ &self,
+ psbt: &mut Psbt,
+ _sign_options: &crate::SignOptions,
+ _secp: &crate::wallet::utils::SecpCtx,
+ ) -> Result<(), SignerError> {
+ psbt.combine(self.client.sign_tx(psbt)?.psbt)
+ .expect("Failed to combine HW signed psbt with passed PSBT");
+ Ok(())
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Wallet
+//!
+//! This module defines the [`Wallet`].
+use crate::collections::{BTreeMap, HashMap};
+use alloc::{
+ boxed::Box,
+ string::{String, ToString},
+ sync::Arc,
+ vec::Vec,
+};
+pub use bdk_chain::keychain::Balance;
+use bdk_chain::{
+ indexed_tx_graph,
+ keychain::{self, KeychainTxOutIndex},
+ local_chain::{
+ self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
+ },
+ spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
+ tx_graph::{CanonicalTx, TxGraph},
+ Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
+ IndexedTxGraph,
+};
+use bdk_persist::{Persist, PersistBackend};
+use bitcoin::secp256k1::{All, Secp256k1};
+use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
+use bitcoin::{
+ absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence,
+ Transaction, TxOut, Txid, Witness,
+};
+use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
+use bitcoin::{constants::genesis_block, Amount};
+use core::fmt;
+use core::ops::Deref;
+use descriptor::error::Error as DescriptorError;
+use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
+
+use bdk_chain::tx_graph::CalculateFeeError;
+
+pub mod coin_selection;
+pub mod export;
+pub mod signer;
+pub mod tx_builder;
+pub(crate) mod utils;
+
+pub mod error;
+
+pub use utils::IsDust;
+
+use coin_selection::DefaultCoinSelectionAlgorithm;
+use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
+use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
+use utils::{check_nsequence_rbf, After, Older, SecpCtx};
+
+use crate::descriptor::policy::BuildSatisfaction;
+use crate::descriptor::{
+ self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
+ ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
+};
+use crate::psbt::PsbtUtils;
+use crate::signer::SignerError;
+use crate::types::*;
+use crate::wallet::coin_selection::Excess::{Change, NoChange};
+use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError};
+
+const COINBASE_MATURITY: u32 = 100;
+
+/// A Bitcoin wallet
+///
+/// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions.
+/// Its main components are:
+///
+/// 1. output *descriptors* from which it can derive addresses.
+/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors.
+///
+/// [`signer`]: crate::signer
+#[derive(Debug)]
+pub struct Wallet {
+ signers: Arc<SignersContainer>,
+ change_signers: Arc<SignersContainer>,
+ chain: LocalChain,
+ indexed_graph: IndexedTxGraph<ConfirmationTimeHeightAnchor, KeychainTxOutIndex<KeychainKind>>,
+ persist: Persist<ChangeSet>,
+ network: Network,
+ secp: SecpCtx,
+}
+
+/// An update to [`Wallet`].
+///
+/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically.
+#[derive(Debug, Clone, Default)]
+pub struct Update {
+ /// Contains the last active derivation indices per keychain (`K`), which is used to update the
+ /// [`KeychainTxOutIndex`].
+ pub last_active_indices: BTreeMap<KeychainKind, u32>,
+
+ /// Update for the wallet's internal [`TxGraph`].
+ pub graph: TxGraph<ConfirmationTimeHeightAnchor>,
+
+ /// Update for the wallet's internal [`LocalChain`].
+ ///
+ /// [`LocalChain`]: local_chain::LocalChain
+ pub chain: Option<CheckPoint>,
+}
+
+impl From<FullScanResult<KeychainKind>> for Update {
+ fn from(value: FullScanResult<KeychainKind>) -> Self {
+ Self {
+ last_active_indices: value.last_active_indices,
+ graph: value.graph_update,
+ chain: Some(value.chain_update),
+ }
+ }
+}
+
+impl From<SyncResult> for Update {
+ fn from(value: SyncResult) -> Self {
+ Self {
+ last_active_indices: BTreeMap::new(),
+ graph: value.graph_update,
+ chain: Some(value.chain_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()
+ }
+ }
+}
+
+/// A derived address and the index it was found at.
+/// For convenience this automatically derefs to `Address`
+#[derive(Debug, PartialEq, Eq)]
+pub struct AddressInfo {
+ /// Child index of this address
+ pub index: u32,
+ /// Address
+ pub address: Address,
+ /// Type of keychain
+ pub keychain: KeychainKind,
+}
+
+impl Deref for AddressInfo {
+ type Target = Address;
+
+ fn deref(&self) -> &Self::Target {
+ &self.address
+ }
+}
+
+impl fmt::Display for AddressInfo {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.address)
+ }
+}
+
+impl Wallet {
+ /// Creates a wallet that does not persist data.
+ pub fn new_no_persist<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ network: Network,
+ ) -> Result<Self, DescriptorError> {
+ Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
+ NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
+ NewError::Descriptor(e) => e,
+ NewError::Persist(_) => unreachable!("mock-write must always succeed"),
+ })
+ }
+
+ /// Creates a wallet that does not persist data, with a custom genesis hash.
+ pub fn new_no_persist_with_genesis_hash<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ network: Network,
+ genesis_hash: BlockHash,
+ ) -> Result<Self, crate::descriptor::DescriptorError> {
+ Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
+ .map_err(|e| match e {
+ NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"),
+ NewError::Descriptor(e) => e,
+ NewError::Persist(_) => unreachable!("mock-write must always succeed"),
+ })
+ }
+}
+
+/// The error type when constructing a fresh [`Wallet`].
+///
+/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
+///
+/// [`new`]: Wallet::new
+/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
+#[derive(Debug)]
+pub enum NewError {
+ /// Database already has data.
+ NonEmptyDatabase,
+ /// There was problem with the passed-in descriptor(s).
+ Descriptor(crate::descriptor::DescriptorError),
+ /// We were unable to write the wallet's data to the persistence backend.
+ Persist(anyhow::Error),
+}
+
+impl fmt::Display for NewError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ NewError::NonEmptyDatabase => write!(
+ f,
+ "database already has data - use `load` or `new_or_load` methods instead"
+ ),
+ NewError::Descriptor(e) => e.fmt(f),
+ NewError::Persist(e) => e.fmt(f),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for NewError {}
+
+/// The error type when loading a [`Wallet`] from persistence.
+///
+/// Method [`load`] may return this error.
+///
+/// [`load`]: Wallet::load
+#[derive(Debug)]
+pub enum LoadError {
+ /// There was a problem with the passed-in descriptor(s).
+ Descriptor(crate::descriptor::DescriptorError),
+ /// Loading data from the persistence backend failed.
+ Persist(anyhow::Error),
+ /// Wallet not initialized, persistence backend is empty.
+ NotInitialized,
+ /// Data loaded from persistence is missing network type.
+ MissingNetwork,
+ /// Data loaded from persistence is missing genesis hash.
+ MissingGenesis,
+ /// Data loaded from persistence is missing descriptor.
+ MissingDescriptor,
+}
+
+impl fmt::Display for LoadError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ LoadError::Descriptor(e) => e.fmt(f),
+ LoadError::Persist(e) => e.fmt(f),
+ LoadError::NotInitialized => {
+ write!(f, "wallet is not initialized, persistence backend is empty")
+ }
+ LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
+ LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
+ LoadError::MissingDescriptor => write!(f, "loaded data is missing descriptor"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for LoadError {}
+
+/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
+///
+/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
+///
+/// [`new_or_load`]: Wallet::new_or_load
+/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
+#[derive(Debug)]
+pub enum NewOrLoadError {
+ /// There is a problem with the passed-in descriptor.
+ Descriptor(crate::descriptor::DescriptorError),
+ /// Either writing to or loading from the persistence backend failed.
+ Persist(anyhow::Error),
+ /// Wallet is not initialized, persistence backend is empty.
+ NotInitialized,
+ /// The loaded genesis hash does not match what was provided.
+ LoadedGenesisDoesNotMatch {
+ /// The expected genesis block hash.
+ expected: BlockHash,
+ /// The block hash loaded from persistence.
+ got: Option<BlockHash>,
+ },
+ /// The loaded network type does not match what was provided.
+ LoadedNetworkDoesNotMatch {
+ /// The expected network type.
+ expected: Network,
+ /// The network type loaded from persistence.
+ got: Option<Network>,
+ },
+ /// The loaded desccriptor does not match what was provided.
+ LoadedDescriptorDoesNotMatch {
+ /// The descriptor loaded from persistence.
+ got: Option<ExtendedDescriptor>,
+ /// The keychain of the descriptor not matching
+ keychain: KeychainKind,
+ },
+}
+
+impl fmt::Display for NewOrLoadError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ NewOrLoadError::Descriptor(e) => e.fmt(f),
+ NewOrLoadError::Persist(e) => write!(
+ f,
+ "failed to either write to or load from persistence, {}",
+ e
+ ),
+ NewOrLoadError::NotInitialized => {
+ write!(f, "wallet is not initialized, persistence backend is empty")
+ }
+ NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
+ write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
+ }
+ NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
+ write!(f, "loaded network type is not {}, got {:?}", expected, got)
+ }
+ NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
+ write!(
+ f,
+ "loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
+ got, keychain
+ )
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for NewOrLoadError {}
+
+/// An error that may occur when inserting a transaction into [`Wallet`].
+#[derive(Debug)]
+pub enum InsertTxError {
+ /// The error variant that occurs when the caller attempts to insert a transaction with a
+ /// confirmation height that is greater than the internal chain tip.
+ ConfirmationHeightCannotBeGreaterThanTip {
+ /// The internal chain's tip height.
+ tip_height: u32,
+ /// The introduced transaction's confirmation height.
+ tx_height: u32,
+ },
+}
+
+impl fmt::Display for InsertTxError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
+ tip_height,
+ tx_height,
+ } => {
+ write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for InsertTxError {}
+
+/// An error that may occur when applying a block to [`Wallet`].
+#[derive(Debug)]
+pub enum ApplyBlockError {
+ /// Occurs when the update chain cannot connect with original chain.
+ CannotConnect(CannotConnectError),
+ /// Occurs when the `connected_to` hash does not match the hash derived from `block`.
+ UnexpectedConnectedToHash {
+ /// Block hash of `connected_to`.
+ connected_to_hash: BlockHash,
+ /// Expected block hash of `connected_to`, as derived from `block`.
+ expected_hash: BlockHash,
+ },
+}
+
+impl fmt::Display for ApplyBlockError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ApplyBlockError::CannotConnect(err) => err.fmt(f),
+ ApplyBlockError::UnexpectedConnectedToHash {
+ expected_hash: block_hash,
+ connected_to_hash: checkpoint_hash,
+ } => write!(
+ f,
+ "`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
+ checkpoint_hash, block_hash
+ ),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ApplyBlockError {}
+
+impl Wallet {
+ /// Initialize an empty [`Wallet`].
+ pub fn new<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ network: Network,
+ ) -> Result<Self, NewError> {
+ let genesis_hash = genesis_block(network).block_hash();
+ Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash)
+ }
+
+ /// Initialize an empty [`Wallet`] with a custom genesis hash.
+ ///
+ /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
+ /// for syncing from alternative networks.
+ pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ network: Network,
+ genesis_hash: BlockHash,
+ ) -> Result<Self, NewError> {
+ if let Ok(changeset) = db.load_from_persistence() {
+ if changeset.is_some() {
+ return Err(NewError::NonEmptyDatabase);
+ }
+ }
+ let secp = Secp256k1::new();
+ let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
+ let mut index = KeychainTxOutIndex::<KeychainKind>::default();
+
+ let (signers, change_signers) =
+ create_signers(&mut index, &secp, descriptor, change_descriptor, network)
+ .map_err(NewError::Descriptor)?;
+
+ let indexed_graph = IndexedTxGraph::new(index);
+
+ let mut persist = Persist::new(db);
+ persist.stage(ChangeSet {
+ chain: chain_changeset,
+ indexed_tx_graph: indexed_graph.initial_changeset(),
+ network: Some(network),
+ });
+ persist.commit().map_err(NewError::Persist)?;
+
+ Ok(Wallet {
+ signers,
+ change_signers,
+ network,
+ chain,
+ indexed_graph,
+ persist,
+ secp,
+ })
+ }
+
+ /// Load [`Wallet`] from the given persistence backend.
+ ///
+ /// Note that the descriptor secret keys are not persisted to the db; this means that after
+ /// calling this method the [`Wallet`] **won't** know the secret keys, and as such, won't be
+ /// able to sign transactions.
+ ///
+ /// If you wish to use the wallet to sign transactions, you need to add the secret keys
+ /// manually to the [`Wallet`]:
+ ///
+ /// ```rust,no_run
+ /// # use bdk_wallet::Wallet;
+ /// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
+ /// # use bdk_wallet::descriptor::Descriptor;
+ /// # use bitcoin::key::Secp256k1;
+ /// # use bdk_wallet::KeychainKind;
+ /// # use bdk_file_store::Store;
+ /// #
+ /// # 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::wallet::ChangeSet> = Store::create_new(&[], &file_path).expect("must create db");
+ /// let secp = Secp256k1::new();
+ ///
+ /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
+ /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
+ ///
+ /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
+ /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
+ ///
+ /// let mut wallet = Wallet::load(db)?;
+ ///
+ /// external_signer_container.signers().into_iter()
+ /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
+ /// internal_signer_container.signers().into_iter()
+ /// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
+ /// passed-in descriptors to the [`Wallet`].
+ pub fn load(
+ mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ ) -> Result<Self, LoadError> {
+ let changeset = db
+ .load_from_persistence()
+ .map_err(LoadError::Persist)?
+ .ok_or(LoadError::NotInitialized)?;
+ Self::load_from_changeset(db, changeset)
+ }
+
+ fn load_from_changeset(
+ db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ changeset: ChangeSet,
+ ) -> Result<Self, LoadError> {
+ let secp = Secp256k1::new();
+ let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
+ let chain =
+ LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
+ let mut index = KeychainTxOutIndex::<KeychainKind>::default();
+ let descriptor = changeset
+ .indexed_tx_graph
+ .indexer
+ .keychains_added
+ .get(&KeychainKind::External)
+ .ok_or(LoadError::MissingDescriptor)?
+ .clone();
+ let change_descriptor = changeset
+ .indexed_tx_graph
+ .indexer
+ .keychains_added
+ .get(&KeychainKind::Internal)
+ .cloned();
+
+ let (signers, change_signers) =
+ create_signers(&mut index, &secp, descriptor, change_descriptor, network)
+ .expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
+
+ let mut indexed_graph = IndexedTxGraph::new(index);
+ indexed_graph.apply_changeset(changeset.indexed_tx_graph);
+
+ let persist = Persist::new(db);
+
+ Ok(Wallet {
+ signers,
+ change_signers,
+ chain,
+ indexed_graph,
+ persist,
+ network,
+ secp,
+ })
+ }
+
+ /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist.
+ ///
+ /// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
+ pub fn new_or_load<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ network: Network,
+ ) -> Result<Self, NewOrLoadError> {
+ let genesis_hash = genesis_block(network).block_hash();
+ Self::new_or_load_with_genesis_hash(
+ descriptor,
+ change_descriptor,
+ db,
+ network,
+ genesis_hash,
+ )
+ }
+
+ /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist, using the
+ /// provided descriptor, change descriptor, network, and custom genesis hash.
+ ///
+ /// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
+ /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
+ /// useful for syncing from alternative networks.
+ pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
+ network: Network,
+ genesis_hash: BlockHash,
+ ) -> Result<Self, NewOrLoadError> {
+ let changeset = db
+ .load_from_persistence()
+ .map_err(NewOrLoadError::Persist)?;
+ match changeset {
+ Some(changeset) => {
+ let mut wallet = Self::load_from_changeset(db, changeset).map_err(|e| match e {
+ LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
+ LoadError::Persist(e) => NewOrLoadError::Persist(e),
+ LoadError::NotInitialized => NewOrLoadError::NotInitialized,
+ LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
+ expected: network,
+ got: None,
+ },
+ LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
+ expected: genesis_hash,
+ got: None,
+ },
+ LoadError::MissingDescriptor => NewOrLoadError::LoadedDescriptorDoesNotMatch {
+ got: None,
+ keychain: KeychainKind::External,
+ },
+ })?;
+ if wallet.network != network {
+ return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
+ expected: network,
+ got: Some(wallet.network),
+ });
+ }
+ if wallet.chain.genesis_hash() != genesis_hash {
+ return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
+ expected: genesis_hash,
+ got: Some(wallet.chain.genesis_hash()),
+ });
+ }
+
+ let (expected_descriptor, expected_descriptor_keymap) = descriptor
+ .into_wallet_descriptor(&wallet.secp, network)
+ .map_err(NewOrLoadError::Descriptor)?;
+ let wallet_descriptor = wallet.public_descriptor(KeychainKind::External).cloned();
+ if wallet_descriptor != Some(expected_descriptor.clone()) {
+ return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
+ got: wallet_descriptor,
+ keychain: KeychainKind::External,
+ });
+ }
+ // if expected descriptor has private keys add them as new signers
+ if !expected_descriptor_keymap.is_empty() {
+ let signer_container = SignersContainer::build(
+ expected_descriptor_keymap,
+ &expected_descriptor,
+ &wallet.secp,
+ );
+ signer_container.signers().into_iter().for_each(|signer| {
+ wallet.add_signer(
+ KeychainKind::External,
+ SignerOrdering::default(),
+ signer.clone(),
+ )
+ });
+ }
+
+ let expected_change_descriptor = if let Some(c) = change_descriptor {
+ Some(
+ c.into_wallet_descriptor(&wallet.secp, network)
+ .map_err(NewOrLoadError::Descriptor)?,
+ )
+ } else {
+ None
+ };
+ let wallet_change_descriptor =
+ wallet.public_descriptor(KeychainKind::Internal).cloned();
+
+ match (expected_change_descriptor, wallet_change_descriptor) {
+ (Some((expected_descriptor, expected_keymap)), Some(wallet_descriptor))
+ if wallet_descriptor == expected_descriptor =>
+ {
+ // if expected change descriptor has private keys add them as new signers
+ if !expected_keymap.is_empty() {
+ let signer_container = SignersContainer::build(
+ expected_keymap,
+ &expected_descriptor,
+ &wallet.secp,
+ );
+ signer_container.signers().into_iter().for_each(|signer| {
+ wallet.add_signer(
+ KeychainKind::Internal,
+ SignerOrdering::default(),
+ signer.clone(),
+ )
+ });
+ }
+ }
+ (None, None) => (),
+ (_, wallet_descriptor) => {
+ return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
+ got: wallet_descriptor,
+ keychain: KeychainKind::Internal,
+ });
+ }
+ }
+
+ Ok(wallet)
+ }
+ None => Self::new_with_genesis_hash(
+ descriptor,
+ change_descriptor,
+ db,
+ network,
+ genesis_hash,
+ )
+ .map_err(|e| match e {
+ NewError::NonEmptyDatabase => {
+ unreachable!("database is already checked to have no data")
+ }
+ NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
+ NewError::Persist(e) => NewOrLoadError::Persist(e),
+ }),
+ }
+ }
+
+ /// Get the Bitcoin network the wallet is using.
+ pub fn network(&self) -> Network {
+ self.network
+ }
+
+ /// Iterator over all keychains in this wallet
+ pub fn keychains(&self) -> impl Iterator<Item = (&KeychainKind, &ExtendedDescriptor)> {
+ self.indexed_graph.index.keychains()
+ }
+
+ /// Peek an address of the given `keychain` at `index` without revealing it.
+ ///
+ /// For non-wildcard descriptors this returns the same address at every provided index.
+ ///
+ /// # Panics
+ ///
+ /// This panics when the caller requests for an address of derivation index greater than the
+ /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index.
+ pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo {
+ let keychain = self.map_keychain(keychain);
+ let mut spk_iter = self
+ .indexed_graph
+ .index
+ .unbounded_spk_iter(&keychain)
+ .expect("Must exist (we called map_keychain)");
+ if !spk_iter.descriptor().has_wildcard() {
+ index = 0;
+ }
+ let (index, spk) = spk_iter
+ .nth(index as usize)
+ .expect("derivation index is out of bounds");
+
+ AddressInfo {
+ index,
+ address: Address::from_script(&spk, self.network).expect("must have address form"),
+ keychain,
+ }
+ }
+
+ /// Attempt to reveal the next address of the given `keychain`.
+ ///
+ /// This will increment the internal derivation index. If the keychain's descriptor doesn't
+ /// contain a wildcard or every address is already revealed up to the maximum derivation
+ /// index defined in [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki),
+ /// then returns the last revealed address.
+ ///
+ /// # Errors
+ ///
+ /// If writing to persistent storage fails.
+ pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
+ let keychain = self.map_keychain(keychain);
+ let ((index, spk), index_changeset) = self
+ .indexed_graph
+ .index
+ .reveal_next_spk(&keychain)
+ .expect("Must exist (we called map_keychain)");
+
+ self.persist
+ .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
+
+ Ok(AddressInfo {
+ index,
+ address: Address::from_script(spk, self.network).expect("must have address form"),
+ keychain,
+ })
+ }
+
+ /// Reveal addresses up to and including the target `index` and return an iterator
+ /// of newly revealed addresses.
+ ///
+ /// If the target `index` is unreachable, we make a best effort to reveal up to the last
+ /// possible index. If all addresses up to the given `index` are already revealed, then
+ /// no new addresses are returned.
+ ///
+ /// # Errors
+ ///
+ /// If writing to persistent storage fails.
+ pub fn reveal_addresses_to(
+ &mut self,
+ keychain: KeychainKind,
+ index: u32,
+ ) -> anyhow::Result<impl Iterator<Item = AddressInfo> + '_> {
+ let keychain = self.map_keychain(keychain);
+ let (spk_iter, index_changeset) = self
+ .indexed_graph
+ .index
+ .reveal_to_target(&keychain, index)
+ .expect("must exist (we called map_keychain)");
+
+ self.persist
+ .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
+
+ Ok(spk_iter.map(move |(index, spk)| AddressInfo {
+ index,
+ address: Address::from_script(&spk, self.network).expect("must have address form"),
+ keychain,
+ }))
+ }
+
+ /// Get the next unused address for the given `keychain`, i.e. the address with the lowest
+ /// derivation index that hasn't been used.
+ ///
+ /// This will attempt to derive and reveal a new address if no newly revealed addresses
+ /// are available. See also [`reveal_next_address`](Self::reveal_next_address).
+ ///
+ /// # Errors
+ ///
+ /// If writing to persistent storage fails.
+ pub fn next_unused_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
+ let keychain = self.map_keychain(keychain);
+ let ((index, spk), index_changeset) = self
+ .indexed_graph
+ .index
+ .next_unused_spk(&keychain)
+ .expect("must exist (we called map_keychain)");
+
+ self.persist
+ .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
+
+ Ok(AddressInfo {
+ index,
+ address: Address::from_script(spk, self.network).expect("must have address form"),
+ keychain,
+ })
+ }
+
+ /// Marks an address used of the given `keychain` at `index`.
+ ///
+ /// Returns whether the given index was present and then removed from the unused set.
+ pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool {
+ self.indexed_graph.index.mark_used(keychain, index)
+ }
+
+ /// Undoes the effect of [`mark_used`] and returns whether the `index` was inserted
+ /// back into the unused set.
+ ///
+ /// Since this is only a superficial marker, it will have no effect if the address at the given
+ /// `index` was actually used, i.e. the wallet has previously indexed a tx output for the
+ /// derived spk.
+ ///
+ /// [`mark_used`]: Self::mark_used
+ pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool {
+ self.indexed_graph.index.unmark_used(keychain, index)
+ }
+
+ /// List addresses that are revealed but unused.
+ ///
+ /// Note if the returned iterator is empty you can reveal more addresses
+ /// by using [`reveal_next_address`](Self::reveal_next_address) or
+ /// [`reveal_addresses_to`](Self::reveal_addresses_to).
+ pub fn list_unused_addresses(
+ &self,
+ keychain: KeychainKind,
+ ) -> impl DoubleEndedIterator<Item = AddressInfo> + '_ {
+ let keychain = self.map_keychain(keychain);
+ self.indexed_graph
+ .index
+ .unused_keychain_spks(&keychain)
+ .map(move |(index, spk)| AddressInfo {
+ index,
+ address: Address::from_script(spk, self.network).expect("must have address form"),
+ keychain,
+ })
+ }
+
+ /// Return whether or not a `script` is part of this wallet (either internal or external)
+ pub fn is_mine(&self, script: &Script) -> bool {
+ self.indexed_graph.index.index_of_spk(script).is_some()
+ }
+
+ /// Finds how the wallet derived the script pubkey `spk`.
+ ///
+ /// Will only return `Some(_)` if the wallet has given out the spk.
+ pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> {
+ self.indexed_graph.index.index_of_spk(spk)
+ }
+
+ /// Return the list of unspent outputs of this wallet
+ pub fn list_unspent(&self) -> impl Iterator<Item = LocalOutput> + '_ {
+ self.indexed_graph
+ .graph()
+ .filter_chain_unspents(
+ &self.chain,
+ self.chain.tip().block_id(),
+ self.indexed_graph.index.outpoints(),
+ )
+ .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
+ }
+
+ /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed).
+ ///
+ /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead.
+ pub fn list_output(&self) -> impl Iterator<Item = LocalOutput> + '_ {
+ self.indexed_graph
+ .graph()
+ .filter_chain_txouts(
+ &self.chain,
+ self.chain.tip().block_id(),
+ self.indexed_graph.index.outpoints(),
+ )
+ .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
+ }
+
+ /// Get all the checkpoints the wallet is currently storing indexed by height.
+ pub fn checkpoints(&self) -> CheckPointIter {
+ self.chain.iter_checkpoints()
+ }
+
+ /// Returns the latest checkpoint.
+ pub fn latest_checkpoint(&self) -> CheckPoint {
+ self.chain.tip()
+ }
+
+ /// Get unbounded script pubkey iterators for both `Internal` and `External` keychains.
+ ///
+ /// This is intended to be used when doing a full scan of your addresses (e.g. after restoring
+ /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g.
+ /// electrum server) which will go through each address until it reaches a *stop gap*.
+ ///
+ /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what
+ /// script pubkeys the wallet is storing internally).
+ pub fn all_unbounded_spk_iters(
+ &self,
+ ) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, ScriptBuf)> + Clone> {
+ self.indexed_graph.index.all_unbounded_spk_iters()
+ }
+
+ /// Get an unbounded script pubkey iterator for the given `keychain`.
+ ///
+ /// See [`all_unbounded_spk_iters`] for more documentation
+ ///
+ /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters
+ pub fn unbounded_spk_iter(
+ &self,
+ keychain: KeychainKind,
+ ) -> impl Iterator<Item = (u32, ScriptBuf)> + Clone {
+ let keychain = self.map_keychain(keychain);
+ self.indexed_graph
+ .index
+ .unbounded_spk_iter(&keychain)
+ .expect("Must exist (we called map_keychain)")
+ }
+
+ /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
+ /// wallet's database.
+ pub fn get_utxo(&self, op: OutPoint) -> Option<LocalOutput> {
+ let (keychain, index, _) = self.indexed_graph.index.txout(op)?;
+ self.indexed_graph
+ .graph()
+ .filter_chain_unspents(
+ &self.chain,
+ self.chain.tip().block_id(),
+ core::iter::once(((), op)),
+ )
+ .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo))
+ .next()
+ }
+
+ /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph.
+ ///
+ /// This is used for providing a previous output's value so that we can use [`calculate_fee`]
+ /// or [`calculate_fee_rate`] on a given transaction. Outputs inserted with this method will
+ /// not be returned in [`list_unspent`] or [`list_output`].
+ ///
+ /// Any inserted `TxOut`s are not persisted until [`commit`] is called.
+ ///
+ /// **WARNING:** This should only be used to add `TxOut`s that the wallet does not own. Only
+ /// insert `TxOut`s that you trust the values for!
+ ///
+ /// [`calculate_fee`]: Self::calculate_fee
+ /// [`calculate_fee_rate`]: Self::calculate_fee_rate
+ /// [`list_unspent`]: Self::list_unspent
+ /// [`list_output`]: Self::list_output
+ /// [`commit`]: Self::commit
+ pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) {
+ let additions = self.indexed_graph.insert_txout(outpoint, txout);
+ self.persist.stage(ChangeSet::from(additions));
+ }
+
+ /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
+ ///
+ /// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must
+ /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function.
+ ///
+ /// Note `tx` does not have to be in the graph for this to work.
+ ///
+ /// # Examples
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Txid;
+ /// # use bdk_wallet::Wallet;
+ /// # let mut wallet: Wallet = todo!();
+ /// # let txid:Txid = todo!();
+ /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
+ /// let fee = wallet.calculate_fee(&tx).expect("fee");
+ /// ```
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Psbt;
+ /// # use bdk_wallet::Wallet;
+ /// # let mut wallet: Wallet = todo!();
+ /// # let mut psbt: Psbt = todo!();
+ /// let tx = &psbt.clone().extract_tx().expect("tx");
+ /// let fee = wallet.calculate_fee(tx).expect("fee");
+ /// ```
+ /// [`insert_txout`]: Self::insert_txout
+ pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
+ self.indexed_graph.graph().calculate_fee(tx)
+ }
+
+ /// Calculate the [`FeeRate`] for a given transaction.
+ ///
+ /// To calculate the fee rate for a [`Transaction`] with inputs not owned by this wallet you must
+ /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function.
+ ///
+ /// Note `tx` does not have to be in the graph for this to work.
+ ///
+ /// # Examples
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Txid;
+ /// # use bdk_wallet::Wallet;
+ /// # let mut wallet: Wallet = todo!();
+ /// # let txid:Txid = todo!();
+ /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
+ /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate");
+ /// ```
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Psbt;
+ /// # use bdk_wallet::Wallet;
+ /// # let mut wallet: Wallet = todo!();
+ /// # let mut psbt: Psbt = todo!();
+ /// let tx = &psbt.clone().extract_tx().expect("tx");
+ /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
+ /// ```
+ /// [`insert_txout`]: Self::insert_txout
+ pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
+ self.calculate_fee(tx)
+ .map(|fee| Amount::from_sat(fee) / tx.weight())
+ }
+
+ /// Compute the `tx`'s sent and received [`Amount`]s.
+ ///
+ /// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
+ /// that spend from previous txouts tracked by this wallet. Received is the summation
+ /// of this tx's outputs that send to script pubkeys tracked by this wallet.
+ ///
+ /// # Examples
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Txid;
+ /// # 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;
+ /// let (sent, received) = wallet.sent_and_received(&tx);
+ /// ```
+ ///
+ /// ```rust, no_run
+ /// # use bitcoin::Psbt;
+ /// # use bdk_wallet::Wallet;
+ /// # let mut wallet: Wallet = todo!();
+ /// # let mut psbt: Psbt = todo!();
+ /// let tx = &psbt.clone().extract_tx().expect("tx");
+ /// let (sent, received) = wallet.sent_and_received(tx);
+ /// ```
+ pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) {
+ self.indexed_graph.index.sent_and_received(tx, ..)
+ }
+
+ /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists).
+ ///
+ /// `CanonicalTx` contains the full transaction alongside meta-data such as:
+ /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist
+ /// in the best chain.
+ /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is
+ /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the
+ /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when
+ /// the transaction was last seen in the mempool is provided.
+ ///
+ /// ```rust, no_run
+ /// use bdk_chain::Anchor;
+ /// use bdk_wallet::{chain::ChainPosition, Wallet};
+ /// # let wallet: Wallet = todo!();
+ /// # let my_txid: bitcoin::Txid = todo!();
+ ///
+ /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist");
+ ///
+ /// // get reference to full transaction
+ /// println!("my tx: {:#?}", canonical_tx.tx_node.tx);
+ ///
+ /// // list all transaction anchors
+ /// for anchor in canonical_tx.tx_node.anchors {
+ /// println!(
+ /// "tx is anchored by block of hash {}",
+ /// anchor.anchor_block().hash
+ /// );
+ /// }
+ ///
+ /// // get confirmation status of transaction
+ /// match canonical_tx.chain_position {
+ /// ChainPosition::Confirmed(anchor) => println!(
+ /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
+ /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash,
+ /// ),
+ /// ChainPosition::Unconfirmed(last_seen) => println!(
+ /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
+ /// last_seen,
+ /// ),
+ /// }
+ /// ```
+ ///
+ /// [`Anchor`]: bdk_chain::Anchor
+ pub fn get_tx(
+ &self,
+ txid: Txid,
+ ) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
+ let graph = self.indexed_graph.graph();
+
+ Some(CanonicalTx {
+ chain_position: graph.get_chain_position(
+ &self.chain,
+ self.chain.tip().block_id(),
+ txid,
+ )?,
+ tx_node: graph.get_tx_node(txid)?,
+ })
+ }
+
+ /// Add a new checkpoint to the wallet's internal view of the chain.
+ /// This stages but does not [`commit`] the change.
+ ///
+ /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already
+ /// there).
+ ///
+ /// [`commit`]: Self::commit
+ pub fn insert_checkpoint(
+ &mut self,
+ block_id: BlockId,
+ ) -> Result<bool, local_chain::AlterCheckPointError> {
+ let changeset = self.chain.insert_block(block_id)?;
+ let changed = !changeset.is_empty();
+ self.persist.stage(changeset.into());
+ Ok(changed)
+ }
+
+ /// Add a transaction to the wallet's internal view of the chain. This stages but does not
+ /// [`commit`] the change.
+ ///
+ /// Returns whether anything changed with the transaction insertion (e.g. `false` if the
+ /// transaction was already inserted at the same position).
+ ///
+ /// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`].
+ /// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually
+ /// inserting new transactions.
+ ///
+ /// **WARNING:** If `position` is confirmed, we anchor the `tx` to a the lowest checkpoint that
+ /// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our
+ /// local view of the best chain's history.
+ ///
+ /// [`commit`]: Self::commit
+ /// [`latest_checkpoint`]: Self::latest_checkpoint
+ /// [`insert_checkpoint`]: Self::insert_checkpoint
+ pub fn insert_tx(
+ &mut self,
+ tx: Transaction,
+ position: ConfirmationTime,
+ ) -> Result<bool, InsertTxError> {
+ let (anchor, last_seen) = match position {
+ ConfirmationTime::Confirmed { height, time } => {
+ // anchor tx to checkpoint with lowest height that is >= position's height
+ let anchor = self
+ .chain
+ .range(height..)
+ .last()
+ .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
+ tip_height: self.chain.tip().height(),
+ tx_height: height,
+ })
+ .map(|anchor_cp| ConfirmationTimeHeightAnchor {
+ anchor_block: anchor_cp.block_id(),
+ confirmation_height: height,
+ confirmation_time: time,
+ })?;
+
+ (Some(anchor), None)
+ }
+ ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)),
+ };
+
+ let mut changeset = ChangeSet::default();
+ let txid = tx.txid();
+ changeset.append(self.indexed_graph.insert_tx(tx).into());
+ if let Some(anchor) = anchor {
+ changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into());
+ }
+ if let Some(last_seen) = last_seen {
+ changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into());
+ }
+
+ let changed = !changeset.is_empty();
+ self.persist.stage(changeset);
+ Ok(changed)
+ }
+
+ /// Iterate over the transactions in the wallet.
+ pub fn transactions(
+ &self,
+ ) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> + '_
+ {
+ self.indexed_graph
+ .graph()
+ .list_chain_txs(&self.chain, self.chain.tip().block_id())
+ }
+
+ /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
+ /// values.
+ pub fn get_balance(&self) -> Balance {
+ self.indexed_graph.graph().balance(
+ &self.chain,
+ self.chain.tip().block_id(),
+ self.indexed_graph.index.outpoints(),
+ |&(k, _), _| k == KeychainKind::Internal,
+ )
+ }
+
+ /// Add an external signer
+ ///
+ /// See [the `signer` module](signer) for an example.
+ pub fn add_signer(
+ &mut self,
+ keychain: KeychainKind,
+ ordering: SignerOrdering,
+ signer: Arc<dyn TransactionSigner>,
+ ) {
+ let signers = match keychain {
+ KeychainKind::External => Arc::make_mut(&mut self.signers),
+ KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
+ };
+
+ signers.add_external(signer.id(&self.secp), ordering, signer);
+ }
+
+ /// Get the signers
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # 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/*
+ /// println!("secret_key: {}", secret_key);
+ /// }
+ ///
+ /// Ok::<(), Box<dyn std::error::Error>>(())
+ /// ```
+ pub fn get_signers(&self, keychain: KeychainKind) -> Arc<SignersContainer> {
+ match keychain {
+ KeychainKind::External => Arc::clone(&self.signers),
+ KeychainKind::Internal => Arc::clone(&self.change_signers),
+ }
+ }
+
+ /// Start building a transaction.
+ ///
+ /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # 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 descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
+ /// # let mut wallet = doctest_wallet!();
+ /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
+ /// let psbt = {
+ /// let mut builder = wallet.build_tx();
+ /// builder
+ /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
+ /// builder.finish()?
+ /// };
+ ///
+ /// // sign and broadcast ...
+ /// # Ok::<(), anyhow::Error>(())
+ /// ```
+ ///
+ /// [`TxBuilder`]: crate::TxBuilder
+ pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> {
+ TxBuilder {
+ wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
+ params: TxParams::default(),
+ coin_selection: DefaultCoinSelectionAlgorithm::default(),
+ phantom: core::marker::PhantomData,
+ }
+ }
+
+ pub(crate) fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm>(
+ &mut self,
+ coin_selection: Cs,
+ params: TxParams,
+ ) -> Result<Psbt, CreateTxError> {
+ let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect();
+ let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist");
+ let internal_descriptor = keychains.get(&KeychainKind::Internal);
+
+ let external_policy = external_descriptor
+ .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
+ .unwrap();
+ let internal_policy = internal_descriptor
+ .as_ref()
+ .map(|desc| {
+ Ok::<_, CreateTxError>(
+ desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
+ .unwrap(),
+ )
+ })
+ .transpose()?;
+
+ // The policy allows spending external outputs, but it requires a policy path that hasn't been
+ // provided
+ if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange
+ && external_policy.requires_path()
+ && params.external_policy_path.is_none()
+ {
+ return Err(CreateTxError::SpendingPolicyRequired(
+ KeychainKind::External,
+ ));
+ };
+ // Same for the internal_policy path, if present
+ if let Some(internal_policy) = &internal_policy {
+ if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden
+ && internal_policy.requires_path()
+ && params.internal_policy_path.is_none()
+ {
+ return Err(CreateTxError::SpendingPolicyRequired(
+ KeychainKind::Internal,
+ ));
+ };
+ }
+
+ let external_requirements = external_policy.get_condition(
+ params
+ .external_policy_path
+ .as_ref()
+ .unwrap_or(&BTreeMap::new()),
+ )?;
+ let internal_requirements = internal_policy
+ .map(|policy| {
+ Ok::<_, CreateTxError>(
+ policy.get_condition(
+ params
+ .internal_policy_path
+ .as_ref()
+ .unwrap_or(&BTreeMap::new()),
+ )?,
+ )
+ })
+ .transpose()?;
+
+ let requirements =
+ external_requirements.merge(&internal_requirements.unwrap_or_default())?;
+
+ let version = match params.version {
+ Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0),
+ Some(tx_builder::Version(1)) if requirements.csv.is_some() => {
+ return Err(CreateTxError::Version1Csv)
+ }
+ Some(tx_builder::Version(x)) => x,
+ None if requirements.csv.is_some() => 2,
+ None => 1,
+ };
+
+ // We use a match here instead of a unwrap_or_else as it's way more readable :)
+ let current_height = match params.current_height {
+ // If they didn't tell us the current height, we assume it's the latest sync height.
+ None => {
+ let tip_height = self.chain.tip().height();
+ absolute::LockTime::from_height(tip_height).expect("invalid height")
+ }
+ Some(h) => h,
+ };
+
+ let lock_time = match params.locktime {
+ // When no nLockTime is specified, we try to prevent fee sniping, if possible
+ None => {
+ // Fee sniping can be partially prevented by setting the timelock
+ // to current_height. If we don't know the current_height,
+ // we default to 0.
+ let fee_sniping_height = current_height;
+
+ // We choose the biggest between the required nlocktime and the fee sniping
+ // height
+ match requirements.timelock {
+ // No requirement, just use the fee_sniping_height
+ None => fee_sniping_height,
+ // There's a block-based requirement, but the value is lower than the fee_sniping_height
+ Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => {
+ fee_sniping_height
+ }
+ // There's a time-based requirement or a block-based requirement greater
+ // than the fee_sniping_height use that value
+ Some(value) => value,
+ }
+ }
+ // Specific nLockTime required and we have no constraints, so just set to that value
+ Some(x) if requirements.timelock.is_none() => x,
+ // Specific nLockTime required and it's compatible with the constraints
+ Some(x)
+ if requirements.timelock.unwrap().is_same_unit(x)
+ && x >= requirements.timelock.unwrap() =>
+ {
+ x
+ }
+ // Invalid nLockTime required
+ Some(x) => {
+ return Err(CreateTxError::LockTime {
+ requested: x,
+ required: requirements.timelock.unwrap(),
+ })
+ }
+ };
+
+ // The nSequence to be by default for inputs unless an explicit sequence is specified.
+ let n_sequence = match (params.rbf, requirements.csv) {
+ // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
+ (None, None) if lock_time != absolute::LockTime::ZERO => {
+ Sequence::ENABLE_LOCKTIME_NO_RBF
+ }
+ // No RBF, CSV or nLockTime, make the transaction final
+ (None, None) => Sequence::MAX,
+
+ // No RBF requested, use the value from CSV. Note that this value is by definition
+ // non-final, so even if a timelock is enabled this nSequence is fine, hence why we
+ // don't bother checking for it here. The same is true for all the other branches below
+ (None, Some(csv)) => csv,
+
+ // RBF with a specific value but that value is too high
+ (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
+ return Err(CreateTxError::RbfSequence)
+ }
+ // RBF with a specific value requested, but the value is incompatible with CSV
+ (Some(tx_builder::RbfValue::Value(rbf)), Some(csv))
+ if !check_nsequence_rbf(rbf, csv) =>
+ {
+ return Err(CreateTxError::RbfSequenceCsv { rbf, csv })
+ }
+
+ // RBF enabled with the default value with CSV also enabled. CSV takes precedence
+ (Some(tx_builder::RbfValue::Default), Some(csv)) => csv,
+ // Valid RBF, either default or with a specific value. We ignore the `CSV` value
+ // because we've already checked it before
+ (Some(rbf), _) => rbf.get_value(),
+ };
+
+ let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() {
+ //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256
+ FeePolicy::FeeAmount(fee) => {
+ if let Some(previous_fee) = params.bumping_fee {
+ if fee < previous_fee.absolute {
+ return Err(CreateTxError::FeeTooLow {
+ required: previous_fee.absolute,
+ });
+ }
+ }
+ (FeeRate::ZERO, fee)
+ }
+ FeePolicy::FeeRate(rate) => {
+ if let Some(previous_fee) = params.bumping_fee {
+ let required_feerate = FeeRate::from_sat_per_kwu(
+ previous_fee.rate.to_sat_per_kwu()
+ + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb
+ );
+ if rate < required_feerate {
+ return Err(CreateTxError::FeeRateTooLow {
+ required: required_feerate,
+ });
+ }
+ }
+ (rate, 0)
+ }
+ };
+
+ let mut tx = Transaction {
+ version: transaction::Version::non_standard(version),
+ lock_time,
+ input: vec![],
+ output: vec![],
+ };
+
+ if params.manually_selected_only && params.utxos.is_empty() {
+ return Err(CreateTxError::NoUtxosSelected);
+ }
+
+ // we keep it as a float while we accumulate it, and only round it at the end
+ let mut outgoing: u64 = 0;
+ let mut received: u64 = 0;
+
+ let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
+
+ for (index, (script_pubkey, value)) in recipients.enumerate() {
+ if !params.allow_dust
+ && value.is_dust(script_pubkey)
+ && !script_pubkey.is_provably_unspendable()
+ {
+ return Err(CreateTxError::OutputBelowDustLimit(index));
+ }
+
+ if self.is_mine(script_pubkey) {
+ received += value;
+ }
+
+ let new_out = TxOut {
+ script_pubkey: script_pubkey.clone(),
+ value: Amount::from_sat(value),
+ };
+
+ tx.output.push(new_out);
+
+ outgoing += value;
+ }
+
+ fee_amount += (fee_rate * tx.weight()).to_sat();
+
+ if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
+ && internal_descriptor.is_none()
+ {
+ return Err(CreateTxError::ChangePolicyDescriptor);
+ }
+
+ let (required_utxos, optional_utxos) =
+ self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32()));
+
+ // get drain script
+ let drain_script = match params.drain_to {
+ Some(ref drain_recipient) => drain_recipient.clone(),
+ None => {
+ let change_keychain = self.map_keychain(KeychainKind::Internal);
+ let ((index, spk), index_changeset) = self
+ .indexed_graph
+ .index
+ .next_unused_spk(&change_keychain)
+ .expect("Keychain exists (we called map_keychain)");
+ let spk = spk.into();
+ self.indexed_graph.index.mark_used(change_keychain, index);
+ self.persist
+ .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
+ index_changeset,
+ )));
+ self.persist.commit().map_err(CreateTxError::Persist)?;
+ spk
+ }
+ };
+
+ let (required_utxos, optional_utxos) =
+ coin_selection::filter_duplicates(required_utxos, optional_utxos);
+
+ let coin_selection = coin_selection.coin_select(
+ required_utxos,
+ optional_utxos,
+ fee_rate,
+ outgoing + fee_amount,
+ &drain_script,
+ )?;
+ fee_amount += coin_selection.fee_amount;
+ let excess = &coin_selection.excess;
+
+ tx.input = coin_selection
+ .selected
+ .iter()
+ .map(|u| bitcoin::TxIn {
+ previous_output: u.outpoint(),
+ script_sig: ScriptBuf::default(),
+ sequence: u.sequence().unwrap_or(n_sequence),
+ witness: Witness::new(),
+ })
+ .collect();
+
+ if tx.output.is_empty() {
+ // Uh oh, our transaction has no outputs.
+ // We allow this when:
+ // - We have a drain_to address and the utxos we must spend (this happens,
+ // for example, when we RBF)
+ // - We have a drain_to address and drain_wallet set
+ // Otherwise, we don't know who we should send the funds to, and how much
+ // we should send!
+ if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) {
+ if let NoChange {
+ dust_threshold,
+ remaining_amount,
+ change_fee,
+ } = excess
+ {
+ return Err(CreateTxError::InsufficientFunds {
+ needed: *dust_threshold,
+ available: remaining_amount.saturating_sub(*change_fee),
+ });
+ }
+ } else {
+ return Err(CreateTxError::NoRecipients);
+ }
+ }
+
+ match excess {
+ NoChange {
+ remaining_amount, ..
+ } => fee_amount += remaining_amount,
+ Change { amount, fee } => {
+ if self.is_mine(&drain_script) {
+ received += amount;
+ }
+ fee_amount += fee;
+
+ // create drain output
+ let drain_output = TxOut {
+ value: Amount::from_sat(*amount),
+ script_pubkey: drain_script,
+ };
+
+ // TODO: We should pay attention when adding a new output: this might increase
+ // the length of the "number of vouts" parameter by 2 bytes, potentially making
+ // our feerate too low
+ tx.output.push(drain_output);
+ }
+ };
+
+ // sort input/outputs according to the chosen algorithm
+ params.ordering.sort_tx(&mut tx);
+
+ let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
+ Ok(psbt)
+ }
+
+ /// Bump the fee of a transaction previously created with this wallet.
+ ///
+ /// Returns an error if the transaction is already confirmed or doesn't explicitly signal
+ /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`]
+ /// pre-populated with the inputs and outputs of the original transaction.
+ ///
+ /// ## Example
+ ///
+ /// ```no_run
+ /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first.
+ /// # 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 descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
+ /// # let mut wallet = doctest_wallet!();
+ /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
+ /// let mut psbt = {
+ /// let mut builder = wallet.build_tx();
+ /// builder
+ /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
+ /// .enable_rbf();
+ /// builder.finish()?
+ /// };
+ /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
+ /// let tx = psbt.clone().extract_tx().expect("tx");
+ /// // broadcast tx but it's taking too long to confirm so we want to bump the fee
+ /// let mut psbt = {
+ /// let mut builder = wallet.build_fee_bump(tx.txid())?;
+ /// builder
+ /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));
+ /// builder.finish()?
+ /// };
+ ///
+ /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
+ /// let fee_bumped_tx = psbt.extract_tx();
+ /// // broadcast fee_bumped_tx to replace original
+ /// # Ok::<(), anyhow::Error>(())
+ /// ```
+ // TODO: support for merging multiple transactions while bumping the fees
+ pub fn build_fee_bump(
+ &mut self,
+ txid: Txid,
+ ) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
+ let graph = self.indexed_graph.graph();
+ let txout_index = &self.indexed_graph.index;
+ let chain_tip = self.chain.tip().block_id();
+
+ let mut tx = graph
+ .get_tx(txid)
+ .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
+ .as_ref()
+ .clone();
+
+ let pos = graph
+ .get_chain_position(&self.chain, chain_tip, txid)
+ .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
+ if let ChainPosition::Confirmed(_) = pos {
+ return Err(BuildFeeBumpError::TransactionConfirmed(txid));
+ }
+
+ if !tx
+ .input
+ .iter()
+ .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
+ {
+ return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid()));
+ }
+
+ let fee = self
+ .calculate_fee(&tx)
+ .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
+ let fee_rate = self
+ .calculate_fee_rate(&tx)
+ .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
+
+ // remove the inputs from the tx and process them
+ let original_txin = tx.input.drain(..).collect::<Vec<_>>();
+ let original_utxos = original_txin
+ .iter()
+ .map(|txin| -> Result<_, BuildFeeBumpError> {
+ let prev_tx = graph
+ .get_tx(txin.previous_output.txid)
+ .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
+ let txout = &prev_tx.output[txin.previous_output.vout as usize];
+
+ let confirmation_time: ConfirmationTime = graph
+ .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
+ .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
+ .cloned()
+ .into();
+
+ let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
+ Some((keychain, derivation_index)) => {
+ let satisfaction_weight = self
+ .get_descriptor_for_keychain(keychain)
+ .max_weight_to_satisfy()
+ .unwrap();
+ WeightedUtxo {
+ utxo: Utxo::Local(LocalOutput {
+ outpoint: txin.previous_output,
+ txout: txout.clone(),
+ keychain,
+ is_spent: true,
+ derivation_index,
+ confirmation_time,
+ }),
+ satisfaction_weight,
+ }
+ }
+ None => {
+ let satisfaction_weight =
+ serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len();
+ WeightedUtxo {
+ utxo: Utxo::Foreign {
+ outpoint: txin.previous_output,
+ sequence: Some(txin.sequence),
+ psbt_input: Box::new(psbt::Input {
+ witness_utxo: Some(txout.clone()),
+ non_witness_utxo: Some(prev_tx.as_ref().clone()),
+ ..Default::default()
+ }),
+ },
+ satisfaction_weight,
+ }
+ }
+ };
+
+ Ok(weighted_utxo)
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ if tx.output.len() > 1 {
+ let mut change_index = None;
+ for (index, txout) in tx.output.iter().enumerate() {
+ let change_type = self.map_keychain(KeychainKind::Internal);
+ match txout_index.index_of_spk(&txout.script_pubkey) {
+ Some((keychain, _)) if keychain == change_type => change_index = Some(index),
+ _ => {}
+ }
+ }
+
+ if let Some(change_index) = change_index {
+ tx.output.remove(change_index);
+ }
+ }
+
+ let params = TxParams {
+ // TODO: figure out what rbf option should be?
+ version: Some(tx_builder::Version(tx.version.0)),
+ recipients: tx
+ .output
+ .into_iter()
+ .map(|txout| (txout.script_pubkey, txout.value.to_sat()))
+ .collect(),
+ utxos: original_utxos,
+ bumping_fee: Some(tx_builder::PreviousFee {
+ absolute: fee,
+ rate: fee_rate,
+ }),
+ ..Default::default()
+ };
+
+ Ok(TxBuilder {
+ wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
+ params,
+ coin_selection: DefaultCoinSelectionAlgorithm::default(),
+ phantom: core::marker::PhantomData,
+ })
+ }
+
+ /// Sign a transaction with all the wallet's signers, in the order specified by every signer's
+ /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that has the value true if the PSBT was finalized, or false otherwise.
+ ///
+ /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way
+ /// the transaction is finalized at the end. Note that it can't be guaranteed that *every*
+ /// signers will follow the options, but the "software signers" (WIF keys and `xprv`) defined
+ /// in this library will.
+ ///
+ /// ## Example
+ ///
+ /// ```
+ /// # use std::str::FromStr;
+ /// # use bitcoin::*;
+ /// # 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!();
+ /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
+ /// let mut psbt = {
+ /// let mut builder = wallet.build_tx();
+ /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
+ /// builder.finish()?
+ /// };
+ /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
+ /// assert!(finalized, "we should have signed all the inputs");
+ /// # Ok::<(),anyhow::Error>(())
+ pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result<bool, SignerError> {
+ // This adds all the PSBT metadata for the inputs, which will help us later figure out how
+ // to derive our keys
+ self.update_psbt_with_descriptor(psbt)
+ .map_err(SignerError::MiniscriptPsbt)?;
+
+ // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
+ // has the `non_witness_utxo`
+ if !sign_options.trust_witness_utxo
+ && psbt
+ .inputs
+ .iter()
+ .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none())
+ .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
+ .any(|i| i.non_witness_utxo.is_none())
+ {
+ return Err(SignerError::MissingNonWitnessUtxo);
+ }
+
+ // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
+ // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot
+ if !sign_options.allow_all_sighashes
+ && !psbt.inputs.iter().all(|i| {
+ i.sighash_type.is_none()
+ || i.sighash_type == Some(EcdsaSighashType::All.into())
+ || i.sighash_type == Some(TapSighashType::All.into())
+ || i.sighash_type == Some(TapSighashType::Default.into())
+ })
+ {
+ return Err(SignerError::NonStandardSighash);
+ }
+
+ for signer in self
+ .signers
+ .signers()
+ .iter()
+ .chain(self.change_signers.signers().iter())
+ {
+ signer.sign_transaction(psbt, &sign_options, &self.secp)?;
+ }
+
+ // attempt to finalize
+ if sign_options.try_finalize {
+ self.finalize_psbt(psbt, sign_options)
+ } else {
+ Ok(false)
+ }
+ }
+
+ /// Return the spending policies for the wallet's descriptor
+ pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, DescriptorError> {
+ let signers = match keychain {
+ KeychainKind::External => &self.signers,
+ KeychainKind::Internal => &self.change_signers,
+ };
+
+ match self.public_descriptor(keychain) {
+ Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?),
+ None => Ok(None),
+ }
+ }
+
+ /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has
+ /// the same structure but with every secret key removed
+ ///
+ /// This can be used to build a watch-only version of a wallet
+ pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> {
+ self.indexed_graph
+ .index
+ .keychains()
+ .find(|(k, _)| *k == &keychain)
+ .map(|(_, d)| d)
+ }
+
+ /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass
+ /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to
+ /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer)
+ /// for further information.
+ ///
+ /// Returns `true` if the PSBT could be finalized, and `false` otherwise.
+ ///
+ /// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
+ pub fn finalize_psbt(
+ &self,
+ psbt: &mut Psbt,
+ sign_options: SignOptions,
+ ) -> Result<bool, SignerError> {
+ let chain_tip = self.chain.tip().block_id();
+
+ let tx = &psbt.unsigned_tx;
+ let mut finished = true;
+
+ for (n, input) in tx.input.iter().enumerate() {
+ let psbt_input = &psbt
+ .inputs
+ .get(n)
+ .ok_or(SignerError::InputIndexOutOfRange)?;
+ if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
+ continue;
+ }
+ let confirmation_height = self
+ .indexed_graph
+ .graph()
+ .get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
+ .map(|chain_position| match chain_position {
+ ChainPosition::Confirmed(a) => a.confirmation_height,
+ ChainPosition::Unconfirmed(_) => u32::MAX,
+ });
+ let current_height = sign_options
+ .assume_height
+ .unwrap_or_else(|| self.chain.tip().height());
+
+ // - Try to derive the descriptor by looking at the txout. If it's in our database, we
+ // know exactly which `keychain` to use, and which derivation index it is
+ // - If that fails, try to derive it by looking at the psbt input: the complete logic
+ // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`,
+ // `redeem_script` and `witness_script` to determine the right derivation
+ // - If that also fails, it will try it on the internal descriptor, if present
+ let desc = psbt
+ .get_utxo_for(n)
+ .and_then(|txout| self.get_descriptor_for_txout(&txout))
+ .or_else(|| {
+ self.indexed_graph.index.keychains().find_map(|(_, desc)| {
+ desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp)
+ })
+ });
+
+ match desc {
+ Some(desc) => {
+ let mut tmp_input = bitcoin::TxIn::default();
+ match desc.satisfy(
+ &mut tmp_input,
+ (
+ PsbtInputSatisfier::new(psbt, n),
+ After::new(Some(current_height), false),
+ Older::new(Some(current_height), confirmation_height, false),
+ ),
+ ) {
+ Ok(_) => {
+ let psbt_input = &mut psbt.inputs[n];
+ psbt_input.final_script_sig = Some(tmp_input.script_sig);
+ psbt_input.final_script_witness = Some(tmp_input.witness);
+ if sign_options.remove_partial_sigs {
+ psbt_input.partial_sigs.clear();
+ }
+ if sign_options.remove_taproot_extras {
+ // We just constructed the final witness, clear these fields.
+ psbt_input.tap_key_sig = None;
+ psbt_input.tap_script_sigs.clear();
+ psbt_input.tap_scripts.clear();
+ psbt_input.tap_key_origins.clear();
+ psbt_input.tap_internal_key = None;
+ psbt_input.tap_merkle_root = None;
+ }
+ }
+ Err(_) => finished = false,
+ }
+ }
+ None => finished = false,
+ }
+ }
+
+ if finished && sign_options.remove_taproot_extras {
+ for output in &mut psbt.outputs {
+ output.tap_key_origins.clear();
+ }
+ }
+
+ Ok(finished)
+ }
+
+ /// Return the secp256k1 context used for all signing operations
+ pub fn secp_ctx(&self) -> &SecpCtx {
+ &self.secp
+ }
+
+ /// Returns the descriptor used to create addresses for a particular `keychain`.
+ pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor {
+ self.public_descriptor(self.map_keychain(keychain))
+ .expect("we mapped it to external if it doesn't exist")
+ }
+
+ /// The derivation index of this wallet. It will return `None` if it has not derived any addresses.
+ /// Otherwise, it will return the index of the highest address it has derived.
+ pub fn derivation_index(&self, keychain: KeychainKind) -> Option<u32> {
+ self.indexed_graph.index.last_revealed_index(&keychain)
+ }
+
+ /// The index of the next address that you would get if you were to ask the wallet for a new address
+ pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 {
+ let keychain = self.map_keychain(keychain);
+ self.indexed_graph
+ .index
+ .next_index(&keychain)
+ .expect("Keychain must exist (we called map_keychain)")
+ .0
+ }
+
+ /// Informs the wallet that you no longer intend to broadcast a tx that was built from it.
+ ///
+ /// This frees up the change address used when creating the tx for use in future transactions.
+ // TODO: Make this free up reserved utxos when that's implemented
+ pub fn cancel_tx(&mut self, tx: &Transaction) {
+ let txout_index = &mut self.indexed_graph.index;
+ for txout in &tx.output {
+ if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) {
+ // NOTE: unmark_used will **not** make something unused if it has actually been used
+ // by a tx in the tracker. It only removes the superficial marking.
+ txout_index.unmark_used(keychain, index);
+ }
+ }
+ }
+
+ fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind {
+ if keychain == KeychainKind::Internal
+ && self.public_descriptor(KeychainKind::Internal).is_none()
+ {
+ KeychainKind::External
+ } else {
+ keychain
+ }
+ }
+
+ fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
+ let (keychain, child) = self
+ .indexed_graph
+ .index
+ .index_of_spk(&txout.script_pubkey)?;
+ let descriptor = self.get_descriptor_for_keychain(keychain);
+ descriptor.at_derivation_index(child).ok()
+ }
+
+ fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> {
+ self.list_unspent()
+ .map(|utxo| {
+ let keychain = utxo.keychain;
+ (utxo, {
+ self.get_descriptor_for_keychain(keychain)
+ .max_weight_to_satisfy()
+ .unwrap()
+ })
+ })
+ .collect()
+ }
+
+ /// Given the options returns the list of utxos that must be used to form the
+ /// transaction and any further that may be used if needed.
+ fn preselect_utxos(
+ &self,
+ params: &TxParams,
+ current_height: Option<u32>,
+ ) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
+ let TxParams {
+ change_policy,
+ unspendable,
+ utxos,
+ drain_wallet,
+ manually_selected_only,
+ bumping_fee,
+ ..
+ } = params;
+
+ let manually_selected = utxos.clone();
+ // we mandate confirmed transactions if we're bumping the fee
+ let must_only_use_confirmed_tx = bumping_fee.is_some();
+ let must_use_all_available = *drain_wallet;
+
+ let chain_tip = self.chain.tip().block_id();
+ // must_spend <- manually selected utxos
+ // may_spend <- all other available utxos
+ let mut may_spend = self.get_available_utxos();
+
+ may_spend.retain(|may_spend| {
+ !manually_selected
+ .iter()
+ .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
+ });
+ let mut must_spend = manually_selected;
+
+ // NOTE: we are intentionally ignoring `unspendable` here. i.e manual
+ // selection overrides unspendable.
+ if *manually_selected_only {
+ return (must_spend, vec![]);
+ }
+
+ let satisfies_confirmed = may_spend
+ .iter()
+ .map(|u| -> bool {
+ let txid = u.0.outpoint.txid;
+ let tx = match self.indexed_graph.graph().get_tx(txid) {
+ Some(tx) => tx,
+ None => return false,
+ };
+ let confirmation_time: ConfirmationTime = match self
+ .indexed_graph
+ .graph()
+ .get_chain_position(&self.chain, chain_tip, txid)
+ {
+ Some(chain_position) => chain_position.cloned().into(),
+ None => return false,
+ };
+
+ // Whether the UTXO is mature and, if needed, confirmed
+ let mut spendable = true;
+ if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
+ return false;
+ }
+ if tx.is_coinbase() {
+ debug_assert!(
+ confirmation_time.is_confirmed(),
+ "coinbase must always be confirmed"
+ );
+ if let Some(current_height) = current_height {
+ match confirmation_time {
+ ConfirmationTime::Confirmed { height, .. } => {
+ // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
+ spendable &=
+ (current_height.saturating_sub(height)) >= COINBASE_MATURITY;
+ }
+ ConfirmationTime::Unconfirmed { .. } => spendable = false,
+ }
+ }
+ }
+ spendable
+ })
+ .collect::<Vec<_>>();
+
+ let mut i = 0;
+ may_spend.retain(|u| {
+ let retain = change_policy.is_satisfied_by(&u.0)
+ && !unspendable.contains(&u.0.outpoint)
+ && satisfies_confirmed[i];
+ i += 1;
+ retain
+ });
+
+ let mut may_spend = may_spend
+ .into_iter()
+ .map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
+ satisfaction_weight,
+ utxo: Utxo::Local(local_utxo),
+ })
+ .collect();
+
+ if must_use_all_available {
+ must_spend.append(&mut may_spend);
+ }
+
+ (must_spend, may_spend)
+ }
+
+ fn complete_transaction(
+ &self,
+ tx: Transaction,
+ selected: Vec<Utxo>,
+ params: TxParams,
+ ) -> Result<Psbt, CreateTxError> {
+ let mut psbt = Psbt::from_unsigned_tx(tx)?;
+
+ if params.add_global_xpubs {
+ let all_xpubs = self
+ .keychains()
+ .flat_map(|(_, desc)| desc.get_extended_keys())
+ .collect::<Vec<_>>();
+
+ for xpub in all_xpubs {
+ let origin = match xpub.origin {
+ Some(origin) => origin,
+ None if xpub.xkey.depth == 0 => {
+ (xpub.root_fingerprint(&self.secp), vec![].into())
+ }
+ _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())),
+ };
+
+ psbt.xpub.insert(xpub.xkey, origin);
+ }
+ }
+
+ let mut lookup_output = selected
+ .into_iter()
+ .map(|utxo| (utxo.outpoint(), utxo))
+ .collect::<HashMap<_, _>>();
+
+ // add metadata for the inputs
+ for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) {
+ let utxo = match lookup_output.remove(&input.previous_output) {
+ Some(utxo) => utxo,
+ None => continue,
+ };
+
+ match utxo {
+ Utxo::Local(utxo) => {
+ *psbt_input =
+ match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
+ Ok(psbt_input) => psbt_input,
+ Err(e) => match e {
+ CreateTxError::UnknownUtxo => psbt::Input {
+ sighash_type: params.sighash,
+ ..psbt::Input::default()
+ },
+ _ => return Err(e),
+ },
+ }
+ }
+ Utxo::Foreign {
+ outpoint,
+ psbt_input: foreign_psbt_input,
+ ..
+ } => {
+ let is_taproot = foreign_psbt_input
+ .witness_utxo
+ .as_ref()
+ .map(|txout| txout.script_pubkey.is_p2tr())
+ .unwrap_or(false);
+ if !is_taproot
+ && !params.only_witness_utxo
+ && foreign_psbt_input.non_witness_utxo.is_none()
+ {
+ return Err(CreateTxError::MissingNonWitnessUtxo(outpoint));
+ }
+ *psbt_input = *foreign_psbt_input;
+ }
+ }
+ }
+
+ self.update_psbt_with_descriptor(&mut psbt)?;
+
+ Ok(psbt)
+ }
+
+ /// get the corresponding PSBT Input for a LocalUtxo
+ pub fn get_psbt_input(
+ &self,
+ utxo: LocalOutput,
+ sighash_type: Option<psbt::PsbtSighashType>,
+ only_witness_utxo: bool,
+ ) -> Result<psbt::Input, CreateTxError> {
+ // Try to find the prev_script in our db to figure out if this is internal or external,
+ // and the derivation index
+ let (keychain, child) = self
+ .indexed_graph
+ .index
+ .index_of_spk(&utxo.txout.script_pubkey)
+ .ok_or(CreateTxError::UnknownUtxo)?;
+
+ let mut psbt_input = psbt::Input {
+ sighash_type,
+ ..psbt::Input::default()
+ };
+
+ let desc = self.get_descriptor_for_keychain(keychain);
+ let derived_descriptor = desc
+ .at_derivation_index(child)
+ .expect("child can't be hardened");
+
+ psbt_input
+ .update_with_descriptor_unchecked(&derived_descriptor)
+ .map_err(MiniscriptPsbtError::Conversion)?;
+
+ let prev_output = utxo.outpoint;
+ if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) {
+ if desc.is_witness() || desc.is_taproot() {
+ psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
+ }
+ if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
+ psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone());
+ }
+ }
+ Ok(psbt_input)
+ }
+
+ fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> {
+ // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
+ // the input utxos and outputs
+ let utxos = (0..psbt.inputs.len())
+ .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
+ .chain(
+ psbt.unsigned_tx
+ .output
+ .iter()
+ .enumerate()
+ .map(|(i, out)| (false, i, out.clone())),
+ )
+ .collect::<Vec<_>>();
+
+ // Try to figure out the keychain and derivation for every input and output
+ for (is_input, index, out) in utxos.into_iter() {
+ if let Some((keychain, child)) =
+ self.indexed_graph.index.index_of_spk(&out.script_pubkey)
+ {
+ let desc = self.get_descriptor_for_keychain(keychain);
+ let desc = desc
+ .at_derivation_index(child)
+ .expect("child can't be hardened");
+
+ if is_input {
+ psbt.update_input_with_descriptor(index, &desc)
+ .map_err(MiniscriptPsbtError::UtxoUpdate)?;
+ } else {
+ psbt.update_output_with_descriptor(index, &desc)
+ .map_err(MiniscriptPsbtError::OutputUpdate)?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Return the checksum of the public descriptor associated to `keychain`
+ ///
+ /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor
+ pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String {
+ self.get_descriptor_for_keychain(keychain)
+ .to_string()
+ .split_once('#')
+ .unwrap()
+ .1
+ .to_string()
+ }
+
+ /// Applies an update to the wallet and stages the changes (but does not [`commit`] them).
+ ///
+ /// Usually you create an `update` by interacting with some blockchain data source and inserting
+ /// transactions related to your wallet into it.
+ ///
+ /// [`commit`]: Self::commit
+ pub fn apply_update(&mut self, update: impl Into<Update>) -> Result<(), CannotConnectError> {
+ let update = update.into();
+ let mut changeset = match update.chain {
+ Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?),
+ None => ChangeSet::default(),
+ };
+
+ let (_, index_changeset) = self
+ .indexed_graph
+ .index
+ .reveal_to_target_multi(&update.last_active_indices);
+ changeset.append(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
+ index_changeset,
+ )));
+ changeset.append(ChangeSet::from(
+ self.indexed_graph.apply_update(update.graph),
+ ));
+ self.persist.stage(changeset);
+ Ok(())
+ }
+
+ /// Commits all currently [`staged`] changed to the persistence backend returning and error when
+ /// this fails.
+ ///
+ /// This returns whether the `update` resulted in any changes.
+ ///
+ /// [`staged`]: Self::staged
+ pub fn commit(&mut self) -> anyhow::Result<bool> {
+ self.persist.commit().map(|c| c.is_some())
+ }
+
+ /// Returns the changes that will be committed with the next call to [`commit`].
+ ///
+ /// [`commit`]: Self::commit
+ pub fn staged(&self) -> &ChangeSet {
+ self.persist.staged()
+ }
+
+ /// Get a reference to the inner [`TxGraph`].
+ pub fn tx_graph(&self) -> &TxGraph<ConfirmationTimeHeightAnchor> {
+ self.indexed_graph.graph()
+ }
+
+ /// Get a reference to the inner [`KeychainTxOutIndex`].
+ pub fn spk_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
+ &self.indexed_graph.index
+ }
+
+ /// Get a reference to the inner [`LocalChain`].
+ pub fn local_chain(&self) -> &LocalChain {
+ &self.chain
+ }
+
+ /// Introduces a `block` of `height` to the wallet, and tries to connect it to the
+ /// `prev_blockhash` of the block's header.
+ ///
+ /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
+ /// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
+ ///
+ /// [`apply_block_connected_to`]: Self::apply_block_connected_to
+ pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> {
+ let connected_to = match height.checked_sub(1) {
+ Some(prev_height) => BlockId {
+ height: prev_height,
+ hash: block.header.prev_blockhash,
+ },
+ None => BlockId {
+ height,
+ hash: block.block_hash(),
+ },
+ };
+ self.apply_block_connected_to(block, height, connected_to)
+ .map_err(|err| match err {
+ ApplyHeaderError::InconsistentBlocks => {
+ unreachable!("connected_to is derived from the block so must be consistent")
+ }
+ ApplyHeaderError::CannotConnect(err) => err,
+ })
+ }
+
+ /// Applies relevant transactions from `block` of `height` to the wallet, and connects the
+ /// block to the internal chain.
+ ///
+ /// The `connected_to` parameter informs the wallet how this block connects to the internal
+ /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
+ /// internal [`TxGraph`].
+ pub fn apply_block_connected_to(
+ &mut self,
+ block: &Block,
+ height: u32,
+ connected_to: BlockId,
+ ) -> Result<(), ApplyHeaderError> {
+ let mut changeset = ChangeSet::default();
+ changeset.append(
+ self.chain
+ .apply_header_connected_to(&block.header, height, connected_to)?
+ .into(),
+ );
+ changeset.append(
+ self.indexed_graph
+ .apply_block_relevant(block, height)
+ .into(),
+ );
+ self.persist.stage(changeset);
+ Ok(())
+ }
+
+ /// Apply relevant unconfirmed transactions to the wallet.
+ ///
+ /// Transactions that are not relevant are filtered out.
+ ///
+ /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
+ /// when the transaction was last seen in the mempool. This is used for conflict resolution
+ /// when there is conflicting unconfirmed transactions. The transaction with the later
+ /// `last_seen` is prioritized.
+ pub fn apply_unconfirmed_txs<'t>(
+ &mut self,
+ unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
+ ) {
+ let indexed_graph_changeset = self
+ .indexed_graph
+ .batch_insert_relevant_unconfirmed(unconfirmed_txs);
+ self.persist.stage(ChangeSet::from(indexed_graph_changeset));
+ }
+}
+
+/// Methods to construct sync/full-scan requests for spk-based chain sources.
+impl Wallet {
+ /// Create a partial [`SyncRequest`] for this wallet for all revealed spks.
+ ///
+ /// This is the first step when performing a spk-based wallet partial sync, the returned
+ /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to
+ /// start a blockchain sync with a spk based blockchain client.
+ pub fn start_sync_with_revealed_spks(&self) -> SyncRequest {
+ SyncRequest::from_chain_tip(self.chain.tip())
+ .cache_graph_txs(self.tx_graph())
+ .populate_with_revealed_spks(&self.indexed_graph.index, ..)
+ }
+
+ /// Create a [`FullScanRequest] for this wallet.
+ ///
+ /// This is the first step when performing a spk-based wallet full scan, the returned
+ /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to
+ /// start a blockchain full scan with a spk based blockchain client.
+ ///
+ /// This operation is generally only used when importing or restoring a previously used wallet
+ /// in which the list of used scripts is not known.
+ pub fn start_full_scan(&self) -> FullScanRequest<KeychainKind> {
+ FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index)
+ .cache_graph_txs(self.tx_graph())
+ }
+}
+
+impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet {
+ fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor> {
+ self.indexed_graph.graph()
+ }
+}
+
+/// Deterministically generate a unique name given the descriptors defining the wallet
+///
+/// Compatible with [`wallet_name_from_descriptor`]
+pub fn wallet_name_from_descriptor<T>(
+ descriptor: T,
+ change_descriptor: Option<T>,
+ network: Network,
+ secp: &SecpCtx,
+) -> Result<String, DescriptorError>
+where
+ T: IntoWalletDescriptor,
+{
+ //TODO check descriptors contains only public keys
+ let descriptor = descriptor
+ .into_wallet_descriptor(secp, network)?
+ .0
+ .to_string();
+ let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
+ if let Some(change_descriptor) = change_descriptor {
+ let change_descriptor = change_descriptor
+ .into_wallet_descriptor(secp, network)?
+ .0
+ .to_string();
+ wallet_name.push_str(
+ calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
+ );
+ }
+
+ Ok(wallet_name)
+}
+
+fn new_local_utxo(
+ keychain: KeychainKind,
+ derivation_index: u32,
+ full_txo: FullTxOut<ConfirmationTimeHeightAnchor>,
+) -> LocalOutput {
+ LocalOutput {
+ outpoint: full_txo.outpoint,
+ txout: full_txo.txout,
+ is_spent: full_txo.spent_by.is_some(),
+ confirmation_time: full_txo.chain_position.into(),
+ keychain,
+ derivation_index,
+ }
+}
+
+fn create_signers<E: IntoWalletDescriptor>(
+ index: &mut KeychainTxOutIndex<KeychainKind>,
+ secp: &Secp256k1<All>,
+ descriptor: E,
+ change_descriptor: Option<E>,
+ network: Network,
+) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), crate::descriptor::error::Error> {
+ let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
+ let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+ let _ = index.insert_descriptor(KeychainKind::External, descriptor);
+
+ let change_signers = match change_descriptor {
+ Some(descriptor) => {
+ let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
+ let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+ let _ = index.insert_descriptor(KeychainKind::Internal, descriptor);
+ signers
+ }
+ None => Arc::new(SignersContainer::new()),
+ };
+
+ Ok((signers, change_signers))
+}
+
+/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
+#[macro_export]
+#[doc(hidden)]
+macro_rules! floating_rate {
+ ($rate:expr) => {{
+ use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
+ // sat_kwu / 250.0 -> sat_vb
+ $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64)
+ }};
+}
+
+#[macro_export]
+#[doc(hidden)]
+/// Macro for getting a wallet for use in a doctest
+macro_rules! doctest_wallet {
+ () => {{
+ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
+ use $crate::chain::{ConfirmationTime, BlockId};
+ use $crate::{KeychainKind, wallet::Wallet};
+ let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
+ let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
+
+ let mut wallet = Wallet::new_no_persist(
+ descriptor,
+ Some(change_descriptor),
+ Network::Regtest,
+ )
+ .unwrap();
+ let address = wallet.peek_address(KeychainKind::External, 0).address;
+ let tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ value: Amount::from_sat(500_000),
+ script_pubkey: address.script_pubkey(),
+ }],
+ };
+ let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
+ let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
+ height: 500,
+ time: 50_000
+ });
+
+ wallet
+ }}
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Generalized signers
+//!
+//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet)
+//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function.
+//!
+//! ```
+//! # use alloc::sync::Arc;
+//! # use core::str::FromStr;
+//! # use bitcoin::secp256k1::{Secp256k1, All};
+//! # use bitcoin::*;
+//! # use bdk_wallet::signer::*;
+//! # use bdk_wallet::*;
+//! # #[derive(Debug)]
+//! # struct CustomHSM;
+//! # impl CustomHSM {
+//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
+//! # Ok(())
+//! # }
+//! # fn connect() -> Self {
+//! # CustomHSM
+//! # }
+//! # fn get_id(&self) -> SignerId {
+//! # SignerId::Dummy(0)
+//! # }
+//! # }
+//! #[derive(Debug)]
+//! struct CustomSigner {
+//! device: CustomHSM,
+//! }
+//!
+//! impl CustomSigner {
+//! fn connect() -> Self {
+//! CustomSigner { device: CustomHSM::connect() }
+//! }
+//! }
+//!
+//! impl SignerCommon for CustomSigner {
+//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
+//! self.device.get_id()
+//! }
+//! }
+//!
+//! impl InputSigner for CustomSigner {
+//! fn sign_input(
+//! &self,
+//! psbt: &mut Psbt,
+//! input_index: usize,
+//! _sign_options: &SignOptions,
+//! _secp: &Secp256k1<All>,
+//! ) -> Result<(), SignerError> {
+//! self.device.hsm_sign_input(psbt, input_index)?;
+//!
+//! Ok(())
+//! }
+//! }
+//!
+//! let custom_signer = CustomSigner::connect();
+//!
+//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
+//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
+//! wallet.add_signer(
+//! KeychainKind::External,
+//! SignerOrdering(200),
+//! Arc::new(custom_signer)
+//! );
+//!
+//! # Ok::<_, anyhow::Error>(())
+//! ```
+
+use crate::collections::BTreeMap;
+use alloc::string::String;
+use alloc::sync::Arc;
+use alloc::vec::Vec;
+use core::cmp::Ordering;
+use core::fmt;
+use core::ops::{Bound::Included, Deref};
+
+use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
+use bitcoin::hashes::hash160;
+use bitcoin::secp256k1::Message;
+use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
+use bitcoin::{ecdsa, psbt, sighash, taproot};
+use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
+use bitcoin::{PrivateKey, Psbt, PublicKey};
+
+use miniscript::descriptor::{
+ Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
+ InnerXKey, KeyMap, SinglePriv, SinglePubKey,
+};
+use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
+
+use super::utils::SecpCtx;
+use crate::descriptor::{DescriptorMeta, XKeyUtils};
+use crate::psbt::PsbtUtils;
+use crate::wallet::error::MiniscriptPsbtError;
+
+/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
+/// multiple of them
+#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)]
+pub enum SignerId {
+ /// Bitcoin HASH160 (RIPEMD160 after SHA256) hash of an ECDSA public key
+ PkHash(hash160::Hash),
+ /// The fingerprint of a BIP32 extended key
+ Fingerprint(Fingerprint),
+ /// Dummy identifier
+ Dummy(u64),
+}
+
+impl From<hash160::Hash> for SignerId {
+ fn from(hash: hash160::Hash) -> SignerId {
+ SignerId::PkHash(hash)
+ }
+}
+
+impl From<Fingerprint> for SignerId {
+ fn from(fing: Fingerprint) -> SignerId {
+ SignerId::Fingerprint(fing)
+ }
+}
+
+/// Signing error
+#[derive(Debug)]
+pub enum SignerError {
+ /// The private key is missing for the required public key
+ MissingKey,
+ /// The private key in use has the right fingerprint but derives differently than expected
+ InvalidKey,
+ /// The user canceled the operation
+ UserCanceled,
+ /// Input index is out of range
+ InputIndexOutOfRange,
+ /// The `non_witness_utxo` field of the transaction is required to sign this input
+ MissingNonWitnessUtxo,
+ /// The `non_witness_utxo` specified is invalid
+ InvalidNonWitnessUtxo,
+ /// The `witness_utxo` field of the transaction is required to sign this input
+ MissingWitnessUtxo,
+ /// The `witness_script` field of the transaction is required to sign this input
+ MissingWitnessScript,
+ /// The fingerprint and derivation path are missing from the psbt input
+ MissingHdKeypath,
+ /// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
+ /// explicitly allowed them
+ ///
+ /// To enable signing transactions with non-standard sighashes set
+ /// [`SignOptions::allow_all_sighashes`] to `true`.
+ NonStandardSighash,
+ /// Invalid SIGHASH for the signing context in use
+ InvalidSighash,
+ /// Error while computing the hash to sign
+ SighashError(sighash::Error),
+ /// Miniscript PSBT error
+ MiniscriptPsbt(MiniscriptPsbtError),
+ /// To be used only by external libraries implementing [`InputSigner`] or
+ /// [`TransactionSigner`], so that they can return their own custom errors, without having to
+ /// modify [`SignerError`] in BDK.
+ External(String),
+}
+
+impl From<sighash::Error> for SignerError {
+ fn from(e: sighash::Error) -> Self {
+ SignerError::SighashError(e)
+ }
+}
+
+impl fmt::Display for SignerError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::MissingKey => write!(f, "Missing private key"),
+ Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"),
+ Self::UserCanceled => write!(f, "The user canceled the operation"),
+ Self::InputIndexOutOfRange => write!(f, "Input index out of range"),
+ Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"),
+ Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"),
+ Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"),
+ Self::MissingWitnessScript => write!(f, "Missing witness script"),
+ Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
+ Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
+ Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
+ Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
+ Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
+ Self::External(err) => write!(f, "{}", err),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for SignerError {}
+
+/// Signing context
+///
+/// Used by our software signers to determine the type of signatures to make
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SignerContext {
+ /// Legacy context
+ Legacy,
+ /// Segwit v0 context (BIP 143)
+ Segwitv0,
+ /// Taproot context (BIP 340)
+ Tap {
+ /// Whether the signer can sign for the internal key or not
+ is_internal_key: bool,
+ },
+}
+
+/// Wrapper to pair a signer with its context
+#[derive(Debug, Clone)]
+pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
+ signer: S,
+ ctx: SignerContext,
+}
+
+impl<S: Sized + fmt::Debug + Clone> SignerWrapper<S> {
+ /// Create a wrapped signer from a signer and a context
+ pub fn new(signer: S, ctx: SignerContext) -> Self {
+ SignerWrapper { signer, ctx }
+ }
+}
+
+impl<S: Sized + fmt::Debug + Clone> Deref for SignerWrapper<S> {
+ type Target = S;
+
+ fn deref(&self) -> &Self::Target {
+ &self.signer
+ }
+}
+
+/// Common signer methods
+pub trait SignerCommon: fmt::Debug + Send + Sync {
+ /// Return the [`SignerId`] for this signer
+ ///
+ /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
+ /// compare two signers.
+ fn id(&self, secp: &SecpCtx) -> SignerId;
+
+ /// Return the secret key for the signer
+ ///
+ /// This is used internally to reconstruct the original descriptor that may contain secrets.
+ /// External signers that are meant to keep key isolated should just return `None` here (which
+ /// is the default for this method, if not overridden).
+ fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
+ None
+ }
+}
+
+/// PSBT Input signer
+///
+/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing
+/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation
+/// for [`TransactionSigner`].
+pub trait InputSigner: SignerCommon {
+ /// Sign a single psbt input
+ fn sign_input(
+ &self,
+ psbt: &mut Psbt,
+ input_index: usize,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError>;
+}
+
+/// PSBT signer
+///
+/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction
+/// at once.
+pub trait TransactionSigner: SignerCommon {
+ /// Sign all the inputs of the psbt
+ fn sign_transaction(
+ &self,
+ psbt: &mut Psbt,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError>;
+}
+
+impl<T: InputSigner> TransactionSigner for T {
+ fn sign_transaction(
+ &self,
+ psbt: &mut Psbt,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError> {
+ for input_index in 0..psbt.inputs.len() {
+ self.sign_input(psbt, input_index, sign_options, secp)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
+ fn id(&self, secp: &SecpCtx) -> SignerId {
+ SignerId::from(self.root_fingerprint(secp))
+ }
+
+ fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
+ Some(DescriptorSecretKey::XPrv(self.signer.clone()))
+ }
+}
+
+impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
+ fn sign_input(
+ &self,
+ psbt: &mut Psbt,
+ input_index: usize,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError> {
+ if input_index >= psbt.inputs.len() {
+ return Err(SignerError::InputIndexOutOfRange);
+ }
+
+ if psbt.inputs[input_index].final_script_sig.is_some()
+ || psbt.inputs[input_index].final_script_witness.is_some()
+ {
+ return Ok(());
+ }
+
+ let tap_key_origins = psbt.inputs[input_index]
+ .tap_key_origins
+ .iter()
+ .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource));
+ let (public_key, full_path) = match psbt.inputs[input_index]
+ .bip32_derivation
+ .iter()
+ .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
+ .chain(tap_key_origins)
+ .find_map(|(pk, keysource)| {
+ if self.matches(keysource, secp).is_some() {
+ Some((pk, keysource.1.clone()))
+ } else {
+ None
+ }
+ }) {
+ Some((pk, full_path)) => (pk, full_path),
+ None => return Ok(()),
+ };
+
+ let derived_key = match self.origin.clone() {
+ Some((_fingerprint, origin_path)) => {
+ let deriv_path = DerivationPath::from(
+ &full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
+ [origin_path.len()..],
+ );
+ self.xkey.derive_priv(secp, &deriv_path).unwrap()
+ }
+ None => self.xkey.derive_priv(secp, &full_path).unwrap(),
+ };
+
+ let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key);
+ let valid_key = match public_key {
+ SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true,
+ SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true,
+ _ => false,
+ };
+ if !valid_key {
+ Err(SignerError::InvalidKey)
+ } else {
+ // HD wallets imply compressed keys
+ let priv_key = PrivateKey {
+ compressed: true,
+ network: self.xkey.network,
+ inner: derived_key.private_key,
+ };
+
+ SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
+ }
+ }
+}
+
+fn multikey_to_xkeys<K: InnerXKey + Clone>(
+ multikey: DescriptorMultiXKey<K>,
+) -> Vec<DescriptorXKey<K>> {
+ multikey
+ .derivation_paths
+ .into_paths()
+ .into_iter()
+ .map(|derivation_path| DescriptorXKey {
+ origin: multikey.origin.clone(),
+ xkey: multikey.xkey.clone(),
+ derivation_path,
+ wildcard: multikey.wildcard,
+ })
+ .collect()
+}
+
+impl SignerCommon for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
+ fn id(&self, secp: &SecpCtx) -> SignerId {
+ SignerId::from(self.root_fingerprint(secp))
+ }
+
+ fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
+ Some(DescriptorSecretKey::MultiXPrv(self.signer.clone()))
+ }
+}
+
+impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
+ fn sign_input(
+ &self,
+ psbt: &mut Psbt,
+ input_index: usize,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError> {
+ let xkeys = multikey_to_xkeys(self.signer.clone());
+ for xkey in xkeys {
+ SignerWrapper::new(xkey, self.ctx).sign_input(psbt, input_index, sign_options, secp)?
+ }
+ Ok(())
+ }
+}
+
+impl SignerCommon for SignerWrapper<PrivateKey> {
+ fn id(&self, secp: &SecpCtx) -> SignerId {
+ SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
+ }
+
+ fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
+ Some(DescriptorSecretKey::Single(SinglePriv {
+ key: self.signer,
+ origin: None,
+ }))
+ }
+}
+
+impl InputSigner for SignerWrapper<PrivateKey> {
+ fn sign_input(
+ &self,
+ psbt: &mut Psbt,
+ input_index: usize,
+ sign_options: &SignOptions,
+ secp: &SecpCtx,
+ ) -> Result<(), SignerError> {
+ if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
+ return Err(SignerError::InputIndexOutOfRange);
+ }
+
+ if psbt.inputs[input_index].final_script_sig.is_some()
+ || psbt.inputs[input_index].final_script_witness.is_some()
+ {
+ return Ok(());
+ }
+
+ let pubkey = PublicKey::from_private_key(secp, self);
+ let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
+
+ if let SignerContext::Tap { is_internal_key } = self.ctx {
+ if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
+ if is_internal_key
+ && psbt.inputs[input_index].tap_key_sig.is_none()
+ && sign_options.sign_with_tap_internal_key
+ && x_only_pubkey == psbt_internal_key
+ {
+ let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
+ sign_psbt_schnorr(
+ &self.inner,
+ x_only_pubkey,
+ None,
+ &mut psbt.inputs[input_index],
+ hash,
+ hash_ty,
+ secp,
+ );
+ }
+ }
+
+ if let Some((leaf_hashes, _)) =
+ psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
+ {
+ let leaf_hashes = leaf_hashes
+ .iter()
+ .filter(|lh| {
+ // Removing the leaves we shouldn't sign for
+ let should_sign = match &sign_options.tap_leaves_options {
+ TapLeavesOptions::All => true,
+ TapLeavesOptions::Include(v) => v.contains(lh),
+ TapLeavesOptions::Exclude(v) => !v.contains(lh),
+ TapLeavesOptions::None => false,
+ };
+ // Filtering out the leaves without our key
+ should_sign
+ && !psbt.inputs[input_index]
+ .tap_script_sigs
+ .contains_key(&(x_only_pubkey, **lh))
+ })
+ .cloned()
+ .collect::<Vec<_>>();
+ for lh in leaf_hashes {
+ let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
+ sign_psbt_schnorr(
+ &self.inner,
+ x_only_pubkey,
+ Some(lh),
+ &mut psbt.inputs[input_index],
+ hash,
+ hash_ty,
+ secp,
+ );
+ }
+ }
+
+ return Ok(());
+ }
+
+ if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
+ return Ok(());
+ }
+
+ let (hash, hash_ty) = match self.ctx {
+ SignerContext::Segwitv0 => {
+ let (h, t) = Segwitv0::sighash(psbt, input_index, ())?;
+ let h = h.to_raw_hash();
+ (h, t)
+ }
+ SignerContext::Legacy => {
+ let (h, t) = Legacy::sighash(psbt, input_index, ())?;
+ let h = h.to_raw_hash();
+ (h, t)
+ }
+ _ => return Ok(()), // handled above
+ };
+ sign_psbt_ecdsa(
+ &self.inner,
+ pubkey,
+ &mut psbt.inputs[input_index],
+ hash,
+ hash_ty,
+ secp,
+ sign_options.allow_grinding,
+ );
+
+ Ok(())
+ }
+}
+
+fn sign_psbt_ecdsa(
+ secret_key: &secp256k1::SecretKey,
+ pubkey: PublicKey,
+ psbt_input: &mut psbt::Input,
+ hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash,
+ hash_ty: EcdsaSighashType,
+ secp: &SecpCtx,
+ allow_grinding: bool,
+) {
+ let msg = &Message::from(hash);
+ let sig = if allow_grinding {
+ secp.sign_ecdsa_low_r(msg, secret_key)
+ } else {
+ secp.sign_ecdsa(msg, secret_key)
+ };
+ secp.verify_ecdsa(msg, &sig, &pubkey.inner)
+ .expect("invalid or corrupted ecdsa signature");
+
+ let final_signature = ecdsa::Signature { sig, hash_ty };
+ psbt_input.partial_sigs.insert(pubkey, final_signature);
+}
+
+// Calling this with `leaf_hash` = `None` will sign for key-spend
+fn sign_psbt_schnorr(
+ secret_key: &secp256k1::SecretKey,
+ pubkey: XOnlyPublicKey,
+ leaf_hash: Option<taproot::TapLeafHash>,
+ psbt_input: &mut psbt::Input,
+ hash: TapSighash,
+ hash_ty: TapSighashType,
+ secp: &SecpCtx,
+) {
+ let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
+ let keypair = match leaf_hash {
+ None => keypair
+ .tap_tweak(secp, psbt_input.tap_merkle_root)
+ .to_inner(),
+ Some(_) => keypair, // no tweak for script spend
+ };
+
+ let msg = &Message::from(hash);
+ let sig = secp.sign_schnorr(msg, &keypair);
+ secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
+ .expect("invalid or corrupted schnorr signature");
+
+ let final_signature = taproot::Signature { sig, hash_ty };
+
+ if let Some(lh) = leaf_hash {
+ psbt_input
+ .tap_script_sigs
+ .insert((pubkey, lh), final_signature);
+ } else {
+ psbt_input.tap_key_sig = Some(final_signature);
+ }
+}
+
+/// Defines the order in which signers are called
+///
+/// The default value is `100`. Signers with an ordering above that will be called later,
+/// and they will thus see the partial signatures added to the transaction once they get to sign
+/// themselves.
+#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
+pub struct SignerOrdering(pub usize);
+
+impl Default for SignerOrdering {
+ fn default() -> Self {
+ SignerOrdering(100)
+ }
+}
+
+#[derive(Debug, Clone)]
+struct SignersContainerKey {
+ id: SignerId,
+ ordering: SignerOrdering,
+}
+
+impl From<(SignerId, SignerOrdering)> for SignersContainerKey {
+ fn from(tuple: (SignerId, SignerOrdering)) -> Self {
+ SignersContainerKey {
+ id: tuple.0,
+ ordering: tuple.1,
+ }
+ }
+}
+
+/// Container for multiple signers
+#[derive(Debug, Default, Clone)]
+pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn TransactionSigner>>);
+
+impl SignersContainer {
+ /// Create a map of public keys to secret keys
+ pub fn as_key_map(&self, secp: &SecpCtx) -> KeyMap {
+ self.0
+ .values()
+ .filter_map(|signer| signer.descriptor_secret_key())
+ .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
+ .collect()
+ }
+
+ /// Build a new signer container from a [`KeyMap`]
+ ///
+ /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to
+ /// the signers
+ pub fn build(
+ keymap: KeyMap,
+ descriptor: &Descriptor<DescriptorPublicKey>,
+ secp: &SecpCtx,
+ ) -> SignersContainer {
+ let mut container = SignersContainer::new();
+
+ for (pubkey, secret) in keymap {
+ let ctx = match descriptor {
+ Descriptor::Tr(tr) => SignerContext::Tap {
+ is_internal_key: tr.internal_key() == &pubkey,
+ },
+ _ if descriptor.is_witness() => SignerContext::Segwitv0,
+ _ => SignerContext::Legacy,
+ };
+
+ match secret {
+ DescriptorSecretKey::Single(private_key) => container.add_external(
+ SignerId::from(
+ private_key
+ .key
+ .public_key(secp)
+ .to_pubkeyhash(SigType::Ecdsa),
+ ),
+ SignerOrdering::default(),
+ Arc::new(SignerWrapper::new(private_key.key, ctx)),
+ ),
+ DescriptorSecretKey::XPrv(xprv) => container.add_external(
+ SignerId::from(xprv.root_fingerprint(secp)),
+ SignerOrdering::default(),
+ Arc::new(SignerWrapper::new(xprv, ctx)),
+ ),
+ DescriptorSecretKey::MultiXPrv(xprv) => container.add_external(
+ SignerId::from(xprv.root_fingerprint(secp)),
+ SignerOrdering::default(),
+ Arc::new(SignerWrapper::new(xprv, ctx)),
+ ),
+ };
+ }
+
+ container
+ }
+}
+
+impl SignersContainer {
+ /// Default constructor
+ pub fn new() -> Self {
+ SignersContainer(Default::default())
+ }
+
+ /// Adds an external signer to the container for the specified id. Optionally returns the
+ /// signer that was previously in the container, if any
+ pub fn add_external(
+ &mut self,
+ id: SignerId,
+ ordering: SignerOrdering,
+ signer: Arc<dyn TransactionSigner>,
+ ) -> Option<Arc<dyn TransactionSigner>> {
+ self.0.insert((id, ordering).into(), signer)
+ }
+
+ /// Removes a signer from the container and returns it
+ pub fn remove(
+ &mut self,
+ id: SignerId,
+ ordering: SignerOrdering,
+ ) -> Option<Arc<dyn TransactionSigner>> {
+ self.0.remove(&(id, ordering).into())
+ }
+
+ /// Returns the list of identifiers of all the signers in the container
+ pub fn ids(&self) -> Vec<&SignerId> {
+ self.0
+ .keys()
+ .map(|SignersContainerKey { id, .. }| id)
+ .collect()
+ }
+
+ /// Returns the list of signers in the container, sorted by lowest to highest `ordering`
+ pub fn signers(&self) -> Vec<&Arc<dyn TransactionSigner>> {
+ self.0.values().collect()
+ }
+
+ /// Finds the signer with lowest ordering for a given id in the container.
+ pub fn find(&self, id: SignerId) -> Option<&Arc<dyn TransactionSigner>> {
+ self.0
+ .range((
+ Included(&(id.clone(), SignerOrdering(0)).into()),
+ Included(&(id.clone(), SignerOrdering(usize::MAX)).into()),
+ ))
+ .filter(|(k, _)| k.id == id)
+ .map(|(_, v)| v)
+ .next()
+ }
+}
+
+/// Options for a software signer
+///
+/// Adjust the behavior of our software signers and the way a transaction is finalized
+#[derive(Debug, Clone)]
+pub struct SignOptions {
+ /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
+ /// provided
+ ///
+ /// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into
+ /// paying a fee larger than expected.
+ ///
+ /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
+ /// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
+ /// should correctly produce a signature, at the expense of an increased trust in the creator
+ /// of the PSBT.
+ ///
+ /// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
+ pub trust_witness_utxo: bool,
+
+ /// Whether the wallet should assume a specific height has been reached when trying to finalize
+ /// a transaction
+ ///
+ /// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
+ /// timelock height has already been reached. This option allows overriding the "current height" to let the
+ /// wallet use timelocks in the future to spend a coin.
+ pub assume_height: Option<u32>,
+
+ /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
+ /// what its value is
+ ///
+ /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
+ pub allow_all_sighashes: bool,
+
+ /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
+ ///
+ /// Defaults to `true` which will remove partial signatures during finalization.
+ pub remove_partial_sigs: bool,
+
+ /// Whether to remove taproot specific fields from the PSBT on finalization.
+ ///
+ /// For inputs this includes the taproot internal key, merkle root, and individual
+ /// scripts and signatures. For both inputs and outputs it includes key origin info.
+ ///
+ /// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
+ ///
+ /// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
+ pub remove_taproot_extras: bool,
+
+ /// Whether to try finalizing the PSBT after the inputs are signed.
+ ///
+ /// Defaults to `true` which will try finalizing PSBT after inputs are signed.
+ pub try_finalize: bool,
+
+ /// Specifies which Taproot script-spend leaves we should sign for. This option is
+ /// ignored if we're signing a non-taproot PSBT.
+ ///
+ /// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
+ pub tap_leaves_options: TapLeavesOptions,
+
+ /// Whether we should try to sign a taproot transaction with the taproot internal key
+ /// or not. This option is ignored if we're signing a non-taproot PSBT.
+ ///
+ /// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
+ pub sign_with_tap_internal_key: bool,
+
+ /// Whether we should grind ECDSA signature to ensure signing with low r
+ /// or not.
+ /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
+ pub allow_grinding: bool,
+}
+
+/// Customize which taproot script-path leaves the signer should sign.
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub enum TapLeavesOptions {
+ /// The signer will sign all the leaves it has a key for.
+ #[default]
+ All,
+ /// The signer won't sign leaves other than the ones specified. Note that it could still ignore
+ /// some of the specified leaves, if it doesn't have the right key to sign them.
+ Include(Vec<taproot::TapLeafHash>),
+ /// The signer won't sign the specified leaves.
+ Exclude(Vec<taproot::TapLeafHash>),
+ /// The signer won't sign any leaf.
+ None,
+}
+
+impl Default for SignOptions {
+ fn default() -> Self {
+ SignOptions {
+ trust_witness_utxo: false,
+ assume_height: None,
+ allow_all_sighashes: false,
+ remove_partial_sigs: true,
+ remove_taproot_extras: true,
+ try_finalize: true,
+ tap_leaves_options: TapLeavesOptions::default(),
+ sign_with_tap_internal_key: true,
+ allow_grinding: true,
+ }
+ }
+}
+
+pub(crate) trait ComputeSighash {
+ type Extra;
+ type Sighash;
+ type SighashType;
+
+ fn sighash(
+ psbt: &Psbt,
+ input_index: usize,
+ extra: Self::Extra,
+ ) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
+}
+
+impl ComputeSighash for Legacy {
+ type Extra = ();
+ type Sighash = sighash::LegacySighash;
+ type SighashType = EcdsaSighashType;
+
+ fn sighash(
+ psbt: &Psbt,
+ input_index: usize,
+ _extra: (),
+ ) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
+ if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
+ return Err(SignerError::InputIndexOutOfRange);
+ }
+
+ let psbt_input = &psbt.inputs[input_index];
+ let tx_input = &psbt.unsigned_tx.input[input_index];
+
+ let sighash = psbt_input
+ .sighash_type
+ .unwrap_or_else(|| EcdsaSighashType::All.into())
+ .ecdsa_hash_ty()
+ .map_err(|_| SignerError::InvalidSighash)?;
+ let script = match psbt_input.redeem_script {
+ Some(ref redeem_script) => redeem_script.clone(),
+ None => {
+ let non_witness_utxo = psbt_input
+ .non_witness_utxo
+ .as_ref()
+ .ok_or(SignerError::MissingNonWitnessUtxo)?;
+ let prev_out = non_witness_utxo
+ .output
+ .get(tx_input.previous_output.vout as usize)
+ .ok_or(SignerError::InvalidNonWitnessUtxo)?;
+
+ prev_out.script_pubkey.clone()
+ }
+ };
+
+ Ok((
+ sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
+ input_index,
+ &script,
+ sighash.to_u32(),
+ )?,
+ sighash,
+ ))
+ }
+}
+
+impl ComputeSighash for Segwitv0 {
+ type Extra = ();
+ type Sighash = sighash::SegwitV0Sighash;
+ type SighashType = EcdsaSighashType;
+
+ fn sighash(
+ psbt: &Psbt,
+ input_index: usize,
+ _extra: (),
+ ) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
+ if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
+ return Err(SignerError::InputIndexOutOfRange);
+ }
+
+ let psbt_input = &psbt.inputs[input_index];
+ let tx_input = &psbt.unsigned_tx.input[input_index];
+
+ let sighash_type = psbt_input
+ .sighash_type
+ .unwrap_or_else(|| EcdsaSighashType::All.into())
+ .ecdsa_hash_ty()
+ .map_err(|_| SignerError::InvalidSighash)?;
+
+ // Always try first with the non-witness utxo
+ let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
+ // Check the provided prev-tx
+ if prev_tx.txid() != tx_input.previous_output.txid {
+ return Err(SignerError::InvalidNonWitnessUtxo);
+ }
+
+ // The output should be present, if it's missing the `non_witness_utxo` is invalid
+ prev_tx
+ .output
+ .get(tx_input.previous_output.vout as usize)
+ .ok_or(SignerError::InvalidNonWitnessUtxo)?
+ } else if let Some(witness_utxo) = &psbt_input.witness_utxo {
+ // Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
+ // before we get to this point
+ witness_utxo
+ } else {
+ // Nothing has been provided
+ return Err(SignerError::MissingNonWitnessUtxo);
+ };
+ let value = utxo.value;
+
+ let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
+
+ let sighash = match psbt_input.witness_script {
+ Some(ref witness_script) => {
+ sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
+ }
+ None => {
+ if utxo.script_pubkey.is_p2wpkh() {
+ sighasher.p2wpkh_signature_hash(
+ input_index,
+ &utxo.script_pubkey,
+ value,
+ sighash_type,
+ )?
+ } else if psbt_input
+ .redeem_script
+ .as_ref()
+ .map(|s| s.is_p2wpkh())
+ .unwrap_or(false)
+ {
+ let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
+ sighasher.p2wpkh_signature_hash(
+ input_index,
+ script_pubkey,
+ value,
+ sighash_type,
+ )?
+ } else {
+ return Err(SignerError::MissingWitnessScript);
+ }
+ }
+ };
+ Ok((sighash, sighash_type))
+ }
+}
+
+impl ComputeSighash for Tap {
+ type Extra = Option<taproot::TapLeafHash>;
+ type Sighash = TapSighash;
+ type SighashType = TapSighashType;
+
+ fn sighash(
+ psbt: &Psbt,
+ input_index: usize,
+ extra: Self::Extra,
+ ) -> Result<(Self::Sighash, TapSighashType), SignerError> {
+ if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
+ return Err(SignerError::InputIndexOutOfRange);
+ }
+
+ let psbt_input = &psbt.inputs[input_index];
+
+ let sighash_type = psbt_input
+ .sighash_type
+ .unwrap_or_else(|| TapSighashType::Default.into())
+ .taproot_hash_ty()
+ .map_err(|_| SignerError::InvalidSighash)?;
+ let witness_utxos = (0..psbt.inputs.len())
+ .map(|i| psbt.get_utxo_for(i))
+ .collect::<Vec<_>>();
+ let mut all_witness_utxos = vec![];
+
+ let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
+ let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
+ let prevouts = if is_anyone_can_pay {
+ sighash::Prevouts::One(
+ input_index,
+ witness_utxos[input_index]
+ .as_ref()
+ .ok_or(SignerError::MissingWitnessUtxo)?,
+ )
+ } else if witness_utxos.iter().all(Option::is_some) {
+ all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
+ sighash::Prevouts::All(&all_witness_utxos)
+ } else {
+ return Err(SignerError::MissingWitnessUtxo);
+ };
+
+ // Assume no OP_CODESEPARATOR
+ let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
+
+ Ok((
+ cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
+ sighash_type,
+ ))
+ }
+}
+
+impl PartialOrd for SignersContainerKey {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for SignersContainerKey {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.ordering
+ .cmp(&other.ordering)
+ .then(self.id.cmp(&other.id))
+ }
+}
+
+impl PartialEq for SignersContainerKey {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id && self.ordering == other.ordering
+ }
+}
+
+impl Eq for SignersContainerKey {}
+
+#[cfg(test)]
+mod signers_container_tests {
+ use super::*;
+ use crate::descriptor;
+ use crate::descriptor::IntoWalletDescriptor;
+ use crate::keys::{DescriptorKey, IntoDescriptorKey};
+ use assert_matches::assert_matches;
+ use bitcoin::bip32;
+ use bitcoin::secp256k1::{All, Secp256k1};
+ use bitcoin::Network;
+ use core::str::FromStr;
+ use miniscript::ScriptContext;
+
+ fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
+ let secp = Secp256k1::new();
+ this.id(&secp) == that.id(&secp)
+ }
+
+ // Signers added with the same ordering (like `Ordering::default`) created from `KeyMap`
+ // should be preserved and not overwritten.
+ // This happens usually when a set of signers is created from a descriptor with private keys.
+ #[test]
+ fn signers_with_same_ordering() {
+ let secp = Secp256k1::new();
+
+ let (prvkey1, _, _) = setup_keys(TPRV0_STR);
+ let (prvkey2, _, _) = setup_keys(TPRV1_STR);
+ let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
+ let (wallet_desc, keymap) = desc
+ .into_wallet_descriptor(&secp, Network::Testnet)
+ .unwrap();
+
+ let signers = SignersContainer::build(keymap, &wallet_desc, &secp);
+ assert_eq!(signers.ids().len(), 2);
+
+ let signers = signers.signers();
+ assert_eq!(signers.len(), 2);
+ }
+
+ #[test]
+ fn signers_sorted_by_ordering() {
+ let mut signers = SignersContainer::new();
+ let signer1 = Arc::new(DummySigner { number: 1 });
+ let signer2 = Arc::new(DummySigner { number: 2 });
+ let signer3 = Arc::new(DummySigner { number: 3 });
+
+ // Mixed order insertions verifies we are not inserting at head or tail.
+ signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone());
+ signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone());
+ signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone());
+
+ // Check that signers are sorted from lowest to highest ordering
+ let signers = signers.signers();
+
+ assert!(is_equal(signers[0], &signer1));
+ assert!(is_equal(signers[1], &signer2));
+ assert!(is_equal(signers[2], &signer3));
+ }
+
+ #[test]
+ fn find_signer_by_id() {
+ let mut signers = SignersContainer::new();
+ let signer1 = Arc::new(DummySigner { number: 1 });
+ let signer2 = Arc::new(DummySigner { number: 2 });
+ let signer3 = Arc::new(DummySigner { number: 3 });
+ let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering.
+
+ let id1 = SignerId::Dummy(1);
+ let id2 = SignerId::Dummy(2);
+ let id3 = SignerId::Dummy(3);
+ let id_nonexistent = SignerId::Dummy(999);
+
+ signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone());
+ signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
+ signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
+
+ assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1));
+ assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2));
+ assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3));
+
+ // The `signer4` has the same ID as `signer3` but lower ordering.
+ // It should be found by `id3` instead of `signer3`.
+ signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
+ assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4));
+
+ // Can't find anything with ID that doesn't exist
+ assert_matches!(signers.find(id_nonexistent), None);
+ }
+
+ #[derive(Debug, Clone, Copy)]
+ struct DummySigner {
+ number: u64,
+ }
+
+ impl SignerCommon for DummySigner {
+ fn id(&self, _secp: &SecpCtx) -> SignerId {
+ SignerId::Dummy(self.number)
+ }
+ }
+
+ impl TransactionSigner for DummySigner {
+ fn sign_transaction(
+ &self,
+ _psbt: &mut Psbt,
+ _sign_options: &SignOptions,
+ _secp: &SecpCtx,
+ ) -> Result<(), SignerError> {
+ Ok(())
+ }
+ }
+
+ const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
+ const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
+
+ const PATH: &str = "m/44'/1'/0'/0";
+
+ fn setup_keys<Ctx: ScriptContext>(
+ tprv: &str,
+ ) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
+ let secp: Secp256k1<All> = Secp256k1::new();
+ let path = bip32::DerivationPath::from_str(PATH).unwrap();
+ let tprv = bip32::Xpriv::from_str(tprv).unwrap();
+ let tpub = bip32::Xpub::from_priv(&secp, &tprv);
+ let fingerprint = tprv.fingerprint(&secp);
+ let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
+ let pubkey = (tpub, path).into_descriptor_key().unwrap();
+
+ (prvkey, pubkey, fingerprint)
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Transaction builder
+//!
+//! ## Example
+//!
+//! ```
+//! # use std::str::FromStr;
+//! # use bitcoin::*;
+//! # use bdk_wallet::*;
+//! # use bdk_wallet::wallet::ChangeSet;
+//! # use bdk_wallet::wallet::error::CreateTxError;
+//! # use bdk_wallet::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!();
+//! // create a TxBuilder from a wallet
+//! let mut tx_builder = wallet.build_tx();
+//!
+//! tx_builder
+//! // Create a transaction with one output to `to_address` of 50_000 satoshi
+//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
+//! // With a custom fee rate of 5.0 satoshi/vbyte
+//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
+//! // Only spend non-change outputs
+//! .do_not_spend_change()
+//! // Turn on RBF signaling
+//! .enable_rbf();
+//! let psbt = tx_builder.finish()?;
+//! # Ok::<(), anyhow::Error>(())
+//! ```
+
+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::{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
+/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and
+/// generate the transaction.
+///
+/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls
+/// as in the following example:
+///
+/// ```
+/// # use bdk_wallet::*;
+/// # use bdk_wallet::wallet::tx_builder::*;
+/// # use bitcoin::*;
+/// # use core::str::FromStr;
+/// # use bdk_wallet::wallet::ChangeSet;
+/// # use bdk_wallet::wallet::error::CreateTxError;
+/// # use bdk_persist::PersistBackend;
+/// # use anyhow::Error;
+/// # let mut wallet = doctest_wallet!();
+/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
+/// # let addr2 = addr1.clone();
+/// // chaining
+/// let psbt1 = {
+/// let mut builder = wallet.build_tx();
+/// builder
+/// .ordering(TxOrdering::Untouched)
+/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
+/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
+/// builder.finish()?
+/// };
+///
+/// // non-chaining
+/// let psbt2 = {
+/// let mut builder = wallet.build_tx();
+/// builder.ordering(TxOrdering::Untouched);
+/// for addr in &[addr1, addr2] {
+/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
+/// }
+/// builder.finish()?
+/// };
+///
+/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
+/// # Ok::<(), anyhow::Error>(())
+/// ```
+///
+/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
+/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it.
+///
+/// For further examples see [this module](super::tx_builder)'s documentation;
+///
+/// [`build_tx`]: Wallet::build_tx
+/// [`build_fee_bump`]: Wallet::build_fee_bump
+/// [`finish`]: Self::finish
+/// [`coin_selection`]: Self::coin_selection
+#[derive(Debug)]
+pub struct TxBuilder<'a, Cs, Ctx> {
+ 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.
+//TODO: TxParams should eventually be exposed publicly.
+#[derive(Default, Debug, Clone)]
+pub(crate) struct TxParams {
+ pub(crate) recipients: Vec<(ScriptBuf, u64)>,
+ pub(crate) drain_wallet: bool,
+ pub(crate) drain_to: Option<ScriptBuf>,
+ pub(crate) fee_policy: Option<FeePolicy>,
+ pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
+ pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
+ pub(crate) utxos: Vec<WeightedUtxo>,
+ pub(crate) unspendable: HashSet<OutPoint>,
+ pub(crate) manually_selected_only: bool,
+ pub(crate) sighash: Option<psbt::PsbtSighashType>,
+ pub(crate) ordering: TxOrdering,
+ pub(crate) locktime: Option<absolute::LockTime>,
+ pub(crate) rbf: Option<RbfValue>,
+ pub(crate) version: Option<Version>,
+ pub(crate) change_policy: ChangeSpendPolicy,
+ pub(crate) only_witness_utxo: bool,
+ pub(crate) add_global_xpubs: bool,
+ pub(crate) include_output_redeem_witness_script: bool,
+ pub(crate) bumping_fee: Option<PreviousFee>,
+ pub(crate) current_height: Option<absolute::LockTime>,
+ pub(crate) allow_dust: bool,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct PreviousFee {
+ pub absolute: u64,
+ pub rate: FeeRate,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum FeePolicy {
+ FeeRate(FeeRate),
+ FeeAmount(u64),
+}
+
+impl Default for FeePolicy {
+ fn default() -> Self {
+ FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
+ }
+}
+
+impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> {
+ fn clone(&self) -> Self {
+ TxBuilder {
+ wallet: self.wallet.clone(),
+ params: self.params.clone(),
+ coin_selection: self.coin_selection.clone(),
+ phantom: PhantomData,
+ }
+ }
+}
+
+// methods supported by both contexts, for any CoinSelectionAlgorithm
+impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
+ /// Set a custom fee rate.
+ ///
+ /// This method sets the mining fee paid by the transaction as a rate on its size.
+ /// This means that the total fee paid is equal to `fee_rate` times the size
+ /// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
+ /// relay policy.
+ ///
+ /// Note that this is really a minimum feerate -- it's possible to
+ /// overshoot it slightly since adding a change output to drain the remaining
+ /// excess might not be viable.
+ pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
+ self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
+ self
+ }
+
+ /// Set an absolute fee
+ /// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
+ /// If anyone sets both the fee_absolute method and the fee_rate method,
+ /// the FeePolicy enum will be set by whichever method was called last,
+ /// as the FeeRate and FeeAmount are mutually exclusive.
+ ///
+ /// Note that this is really a minimum absolute fee -- it's possible to
+ /// overshoot it slightly since adding a change output to drain the remaining
+ /// excess might not be viable.
+ pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
+ self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
+ self
+ }
+
+ /// Set the policy path to use while creating the transaction for a given keychain.
+ ///
+ /// This method accepts a map where the key is the policy node id (see
+ /// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of
+ /// the items that are intended to be satisfied from the policy node (see
+ /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)).
+ ///
+ /// ## Example
+ ///
+ /// An example of when the policy path is needed is the following descriptor:
+ /// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`,
+ /// derived from the miniscript policy `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`.
+ /// It declares three descriptor fragments, and at the top level it uses `thresh()` to
+ /// ensure that at least two of them are satisfied. The individual fragments are:
+ ///
+ /// 1. `pk(A)`
+ /// 2. `and(pk(B),older(6))`
+ /// 3. `and(pk(C),after(630000))`
+ ///
+ /// When those conditions are combined in pairs, it's clear that the transaction needs to be created
+ /// differently depending on how the user intends to satisfy the policy afterwards:
+ ///
+ /// * If fragments `1` and `2` are used, the transaction will need to use a specific
+ /// `n_sequence` in order to spend an `OP_CSV` branch.
+ /// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime`
+ /// in order to spend an `OP_CLTV` branch.
+ /// * If fragments `2` and `3` are used, the transaction will need both.
+ ///
+ /// When the spending policy is represented as a tree (see
+ /// [`Wallet::policies`](super::Wallet::policies)), every node
+ /// is assigned a unique identifier that can be used in the policy path to specify which of
+ /// the node's children the user intends to satisfy: for instance, assuming the `thresh()`
+ /// root node of this example has an id of `aabbccdd`, the policy path map would look like:
+ ///
+ /// `{ "aabbccdd" => [0, 1] }`
+ ///
+ /// where the key is the node's id, and the value is a list of the children that should be
+ /// used, in no particular order.
+ ///
+ /// If a particularly complex descriptor has multiple ambiguous thresholds in its structure,
+ /// multiple entries can be added to the map, one for each node that requires an explicit path.
+ ///
+ /// ```
+ /// # use std::str::FromStr;
+ /// # use std::collections::BTreeMap;
+ /// # use bitcoin::*;
+ /// # use bdk_wallet::*;
+ /// # let to_address =
+ /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
+ /// .unwrap()
+ /// .assume_checked();
+ /// # let mut wallet = doctest_wallet!();
+ /// let mut path = BTreeMap::new();
+ /// path.insert("aabbccdd".to_string(), vec![0, 1]);
+ ///
+ /// let builder = wallet
+ /// .build_tx()
+ /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
+ /// .policy_path(path, KeychainKind::External);
+ ///
+ /// # Ok::<(), anyhow::Error>(())
+ /// ```
+ pub fn policy_path(
+ &mut self,
+ policy_path: BTreeMap<String, Vec<usize>>,
+ keychain: KeychainKind,
+ ) -> &mut Self {
+ let to_update = match keychain {
+ KeychainKind::Internal => &mut self.params.internal_policy_path,
+ KeychainKind::External => &mut self.params.external_policy_path,
+ };
+
+ *to_update = Some(policy_path);
+ self
+ }
+
+ /// Add the list of outpoints to the internal list of UTXOs that **must** be spent.
+ ///
+ /// If an error occurs while adding any of the UTXOs then none of them are added and the error is returned.
+ ///
+ /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
+ /// the "utxos" and the "unspendable" list, it will be spent.
+ pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
+ {
+ let wallet = self.wallet.borrow();
+ let utxos = outpoints
+ .iter()
+ .map(|outpoint| {
+ wallet
+ .get_utxo(*outpoint)
+ .ok_or(AddUtxoError::UnknownUtxo(*outpoint))
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ for utxo in utxos {
+ let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
+ let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
+ self.params.utxos.push(WeightedUtxo {
+ satisfaction_weight,
+ utxo: Utxo::Local(utxo),
+ });
+ }
+ }
+
+ Ok(self)
+ }
+
+ /// Add a utxo to the internal list of utxos that **must** be spent
+ ///
+ /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
+ /// the "utxos" and the "unspendable" list, it will be spent.
+ pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> {
+ self.add_utxos(&[outpoint])
+ }
+
+ /// Add a foreign UTXO i.e. a UTXO not owned by this wallet.
+ ///
+ /// At a minimum to add a foreign UTXO we need:
+ ///
+ /// 1. `outpoint`: To add it to the raw transaction.
+ /// 2. `psbt_input`: To know the value.
+ /// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation.
+ ///
+ /// There are several security concerns about adding foreign UTXOs that application
+ /// developers should consider. First, how do you know the value of the input is correct? If a
+ /// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the
+ /// value by checking it against the transaction. If only a `witness_utxo` is provided then this
+ /// method doesn't verify the value but just takes it as a given -- it is up to you to check
+ /// that whoever sent you the `input_psbt` was not lying!
+ ///
+ /// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your
+ /// application it may be important that this be known precisely. If not, a malicious
+ /// counterparty may fool you into putting in a value that is too low, giving the transaction a
+ /// lower than expected feerate. They could also fool you into putting a value that is too high
+ /// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
+ /// of course check the real input weight matches the expected weight prior to broadcasting.
+ ///
+ /// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
+ /// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
+ /// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
+ ///
+ /// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
+ ///
+ /// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction
+ /// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx
+ /// graph using the [`Wallet::insert_txout`] function.
+ ///
+ /// # Errors
+ ///
+ /// This method returns errors in the following circumstances:
+ ///
+ /// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
+ /// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
+ ///
+ /// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
+ /// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
+ /// is called.
+ ///
+ /// [`only_witness_utxo`]: Self::only_witness_utxo
+ /// [`finish`]: Self::finish
+ /// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
+ pub fn add_foreign_utxo(
+ &mut self,
+ outpoint: OutPoint,
+ psbt_input: psbt::Input,
+ satisfaction_weight: usize,
+ ) -> Result<&mut Self, AddForeignUtxoError> {
+ self.add_foreign_utxo_with_sequence(
+ outpoint,
+ psbt_input,
+ satisfaction_weight,
+ Sequence::MAX,
+ )
+ }
+
+ /// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
+ pub fn add_foreign_utxo_with_sequence(
+ &mut self,
+ outpoint: OutPoint,
+ psbt_input: psbt::Input,
+ satisfaction_weight: usize,
+ sequence: Sequence,
+ ) -> Result<&mut Self, AddForeignUtxoError> {
+ if psbt_input.witness_utxo.is_none() {
+ match psbt_input.non_witness_utxo.as_ref() {
+ Some(tx) => {
+ if tx.txid() != outpoint.txid {
+ return Err(AddForeignUtxoError::InvalidTxid {
+ input_txid: tx.txid(),
+ foreign_utxo: outpoint,
+ });
+ }
+ if tx.output.len() <= outpoint.vout as usize {
+ return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
+ }
+ }
+ None => {
+ return Err(AddForeignUtxoError::MissingUtxo);
+ }
+ }
+ }
+
+ self.params.utxos.push(WeightedUtxo {
+ satisfaction_weight,
+ utxo: Utxo::Foreign {
+ outpoint,
+ sequence: Some(sequence),
+ psbt_input: Box::new(psbt_input),
+ },
+ });
+
+ Ok(self)
+ }
+
+ /// Only spend utxos added by [`add_utxo`].
+ ///
+ /// The wallet will **not** add additional utxos to the transaction even if they are needed to
+ /// make the transaction valid.
+ ///
+ /// [`add_utxo`]: Self::add_utxo
+ pub fn manually_selected_only(&mut self) -> &mut Self {
+ self.params.manually_selected_only = true;
+ self
+ }
+
+ /// Replace the internal list of unspendable utxos with a new list
+ ///
+ /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
+ /// have priority over these. See the docs of the two linked methods for more details.
+ pub fn unspendable(&mut self, unspendable: Vec<OutPoint>) -> &mut Self {
+ self.params.unspendable = unspendable.into_iter().collect();
+ self
+ }
+
+ /// Add a utxo to the internal list of unspendable utxos
+ ///
+ /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
+ /// have priority over this. See the docs of the two linked methods for more details.
+ pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut Self {
+ self.params.unspendable.insert(unspendable);
+ self
+ }
+
+ /// Sign with a specific sig hash
+ ///
+ /// **Use this option very carefully**
+ pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self {
+ self.params.sighash = Some(sighash);
+ self
+ }
+
+ /// Choose the ordering for inputs and outputs of the transaction
+ pub fn ordering(&mut self, ordering: TxOrdering) -> &mut Self {
+ self.params.ordering = ordering;
+ self
+ }
+
+ /// Use a specific nLockTime while creating the transaction
+ ///
+ /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
+ pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self {
+ self.params.locktime = Some(locktime);
+ self
+ }
+
+ /// Build a transaction with a specific version
+ ///
+ /// The `version` should always be greater than `0` and greater than `1` if the wallet's
+ /// descriptors contain an "older" (OP_CSV) operator.
+ pub fn version(&mut self, version: i32) -> &mut Self {
+ self.params.version = Some(Version(version));
+ self
+ }
+
+ /// Do not spend change outputs
+ ///
+ /// This effectively adds all the change outputs to the "unspendable" list. See
+ /// [`TxBuilder::unspendable`].
+ pub fn do_not_spend_change(&mut self) -> &mut Self {
+ self.params.change_policy = ChangeSpendPolicy::ChangeForbidden;
+ self
+ }
+
+ /// Only spend change outputs
+ ///
+ /// This effectively adds all the non-change outputs to the "unspendable" list. See
+ /// [`TxBuilder::unspendable`].
+ pub fn only_spend_change(&mut self) -> &mut Self {
+ self.params.change_policy = ChangeSpendPolicy::OnlyChange;
+ self
+ }
+
+ /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and
+ /// [`TxBuilder::only_spend_change`] for some shortcuts.
+ pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self {
+ self.params.change_policy = change_policy;
+ self
+ }
+
+ /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from
+ /// SegWit descriptors.
+ ///
+ /// This reduces the size of the PSBT, but some signers might reject them due to the lack of
+ /// the `non_witness_utxo`.
+ pub fn only_witness_utxo(&mut self) -> &mut Self {
+ self.params.only_witness_utxo = true;
+ self
+ }
+
+ /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::psbt::Output::redeem_script) and
+ /// [`psbt::Output::witness_script`](bitcoin::psbt::Output::witness_script) fields.
+ ///
+ /// This is useful for signers which always require it, like ColdCard hardware wallets.
+ pub fn include_output_redeem_witness_script(&mut self) -> &mut Self {
+ self.params.include_output_redeem_witness_script = true;
+ self
+ }
+
+ /// Fill-in the `PSBT_GLOBAL_XPUB` field with the extended keys contained in both the external
+ /// and internal descriptors
+ ///
+ /// This is useful for offline signers that take part to a multisig. Some hardware wallets like
+ /// BitBox and ColdCard are known to require this.
+ pub fn add_global_xpubs(&mut self) -> &mut Self {
+ self.params.add_global_xpubs = true;
+ self
+ }
+
+ /// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy.
+ pub fn drain_wallet(&mut self) -> &mut Self {
+ self.params.drain_wallet = true;
+ self
+ }
+
+ /// Choose the coin selection algorithm
+ ///
+ /// Overrides the [`DefaultCoinSelectionAlgorithm`].
+ ///
+ /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
+ pub fn coin_selection<P: CoinSelectionAlgorithm>(
+ self,
+ coin_selection: P,
+ ) -> TxBuilder<'a, P, Ctx> {
+ TxBuilder {
+ wallet: self.wallet,
+ params: self.params,
+ coin_selection,
+ phantom: PhantomData,
+ }
+ }
+
+ /// Enable signaling RBF
+ ///
+ /// This will use the default nSequence value of `0xFFFFFFFD`.
+ pub fn enable_rbf(&mut self) -> &mut Self {
+ self.params.rbf = Some(RbfValue::Default);
+ self
+ }
+
+ /// Enable signaling RBF with a specific nSequence value
+ ///
+ /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
+ /// and the given `nsequence` is lower than the CSV value.
+ ///
+ /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
+ /// be a valid nSequence to signal RBF.
+ pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
+ self.params.rbf = Some(RbfValue::Value(nsequence));
+ self
+ }
+
+ /// Set the current blockchain height.
+ ///
+ /// This will be used to:
+ /// 1. Set the nLockTime for preventing fee sniping.
+ /// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
+ /// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
+ /// mature at `current_height`, we ignore them in the coin selection.
+ /// If you want to create a transaction that spends immature coinbase inputs, manually
+ /// add them using [`TxBuilder::add_utxos`].
+ ///
+ /// In both cases, if you don't provide a current height, we use the last sync height.
+ pub fn current_height(&mut self, height: u32) -> &mut Self {
+ self.params.current_height =
+ Some(absolute::LockTime::from_height(height).expect("Invalid height"));
+ self
+ }
+
+ /// Set whether or not the dust limit is checked.
+ ///
+ /// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
+ pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
+ self.params.allow_dust = allow_dust;
+ self
+ }
+}
+
+impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> {
+ /// Finish building the transaction.
+ ///
+ /// Returns a new [`Psbt`] per [`BIP174`].
+ ///
+ /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
+ pub fn finish(self) -> Result<Psbt, CreateTxError> {
+ self.wallet
+ .borrow_mut()
+ .create_tx(self.coin_selection, self.params)
+ }
+}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
+pub enum AddUtxoError {
+ /// Happens when trying to spend an UTXO that is not in the internal database
+ UnknownUtxo(OutPoint),
+}
+
+impl fmt::Display for AddUtxoError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnknownUtxo(outpoint) => write!(
+ f,
+ "UTXO not found in the internal database for txid: {} with vout: {}",
+ outpoint.txid, outpoint.vout
+ ),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AddUtxoError {}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::add_foreign_utxo`].
+pub enum AddForeignUtxoError {
+ /// Foreign utxo outpoint txid does not match PSBT input txid
+ InvalidTxid {
+ /// PSBT input txid
+ input_txid: Txid,
+ /// Foreign UTXO outpoint
+ foreign_utxo: OutPoint,
+ },
+ /// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
+ InvalidOutpoint(OutPoint),
+ /// Foreign utxo missing witness_utxo or non_witness_utxo
+ MissingUtxo,
+}
+
+impl fmt::Display for AddForeignUtxoError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidTxid {
+ input_txid,
+ foreign_utxo,
+ } => write!(
+ f,
+ "Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
+ foreign_utxo.txid, input_txid,
+ ),
+ Self::InvalidOutpoint(outpoint) => write!(
+ f,
+ "Requested outpoint doesn't exist for txid: {} with vout: {}",
+ outpoint.txid, outpoint.vout,
+ ),
+ Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
+ }
+ }
+}
+
+#[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_wallet::*;
+ /// # use bdk_wallet::wallet::ChangeSet;
+ /// # use bdk_wallet::wallet::error::CreateTxError;
+ /// # use bdk_wallet::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 {
+ /// Randomized (default)
+ #[default]
+ Shuffle,
+ /// Unchanged
+ Untouched,
+ /// BIP69 / Lexicographic
+ Bip69Lexicographic,
+}
+
+impl TxOrdering {
+ /// Sort transaction inputs and outputs by [`TxOrdering`] variant
+ pub fn sort_tx(&self, tx: &mut Transaction) {
+ match self {
+ TxOrdering::Untouched => {}
+ TxOrdering::Shuffle => {
+ use rand::seq::SliceRandom;
+ let mut rng = rand::thread_rng();
+ tx.input.shuffle(&mut rng);
+ tx.output.shuffle(&mut rng);
+ }
+ TxOrdering::Bip69Lexicographic => {
+ tx.input.sort_unstable_by_key(|txin| {
+ (txin.previous_output.txid, txin.previous_output.vout)
+ });
+ tx.output
+ .sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
+ }
+ }
+ }
+}
+
+/// Transaction version
+///
+/// Has a default value of `1`
+#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
+pub(crate) struct Version(pub(crate) i32);
+
+impl Default for Version {
+ fn default() -> Self {
+ Version(1)
+ }
+}
+
+/// RBF nSequence value
+///
+/// Has a default value of `0xFFFFFFFD`
+#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
+pub(crate) enum RbfValue {
+ Default,
+ Value(Sequence),
+}
+
+impl RbfValue {
+ pub(crate) fn get_value(&self) -> Sequence {
+ match self {
+ RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
+ RbfValue::Value(v) => *v,
+ }
+ }
+}
+
+/// Policy regarding the use of change outputs when creating a transaction
+#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
+pub enum ChangeSpendPolicy {
+ /// Use both change and non-change outputs (default)
+ #[default]
+ ChangeAllowed,
+ /// Only use change outputs (see [`TxBuilder::only_spend_change`])
+ OnlyChange,
+ /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`])
+ ChangeForbidden,
+}
+
+impl ChangeSpendPolicy {
+ pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
+ match self {
+ ChangeSpendPolicy::ChangeAllowed => true,
+ ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
+ ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\
+ 85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\
+ 79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\
+ dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\
+ 03e80300000000000002aaeee80300000000000001aa200300000000000001ff\
+ 00000000";
+ macro_rules! ordering_test_tx {
+ () => {
+ deserialize::<bitcoin::Transaction>(&Vec::<u8>::from_hex(ORDERING_TEST_TX).unwrap())
+ .unwrap()
+ };
+ }
+
+ use bdk_chain::ConfirmationTime;
+ use bitcoin::consensus::deserialize;
+ use bitcoin::hex::FromHex;
+ use bitcoin::TxOut;
+
+ use super::*;
+
+ #[test]
+ fn test_output_ordering_default_shuffle() {
+ assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
+ }
+
+ #[test]
+ fn test_output_ordering_untouched() {
+ let original_tx = ordering_test_tx!();
+ let mut tx = original_tx.clone();
+
+ TxOrdering::Untouched.sort_tx(&mut tx);
+
+ assert_eq!(original_tx, tx);
+ }
+
+ #[test]
+ fn test_output_ordering_shuffle() {
+ let original_tx = ordering_test_tx!();
+ let mut tx = original_tx.clone();
+
+ (0..40)
+ .find(|_| {
+ TxOrdering::Shuffle.sort_tx(&mut tx);
+ original_tx.input != tx.input
+ })
+ .expect("it should have moved the inputs at least once");
+
+ let mut tx = original_tx.clone();
+ (0..40)
+ .find(|_| {
+ TxOrdering::Shuffle.sort_tx(&mut tx);
+ original_tx.output != tx.output
+ })
+ .expect("it should have moved the outputs at least once");
+ }
+
+ #[test]
+ fn test_output_ordering_bip69() {
+ use core::str::FromStr;
+
+ let original_tx = ordering_test_tx!();
+ let mut tx = original_tx;
+
+ TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
+
+ assert_eq!(
+ tx.input[0].previous_output,
+ bitcoin::OutPoint::from_str(
+ "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57:5"
+ )
+ .unwrap()
+ );
+ assert_eq!(
+ tx.input[1].previous_output,
+ bitcoin::OutPoint::from_str(
+ "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:0"
+ )
+ .unwrap()
+ );
+ assert_eq!(
+ tx.input[2].previous_output,
+ bitcoin::OutPoint::from_str(
+ "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:1"
+ )
+ .unwrap()
+ );
+
+ assert_eq!(tx.output[0].value.to_sat(), 800);
+ assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
+ assert_eq!(
+ tx.output[2].script_pubkey,
+ ScriptBuf::from(vec![0xAA, 0xEE])
+ );
+ }
+
+ fn get_test_utxos() -> Vec<LocalOutput> {
+ use bitcoin::hashes::Hash;
+
+ vec![
+ LocalOutput {
+ outpoint: OutPoint {
+ txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
+ vout: 0,
+ },
+ txout: TxOut::NULL,
+ keychain: KeychainKind::External,
+ is_spent: false,
+ confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
+ derivation_index: 0,
+ },
+ LocalOutput {
+ outpoint: OutPoint {
+ txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
+ vout: 1,
+ },
+ txout: TxOut::NULL,
+ keychain: KeychainKind::Internal,
+ is_spent: false,
+ confirmation_time: ConfirmationTime::Confirmed {
+ height: 32,
+ time: 42,
+ },
+ derivation_index: 1,
+ },
+ ]
+ }
+
+ #[test]
+ fn test_change_spend_policy_default() {
+ let change_spend_policy = ChangeSpendPolicy::default();
+ let filtered = get_test_utxos()
+ .into_iter()
+ .filter(|u| change_spend_policy.is_satisfied_by(u))
+ .count();
+
+ assert_eq!(filtered, 2);
+ }
+
+ #[test]
+ fn test_change_spend_policy_no_internal() {
+ let change_spend_policy = ChangeSpendPolicy::ChangeForbidden;
+ let filtered = get_test_utxos()
+ .into_iter()
+ .filter(|u| change_spend_policy.is_satisfied_by(u))
+ .collect::<Vec<_>>();
+
+ assert_eq!(filtered.len(), 1);
+ assert_eq!(filtered[0].keychain, KeychainKind::External);
+ }
+
+ #[test]
+ fn test_change_spend_policy_only_internal() {
+ let change_spend_policy = ChangeSpendPolicy::OnlyChange;
+ let filtered = get_test_utxos()
+ .into_iter()
+ .filter(|u| change_spend_policy.is_satisfied_by(u))
+ .collect::<Vec<_>>();
+
+ assert_eq!(filtered.len(), 1);
+ assert_eq!(filtered[0].keychain, KeychainKind::Internal);
+ }
+
+ #[test]
+ fn test_default_tx_version_1() {
+ let version = Version::default();
+ assert_eq!(version.0, 1);
+ }
+}
--- /dev/null
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+use bitcoin::secp256k1::{All, Secp256k1};
+use bitcoin::{absolute, Script, Sequence};
+
+use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
+
+/// Trait to check if a value is below the dust limit.
+/// We are performing dust value calculation for a given script public key using rust-bitcoin to
+/// keep it compatible with network dust rate
+// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
+// instead of a <= etc.
+pub trait IsDust {
+ /// Check whether or not a value is below dust limit
+ fn is_dust(&self, script: &Script) -> bool;
+}
+
+impl IsDust for u64 {
+ fn is_dust(&self, script: &Script) -> bool {
+ *self < script.dust_value().to_sat()
+ }
+}
+
+pub struct After {
+ pub current_height: Option<u32>,
+ pub assume_height_reached: bool,
+}
+
+impl After {
+ pub(crate) fn new(current_height: Option<u32>, assume_height_reached: bool) -> After {
+ After {
+ current_height,
+ assume_height_reached,
+ }
+ }
+}
+
+pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
+ // The RBF value must enable relative timelocks
+ if !rbf.is_relative_lock_time() {
+ return false;
+ }
+
+ // Both values should be represented in the same unit (either time-based or
+ // block-height based)
+ if rbf.is_time_locked() != csv.is_time_locked() {
+ return false;
+ }
+
+ // The value should be at least `csv`
+ if rbf < csv {
+ return false;
+ }
+
+ true
+}
+
+impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
+ fn check_after(&self, n: absolute::LockTime) -> bool {
+ if let Some(current_height) = self.current_height {
+ current_height >= n.to_consensus_u32()
+ } else {
+ self.assume_height_reached
+ }
+ }
+}
+
+pub struct Older {
+ pub current_height: Option<u32>,
+ pub create_height: Option<u32>,
+ pub assume_height_reached: bool,
+}
+
+impl Older {
+ pub(crate) fn new(
+ current_height: Option<u32>,
+ create_height: Option<u32>,
+ assume_height_reached: bool,
+ ) -> Older {
+ Older {
+ current_height,
+ create_height,
+ assume_height_reached,
+ }
+ }
+}
+
+impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
+ fn check_older(&self, n: Sequence) -> bool {
+ if let Some(current_height) = self.current_height {
+ // TODO: test >= / >
+ current_height
+ >= self
+ .create_height
+ .unwrap_or(0)
+ .checked_add(n.to_consensus_u32())
+ .expect("Overflowing addition")
+ } else {
+ self.assume_height_reached
+ }
+ }
+}
+
+pub(crate) type SecpCtx = Secp256k1<All>;
+
+#[cfg(test)]
+mod test {
+ // When nSequence is lower than this flag the timelock is interpreted as block-height-based,
+ // otherwise it's time-based
+ pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
+
+ use super::{check_nsequence_rbf, IsDust};
+ use crate::bitcoin::{Address, Network, Sequence};
+ use core::str::FromStr;
+
+ #[test]
+ fn test_is_dust() {
+ let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe")
+ .unwrap()
+ .require_network(Network::Bitcoin)
+ .unwrap()
+ .script_pubkey();
+ assert!(script_p2pkh.is_p2pkh());
+ assert!(545.is_dust(&script_p2pkh));
+ assert!(!546.is_dust(&script_p2pkh));
+
+ let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8")
+ .unwrap()
+ .require_network(Network::Bitcoin)
+ .unwrap()
+ .script_pubkey();
+ assert!(script_p2wpkh.is_p2wpkh());
+ assert!(293.is_dust(&script_p2wpkh));
+ assert!(!294.is_dust(&script_p2wpkh));
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_msb_set() {
+ let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_lt_csv() {
+ let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_different_unit() {
+ let result =
+ check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_mask() {
+ let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
+ assert!(result);
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_same_unit_blocks() {
+ let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
+ assert!(result);
+ }
+
+ #[test]
+ fn test_check_nsequence_rbf_same_unit_time() {
+ let result = check_nsequence_rbf(
+ Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
+ Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
+ );
+ assert!(result);
+ }
+}
--- /dev/null
+#![allow(unused)]
+
+use bdk_chain::indexed_tx_graph::Indexer;
+use bdk_chain::{BlockId, ConfirmationTime};
+use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
+use bitcoin::hashes::Hash;
+use bitcoin::{
+ transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
+ Txid,
+};
+use std::str::FromStr;
+
+// Return a fake wallet that appears to be funded for testing.
+//
+// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
+// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+// sats are the transaction fee.
+pub fn get_funded_wallet_with_change(
+ descriptor: &str,
+ change: Option<&str>,
+) -> (Wallet, bitcoin::Txid) {
+ let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
+ let change_address = wallet.peek_address(KeychainKind::External, 0).address;
+ let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
+ .expect("address")
+ .require_network(Network::Regtest)
+ .unwrap();
+
+ let tx0 = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: bitcoin::absolute::LockTime::ZERO,
+ input: vec![TxIn {
+ previous_output: OutPoint {
+ txid: Txid::all_zeros(),
+ vout: 0,
+ },
+ script_sig: Default::default(),
+ sequence: Default::default(),
+ witness: Default::default(),
+ }],
+ output: vec![TxOut {
+ value: Amount::from_sat(76_000),
+ script_pubkey: change_address.script_pubkey(),
+ }],
+ };
+
+ let tx1 = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: bitcoin::absolute::LockTime::ZERO,
+ input: vec![TxIn {
+ previous_output: OutPoint {
+ txid: tx0.txid(),
+ vout: 0,
+ },
+ script_sig: Default::default(),
+ sequence: Default::default(),
+ witness: Default::default(),
+ }],
+ output: vec![
+ TxOut {
+ value: Amount::from_sat(50_000),
+ script_pubkey: change_address.script_pubkey(),
+ },
+ TxOut {
+ value: Amount::from_sat(25_000),
+ script_pubkey: sendto_address.script_pubkey(),
+ },
+ ],
+ };
+
+ wallet
+ .insert_checkpoint(BlockId {
+ height: 1_000,
+ hash: BlockHash::all_zeros(),
+ })
+ .unwrap();
+ wallet
+ .insert_checkpoint(BlockId {
+ height: 2_000,
+ hash: BlockHash::all_zeros(),
+ })
+ .unwrap();
+ wallet
+ .insert_tx(
+ tx0,
+ ConfirmationTime::Confirmed {
+ height: 1_000,
+ time: 100,
+ },
+ )
+ .unwrap();
+ wallet
+ .insert_tx(
+ tx1.clone(),
+ ConfirmationTime::Confirmed {
+ height: 2_000,
+ time: 200,
+ },
+ )
+ .unwrap();
+
+ (wallet, tx1.txid())
+}
+
+// Return a fake wallet that appears to be funded for testing.
+//
+// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
+// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+// sats are the transaction fee.
+pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
+ get_funded_wallet_with_change(descriptor, None)
+}
+
+pub fn get_test_wpkh() -> &'static str {
+ "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
+}
+
+pub fn get_test_single_sig_csv() -> &'static str {
+ // and(pk(Alice),older(6))
+ "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
+}
+
+pub fn get_test_a_or_b_plus_csv() -> &'static str {
+ // or(pk(Alice),and(pk(Bob),older(144)))
+ "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
+}
+
+pub fn get_test_single_sig_cltv() -> &'static str {
+ // and(pk(Alice),after(100000))
+ "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
+}
+
+pub fn get_test_tr_single_sig() -> &'static str {
+ "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
+}
+
+pub fn get_test_tr_with_taptree() -> &'static str {
+ "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
+}
+
+pub fn get_test_tr_with_taptree_both_priv() -> &'static str {
+ "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
+}
+
+pub fn get_test_tr_repeated_key() -> &'static str {
+ "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
+}
+
+pub fn get_test_tr_single_sig_xprv() -> &'static str {
+ "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
+}
+
+pub fn get_test_tr_with_taptree_xprv() -> &'static str {
+ "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
+}
+
+pub fn get_test_tr_dup_keys() -> &'static str {
+ "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
+}
+
+/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
+/// useful in cases where we want to create a feerate from a `f64`, as the
+/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
+///
+/// **Note** this 'quick and dirty' conversion should only be used when the input
+/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
+/// or else the resulting value will be inaccurate.
+pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
+ // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
+ let sat_kwu = (sat_vb * 250.0).ceil() as u64;
+ FeeRate::from_sat_per_kwu(sat_kwu)
+}
--- /dev/null
+use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
+use bdk_wallet::{psbt, KeychainKind, SignOptions};
+use core::str::FromStr;
+mod common;
+use common::*;
+
+// from bip 174
+const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
+
+#[test]
+#[should_panic(expected = "InputIndexOutOfRange")]
+fn test_psbt_malformed_psbt_input_legacy() {
+ let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let send_to = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
+ let mut psbt = builder.finish().unwrap();
+ psbt.inputs.push(psbt_bip.inputs[0].clone());
+ let options = SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ };
+ let _ = wallet.sign(&mut psbt, options).unwrap();
+}
+
+#[test]
+#[should_panic(expected = "InputIndexOutOfRange")]
+fn test_psbt_malformed_psbt_input_segwit() {
+ let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let send_to = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
+ let mut psbt = builder.finish().unwrap();
+ psbt.inputs.push(psbt_bip.inputs[1].clone());
+ let options = SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ };
+ let _ = wallet.sign(&mut psbt, options).unwrap();
+}
+
+#[test]
+#[should_panic(expected = "InputIndexOutOfRange")]
+fn test_psbt_malformed_tx_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let send_to = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
+ let mut psbt = builder.finish().unwrap();
+ psbt.unsigned_tx.input.push(TxIn::default());
+ let options = SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ };
+ let _ = wallet.sign(&mut psbt, options).unwrap();
+}
+
+#[test]
+fn test_psbt_sign_with_finalized() {
+ let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let send_to = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
+ let mut psbt = builder.finish().unwrap();
+
+ // add a finalized input
+ psbt.inputs.push(psbt_bip.inputs[0].clone());
+ psbt.unsigned_tx
+ .input
+ .push(psbt_bip.unsigned_tx.input[0].clone());
+
+ let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
+}
+
+#[test]
+fn test_psbt_fee_rate_with_witness_utxo() {
+ use psbt::PsbtUtils;
+
+ let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
+
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ builder.fee_rate(expected_fee_rate);
+ let mut psbt = builder.finish().unwrap();
+ let fee_amount = psbt.fee_amount();
+ assert!(fee_amount.is_some());
+
+ let unfinalized_fee_rate = psbt.fee_rate().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let finalized_fee_rate = psbt.fee_rate().unwrap();
+ assert!(finalized_fee_rate >= expected_fee_rate);
+ assert!(finalized_fee_rate < unfinalized_fee_rate);
+}
+
+#[test]
+fn test_psbt_fee_rate_with_nonwitness_utxo() {
+ use psbt::PsbtUtils;
+
+ let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
+
+ let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ builder.fee_rate(expected_fee_rate);
+ let mut psbt = builder.finish().unwrap();
+ let fee_amount = psbt.fee_amount();
+ assert!(fee_amount.is_some());
+ let unfinalized_fee_rate = psbt.fee_rate().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let finalized_fee_rate = psbt.fee_rate().unwrap();
+ assert!(finalized_fee_rate >= expected_fee_rate);
+ assert!(finalized_fee_rate < unfinalized_fee_rate);
+}
+
+#[test]
+fn test_psbt_fee_rate_with_missing_txout() {
+ use psbt::PsbtUtils;
+
+ let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
+
+ let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wpkh_wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wpkh_wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ builder.fee_rate(expected_fee_rate);
+ let mut wpkh_psbt = builder.finish().unwrap();
+
+ wpkh_psbt.inputs[0].witness_utxo = None;
+ wpkh_psbt.inputs[0].non_witness_utxo = None;
+ assert!(wpkh_psbt.fee_amount().is_none());
+ assert!(wpkh_psbt.fee_rate().is_none());
+
+ let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = pkh_wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = pkh_wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ builder.fee_rate(expected_fee_rate);
+ let mut pkh_psbt = builder.finish().unwrap();
+
+ pkh_psbt.inputs[0].non_witness_utxo = None;
+ assert!(pkh_psbt.fee_amount().is_none());
+ assert!(pkh_psbt.fee_rate().is_none());
+}
+
+#[test]
+fn test_psbt_multiple_internalkey_signers() {
+ use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper};
+ use bdk_wallet::KeychainKind;
+ use bitcoin::key::TapTweak;
+ use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
+ use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
+ use bitcoin::{PrivateKey, TxOut};
+ use std::sync::Arc;
+
+ let secp = Secp256k1::new();
+ let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
+ let desc = format!("tr({})", wif);
+ let prv = PrivateKey::from_wif(wif).unwrap();
+ let keypair = Keypair::from_secret_key(&secp, &prv.inner);
+
+ let (mut wallet, _) = get_funded_wallet(&desc);
+ let to_spend = wallet.get_balance().total();
+ let send_to = wallet.peek_address(KeychainKind::External, 0);
+ let mut builder = wallet.build_tx();
+ builder.drain_to(send_to.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+ let unsigned_tx = psbt.unsigned_tx.clone();
+
+ // Adds a signer for the wrong internal key, bdk should not use this key to sign
+ wallet.add_signer(
+ KeychainKind::External,
+ // A signerordering lower than 100, bdk will use this signer first
+ SignerOrdering(0),
+ Arc::new(SignerWrapper::new(
+ PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(),
+ SignerContext::Tap {
+ is_internal_key: true,
+ },
+ )),
+ );
+ let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
+ assert!(finalized);
+
+ // To verify, we need the signature, message, and pubkey
+ let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
+ assert!(!witness.is_empty());
+ let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
+
+ // the prevout we're spending
+ let prevouts = &[TxOut {
+ script_pubkey: send_to.script_pubkey(),
+ value: to_spend,
+ }];
+ let prevouts = Prevouts::All(prevouts);
+ let input_index = 0;
+ let mut sighash_cache = SighashCache::new(unsigned_tx);
+ let sighash = sighash_cache
+ .taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
+ .unwrap();
+ let message = Message::from(sighash);
+
+ // add tweak. this was taken from `signer::sign_psbt_schnorr`
+ let keypair = keypair.tap_tweak(&secp, None).to_inner();
+ let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
+
+ // Must verify if we used the correct key to sign
+ let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
+ assert!(verify_res.is_ok(), "The wrong internal key was used");
+}
--- /dev/null
+use std::str::FromStr;
+
+use assert_matches::assert_matches;
+use bdk_chain::collections::BTreeMap;
+use bdk_chain::COINBASE_MATURITY;
+use bdk_chain::{BlockId, ConfirmationTime};
+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;
+use bitcoin::script::PushBytesBuf;
+use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
+use bitcoin::taproot::TapNodeHash;
+use bitcoin::{
+ absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf,
+ Sequence, Transaction, TxIn, TxOut, Txid, Weight,
+};
+
+mod common;
+use common::*;
+
+fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint {
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: addr.script_pubkey(),
+ value: Amount::from_sat(value),
+ }],
+ };
+
+ wallet.insert_tx(tx.clone(), height).unwrap();
+
+ OutPoint {
+ txid: tx.txid(),
+ vout: 0,
+ }
+}
+
+fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
+ let latest_cp = wallet.latest_checkpoint();
+ let height = latest_cp.height();
+ let anchor = if height == 0 {
+ ConfirmationTime::Unconfirmed { last_seen: 0 }
+ } else {
+ ConfirmationTime::Confirmed { height, time: 0 }
+ };
+ receive_output(wallet, value, anchor)
+}
+
+// The satisfaction size of a P2WPKH is 112 WU =
+// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
+// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
+// a total of 105 WU.
+// Here, we push just once for simplicity, so we have to add an extra byte for the missing
+// OP_PUSH.
+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
+ {
+ 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
+ );
+ }
+
+ // `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)));
+ }
+}
+
+#[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
+ {
+ 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!(
+ err,
+ bdk_wallet::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();
+
+ 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!(
+ err,
+ bdk_wallet::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;
+
+ 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!(
+ err,
+ bdk_wallet::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;
+
+ 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!(
+ err,
+ bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
+ if got == &got_descriptor && keychain == KeychainKind::Internal
+ ),
+ "err: {}",
+ err,
+ );
+ }
+
+ // 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));
+ }
+}
+
+#[test]
+fn test_descriptor_checksum() {
+ let (wallet, _) = get_funded_wallet(get_test_wpkh());
+ let checksum = wallet.descriptor_checksum(KeychainKind::External);
+ assert_eq!(checksum.len(), 8);
+
+ let raw_descriptor = wallet
+ .keychains()
+ .next()
+ .unwrap()
+ .1
+ .to_string()
+ .split_once('#')
+ .unwrap()
+ .0
+ .to_string();
+ assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum);
+}
+
+#[test]
+fn test_get_funded_wallet_balance() {
+ let (wallet, _) = get_funded_wallet(get_test_wpkh());
+
+ // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
+ // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+ // sats are the transaction fee.
+ assert_eq!(wallet.get_balance().confirmed, Amount::from_sat(50_000));
+}
+
+#[test]
+fn test_get_funded_wallet_sent_and_received() {
+ let (wallet, txid) = get_funded_wallet(get_test_wpkh());
+
+ let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet
+ .transactions()
+ .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node)))
+ .collect();
+ tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0));
+
+ let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
+ let (sent, received) = wallet.sent_and_received(&tx);
+
+ // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
+ // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+ // sats are the transaction fee.
+ assert_eq!(sent.to_sat(), 76_000);
+ assert_eq!(received.to_sat(), 50_000);
+}
+
+#[test]
+fn test_get_funded_wallet_tx_fees() {
+ let (wallet, txid) = get_funded_wallet(get_test_wpkh());
+
+ let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
+ let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee");
+
+ // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
+ // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+ // sats are the transaction fee.
+ assert_eq!(tx_fee, 1000)
+}
+
+#[test]
+fn test_get_funded_wallet_tx_fee_rate() {
+ let (wallet, txid) = get_funded_wallet(get_test_wpkh());
+
+ let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
+ let tx_fee_rate = wallet
+ .calculate_fee_rate(&tx)
+ .expect("transaction fee rate");
+
+ // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
+ // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
+ // sats are the transaction fee.
+
+ // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113
+ // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212
+ // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9
+ assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212);
+ assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9);
+}
+
+#[test]
+fn test_list_output() {
+ let (wallet, txid) = get_funded_wallet(get_test_wpkh());
+ let txos = wallet
+ .list_output()
+ .map(|op| (op.outpoint, op))
+ .collect::<std::collections::BTreeMap<_, _>>();
+ assert_eq!(txos.len(), 2);
+ for (op, txo) in txos {
+ if op.txid == txid {
+ assert_eq!(txo.txout.value.to_sat(), 50_000);
+ assert!(!txo.is_spent);
+ } else {
+ assert_eq!(txo.txout.value.to_sat(), 76_000);
+ assert!(txo.is_spent);
+ }
+ }
+}
+
+macro_rules! assert_fee_rate {
+ ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
+ let psbt = $psbt.clone();
+ #[allow(unused_mut)]
+ let mut tx = $psbt.clone().extract_tx().expect("failed to extract tx");
+ $(
+ $( $add_signature )*
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
+ }
+ )*
+
+ #[allow(unused_mut)]
+ #[allow(unused_assignments)]
+ let mut dust_change = false;
+ $(
+ $( $dust_change )*
+ dust_change = true;
+ )*
+
+ let fee_amount = psbt
+ .inputs
+ .iter()
+ .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value.to_sat())
+ - psbt
+ .unsigned_tx
+ .output
+ .iter()
+ .fold(0, |acc, o| acc + o.value.to_sat());
+
+ assert_eq!(fee_amount, $fees);
+
+ let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight())
+ .to_sat_per_kwu();
+ let fee_rate = $fee_rate.to_sat_per_kwu();
+ let half_default = FeeRate::BROADCAST_MIN.checked_div(2)
+ .unwrap()
+ .to_sat_per_kwu();
+
+ if !dust_change {
+ assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
+ } else {
+ assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
+ }
+ });
+}
+
+macro_rules! from_str {
+ ($e:expr, $t:ty) => {{
+ use core::str::FromStr;
+ <$t>::from_str($e).unwrap()
+ }};
+
+ ($e:expr) => {
+ from_str!($e, _)
+ };
+}
+
+#[test]
+#[should_panic(expected = "NoRecipients")]
+fn test_create_tx_empty_recipients() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ wallet.build_tx().finish().unwrap();
+}
+
+#[test]
+#[should_panic(expected = "NoUtxosSelected")]
+fn test_create_tx_manually_selected_empty_utxos() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .manually_selected_only();
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_version_0() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .version(0);
+ assert!(matches!(builder.finish(), Err(CreateTxError::Version0)));
+}
+
+#[test]
+fn test_create_tx_version_1_csv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .version(1);
+ assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv)));
+}
+
+#[test]
+fn test_create_tx_custom_version() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .version(42);
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.version.0, 42);
+}
+
+#[test]
+fn test_create_tx_default_locktime_is_last_sync_height() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ // Since we never synced the wallet we don't have a last_sync_height
+ // we could use to try to prevent fee sniping. We default to 0.
+ assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000);
+}
+
+#[test]
+fn test_create_tx_fee_sniping_locktime_last_sync() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+
+ let psbt = builder.finish().unwrap();
+
+ // If there's no current_height we're left with using the last sync height
+ assert_eq!(
+ psbt.unsigned_tx.lock_time.to_consensus_u32(),
+ wallet.latest_checkpoint().height()
+ );
+}
+
+#[test]
+fn test_create_tx_default_locktime_cltv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000);
+}
+
+#[test]
+fn test_create_tx_custom_locktime() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .current_height(630_001)
+ .nlocktime(absolute::LockTime::from_height(630_000).unwrap());
+ let psbt = builder.finish().unwrap();
+
+ // When we explicitly specify a nlocktime
+ // we don't try any fee sniping prevention trick
+ // (we ignore the current_height)
+ assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000);
+}
+
+#[test]
+fn test_create_tx_custom_locktime_compatible_with_cltv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .nlocktime(absolute::LockTime::from_height(630_000).unwrap());
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000);
+}
+
+#[test]
+fn test_create_tx_custom_locktime_incompatible_with_cltv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .nlocktime(absolute::LockTime::from_height(50000).unwrap());
+ assert!(matches!(builder.finish(),
+ Err(CreateTxError::LockTime { requested, required })
+ if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000));
+}
+
+#[test]
+fn test_create_tx_no_rbf_csv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
+}
+
+#[test]
+fn test_create_tx_with_default_rbf_csv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ // When CSV is enabled it takes precedence over the rbf value (unless forced by the user).
+ // It will be set to the OP_CSV value, in this case 6
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
+}
+
+#[test]
+fn test_create_tx_with_custom_rbf_csv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf_with_sequence(Sequence(3));
+ assert!(matches!(builder.finish(),
+ Err(CreateTxError::RbfSequenceCsv { rbf, csv })
+ if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6));
+}
+
+#[test]
+fn test_create_tx_no_rbf_cltv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
+}
+
+#[test]
+fn test_create_tx_invalid_rbf_sequence() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
+ assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence)));
+}
+
+#[test]
+fn test_create_tx_custom_rbf_sequence() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf_with_sequence(Sequence(0xDEADBEEF));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF));
+}
+
+#[test]
+fn test_create_tx_default_sequence() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
+}
+
+#[test]
+fn test_create_tx_change_policy_no_internal() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .do_not_spend_change();
+ assert!(matches!(
+ builder.finish(),
+ Err(CreateTxError::ChangePolicyDescriptor)
+ ));
+}
+
+macro_rules! check_fee {
+ ($wallet:expr, $psbt: expr) => {{
+ let tx = $psbt.clone().extract_tx().expect("failed to extract tx");
+ let tx_fee = $wallet.calculate_fee(&tx).ok();
+ assert_eq!(tx_fee, $psbt.fee_amount());
+ tx_fee
+ }};
+}
+
+#[test]
+fn test_create_tx_drain_wallet_and_drain_to() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(
+ psbt.unsigned_tx.output[0].value.to_sat(),
+ 50_000 - fee.unwrap_or(0)
+ );
+}
+
+#[test]
+fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
+ .unwrap()
+ .assume_checked();
+ let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000))
+ .drain_to(drain_addr.script_pubkey())
+ .drain_wallet();
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+ let outputs = psbt.unsigned_tx.output;
+
+ assert_eq!(outputs.len(), 2);
+ let main_output = outputs
+ .iter()
+ .find(|x| x.script_pubkey == addr.script_pubkey())
+ .unwrap();
+ let drain_output = outputs
+ .iter()
+ .find(|x| x.script_pubkey == drain_addr.script_pubkey())
+ .unwrap();
+ assert_eq!(main_output.value.to_sat(), 20_000,);
+ assert_eq!(drain_output.value.to_sat(), 30_000 - fee.unwrap_or(0));
+}
+
+#[test]
+fn test_create_tx_drain_to_and_utxos() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .add_utxos(&utxos)
+ .unwrap();
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(
+ psbt.unsigned_tx.output[0].value.to_sat(),
+ 50_000 - fee.unwrap_or(0)
+ );
+}
+
+#[test]
+#[should_panic(expected = "NoRecipients")]
+fn test_create_tx_drain_to_no_drain_wallet_no_utxos() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(drain_addr.script_pubkey());
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_default_fee_rate() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature);
+}
+
+#[test]
+fn test_create_tx_custom_fee_rate() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .fee_rate(FeeRate::from_sat_per_vb_unchecked(5));
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
+}
+
+#[test]
+fn test_create_tx_absolute_fee() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(100);
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(fee.unwrap_or(0), 100);
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(
+ psbt.unsigned_tx.output[0].value.to_sat(),
+ 50_000 - fee.unwrap_or(0)
+ );
+}
+
+#[test]
+fn test_create_tx_absolute_zero_fee() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(0);
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(fee.unwrap_or(0), 0);
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(
+ psbt.unsigned_tx.output[0].value.to_sat(),
+ 50_000 - fee.unwrap_or(0)
+ );
+}
+
+#[test]
+#[should_panic(expected = "InsufficientFunds")]
+fn test_create_tx_absolute_high_fee() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(60_000);
+ let _ = builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_add_change() {
+ 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();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .ordering(TxOrdering::Untouched);
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.unsigned_tx.output.len(), 2);
+ assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 25_000);
+ assert_eq!(
+ psbt.unsigned_tx.output[1].value.to_sat(),
+ 25_000 - fee.unwrap_or(0)
+ );
+}
+
+#[test]
+fn test_create_tx_skip_change_dust() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(49_800));
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.unsigned_tx.output.len(), 1);
+ assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 49_800);
+ assert_eq!(fee.unwrap_or(0), 200);
+}
+
+#[test]
+#[should_panic(expected = "InsufficientFunds")]
+fn test_create_tx_drain_to_dust_amount() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ // very high fee rate, so that the only output would be below dust
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_rate(FeeRate::from_sat_per_vb_unchecked(454));
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_ordering_respected() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(10_000))
+ .ordering(bdk_wallet::wallet::tx_builder::TxOrdering::Bip69Lexicographic);
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.unsigned_tx.output.len(), 3);
+ assert_eq!(
+ psbt.unsigned_tx.output[0].value.to_sat(),
+ 10_000 - fee.unwrap_or(0)
+ );
+ assert_eq!(psbt.unsigned_tx.output[1].value.to_sat(), 10_000);
+ assert_eq!(psbt.unsigned_tx.output[2].value.to_sat(), 30_000);
+}
+
+#[test]
+fn test_create_tx_default_sighash() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.inputs[0].sighash_type, None);
+}
+
+#[test]
+fn test_create_tx_custom_sighash() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .sighash(EcdsaSighashType::Single.into());
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(
+ psbt.inputs[0].sighash_type,
+ Some(EcdsaSighashType::Single.into())
+ );
+}
+
+#[test]
+fn test_create_tx_input_hd_keypaths() {
+ use bitcoin::bip32::{DerivationPath, Fingerprint};
+ use core::str::FromStr;
+
+ let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
+ assert_eq!(
+ psbt.inputs[0].bip32_derivation.values().next().unwrap(),
+ &(
+ Fingerprint::from_str("d34db33f").unwrap(),
+ DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap()
+ )
+ );
+}
+
+#[test]
+fn test_create_tx_output_hd_keypaths() {
+ use bitcoin::bip32::{DerivationPath, Fingerprint};
+ use core::str::FromStr;
+
+ let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
+
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
+ let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index);
+ assert_eq!(
+ psbt.outputs[0].bip32_derivation.values().next().unwrap(),
+ &(
+ Fingerprint::from_str("d34db33f").unwrap(),
+ DerivationPath::from_str(&expected_derivation_path).unwrap()
+ )
+ );
+}
+
+#[test]
+fn test_create_tx_set_redeem_script_p2sh() {
+ use bitcoin::hex::FromHex;
+
+ let (mut wallet, _) =
+ get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(
+ psbt.inputs[0].redeem_script,
+ Some(ScriptBuf::from(
+ Vec::<u8>::from_hex(
+ "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
+ )
+ .unwrap()
+ ))
+ );
+ assert_eq!(psbt.inputs[0].witness_script, None);
+}
+
+#[test]
+fn test_create_tx_set_witness_script_p2wsh() {
+ use bitcoin::hex::FromHex;
+
+ let (mut wallet, _) =
+ get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.inputs[0].redeem_script, None);
+ assert_eq!(
+ psbt.inputs[0].witness_script,
+ Some(ScriptBuf::from(
+ Vec::<u8>::from_hex(
+ "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
+ )
+ .unwrap()
+ ))
+ );
+}
+
+#[test]
+fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() {
+ let (mut wallet, _) =
+ get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ let script = ScriptBuf::from_hex(
+ "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac",
+ )
+ .unwrap();
+
+ assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_p2wsh()));
+ assert_eq!(psbt.inputs[0].witness_script, Some(script));
+}
+
+#[test]
+fn test_create_tx_non_witness_utxo() {
+ let (mut wallet, _) =
+ get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert!(psbt.inputs[0].non_witness_utxo.is_some());
+ assert!(psbt.inputs[0].witness_utxo.is_none());
+}
+
+#[test]
+fn test_create_tx_only_witness_utxo() {
+ let (mut wallet, _) =
+ get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .only_witness_utxo()
+ .drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert!(psbt.inputs[0].non_witness_utxo.is_none());
+ assert!(psbt.inputs[0].witness_utxo.is_some());
+}
+
+#[test]
+fn test_create_tx_shwpkh_has_witness_utxo() {
+ let (mut wallet, _) =
+ get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert!(psbt.inputs[0].witness_utxo.is_some());
+}
+
+#[test]
+fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() {
+ let (mut wallet, _) =
+ get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert!(psbt.inputs[0].non_witness_utxo.is_some());
+ assert!(psbt.inputs[0].witness_utxo.is_some());
+}
+
+#[test]
+fn test_create_tx_add_utxo() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let small_output_tx = Transaction {
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ version: transaction::Version::non_standard(0),
+ lock_time: absolute::LockTime::ZERO,
+ };
+ wallet
+ .insert_tx(
+ small_output_tx.clone(),
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ )
+ .unwrap();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .add_utxo(OutPoint {
+ txid: small_output_tx.txid(),
+ vout: 0,
+ })
+ .unwrap();
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+
+ assert_eq!(
+ psbt.unsigned_tx.input.len(),
+ 2,
+ "should add an additional input since 25_000 < 30_000"
+ );
+ assert_eq!(
+ sent_received.0,
+ Amount::from_sat(75_000),
+ "total should be sum of both inputs"
+ );
+}
+
+#[test]
+#[should_panic(expected = "InsufficientFunds")]
+fn test_create_tx_manually_selected_insufficient() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let small_output_tx = Transaction {
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ version: transaction::Version::non_standard(0),
+ lock_time: absolute::LockTime::ZERO,
+ };
+
+ wallet
+ .insert_tx(
+ small_output_tx.clone(),
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ )
+ .unwrap();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .add_utxo(OutPoint {
+ txid: small_output_tx.txid(),
+ vout: 0,
+ })
+ .unwrap()
+ .manually_selected_only();
+ builder.finish().unwrap();
+}
+
+#[test]
+#[should_panic(expected = "SpendingPolicyRequired(External)")]
+fn test_create_tx_policy_path_required() {
+ let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000));
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_policy_path_no_csv() {
+ let descriptors = get_test_wpkh();
+ let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap();
+
+ let tx = Transaction {
+ version: transaction::Version::non_standard(0),
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(50_000),
+ }],
+ };
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
+ let root_id = external_policy.id;
+ // child #0 is just the key "A"
+ let path = vec![(root_id, vec![0])].into_iter().collect();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .policy_path(path, KeychainKind::External);
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
+}
+
+#[test]
+fn test_create_tx_policy_path_use_csv() {
+ let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
+
+ let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
+ let root_id = external_policy.id;
+ // child #1 is or(pk(B),older(144))
+ let path = vec![(root_id, vec![1])].into_iter().collect();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .policy_path(path, KeychainKind::External);
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144));
+}
+
+#[test]
+fn test_create_tx_policy_path_ignored_subtree_with_csv() {
+ let (mut wallet, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))");
+
+ let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
+ let root_id = external_policy.id;
+ // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)
+ let path = vec![(root_id, vec![0])].into_iter().collect();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
+ .policy_path(path, KeychainKind::External);
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
+}
+
+#[test]
+fn test_create_tx_global_xpubs_with_origin() {
+ use bitcoin::bip32;
+ use bitcoin::hex::FromHex;
+
+ let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .add_global_xpubs();
+ let psbt = builder.finish().unwrap();
+
+ let key = bip32::Xpub::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap();
+ let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap();
+ let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
+
+ assert_eq!(psbt.xpub.len(), 1);
+ assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path)));
+}
+
+#[test]
+fn test_add_foreign_utxo() {
+ let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
+ let (wallet2, _) =
+ get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let utxo = wallet2.list_unspent().next().expect("must take!");
+ let foreign_utxo_satisfaction = wallet2
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ let psbt_input = psbt::Input {
+ witness_utxo: Some(utxo.txout.clone()),
+ ..Default::default()
+ };
+
+ let mut builder = wallet1.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
+ .only_witness_utxo()
+ .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
+ .unwrap();
+ let mut psbt = builder.finish().unwrap();
+ wallet1.insert_txout(utxo.outpoint, utxo.txout);
+ let fee = check_fee!(wallet1, psbt);
+ let sent_received =
+ wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+
+ assert_eq!(
+ (sent_received.0 - sent_received.1).to_sat(),
+ 10_000 + fee.unwrap_or(0),
+ "we should have only net spent ~10_000"
+ );
+
+ assert!(
+ psbt.unsigned_tx
+ .input
+ .iter()
+ .any(|input| input.previous_output == utxo.outpoint),
+ "foreign_utxo should be in there"
+ );
+
+ let finished = wallet1
+ .sign(
+ &mut psbt,
+ SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+
+ assert!(
+ !finished,
+ "only one of the inputs should have been signed so far"
+ );
+
+ let finished = wallet2
+ .sign(
+ &mut psbt,
+ SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+ assert!(finished, "all the inputs should have been signed now");
+}
+
+#[test]
+#[should_panic(
+ expected = "MissingTxOut([OutPoint { txid: 0x21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])"
+)]
+fn test_calculate_fee_with_missing_foreign_utxo() {
+ let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
+ let (wallet2, _) =
+ get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let utxo = wallet2.list_unspent().next().expect("must take!");
+ let foreign_utxo_satisfaction = wallet2
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ let psbt_input = psbt::Input {
+ witness_utxo: Some(utxo.txout.clone()),
+ ..Default::default()
+ };
+
+ let mut builder = wallet1.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
+ .only_witness_utxo()
+ .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
+ .unwrap();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ wallet1.calculate_fee(&tx).unwrap();
+}
+
+#[test]
+fn test_add_foreign_utxo_invalid_psbt_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
+ let foreign_utxo_satisfaction = wallet
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ let mut builder = wallet.build_tx();
+ let result =
+ builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
+ assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
+}
+
+#[test]
+fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
+ let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh());
+ let (wallet2, txid2) =
+ get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
+
+ let utxo2 = wallet2.list_unspent().next().unwrap();
+ let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone();
+ let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone();
+
+ let satisfaction_weight = wallet2
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ let mut builder = wallet1.build_tx();
+ assert!(
+ builder
+ .add_foreign_utxo(
+ utxo2.outpoint,
+ psbt::Input {
+ non_witness_utxo: Some(tx1.as_ref().clone()),
+ ..Default::default()
+ },
+ satisfaction_weight
+ )
+ .is_err(),
+ "should fail when outpoint doesn't match psbt_input"
+ );
+ assert!(
+ builder
+ .add_foreign_utxo(
+ utxo2.outpoint,
+ psbt::Input {
+ non_witness_utxo: Some(tx2.as_ref().clone()),
+ ..Default::default()
+ },
+ satisfaction_weight
+ )
+ .is_ok(),
+ "should be ok when outpoint does match psbt_input"
+ );
+}
+
+#[test]
+fn test_add_foreign_utxo_only_witness_utxo() {
+ let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
+ let (wallet2, txid2) =
+ get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let utxo2 = wallet2.list_unspent().next().unwrap();
+
+ let satisfaction_weight = wallet2
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ let mut builder = wallet1.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000));
+
+ {
+ let mut builder = builder.clone();
+ let psbt_input = psbt::Input {
+ witness_utxo: Some(utxo2.txout.clone()),
+ ..Default::default()
+ };
+ builder
+ .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
+ .unwrap();
+ assert!(
+ builder.finish().is_err(),
+ "psbt_input with witness_utxo should fail with only witness_utxo"
+ );
+ }
+
+ {
+ let mut builder = builder.clone();
+ let psbt_input = psbt::Input {
+ witness_utxo: Some(utxo2.txout.clone()),
+ ..Default::default()
+ };
+ builder
+ .only_witness_utxo()
+ .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
+ .unwrap();
+ assert!(
+ builder.finish().is_ok(),
+ "psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled"
+ );
+ }
+
+ {
+ let mut builder = builder.clone();
+ let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx;
+ let psbt_input = psbt::Input {
+ non_witness_utxo: Some(tx2.as_ref().clone()),
+ ..Default::default()
+ };
+ builder
+ .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
+ .unwrap();
+ assert!(
+ builder.finish().is_ok(),
+ "psbt_input with non_witness_utxo should succeed by default"
+ );
+ }
+}
+
+#[test]
+fn test_get_psbt_input() {
+ // this should grab a known good utxo and set the input
+ let (wallet, _) = get_funded_wallet(get_test_wpkh());
+ for utxo in wallet.list_unspent() {
+ let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap();
+ assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some());
+ }
+}
+
+#[test]
+#[should_panic(
+ expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
+)]
+fn test_create_tx_global_xpubs_origin_missing() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .add_global_xpubs();
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_create_tx_global_xpubs_master_without_origin() {
+ use bitcoin::bip32;
+ use bitcoin::hex::FromHex;
+
+ let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .add_global_xpubs();
+ let psbt = builder.finish().unwrap();
+
+ let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap();
+ let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap();
+
+ assert_eq!(psbt.xpub.len(), 1);
+ assert_eq!(
+ psbt.xpub.get(&key),
+ Some(&(fingerprint, bip32::DerivationPath::default()))
+ );
+}
+
+#[test]
+#[should_panic(expected = "IrreplaceableTransaction")]
+fn test_bump_fee_irreplaceable_tx() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+ wallet.build_fee_bump(txid).unwrap().finish().unwrap();
+}
+
+#[test]
+#[should_panic(expected = "TransactionConfirmed")]
+fn test_bump_fee_confirmed_tx() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+
+ wallet
+ .insert_tx(
+ tx,
+ ConfirmationTime::Confirmed {
+ height: 42,
+ time: 42_000,
+ },
+ )
+ .unwrap();
+
+ wallet.build_fee_bump(txid).unwrap().finish().unwrap();
+}
+
+#[test]
+fn test_bump_fee_low_fee_rate() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let feerate = psbt.fee_rate().unwrap();
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_rate(FeeRate::BROADCAST_MIN);
+ let res = builder.finish();
+ assert_matches!(
+ res,
+ Err(CreateTxError::FeeRateTooLow { .. }),
+ "expected FeeRateTooLow error"
+ );
+
+ let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb
+ let sat_vb = required as f64 / 250.0;
+ let expect = format!("Fee rate too low: required {} sat/vb", sat_vb);
+ assert_eq!(res.unwrap_err().to_string(), expect);
+}
+
+#[test]
+#[should_panic(expected = "FeeTooLow")]
+fn test_bump_fee_low_abs() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_absolute(10);
+ builder.finish().unwrap();
+}
+
+#[test]
+#[should_panic(expected = "FeeTooLow")]
+fn test_bump_fee_zero_abs() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_absolute(0);
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_bump_fee_reduce_change() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let original_sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let original_fee = check_fee!(wallet, psbt);
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_rate(feerate).enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(sent_received.0, original_sent_received.0);
+ assert_eq!(
+ sent_received.1 + Amount::from_sat(fee.unwrap_or(0)),
+ original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0))
+ );
+ assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ 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.enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(sent_received.0, original_sent_received.0);
+ assert_eq!(
+ sent_received.1 + Amount::from_sat(fee.unwrap_or(0)),
+ original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0))
+ );
+ assert!(
+ fee.unwrap_or(0) > original_fee.unwrap_or(0),
+ "{} > {}",
+ fee.unwrap_or(0),
+ original_fee.unwrap_or(0)
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ assert_eq!(fee.unwrap_or(0), 200);
+}
+
+#[test]
+fn test_bump_fee_reduce_single_recipient() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.clone().extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let original_fee = check_fee!(wallet, psbt);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder
+ .fee_rate(feerate)
+ .allow_shrinking(addr.script_pubkey())
+ .unwrap();
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(sent_received.0, original_sent_received.0);
+ assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.output.len(), 1);
+ assert_eq!(
+ tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)),
+ sent_received.0
+ );
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature);
+}
+
+#[test]
+fn test_bump_fee_absolute_reduce_single_recipient() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let original_fee = check_fee!(wallet, psbt);
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder
+ .allow_shrinking(addr.script_pubkey())
+ .unwrap()
+ .fee_absolute(300);
+ let psbt = builder.finish().unwrap();
+ let tx = &psbt.unsigned_tx;
+ let sent_received = wallet.sent_and_received(tx);
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(sent_received.0, original_sent_received.0);
+ assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0));
+
+ assert_eq!(tx.output.len(), 1);
+ assert_eq!(
+ tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)),
+ sent_received.0
+ );
+
+ assert_eq!(fee.unwrap_or(0), 300);
+}
+
+#[test]
+fn test_bump_fee_drain_wallet() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ // receive an extra tx so that our wallet has two utxos.
+ let tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ };
+ wallet
+ .insert_tx(
+ tx.clone(),
+ ConfirmationTime::Confirmed {
+ height: wallet.latest_checkpoint().height(),
+ time: 42_000,
+ },
+ )
+ .unwrap();
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .add_utxo(OutPoint {
+ txid: tx.txid(),
+ vout: 0,
+ })
+ .unwrap()
+ .manually_selected_only()
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+ assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
+
+ // for the new feerate, it should be enough to reduce the output, but since we specify
+ // `drain_wallet` we expect to spend everything
+ 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"));
+
+ assert_eq!(sent_received.0, Amount::from_sat(75_000));
+}
+
+#[test]
+#[should_panic(expected = "InsufficientFunds")]
+fn test_bump_fee_remove_output_manually_selected_only() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ // receive an extra tx so that our wallet has two utxos. then we manually pick only one of
+ // 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.
+ let init_tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ };
+ wallet
+ .insert_tx(
+ init_tx.clone(),
+ wallet
+ .transactions()
+ .last()
+ .unwrap()
+ .chain_position
+ .cloned()
+ .into(),
+ )
+ .unwrap();
+ let outpoint = OutPoint {
+ txid: init_tx.txid(),
+ vout: 0,
+ };
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .add_utxo(outpoint)
+ .unwrap()
+ .manually_selected_only()
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+ assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder
+ .manually_selected_only()
+ .fee_rate(FeeRate::from_sat_per_vb_unchecked(255));
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_bump_fee_add_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let init_tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ };
+ let pos = wallet
+ .transactions()
+ .last()
+ .unwrap()
+ .chain_position
+ .cloned()
+ .into();
+ wallet.insert_tx(init_tx, pos).unwrap();
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_details = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+ assert_eq!(
+ sent_received.0,
+ original_details.0 + Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
+ Amount::from_sat(30_000)
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(45_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
+}
+
+#[test]
+fn test_bump_fee_absolute_add_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ receive_output_in_latest_block(&mut wallet, 25_000);
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_absolute(6_000);
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(
+ sent_received.0,
+ original_sent_received.0 + Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
+ Amount::from_sat(30_000)
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(45_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ assert_eq!(fee.unwrap_or(0), 6_000);
+}
+
+#[test]
+fn test_bump_fee_no_change_add_input_and_change() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let op = receive_output_in_latest_block(&mut wallet, 25_000);
+
+ // initially make a tx without change by using `drain_to`
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .add_utxo(op)
+ .unwrap()
+ .manually_selected_only()
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let original_sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let original_fee = check_fee!(wallet, psbt);
+
+ let tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ wallet
+ .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
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ let original_send_all_amount =
+ original_sent_received.0 - Amount::from_sat(original_fee.unwrap_or(0));
+ assert_eq!(
+ sent_received.0,
+ original_sent_received.0 + Amount::from_sat(50_000)
+ );
+ assert_eq!(
+ sent_received.1,
+ Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0))
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ original_send_all_amount
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0))
+ );
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature);
+}
+
+#[test]
+fn test_bump_fee_add_input_change_dust() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ receive_output_in_latest_block(&mut wallet, 25_000);
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let original_sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let original_fee = check_fee!(wallet, psbt);
+
+ let mut tx = psbt.extract_tx().expect("failed to extract tx");
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight
+ }
+ let original_tx_weight = tx.weight();
+ assert_eq!(tx.input.len(), 1);
+ assert_eq!(tx.output.len(), 2);
+ let txid = tx.txid();
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ // We set a fee high enough that during rbf we are forced to add
+ // a new input and also that we have to remove the change
+ // that we had previously
+
+ // We calculate the new weight as:
+ // original weight
+ // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4
+ // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4
+ // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4
+ let new_tx_weight =
+ original_tx_weight + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(124);
+ // two inputs (50k, 25k) and one output (45k) - epsilon
+ // We use epsilon here to avoid asking for a slightly too high feerate
+ let fee_abs = 50_000 + 25_000 - 45_000 - 10;
+ builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight);
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(
+ original_sent_received.1,
+ Amount::from_sat(5_000 - original_fee.unwrap_or(0))
+ );
+
+ assert_eq!(
+ sent_received.0,
+ original_sent_received.0 + Amount::from_sat(25_000)
+ );
+ assert_eq!(fee.unwrap_or(0), 30_000);
+ assert_eq!(sent_received.1, Amount::ZERO);
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 1);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(45_000)
+ );
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature);
+}
+
+#[test]
+fn test_bump_fee_force_add_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000);
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let mut tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
+ }
+ wallet
+ .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+ // 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_rate(FeeRate::from_sat_per_vb_unchecked(5));
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(
+ sent_received.0,
+ original_sent_received.0 + Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
+ Amount::from_sat(30_000)
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(45_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature);
+}
+
+#[test]
+fn test_bump_fee_absolute_force_add_input() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000);
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let mut tx = psbt.extract_tx().expect("failed to extract tx");
+ let original_sent_received = wallet.sent_and_received(&tx);
+ let txid = tx.txid();
+ // skip saving the new utxos, we know they can't be used anyways
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
+ }
+ wallet
+ .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ // 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);
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(
+ sent_received.0,
+ original_sent_received.0 + Amount::from_sat(25_000)
+ );
+ assert_eq!(
+ Amount::from_sat(fee.unwrap_or(0)) + sent_received.1,
+ Amount::from_sat(30_000)
+ );
+
+ let tx = &psbt.unsigned_tx;
+ assert_eq!(tx.input.len(), 2);
+ assert_eq!(tx.output.len(), 2);
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey == addr.script_pubkey())
+ .unwrap()
+ .value,
+ Amount::from_sat(45_000)
+ );
+ assert_eq!(
+ tx.output
+ .iter()
+ .find(|txout| txout.script_pubkey != addr.script_pubkey())
+ .unwrap()
+ .value,
+ sent_received.1
+ );
+
+ assert_eq!(fee.unwrap_or(0), 250);
+}
+
+#[test]
+#[should_panic(expected = "InsufficientFunds")]
+fn test_bump_fee_unconfirmed_inputs_only() {
+ // We try to bump the fee, but:
+ // - We can't reduce the change, as we have no change
+ // - All our UTXOs are unconfirmed
+ // So, we fail with "InsufficientFunds", as per RBF rule 2:
+ // The replacement transaction may only include an unconfirmed input
+ // if that input was included in one of the original transactions.
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_wallet()
+ .drain_to(addr.script_pubkey())
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ // Now we receive one transaction with 0 confirmations. We won't be able to use that for
+ // fee bumping, as it's still unconfirmed!
+ receive_output(
+ &mut wallet,
+ 25_000,
+ ConfirmationTime::Unconfirmed { last_seen: 0 },
+ );
+ let mut tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
+ }
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_bump_fee_unconfirmed_input() {
+ // We create a tx draining the wallet and spending one confirmed
+ // and one unconfirmed UTXO. We check that we can fee bump normally
+ // (BIP125 rule 2 only apply to newly added unconfirmed input, you can
+ // always fee bump with an unconfirmed input if it was included in the
+ // original transaction)
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ // We receive a tx with 0 confirmations, which will be used as an input
+ // in the drain tx.
+ receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0));
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_wallet()
+ .drain_to(addr.script_pubkey())
+ .enable_rbf();
+ let psbt = builder.finish().unwrap();
+ let mut tx = psbt.extract_tx().expect("failed to extract tx");
+ let txid = tx.txid();
+ for txin in &mut tx.input {
+ txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
+ }
+ wallet
+ .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
+ .unwrap();
+
+ let mut builder = wallet.build_fee_bump(txid).unwrap();
+ builder
+ .fee_rate(FeeRate::from_sat_per_vb_unchecked(15))
+ .allow_shrinking(addr.script_pubkey())
+ .unwrap();
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_fee_amount_negative_drain_val() {
+ // While building the transaction, bdk would calculate the drain_value
+ // as
+ // current_delta - fee_amount - drain_fee
+ // using saturating_sub, meaning that if the result would end up negative,
+ // it'll remain to zero instead.
+ // This caused a bug in master where we would calculate the wrong fee
+ // for a transaction.
+ // See https://github.com/bitcoindevkit/bdk/issues/660
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
+ .unwrap()
+ .assume_checked();
+ let fee_rate = FeeRate::from_sat_per_kwu(500);
+ let incoming_op = receive_output_in_latest_block(&mut wallet, 8859);
+
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630))
+ .add_utxo(incoming_op)
+ .unwrap()
+ .enable_rbf()
+ .fee_rate(fee_rate);
+ let psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ assert_eq!(psbt.inputs.len(), 1);
+ assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature);
+}
+
+#[test]
+fn test_sign_single_xprv() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_sign_single_xprv_with_master_fingerprint_and_path() {
+ let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_sign_single_xprv_bip44_path() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_sign_single_xprv_sh_wpkh() {
+ let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_sign_single_wif() {
+ let (mut wallet, _) =
+ get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_sign_single_xprv_no_hd_keypaths() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ psbt.inputs[0].bip32_derivation.clear();
+ assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0);
+
+ let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
+ assert!(finalized);
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(extracted.input[0].witness.len(), 2);
+}
+
+#[test]
+fn test_include_output_redeem_witness_script() {
+ let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))");
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .include_output_redeem_witness_script();
+ let psbt = builder.finish().unwrap();
+
+ // p2sh-p2wsh transaction should contain both witness and redeem scripts
+ assert!(psbt
+ .outputs
+ .iter()
+ .any(|output| output.redeem_script.is_some() && output.witness_script.is_some()));
+}
+
+#[test]
+fn test_signing_only_one_of_multiple_inputs() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000))
+ .include_output_redeem_witness_script();
+ let mut psbt = builder.finish().unwrap();
+
+ // add another input to the psbt that is at least passable.
+ let dud_input = bitcoin::psbt::Input {
+ witness_utxo: Some(TxOut {
+ value: Amount::from_sat(100_000),
+ script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str(
+ "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)",
+ )
+ .unwrap()
+ .script_pubkey(),
+ }),
+ ..Default::default()
+ };
+
+ psbt.inputs.push(dud_input);
+ psbt.unsigned_tx.input.push(bitcoin::TxIn::default());
+ let is_final = wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ trust_witness_utxo: true,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+ assert!(
+ !is_final,
+ "shouldn't be final since we can't sign one of the inputs"
+ );
+ assert!(
+ psbt.inputs[0].final_script_witness.is_some(),
+ "should finalized input it signed"
+ )
+}
+
+#[test]
+fn test_remove_partial_sigs_after_finalize_sign_option() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+
+ for remove_partial_sigs in &[true, false] {
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ assert!(wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ remove_partial_sigs: *remove_partial_sigs,
+ ..Default::default()
+ },
+ )
+ .unwrap());
+
+ psbt.inputs.iter().for_each(|input| {
+ if *remove_partial_sigs {
+ assert!(input.partial_sigs.is_empty())
+ } else {
+ assert!(!input.partial_sigs.is_empty())
+ }
+ });
+ }
+}
+
+#[test]
+fn test_try_finalize_sign_option() {
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+
+ for try_finalize in &[true, false] {
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let finalized = wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ try_finalize: *try_finalize,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+
+ psbt.inputs.iter().for_each(|input| {
+ if *try_finalize {
+ assert!(finalized);
+ assert!(input.final_script_sig.is_some());
+ assert!(input.final_script_witness.is_some());
+ } else {
+ assert!(!finalized);
+ assert!(input.final_script_sig.is_none());
+ assert!(input.final_script_witness.is_none());
+ }
+ });
+ }
+}
+
+#[test]
+fn test_sign_nonstandard_sighash() {
+ let sighash = EcdsaSighashType::NonePlusAnyoneCanPay;
+
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .sighash(sighash.into())
+ .drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let result = wallet.sign(&mut psbt, Default::default());
+ assert!(
+ result.is_err(),
+ "Signing should have failed because the TX uses non-standard sighashes"
+ );
+ assert_matches!(
+ result,
+ Err(SignerError::NonStandardSighash),
+ "Signing failed with the wrong error type"
+ );
+
+ // try again after opting-in
+ let result = wallet.sign(
+ &mut psbt,
+ SignOptions {
+ allow_all_sighashes: true,
+ ..Default::default()
+ },
+ );
+ assert!(result.is_ok(), "Signing should have worked");
+ assert!(
+ result.unwrap(),
+ "Should finalize the input since we can produce signatures"
+ );
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(
+ *extracted.input[0].witness.to_vec()[0].last().unwrap(),
+ sighash.to_u32() as u8,
+ "The signature should have been made with the right sighash"
+ );
+}
+
+#[test]
+fn test_unused_address() {
+ let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
+ None, Network::Testnet).unwrap();
+
+ // `list_unused_addresses` should be empty if we haven't revealed any
+ assert!(wallet
+ .list_unused_addresses(KeychainKind::External)
+ .next()
+ .is_none());
+
+ assert_eq!(
+ wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+ assert_eq!(
+ wallet
+ .list_unused_addresses(KeychainKind::External)
+ .next()
+ .unwrap()
+ .to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+}
+
+#[test]
+fn test_next_unused_address() {
+ let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
+ let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap();
+ assert_eq!(wallet.derivation_index(KeychainKind::External), None);
+
+ assert_eq!(
+ wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+ assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0));
+ // calling next_unused again gives same address
+ assert_eq!(
+ wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+ assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0));
+
+ // test mark used / unused
+ assert!(wallet.mark_used(KeychainKind::External, 0));
+ let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ assert_eq!(next_unused_addr.index, 1);
+
+ assert!(wallet.unmark_used(KeychainKind::External, 0));
+ let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ assert_eq!(next_unused_addr.index, 0);
+
+ // use the above address
+ receive_output_in_latest_block(&mut wallet, 25_000);
+
+ assert_eq!(
+ wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+ assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1));
+
+ // trying to mark index 0 unused should return false
+ assert!(!wallet.unmark_used(KeychainKind::External, 0));
+}
+
+#[test]
+fn test_peek_address_at_index() {
+ let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
+ None, Network::Testnet).unwrap();
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 1).to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 0).to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 2).to_string(),
+ "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
+ );
+
+ // current new address is not affected
+ assert_eq!(
+ wallet
+ .reveal_next_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
+ );
+
+ assert_eq!(
+ wallet
+ .reveal_next_address(KeychainKind::External)
+ .unwrap()
+ .to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+}
+
+#[test]
+fn test_peek_address_at_index_not_derivable() {
+ let wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
+ None, Network::Testnet).unwrap();
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 1).to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 0).to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 2).to_string(),
+ "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
+ );
+}
+
+#[test]
+fn test_returns_index_and_address() {
+ let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
+ None, Network::Testnet).unwrap();
+
+ // new index 0
+ assert_eq!(
+ wallet.reveal_next_address(KeychainKind::External).unwrap(),
+ AddressInfo {
+ index: 0,
+ address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ }
+ );
+
+ // new index 1
+ assert_eq!(
+ wallet.reveal_next_address(KeychainKind::External).unwrap(),
+ AddressInfo {
+ index: 1,
+ address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ }
+ );
+
+ // peek index 25
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 25),
+ AddressInfo {
+ index: 25,
+ address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ }
+ );
+
+ // new index 2
+ assert_eq!(
+ wallet.reveal_next_address(KeychainKind::External).unwrap(),
+ AddressInfo {
+ index: 2,
+ address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ }
+ );
+}
+
+#[test]
+fn test_sending_to_bip350_bech32m_address() {
+ let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
+ let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000));
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_get_address() {
+ 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),
+ Some(Bip84(key, KeychainKind::Internal)),
+ Network::Regtest,
+ )
+ .unwrap();
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::External, 0),
+ AddressInfo {
+ index: 0,
+ address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ }
+ );
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::Internal, 0),
+ AddressInfo {
+ index: 0,
+ address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::Internal,
+ }
+ );
+
+ let wallet =
+ Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap();
+
+ assert_eq!(
+ wallet.peek_address(KeychainKind::Internal, 0),
+ AddressInfo {
+ index: 0,
+ address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
+ .unwrap()
+ .assume_checked(),
+ keychain: KeychainKind::External,
+ },
+ "when there's no internal descriptor it should just use external"
+ );
+}
+
+#[test]
+fn test_reveal_addresses() {
+ let desc = get_test_tr_single_sig_xprv();
+ let mut wallet = Wallet::new_no_persist(desc, None, Network::Signet).unwrap();
+ let keychain = KeychainKind::External;
+
+ let last_revealed_addr = wallet
+ .reveal_addresses_to(keychain, 9)
+ .unwrap()
+ .last()
+ .unwrap();
+ assert_eq!(wallet.derivation_index(keychain), Some(9));
+
+ let unused_addrs = wallet.list_unused_addresses(keychain).collect::<Vec<_>>();
+ assert_eq!(unused_addrs.len(), 10);
+ assert_eq!(unused_addrs.last().unwrap(), &last_revealed_addr);
+
+ // revealing to an already revealed index returns nothing
+ let mut already_revealed = wallet.reveal_addresses_to(keychain, 9).unwrap();
+ assert!(already_revealed.next().is_none());
+}
+
+#[test]
+fn test_get_address_no_reuse_single_descriptor() {
+ use bdk_wallet::descriptor::template::Bip84;
+ use std::collections::HashSet;
+
+ let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
+ let mut wallet =
+ Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap();
+
+ let mut used_set = HashSet::new();
+
+ (0..3).for_each(|_| {
+ let external_addr = wallet
+ .reveal_next_address(KeychainKind::External)
+ .unwrap()
+ .address;
+ assert!(used_set.insert(external_addr));
+
+ let internal_addr = wallet
+ .reveal_next_address(KeychainKind::Internal)
+ .unwrap()
+ .address;
+ assert!(used_set.insert(internal_addr));
+ });
+}
+
+#[test]
+fn test_taproot_remove_tapfields_after_finalize_sign_option() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
+
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+ let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
+ assert!(finalized);
+
+ // removes tap_* from inputs
+ for input in &psbt.inputs {
+ assert!(input.tap_key_sig.is_none());
+ assert!(input.tap_script_sigs.is_empty());
+ assert!(input.tap_scripts.is_empty());
+ assert!(input.tap_key_origins.is_empty());
+ assert!(input.tap_internal_key.is_none());
+ assert!(input.tap_merkle_root.is_none());
+ }
+ // removes key origins from outputs
+ for output in &psbt.outputs {
+ assert!(output.tap_key_origins.is_empty());
+ }
+}
+
+#[test]
+fn test_taproot_psbt_populate_tap_key_origins() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
+ let addr = wallet.reveal_next_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(
+ psbt.inputs[0]
+ .tap_key_origins
+ .clone()
+ .into_iter()
+ .collect::<Vec<_>>(),
+ vec![(
+ from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"),
+ (vec![], (from_str!("f6a5cb8b"), from_str!("m/0")))
+ )],
+ "Wrong input tap_key_origins"
+ );
+ assert_eq!(
+ psbt.outputs[0]
+ .tap_key_origins
+ .clone()
+ .into_iter()
+ .collect::<Vec<_>>(),
+ vec![(
+ from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"),
+ (vec![], (from_str!("f6a5cb8b"), from_str!("m/1")))
+ )],
+ "Wrong output tap_key_origins"
+ );
+}
+
+#[test]
+fn test_taproot_psbt_populate_tap_key_origins_repeated_key() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key());
+ let addr = wallet.reveal_next_address(KeychainKind::External).unwrap();
+
+ let path = vec![("rn4nre9c".to_string(), vec![0])]
+ .into_iter()
+ .collect();
+
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
+ .policy_path(path, KeychainKind::External);
+ let psbt = builder.finish().unwrap();
+
+ let mut input_key_origins = psbt.inputs[0]
+ .tap_key_origins
+ .clone()
+ .into_iter()
+ .collect::<Vec<_>>();
+ input_key_origins.sort();
+
+ assert_eq!(
+ input_key_origins,
+ vec![
+ (
+ from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
+ (
+ vec![
+ from_str!(
+ "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
+ ),
+ from_str!(
+ "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
+ ),
+ ],
+ (FromStr::from_str("ece52657").unwrap(), vec![].into())
+ )
+ ),
+ (
+ from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
+ (
+ vec![],
+ (FromStr::from_str("871fd295").unwrap(), vec![].into())
+ )
+ )
+ ],
+ "Wrong input tap_key_origins"
+ );
+
+ let mut output_key_origins = psbt.outputs[0]
+ .tap_key_origins
+ .clone()
+ .into_iter()
+ .collect::<Vec<_>>();
+ output_key_origins.sort();
+
+ assert_eq!(
+ input_key_origins, output_key_origins,
+ "Wrong output tap_key_origins"
+ );
+}
+
+#[test]
+fn test_taproot_psbt_input_tap_tree() {
+ use bitcoin::hex::FromHex;
+ use bitcoin::taproot;
+
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let psbt = builder.finish().unwrap();
+
+ assert_eq!(
+ psbt.inputs[0].tap_merkle_root,
+ Some(
+ TapNodeHash::from_str(
+ "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986"
+ )
+ .unwrap()
+ ),
+ );
+ assert_eq!(
+ psbt.inputs[0].tap_scripts.clone().into_iter().collect::<Vec<_>>(),
+ vec![
+ (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)),
+ (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)),
+ ],
+ );
+ assert_eq!(
+ psbt.inputs[0].tap_internal_key,
+ Some(from_str!(
+ "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"
+ ))
+ );
+
+ // Since we are creating an output to the same address as the input, assert that the
+ // internal_key is the same
+ assert_eq!(
+ psbt.inputs[0].tap_internal_key,
+ psbt.outputs[0].tap_internal_key
+ );
+
+ let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap();
+ assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree));
+}
+
+#[test]
+fn test_taproot_sign_missing_witness_utxo() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+ let witness_utxo = psbt.inputs[0].witness_utxo.take();
+
+ let result = wallet.sign(
+ &mut psbt,
+ SignOptions {
+ allow_all_sighashes: true,
+ ..Default::default()
+ },
+ );
+ assert_matches!(
+ result,
+ Err(SignerError::MissingWitnessUtxo),
+ "Signing should have failed with the correct error because the witness_utxo is missing"
+ );
+
+ // restore the witness_utxo
+ psbt.inputs[0].witness_utxo = witness_utxo;
+
+ let result = wallet.sign(
+ &mut psbt,
+ SignOptions {
+ allow_all_sighashes: true,
+ ..Default::default()
+ },
+ );
+
+ assert_matches!(
+ result,
+ Ok(true),
+ "Should finalize the input since we can produce signatures"
+ );
+}
+
+#[test]
+fn test_taproot_sign_using_non_witness_utxo() {
+ let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder.drain_to(addr.script_pubkey()).drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ psbt.inputs[0].witness_utxo = None;
+ psbt.inputs[0].non_witness_utxo =
+ Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone());
+ assert!(
+ psbt.inputs[0].non_witness_utxo.is_some(),
+ "Previous tx should be present in the database"
+ );
+
+ let result = wallet.sign(&mut psbt, Default::default());
+ assert!(result.is_ok(), "Signing should have worked");
+ assert!(
+ result.unwrap(),
+ "Should finalize the input since we can produce signatures"
+ );
+}
+
+#[test]
+fn test_taproot_foreign_utxo() {
+ let (mut wallet1, _) = get_funded_wallet(get_test_wpkh());
+ let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig());
+
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let utxo = wallet2.list_unspent().next().unwrap();
+ let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap();
+ let foreign_utxo_satisfaction = wallet2
+ .get_descriptor_for_keychain(KeychainKind::External)
+ .max_weight_to_satisfy()
+ .unwrap();
+
+ assert!(
+ psbt_input.non_witness_utxo.is_none(),
+ "`non_witness_utxo` should never be populated for taproot"
+ );
+
+ let mut builder = wallet1.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000))
+ .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
+ .unwrap();
+ let psbt = builder.finish().unwrap();
+ let sent_received =
+ wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx"));
+ wallet1.insert_txout(utxo.outpoint, utxo.txout);
+ let fee = check_fee!(wallet1, psbt);
+
+ assert_eq!(
+ sent_received.0 - sent_received.1,
+ Amount::from_sat(10_000 + fee.unwrap_or(0)),
+ "we should have only net spent ~10_000"
+ );
+
+ assert!(
+ psbt.unsigned_tx
+ .input
+ .iter()
+ .any(|input| input.previous_output == utxo.outpoint),
+ "foreign_utxo should be in there"
+ );
+}
+
+fn test_spend_from_wallet(mut wallet: Wallet) {
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+
+ assert!(
+ wallet.sign(&mut psbt, Default::default()).unwrap(),
+ "Unable to finalize tx"
+ );
+}
+
+// #[test]
+// fn test_taproot_key_spend() {
+// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
+// test_spend_from_wallet(wallet);
+
+// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
+// test_spend_from_wallet(wallet);
+// }
+
+#[test]
+fn test_taproot_no_key_spend() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+
+ assert!(
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ sign_with_tap_internal_key: false,
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ "Unable to finalize tx"
+ );
+
+ assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none()));
+}
+
+#[test]
+fn test_taproot_script_spend() {
+ let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree());
+ test_spend_from_wallet(wallet);
+
+ let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv());
+ test_spend_from_wallet(wallet);
+}
+
+#[test]
+fn test_taproot_script_spend_sign_all_leaves() {
+ 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();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+
+ assert!(
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ tap_leaves_options: TapLeavesOptions::All,
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ "Unable to finalize tx"
+ );
+
+ assert!(psbt
+ .inputs
+ .iter()
+ .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len()));
+}
+
+#[test]
+fn test_taproot_script_spend_sign_include_some_leaves() {
+ use bdk_wallet::signer::TapLeavesOptions;
+ use bitcoin::taproot::TapLeafHash;
+
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+ let mut script_leaves: Vec<_> = psbt.inputs[0]
+ .tap_scripts
+ .clone()
+ .values()
+ .map(|(script, version)| TapLeafHash::from_script(script, *version))
+ .collect();
+ let included_script_leaves = vec![script_leaves.pop().unwrap()];
+ let excluded_script_leaves = script_leaves;
+
+ assert!(
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()),
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ "Unable to finalize tx"
+ );
+
+ assert!(psbt.inputs[0]
+ .tap_script_sigs
+ .iter()
+ .all(|s| included_script_leaves.contains(&s.0 .1)
+ && !excluded_script_leaves.contains(&s.0 .1)));
+}
+
+#[test]
+fn test_taproot_script_spend_sign_exclude_some_leaves() {
+ use bdk_wallet::signer::TapLeavesOptions;
+ use bitcoin::taproot::TapLeafHash;
+
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+ let mut script_leaves: Vec<_> = psbt.inputs[0]
+ .tap_scripts
+ .clone()
+ .values()
+ .map(|(script, version)| TapLeafHash::from_script(script, *version))
+ .collect();
+ let included_script_leaves = [script_leaves.pop().unwrap()];
+ let excluded_script_leaves = script_leaves;
+
+ assert!(
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()),
+ ..Default::default()
+ },
+ )
+ .unwrap(),
+ "Unable to finalize tx"
+ );
+
+ assert!(psbt.inputs[0]
+ .tap_script_sigs
+ .iter()
+ .all(|s| included_script_leaves.contains(&s.0 .1)
+ && !excluded_script_leaves.contains(&s.0 .1)));
+}
+
+#[test]
+fn test_taproot_script_spend_sign_no_leaves() {
+ 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();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ tap_leaves_options: TapLeavesOptions::None,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+
+ assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty()));
+}
+
+#[test]
+fn test_taproot_sign_derive_index_from_psbt() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
+
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
+ let mut psbt = builder.finish().unwrap();
+
+ // re-create the wallet with an empty db
+ let wallet_empty =
+ Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap();
+
+ // signing with an empty db means that we will only look at the psbt to infer the
+ // derivation index
+ assert!(
+ wallet_empty.sign(&mut psbt, Default::default()).unwrap(),
+ "Unable to finalize tx"
+ );
+}
+
+#[test]
+fn test_taproot_sign_explicit_sighash_all() {
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .sighash(TapSighashType::All.into())
+ .drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let result = wallet.sign(&mut psbt, Default::default());
+ assert!(
+ result.is_ok(),
+ "Signing should work because SIGHASH_ALL is safe"
+ )
+}
+
+#[test]
+fn test_taproot_sign_non_default_sighash() {
+ let sighash = TapSighashType::NonePlusAnyoneCanPay;
+
+ let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .sighash(sighash.into())
+ .drain_wallet();
+ let mut psbt = builder.finish().unwrap();
+
+ let witness_utxo = psbt.inputs[0].witness_utxo.take();
+
+ let result = wallet.sign(&mut psbt, Default::default());
+ assert!(
+ result.is_err(),
+ "Signing should have failed because the TX uses non-standard sighashes"
+ );
+ assert_matches!(
+ result,
+ Err(SignerError::NonStandardSighash),
+ "Signing failed with the wrong error type"
+ );
+
+ // try again after opting-in
+ let result = wallet.sign(
+ &mut psbt,
+ SignOptions {
+ allow_all_sighashes: true,
+ ..Default::default()
+ },
+ );
+ assert!(
+ result.is_err(),
+ "Signing should have failed because the witness_utxo is missing"
+ );
+ assert_matches!(
+ result,
+ Err(SignerError::MissingWitnessUtxo),
+ "Signing failed with the wrong error type"
+ );
+
+ // restore the witness_utxo
+ psbt.inputs[0].witness_utxo = witness_utxo;
+
+ let result = wallet.sign(
+ &mut psbt,
+ SignOptions {
+ allow_all_sighashes: true,
+ ..Default::default()
+ },
+ );
+
+ assert!(result.is_ok(), "Signing should have worked");
+ assert!(
+ result.unwrap(),
+ "Should finalize the input since we can produce signatures"
+ );
+
+ let extracted = psbt.extract_tx().expect("failed to extract tx");
+ assert_eq!(
+ *extracted.input[0].witness.to_vec()[0].last().unwrap(),
+ sighash as u8,
+ "The signature should have been made with the right sighash"
+ );
+}
+
+#[test]
+fn test_spend_coinbase() {
+ let descriptor = get_test_wpkh();
+ let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap();
+
+ let confirmation_height = 5;
+ wallet
+ .insert_checkpoint(BlockId {
+ height: confirmation_height,
+ hash: BlockHash::all_zeros(),
+ })
+ .unwrap();
+ let coinbase_tx = Transaction {
+ version: transaction::Version::ONE,
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ }],
+ output: vec![TxOut {
+ script_pubkey: wallet
+ .next_unused_address(KeychainKind::External)
+ .unwrap()
+ .script_pubkey(),
+ value: Amount::from_sat(25_000),
+ }],
+ };
+ wallet
+ .insert_tx(
+ coinbase_tx,
+ ConfirmationTime::Confirmed {
+ height: confirmation_height,
+ time: 30_000,
+ },
+ )
+ .unwrap();
+
+ let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
+ let maturity_time = confirmation_height + COINBASE_MATURITY;
+
+ let balance = wallet.get_balance();
+ assert_eq!(
+ balance,
+ Balance {
+ immature: Amount::from_sat(25_000),
+ trusted_pending: Amount::ZERO,
+ untrusted_pending: Amount::ZERO,
+ confirmed: Amount::ZERO
+ }
+ );
+
+ // We try to create a transaction, only to notice that all
+ // our funds are unspendable
+ let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
+ .unwrap()
+ .assume_checked();
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), balance.immature / 2)
+ .current_height(confirmation_height);
+ assert!(matches!(
+ builder.finish(),
+ Err(CreateTxError::CoinSelection(
+ coin_selection::Error::InsufficientFunds {
+ needed: _,
+ available: 0
+ }
+ ))
+ ));
+
+ // Still unspendable...
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), balance.immature / 2)
+ .current_height(not_yet_mature_time);
+ assert_matches!(
+ builder.finish(),
+ Err(CreateTxError::CoinSelection(
+ coin_selection::Error::InsufficientFunds {
+ needed: _,
+ available: 0
+ }
+ ))
+ );
+
+ wallet
+ .insert_checkpoint(BlockId {
+ height: maturity_time,
+ hash: BlockHash::all_zeros(),
+ })
+ .unwrap();
+ let balance = wallet.get_balance();
+ assert_eq!(
+ balance,
+ Balance {
+ immature: Amount::ZERO,
+ trusted_pending: Amount::ZERO,
+ untrusted_pending: Amount::ZERO,
+ confirmed: Amount::from_sat(25_000)
+ }
+ );
+ let mut builder = wallet.build_tx();
+ builder
+ .add_recipient(addr.script_pubkey(), balance.confirmed / 2)
+ .current_height(maturity_time);
+ builder.finish().unwrap();
+}
+
+#[test]
+fn test_allow_dust_limit() {
+ let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
+
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+
+ let mut builder = wallet.build_tx();
+
+ builder.add_recipient(addr.script_pubkey(), Amount::ZERO);
+
+ assert_matches!(
+ builder.finish(),
+ Err(CreateTxError::OutputBelowDustLimit(0))
+ );
+
+ let mut builder = wallet.build_tx();
+
+ builder
+ .allow_dust(true)
+ .add_recipient(addr.script_pubkey(), Amount::ZERO);
+
+ assert!(builder.finish().is_ok());
+}
+
+#[test]
+fn test_fee_rate_sign_no_grinding_high_r() {
+ // Our goal is to obtain a transaction with a signature with high-R (71 bytes
+ // instead of 70). We then check that our fee rate and fee calculation is
+ // alright.
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
+ let mut builder = wallet.build_tx();
+ let mut data = PushBytesBuf::try_from(vec![0]).unwrap();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_rate(fee_rate)
+ .add_data(&data);
+ let mut psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+ let (op_return_vout, _) = psbt
+ .unsigned_tx
+ .output
+ .iter()
+ .enumerate()
+ .find(|(_n, i)| i.script_pubkey.is_op_return())
+ .unwrap();
+
+ let mut sig_len: usize = 0;
+ // We try to sign many different times until we find a longer signature (71 bytes)
+ while sig_len < 71 {
+ // Changing the OP_RETURN data will make the signature change (but not the fee, until
+ // data[0] is small enough)
+ data.as_mut_bytes()[0] += 1;
+ psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data);
+ // Clearing the previous signature
+ psbt.inputs[0].partial_sigs.clear();
+ // Signing
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ remove_partial_sigs: false,
+ try_finalize: false,
+ allow_grinding: false,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+ // We only have one key in the partial_sigs map, this is a trick to retrieve it
+ let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
+ sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
+ }
+ // Actually finalizing the transaction...
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ remove_partial_sigs: false,
+ allow_grinding: false,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+ // ...and checking that everything is fine
+ assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
+}
+
+#[test]
+fn test_fee_rate_sign_grinding_low_r() {
+ // Our goal is to obtain a transaction with a signature with low-R (70 bytes)
+ // by setting the `allow_grinding` signing option as true.
+ // We then check that our fee rate and fee calculation is alright and that our
+ // signature is 70 bytes.
+ let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
+ let addr = wallet.next_unused_address(KeychainKind::External).unwrap();
+ let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
+ let mut builder = wallet.build_tx();
+ builder
+ .drain_to(addr.script_pubkey())
+ .drain_wallet()
+ .fee_rate(fee_rate);
+ let mut psbt = builder.finish().unwrap();
+ let fee = check_fee!(wallet, psbt);
+
+ wallet
+ .sign(
+ &mut psbt,
+ SignOptions {
+ remove_partial_sigs: false,
+ allow_grinding: true,
+ ..Default::default()
+ },
+ )
+ .unwrap();
+
+ let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
+ let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
+ assert_eq!(sig_len, 70);
+ assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate);
+}
+
+#[test]
+fn test_taproot_load_descriptor_duplicated_keys() {
+ // Added after issue https://github.com/bitcoindevkit/bdk/issues/760
+ //
+ // Having the same key in multiple taproot leaves is safe and should be accepted by BDK
+
+ let (wallet, _) = get_funded_wallet(get_test_tr_dup_keys());
+ let addr = wallet.peek_address(KeychainKind::External, 0);
+
+ assert_eq!(
+ addr.to_string(),
+ "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5"
+ );
+}
+
+#[test]
+/// The wallet should re-use previously allocated change addresses when the tx using them is cancelled
+fn test_tx_cancellation() {
+ macro_rules! new_tx {
+ ($wallet:expr) => {{
+ let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
+ .unwrap()
+ .assume_checked();
+ let mut builder = $wallet.build_tx();
+ builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000));
+
+ let psbt = builder.finish().unwrap();
+
+ psbt
+ }};
+ }
+
+ let (mut wallet, _) =
+ get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv()));
+
+ let psbt1 = new_tx!(wallet);
+ let change_derivation_1 = psbt1
+ .unsigned_tx
+ .output
+ .iter()
+ .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
+ .unwrap();
+ assert_eq!(change_derivation_1, (KeychainKind::Internal, 0));
+
+ let psbt2 = new_tx!(wallet);
+
+ let change_derivation_2 = psbt2
+ .unsigned_tx
+ .output
+ .iter()
+ .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
+ .unwrap();
+ assert_eq!(change_derivation_2, (KeychainKind::Internal, 1));
+
+ wallet.cancel_tx(&psbt1.extract_tx().expect("failed to extract tx"));
+
+ let psbt3 = new_tx!(wallet);
+ let change_derivation_3 = psbt3
+ .unsigned_tx
+ .output
+ .iter()
+ .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
+ .unwrap();
+ assert_eq!(change_derivation_3, (KeychainKind::Internal, 0));
+
+ let psbt3 = new_tx!(wallet);
+ let change_derivation_3 = psbt3
+ .unsigned_tx
+ .output
+ .iter()
+ .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
+ .unwrap();
+ assert_eq!(change_derivation_3, (KeychainKind::Internal, 2));
+
+ wallet.cancel_tx(&psbt3.extract_tx().expect("failed to extract tx"));
+
+ let psbt3 = new_tx!(wallet);
+ let change_derivation_4 = psbt3
+ .unsigned_tx
+ .output
+ .iter()
+ .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey))
+ .unwrap();
+ assert_eq!(change_derivation_4, (KeychainKind::Internal, 2));
+}
+
+#[test]
+fn test_thread_safety() {
+ fn thread_safe<T: Send + Sync>() {}
+ thread_safe::<Wallet>(); // compiles only if true
+}
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"
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/*)";
# 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" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
use std::{collections::BTreeSet, io::Write, str::FromStr};
-use bdk::{
+use bdk_esplora::{esplora_client, EsploraAsyncExt};
+use bdk_file_store::Store;
+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";
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
#[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 =
+ 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/*)";
# 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"
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/*)";
# 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" }
$ 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]
-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.
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!(