Compare commits

...

24 Commits

Author SHA1 Message Date
Alekos Filini
bddd418c8e Bump version to 0.3.0 2021-01-20 10:39:24 -05:00
Alekos Filini
49db898acb Update CHANGELOG.md in preparation of tag v0.3.0 2021-01-20 10:27:28 -05:00
Tobin Harding
01585227c5 Use contains combinator
As suggested by clippy, use the `contains` combinator instead of doing
manual range check on floats.
2021-01-18 11:39:37 -08:00
Justin Moon
52b45c5b89 [wallet] Add "needed" and "available" metadata to Error::InsufficientFunds 2021-01-18 11:15:10 +01:00
Alekos Filini
733355a6ae Bump version to 0.3.0-rc.1 2021-01-12 21:41:30 +01:00
Alekos Filini
6955a7776d Merge commit 'refs/pull/264/head' of github.com:bitcoindevkit/bdk 2021-01-12 14:02:41 +01:00
Alekos Filini
bf04a2cf69 descriptor: Use DescriptorError instead of Error when reasonable
Change the return type of the `descriptor!()` macro and `ToWalletDescriptor` to
avoid having to map errors.

Also introduce more checks to validate descriptors built using the macro.
2021-01-12 12:21:22 +01:00
Riccardo Casatta
2b669afd3e Permit to not set timeout in ElectrumBlockchainConfig
Allowing to use socks5 which requires None timeout
2021-01-11 14:06:56 +01:00
Steve Myers
8510b2b86e Fix crates.io license info 2021-01-09 10:41:48 -08:00
Alekos Filini
a95a9f754c Merge commit 'refs/pull/260/head' of github.com:bitcoindevkit/bdk 2021-01-05 16:06:32 +01:00
Alekos Filini
3980b90bff Merge commit 'refs/pull/248/head' of github.com:bitcoindevkit/bdk 2021-01-05 16:04:53 +01:00
Alekos Filini
b2bd1b5831 Merge commit 'refs/pull/257/head' of github.com:bitcoindevkit/bdk 2021-01-05 16:01:15 +01:00
Steve Myers
aa31c96821 [ci] Fail 'Build docs' job if warnings 2021-01-04 16:39:11 -08:00
Steve Myers
f74bfdd493 Remove 'cli.rs' module, 'cli-utils' feature and 'repl.rs' example 2020-12-31 09:44:30 -08:00
Steve Myers
5034ca2267 Fix clippy warnings for compact_filters feature 2020-12-30 19:23:35 -08:00
Steve Myers
8094263028 [ci] Fix clippy step to check matrix features 2020-12-30 19:23:00 -08:00
LLFourn
0c9c0716a4 [wallet] Fix details.fees being wrong when change is dust 2020-12-29 16:36:35 +11:00
Alekos Filini
c2b2da7601 Merge commit 'refs/pull/252/head' of github.com:bitcoindevkit/bdk 2020-12-23 18:39:05 +01:00
Alekos Filini
407f14add9 Merge commit 'refs/pull/250/head' of github.com:bitcoindevkit/bdk 2020-12-23 17:48:59 +01:00
LLFourn
656c9c9da8 Use () to indicate a missing blockchain
So that:
1. There are no runtime errors
2. There less type annotations needed
3. Less traits and stuff to document
2020-12-23 14:52:29 +11:00
LLFourn
a578d20282 Fix incredibly annoying cargo-fmt problem
I must have a newer version of cargo-fmt which stops me from making
commits every time because of this.
2020-12-22 14:37:53 +11:00
Steve Myers
2e222c7ad9 [docs] Add badges for crates.io, mit license. Fix docs.rs badge and link 2020-12-21 20:14:25 +01:00
Alekos Filini
7d6cd6d4f5 Fix the changelog after release v0.2.0 2020-12-21 20:14:23 +01:00
Alekos Filini
e31bd812ed Bump version to 0.2.1-dev 2020-12-21 14:51:49 +01:00
36 changed files with 404 additions and 1243 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- name: Install tarpaulin - name: Install tarpaulin
run: cargo install cargo-tarpaulin run: cargo install cargo-tarpaulin
- name: Tarpaulin - name: Tarpaulin
run: cargo tarpaulin --features all-keys,cli-utils,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
- name: Publish to codecov.io - name: Publish to codecov.io
uses: codecov/codecov-action@v1.0.15 uses: codecov/codecov-action@v1.0.15

View File

@@ -20,7 +20,7 @@ jobs:
- key-value-db - key-value-db
- electrum - electrum
- compact_filters - compact_filters
- cli-utils,esplora,key-value-db,electrum - esplora,key-value-db,electrum
- compiler - compiler
steps: steps:
- name: checkout - name: checkout
@@ -44,7 +44,7 @@ jobs:
- name: Build - name: Build
run: cargo build --features ${{ matrix.features }} --no-default-features run: cargo build --features ${{ matrix.features }} --no-default-features
- name: Clippy - name: Clippy
run: cargo clippy -- -D warnings run: cargo clippy --features ${{ matrix.features }} --no-default-features -- -D warnings
- name: Test - name: Test
run: cargo test --features ${{ matrix.features }} --no-default-features run: cargo test --features ${{ matrix.features }} --no-default-features
@@ -130,7 +130,7 @@ jobs:
- name: Add target wasm32 - name: Add target wasm32
run: rustup target add wasm32-unknown-unknown run: rustup target add wasm32-unknown-unknown
- name: Check - name: Check
run: cargo check --target wasm32-unknown-unknown --features cli-utils,esplora --no-default-features run: cargo check --target wasm32-unknown-unknown --features esplora --no-default-features
fmt: fmt:
name: Rust fmt name: Rust fmt

View File

@@ -27,7 +27,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: rustdoc command: rustdoc
args: --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs args: --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:

View File

@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [v0.3.0] - [v0.2.0]
### Descriptor
#### Changed
- Added an alias `DescriptorError` for `descriptor::error::Error`
- Changed the error returned by `descriptor!()` and `fragment!()` to `DescriptorError`
- Changed the error type in `ToWalletDescriptor` to `DescriptorError`
- Improved checks on descriptors built using the macros
### Blockchain
#### Changed
- Remove `BlockchainMarker`, `OfflineClient` and `OfflineWallet` in favor of just using the unit
type to mark for a missing client.
### CLI
#### Changed
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository
## [v0.2.0] - [0.1.0-beta.1]
### Project ### Project
#### Added #### Added
- Add CONTRIBUTING.md - Add CONTRIBUTING.md
@@ -209,5 +229,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `MemoryDatabase` in the compiler example - Use `MemoryDatabase` in the compiler example
- Make the REPL return JSON - Make the REPL return JSON
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...HEAD [unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...HEAD
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1 [0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bdk" name = "bdk"
version = "0.2.0" version = "0.3.0"
edition = "2018" edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"] authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"
@@ -9,7 +9,7 @@ documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library" description = "A modern, lightweight, descriptor-based wallet library"
keywords = ["bitcoin", "wallet", "descriptor", "psbt"] keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
readme = "README.md" readme = "README.md"
license-file = "LICENSE" license = "MIT"
[dependencies] [dependencies]
bdk-macros = "0.2" bdk-macros = "0.2"
@@ -22,11 +22,9 @@ rand = "^0.7"
# Optional dependencies # Optional dependencies
sled = { version = "0.34", optional = true } sled = { version = "0.34", optional = true }
electrum-client = { version = "0.4.0-beta.1", optional = true } electrum-client = { version = "0.5.0-beta.1", optional = true }
reqwest = { version = "0.10", optional = true, features = ["json"] } reqwest = { version = "0.10", optional = true, features = ["json"] }
futures = { version = "0.3", optional = true } futures = { version = "0.3", optional = true }
clap = { version = "2.33", optional = true }
base64 = { version = "^0.11", optional = true }
async-trait = { version = "0.1", optional = true } async-trait = { version = "0.1", optional = true }
rocksdb = { version = "0.14", optional = true } rocksdb = { version = "0.14", optional = true }
# pin cc version to 1.0.62 because 1.0.63 break rocksdb build # pin cc version to 1.0.62 because 1.0.63 break rocksdb build
@@ -34,7 +32,6 @@ cc = { version = "=1.0.62", optional = true }
socks = { version = "0.3", optional = true } socks = { version = "0.3", optional = true }
lazy_static = { version = "1.4", optional = true } lazy_static = { version = "1.4", optional = true }
tiny-bip39 = { version = "^0.8", optional = true } tiny-bip39 = { version = "^0.8", optional = true }
structopt = { version = "^0.3", optional = true }
# Platform-specific dependencies # Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -47,13 +44,12 @@ rand = { version = "^0.7", features = ["wasm-bindgen"] }
[features] [features]
minimal = [] minimal = []
compiler = ["clap", "miniscript/compiler"] compiler = ["miniscript/compiler"]
default = ["key-value-db", "electrum"] default = ["key-value-db", "electrum"]
electrum = ["electrum-client"] electrum = ["electrum-client"]
esplora = ["reqwest", "futures"] esplora = ["reqwest", "futures"]
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"] compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
key-value-db = ["sled"] key-value-db = ["sled"]
cli-utils = ["clap", "base64", "structopt"]
async-interface = ["async-trait"] async-interface = ["async-trait"]
all-keys = ["keys-bip39"] all-keys = ["keys-bip39"]
keys-bip39 = ["tiny-bip39"] keys-bip39 = ["tiny-bip39"]
@@ -61,20 +57,17 @@ keys-bip39 = ["tiny-bip39"]
# Debug/Test features # Debug/Test features
debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"] debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"]
test-electrum = ["electrum"] test-electrum = ["electrum"]
test-md-docs = ["base64", "electrum"] test-md-docs = ["electrum"]
[dev-dependencies] [dev-dependencies]
bdk-testutils = "0.2" bdk-testutils = "0.2"
bdk-testutils-macros = "0.2" bdk-testutils-macros = "0.2"
serial_test = "0.4" serial_test = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
rustyline = "6.0"
dirs-next = "2.0"
env_logger = "0.7" env_logger = "0.7"
base64 = "^0.11"
clap = "2.33"
[[example]]
name = "repl"
required-features = ["cli-utils"]
[[example]] [[example]]
name = "parse_descriptor" name = "parse_descriptor"
[[example]] [[example]]

View File

@@ -35,12 +35,14 @@ Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordin
5. If a bug is found: 5. If a bug is found:
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually - If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch - For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
6. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z". 6. Update the changelog with the new release version
7. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag. 7. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
8. Push the new commits to the upstream release branch, wait for the CI to finish one last time. 8. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
9. Publish **all** the updated crates to crates.io. 9. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
10. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev". 10. Publish **all** the updated crates to crates.io.
11. Merge the release branch back into `master`. 11. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
12. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs. 12. Merge the release branch back into `master`.
13. Announce the release on Twitter, Discord and Telegram. 13. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
14. Celebrate :tada: 14. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
15. Announce the release on Twitter, Discord and Telegram.
16. Celebrate :tada:

View File

@@ -8,10 +8,11 @@
</p> </p>
<p> <p>
<!-- <a href="https://crates.io/crates/magical"><img alt="Crate Info" src="https://img.shields.io/crates/v/magical.svg"/></a> --> <a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT Licensed" src="https://img.shields.io/badge/license-MIT-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://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://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a> <a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
<a href="https://bitcoindevkit.org/docs-rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a> <a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
<a href="https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html"><img alt="Rustc Version 1.45+" src="https://img.shields.io/badge/rustc-1.45%2B-lightgrey.svg"/></a> <a href="https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html"><img alt="Rustc Version 1.45+" src="https://img.shields.io/badge/rustc-1.45%2B-lightgrey.svg"/></a>
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a> <a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
</p> </p>
@@ -19,7 +20,7 @@
<h4> <h4>
<a href="https://bitcoindevkit.org">Project Homepage</a> <a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span> <span> | </span>
<a href="https://bitcoindevkit.org/docs-rs/bdk">Documentation</a> <a href="https://docs.rs/bdk">Documentation</a>
</h4> </h4>
</div> </div>
@@ -65,11 +66,10 @@ fn main() -> Result<(), bdk::Error> {
### Generate a few addresses ### Generate a few addresses
```rust ```rust
use bdk::{Wallet, OfflineWallet}; use bdk::{Wallet, database::MemoryDatabase};
use bdk::database::MemoryDatabase;
fn main() -> Result<(), bdk::Error> { fn main() -> Result<(), bdk::Error> {
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet, bitcoin::Network::Testnet,
@@ -125,13 +125,12 @@ fn main() -> Result<(), bdk::Error> {
### Sign a transaction ### Sign a transaction
```rust,no_run ```rust,no_run
use bdk::{Wallet, OfflineWallet}; use bdk::{Wallet, database::MemoryDatabase};
use bdk::database::MemoryDatabase;
use bitcoin::consensus::deserialize; use bitcoin::consensus::deserialize;
fn main() -> Result<(), bdk::Error> { fn main() -> Result<(), bdk::Error> {
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
bitcoin::Network::Testnet, bitcoin::Network::Testnet,

View File

@@ -29,7 +29,7 @@ use bdk::database::MemoryDatabase;
use bdk::descriptor::HDKeyPaths; use bdk::descriptor::HDKeyPaths;
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError}; use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
use bdk::KeychainKind; use bdk::KeychainKind;
use bdk::{OfflineWallet, Wallet}; use bdk::Wallet;
use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::hex::FromHex;
use bitcoin::util::bip32::Fingerprint; use bitcoin::util::bip32::Fingerprint;
@@ -59,7 +59,7 @@ impl AddressValidator for DummyValidator {
fn main() -> Result<(), bdk::Error> { fn main() -> Result<(), bdk::Error> {
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))"; let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
let mut wallet: OfflineWallet<_> = let mut wallet =
Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?; Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
wallet.add_address_validator(Arc::new(DummyValidator)); wallet.add_address_validator(Arc::new(DummyValidator));

View File

@@ -40,7 +40,7 @@ use miniscript::policy::Concrete;
use miniscript::Descriptor; use miniscript::Descriptor;
use bdk::database::memory::MemoryDatabase; use bdk::database::memory::MemoryDatabase;
use bdk::{KeychainKind, OfflineWallet, Wallet}; use bdk::{KeychainKind, Wallet};
fn main() { fn main() {
env_logger::init_from_env( env_logger::init_from_env(
@@ -98,8 +98,7 @@ fn main() {
Some("regtest") => Network::Regtest, Some("regtest") => Network::Regtest,
Some("testnet") | _ => Network::Testnet, Some("testnet") | _ => Network::Testnet,
}; };
let wallet: OfflineWallet<_> = let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database).unwrap();
Wallet::new_offline(&format!("{}", descriptor), None, network, database).unwrap();
info!("... First address: {}", wallet.get_new_address().unwrap()); info!("... First address: {}", wallet.get_new_address().unwrap());

View File

@@ -1,174 +0,0 @@
// Magical Bitcoin Library
// Written in 2020 by
// Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020 Magical Bitcoin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use bitcoin::Network;
use clap::AppSettings;
use log::{debug, info, warn, LevelFilter};
use rustyline::error::ReadlineError;
use rustyline::Editor;
use structopt::StructOpt;
use bdk::bitcoin;
#[cfg(feature = "esplora")]
use bdk::blockchain::esplora::EsploraBlockchainConfig;
use bdk::blockchain::{
AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, ElectrumBlockchainConfig,
};
use bdk::cli::{self, WalletOpt, WalletSubCommand};
use bdk::sled;
use bdk::Wallet;
#[derive(Debug, StructOpt, Clone, PartialEq)]
#[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
struct ReplOpt {
/// Wallet sub-command
#[structopt(subcommand)]
pub subcommand: WalletSubCommand,
}
fn prepare_home_dir() -> PathBuf {
let mut dir = PathBuf::new();
dir.push(&dirs_next::home_dir().unwrap());
dir.push(".bdk-bitcoin");
if !dir.exists() {
info!("Creating home directory {}", dir.as_path().display());
fs::create_dir(&dir).unwrap();
}
dir.push("database.sled");
dir
}
fn main() {
let cli_opt: WalletOpt = WalletOpt::from_args();
let level = LevelFilter::from_str(cli_opt.log_level.as_str()).unwrap_or(LevelFilter::Info);
env_logger::builder().filter_level(level).init();
let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
debug!("network: {:?}", network);
if network == Network::Bitcoin {
warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
}
let descriptor = cli_opt.descriptor.as_str();
let change_descriptor = cli_opt.change_descriptor.as_deref();
debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
let tree = database.open_tree(cli_opt.wallet).unwrap();
debug!("database opened successfully");
// Try to use Esplora config if "esplora" feature is enabled
#[cfg(feature = "esplora")]
let config_esplora: Option<AnyBlockchainConfig> = {
let esplora_concurrency = cli_opt.esplora_concurrency;
cli_opt.esplora.map(|base_url| {
AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
base_url: base_url.to_string(),
concurrency: Some(esplora_concurrency),
})
})
};
#[cfg(not(feature = "esplora"))]
let config_esplora = None;
// Fall back to Electrum config if Esplora config isn't provided
let config =
config_esplora.unwrap_or(AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
url: cli_opt.electrum,
socks5: cli_opt.proxy,
retry: 10,
timeout: 10,
}));
let wallet = Wallet::new(
descriptor,
change_descriptor,
network,
tree,
AnyBlockchain::from_config(&config).unwrap(),
)
.unwrap();
let wallet = Arc::new(wallet);
match cli_opt.subcommand {
WalletSubCommand::Other(external) if external.contains(&"repl".to_string()) => {
let mut rl = Editor::<()>::new();
// if rl.load_history("history.txt").is_err() {
// println!("No previous history.");
// }
loop {
let readline = rl.readline(">> ");
match readline {
Ok(line) => {
if line.trim() == "" {
continue;
}
rl.add_history_entry(line.as_str());
let split_line: Vec<&str> = line.split(" ").collect();
let repl_subcommand: Result<ReplOpt, clap::Error> =
ReplOpt::from_iter_safe(split_line);
debug!("repl_subcommand = {:?}", repl_subcommand);
if let Err(err) = repl_subcommand {
println!("{}", err.message);
continue;
}
let result = cli::handle_wallet_subcommand(
&Arc::clone(&wallet),
repl_subcommand.unwrap().subcommand,
)
.unwrap();
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
Err(ReadlineError::Interrupted) => continue,
Err(ReadlineError::Eof) => break,
Err(err) => {
println!("{:?}", err);
break;
}
}
}
// rl.save_history("history.txt").unwrap();
}
_ => {
let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
}
}

View File

@@ -8,7 +8,7 @@ repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-macros" documentation = "https://docs.rs/bdk-macros"
description = "Supporting macros for `bdk`" description = "Supporting macros for `bdk`"
keywords = ["bdk"] keywords = ["bdk"]
license-file = "../LICENSE" license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -131,7 +131,7 @@ impl CompactFiltersBlockchain {
let network = peers[0].get_network(); let network = peers[0].get_network();
let cfs = DB::list_cf(&opts, &storage_dir).unwrap_or(vec!["default".to_string()]); let cfs = DB::list_cf(&opts, &storage_dir).unwrap_or_else(|_| vec!["default".to_string()]);
let db = DB::open_cf(&opts, &storage_dir, &cfs)?; let db = DB::open_cf(&opts, &storage_dir, &cfs)?;
let headers = Arc::new(ChainStore::new(db, network)?); let headers = Arc::new(ChainStore::new(db, network)?);
@@ -221,7 +221,7 @@ impl CompactFiltersBlockchain {
sent: outgoing, sent: outgoing,
height, height,
timestamp, timestamp,
fees: inputs_sum.checked_sub(outputs_sum).unwrap_or(0), fees: inputs_sum.saturating_sub(outputs_sum),
}; };
info!("Saving tx {}", tx.txid); info!("Saving tx {}", tx.txid);
@@ -257,13 +257,10 @@ impl Blockchain for CompactFiltersBlockchain {
.map(|x| x / 1000) .map(|x| x / 1000)
.unwrap_or(0) .unwrap_or(0)
+ 1; + 1;
let expected_bundles_to_sync = total_bundles let expected_bundles_to_sync = total_bundles.saturating_sub(cf_sync.pruned_bundles()?);
.checked_sub(cf_sync.pruned_bundles()?)
.unwrap_or(0);
let headers_cost = (first_peer.get_version().start_height as usize) let headers_cost = (first_peer.get_version().start_height as usize)
.checked_sub(initial_height) .saturating_sub(initial_height) as f32
.unwrap_or(0) as f32
* SYNC_HEADERS_COST; * SYNC_HEADERS_COST;
let filters_cost = expected_bundles_to_sync as f32 * SYNC_FILTERS_COST; let filters_cost = expected_bundles_to_sync as f32 * SYNC_FILTERS_COST;
@@ -274,7 +271,7 @@ impl Blockchain for CompactFiltersBlockchain {
Arc::clone(&self.headers), Arc::clone(&self.headers),
|new_height| { |new_height| {
let local_headers_cost = let local_headers_cost =
new_height.checked_sub(initial_height).unwrap_or(0) as f32 * SYNC_HEADERS_COST; new_height.saturating_sub(initial_height) as f32 * SYNC_HEADERS_COST;
progress_update.update( progress_update.update(
local_headers_cost / total_cost * 100.0, local_headers_cost / total_cost * 100.0,
Some(format!("Synced headers to {}", new_height)), Some(format!("Synced headers to {}", new_height)),
@@ -288,9 +285,7 @@ impl Blockchain for CompactFiltersBlockchain {
} }
let synced_height = self.headers.get_height()?; let synced_height = self.headers.get_height()?;
let buried_height = synced_height let buried_height = synced_height.saturating_sub(sync::BURIED_CONFIRMATIONS);
.checked_sub(sync::BURIED_CONFIRMATIONS)
.unwrap_or(0);
info!("Synced headers to height: {}", synced_height); info!("Synced headers to height: {}", synced_height);
cf_sync.prepare_sync(Arc::clone(&first_peer))?; cf_sync.prepare_sync(Arc::clone(&first_peer))?;
@@ -303,7 +298,9 @@ impl Blockchain for CompactFiltersBlockchain {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
#[allow(clippy::mutex_atomic)]
let last_synced_block = Arc::new(Mutex::new(synced_height)); let last_synced_block = Arc::new(Mutex::new(synced_height));
let synced_bundles = Arc::new(AtomicUsize::new(0)); let synced_bundles = Arc::new(AtomicUsize::new(0));
let progress_update = Arc::new(Mutex::new(progress_update)); let progress_update = Arc::new(Mutex::new(progress_update));
@@ -328,10 +325,7 @@ impl Blockchain for CompactFiltersBlockchain {
} }
let block_height = headers.get_height_for(block_hash)?.unwrap_or(0); let block_height = headers.get_height_for(block_hash)?.unwrap_or(0);
let saved_correct_block = match headers.get_full_block(block_height)? { let saved_correct_block = matches!(headers.get_full_block(block_height)?, Some(block) if &block.block_hash() == block_hash);
Some(block) if &block.block_hash() == block_hash => true,
_ => false,
};
if saved_correct_block { if saved_correct_block {
Ok(false) Ok(false)

View File

@@ -333,7 +333,7 @@ impl Peer {
NetworkMessage::Alert(_) => continue, NetworkMessage::Alert(_) => continue,
NetworkMessage::GetData(ref inv) => { NetworkMessage::GetData(ref inv) => {
let (found, not_found): (Vec<_>, Vec<_>) = inv let (found, not_found): (Vec<_>, Vec<_>) = inv
.into_iter() .iter()
.map(|item| (*item, reader_thread_mempool.get_tx(item))) .map(|item| (*item, reader_thread_mempool.get_tx(item)))
.partition(|(_, d)| d.is_some()); .partition(|(_, d)| d.is_some());
for (_, found_tx) in found { for (_, found_tx) in found {
@@ -518,10 +518,9 @@ impl InvPeer for Peer {
let getdata = inv let getdata = inv
.iter() .iter()
.cloned() .cloned()
.filter(|item| match item { .filter(
Inventory::Transaction(txid) if !self.mempool.has_tx(txid) => true, |item| matches!(item, Inventory::Transaction(txid) if !self.mempool.has_tx(txid)),
_ => false, )
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let num_txs = getdata.len(); let num_txs = getdata.len();
self.send(NetworkMessage::GetData(getdata))?; self.send(NetworkMessage::GetData(getdata))?;

View File

@@ -375,7 +375,7 @@ impl ChainStore<Full> {
let min_height = match iterator let min_height = match iterator
.next() .next()
.and_then(|(k, _)| k[1..].try_into().ok()) .and_then(|(k, _)| k[1..].try_into().ok())
.map(|bytes| usize::from_be_bytes(bytes)) .map(usize::from_be_bytes)
{ {
None => { None => {
std::mem::drop(iterator); std::mem::drop(iterator);
@@ -444,9 +444,6 @@ impl ChainStore<Full> {
} }
read_store.write(batch)?; read_store.write(batch)?;
std::mem::drop(snapshot_cf_handle);
std::mem::drop(cf_handle);
std::mem::drop(read_store); std::mem::drop(read_store);
self.store.write().unwrap().drop_cf(&snaphost.cf_name)?; self.store.write().unwrap().drop_cf(&snaphost.cf_name)?;
@@ -461,7 +458,7 @@ impl ChainStore<Full> {
let read_store = self.store.read().unwrap(); let read_store = self.store.read().unwrap();
let cf_handle = read_store.cf_handle(&self.cf_name).unwrap(); let cf_handle = read_store.cf_handle(&self.cf_name).unwrap();
let key = StoreEntry::BlockHeaderIndex(Some(block_hash.clone())).get_key(); let key = StoreEntry::BlockHeaderIndex(Some(*block_hash)).get_key();
let data = read_store.get_pinned_cf(cf_handle, key)?; let data = read_store.get_pinned_cf(cf_handle, key)?;
Ok(data Ok(data
.map(|data| { .map(|data| {
@@ -642,7 +639,6 @@ impl<T: StoreType> ChainStore<T> {
); );
} }
std::mem::drop(cf_handle);
std::mem::drop(read_store); std::mem::drop(read_store);
self.store.write().unwrap().write(batch)?; self.store.write().unwrap().write(batch)?;

View File

@@ -204,9 +204,8 @@ impl CFSync {
if let BundleStatus::CFilters { cf_filters } = status { if let BundleStatus::CFilters { cf_filters } = status {
log::trace!("status: CFilters"); log::trace!("status: CFilters");
let last_sync_buried_height = (start_height + already_processed) let last_sync_buried_height =
.checked_sub(BURIED_CONFIRMATIONS) (start_height + already_processed).saturating_sub(BURIED_CONFIRMATIONS);
.unwrap_or(0);
for (filter_index, filter) in cf_filters.iter().enumerate() { for (filter_index, filter) in cf_filters.iter().enumerate() {
let height = filter_index + start_height; let height = filter_index + start_height;
@@ -280,10 +279,7 @@ where
match locators_map.get(&headers[0].prev_blockhash) { match locators_map.get(&headers[0].prev_blockhash) {
None => return Err(CompactFiltersError::InvalidHeaders), None => return Err(CompactFiltersError::InvalidHeaders),
Some(from) => ( Some(from) => (store.start_snapshot(*from)?, headers[0].prev_blockhash),
store.start_snapshot(*from)?,
headers[0].prev_blockhash.clone(),
),
} }
} else { } else {
return Err(CompactFiltersError::InvalidResponse); return Err(CompactFiltersError::InvalidResponse);

View File

@@ -168,7 +168,7 @@ pub struct ElectrumBlockchainConfig {
/// Request retry count /// Request retry count
pub retry: u8, pub retry: u8,
/// Request timeout (seconds) /// Request timeout (seconds)
pub timeout: u8, pub timeout: Option<u8>,
} }
impl ConfigurableBlockchain for ElectrumBlockchain { impl ConfigurableBlockchain for ElectrumBlockchain {
@@ -178,8 +178,8 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
let socks5 = config.socks5.as_ref().map(Socks5Config::new); let socks5 = config.socks5.as_ref().map(Socks5Config::new);
let electrum_config = ConfigBuilder::new() let electrum_config = ConfigBuilder::new()
.retry(config.retry) .retry(config.retry)
.socks5(socks5)?
.timeout(config.timeout)? .timeout(config.timeout)?
.socks5(socks5)?
.build(); .build();
Ok(ElectrumBlockchain(Client::from_config( Ok(ElectrumBlockchain(Client::from_config(

View File

@@ -79,29 +79,9 @@ pub enum Capability {
AccurateFees, AccurateFees,
} }
/// Marker trait for a blockchain backend
///
/// This is a marker trait for blockchain types. It is automatically implemented for types that
/// implement [`Blockchain`], so as a user of the library you won't have to implement this
/// manually.
///
/// Users of the library will probably never have to implement this trait manually, but they
/// could still need to import it to define types and structs with generics;
/// Implementing only the marker trait is pointless, since [`OfflineBlockchain`]
/// already does that, and whenever [`Blockchain`] is implemented, the marker trait is also
/// automatically implemented by the library.
pub trait BlockchainMarker {}
/// The [`BlockchainMarker`] marker trait is automatically implemented for [`Blockchain`] types
impl<T: Blockchain> BlockchainMarker for T {}
/// Type that only implements [`BlockchainMarker`] and is always "offline"
pub struct OfflineBlockchain;
impl BlockchainMarker for OfflineBlockchain {}
/// Trait that defines the actions that must be supported by a blockchain backend /// Trait that defines the actions that must be supported by a blockchain backend
#[maybe_async] #[maybe_async]
pub trait Blockchain: BlockchainMarker { pub trait Blockchain {
/// Return the set of [`Capability`] supported by this backend /// Return the set of [`Capability`] supported by this backend
fn get_capabilities(&self) -> HashSet<Capability>; fn get_capabilities(&self) -> HashSet<Capability>;
@@ -189,7 +169,7 @@ pub fn progress() -> (Sender<ProgressData>, Receiver<ProgressData>) {
impl Progress for Sender<ProgressData> { impl Progress for Sender<ProgressData> {
fn update(&self, progress: f32, message: Option<String>) -> Result<(), Error> { fn update(&self, progress: f32, message: Option<String>) -> Result<(), Error> {
if progress < 0.0 || progress > 100.0 { if !(0.0..=100.0).contains(&progress) {
return Err(Error::InvalidProgressValue(progress)); return Err(Error::InvalidProgressValue(progress));
} }

View File

@@ -1,750 +0,0 @@
// Magical Bitcoin Library
// Written in 2020 by
// Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020 Magical Bitcoin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Command line interface
//!
//! This module provides a [structopt](https://docs.rs/crate/structopt) `struct` and `enum` that
//! parse global wallet options and wallet subcommand options needed for a wallet command line
//! interface.
//!
//! See the `repl.rs` example for how to use this module to create a simple command line REPL
//! wallet application.
//!
//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands.
//!
//! # Example
//!
//! ```
//! # use bdk::bitcoin::Network;
//! # use bdk::blockchain::esplora::EsploraBlockchainConfig;
//! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain};
//! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig};
//! # use bdk::cli::{self, WalletOpt, WalletSubCommand};
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
//! # use bitcoin::hashes::core::str::FromStr;
//! # use std::sync::Arc;
//! # use structopt::StructOpt;
//!
//! // to get args from cli use:
//! // let cli_opt = WalletOpt::from_args();
//!
//! let cli_args = vec!["repl", "--network", "testnet", "--descriptor",
//! "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
//! "sync", "--max_addresses", "50"];
//! let cli_opt = WalletOpt::from_iter(&cli_args);
//!
//! let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
//!
//! let descriptor = cli_opt.descriptor.as_str();
//! let change_descriptor = cli_opt.change_descriptor.as_deref();
//!
//! let database = MemoryDatabase::new();
//!
//! let config = match cli_opt.esplora {
//! Some(base_url) => AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
//! base_url: base_url.to_string(),
//! concurrency: Some(cli_opt.esplora_concurrency),
//! }),
//! None => AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
//! url: cli_opt.electrum,
//! socks5: cli_opt.proxy,
//! retry: 3,
//! timeout: 5,
//! }),
//! };
//!
//! let wallet = Wallet::new(
//! descriptor,
//! change_descriptor,
//! network,
//! database,
//! AnyBlockchain::from_config(&config).unwrap(),
//! ).unwrap();
//!
//! let wallet = Arc::new(wallet);
//!
//! let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
//! println!("{}", serde_json::to_string_pretty(&result).unwrap());
//! ```
use std::collections::BTreeMap;
use std::str::FromStr;
use structopt::StructOpt;
#[allow(unused_imports)]
use log::{debug, error, info, trace, LevelFilter};
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
use bitcoin::hashes::hex::FromHex;
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin::{Address, OutPoint, Script, Txid};
use crate::blockchain::log_progress;
use crate::error::Error;
use crate::types::KeychainKind;
use crate::{FeeRate, TxBuilder, Wallet};
/// Wallet global options and sub-command
///
/// A [structopt](https://docs.rs/crate/structopt) `struct` that parses wallet global options and
/// sub-command from the command line or from a `String` vector. See [`WalletSubCommand`] for details
/// on parsing sub-commands.
///
/// # Example
///
/// ```
/// # use bdk::cli::{WalletOpt, WalletSubCommand};
/// # use structopt::StructOpt;
///
/// let cli_args = vec!["repl", "--network", "testnet",
/// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)",
/// "sync", "--max_addresses", "50"];
///
/// // to get WalletOpt from OS command line args use:
/// // let wallet_opt = WalletOpt::from_args();
///
/// let wallet_opt = WalletOpt::from_iter(&cli_args);
///
/// let expected_wallet_opt = WalletOpt {
/// network: "testnet".to_string(),
/// wallet: "main".to_string(),
/// proxy: None,
/// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(),
/// change_descriptor: None,
/// log_level: "info".to_string(),
/// #[cfg(feature = "esplora")]
/// esplora: None,
/// #[cfg(feature = "esplora")]
/// esplora_concurrency: 4,
/// electrum: "ssl://electrum.blockstream.info:60002".to_string(),
/// subcommand: WalletSubCommand::Sync {
/// max_addresses: Some(50)
/// },
/// };
///
/// assert_eq!(expected_wallet_opt, wallet_opt);
/// ```
#[derive(Debug, StructOpt, Clone, PartialEq)]
#[structopt(name = "BDK Wallet",
version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
pub struct WalletOpt {
/// Sets the network
#[structopt(
name = "NETWORK",
short = "n",
long = "network",
default_value = "testnet"
)]
pub network: String,
/// Selects the wallet to use
#[structopt(
name = "WALLET_NAME",
short = "w",
long = "wallet",
default_value = "main"
)]
pub wallet: String,
#[cfg(feature = "electrum")]
/// Sets the SOCKS5 proxy for the Electrum client
#[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")]
pub proxy: Option<String>,
/// Sets the descriptor to use for the external addresses
#[structopt(name = "DESCRIPTOR", short = "d", long = "descriptor", required = true)]
pub descriptor: String,
/// Sets the descriptor to use for internal addresses
#[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")]
pub change_descriptor: Option<String>,
/// Sets the logging level filter (off, error, warn, info, debug, trace)
#[structopt(long = "log_level", short = "l", default_value = "info")]
pub log_level: String,
#[cfg(feature = "esplora")]
/// Use the esplora server if given as parameter
#[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")]
pub esplora: Option<String>,
#[cfg(feature = "esplora")]
/// Concurrency of requests made to the esplora server
#[structopt(
name = "ESPLORA_CONCURRENCY",
long = "esplora_concurrency",
default_value = "4"
)]
pub esplora_concurrency: u8,
#[cfg(feature = "electrum")]
/// Sets the Electrum server to use
#[structopt(
name = "SERVER:PORT",
short = "s",
long = "server",
default_value = "ssl://electrum.blockstream.info:60002"
)]
pub electrum: String,
/// Wallet sub-command
#[structopt(subcommand)]
pub subcommand: WalletSubCommand,
}
/// Wallet sub-command
///
/// A [structopt](https://docs.rs/crate/structopt) enum that parses wallet sub-command arguments from
/// the command line or from a `String` vector, such as in the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
/// example app.
///
/// Additional "external" sub-commands can be captured via the [`WalletSubCommand::Other`] enum and passed to a
/// custom `structopt` or another parser. See [structopt "External subcommands"](https://docs.rs/structopt/0.3.21/structopt/index.html#external-subcommands)
/// for more information.
///
/// # Example
///
/// ```
/// # use bdk::cli::WalletSubCommand;
/// # use structopt::StructOpt;
///
/// let sync_sub_command = WalletSubCommand::from_iter(&["repl", "sync", "--max_addresses", "50"]);
/// assert!(matches!(
/// sync_sub_command,
/// WalletSubCommand::Sync {
/// max_addresses: Some(50)
/// }
/// ));
///
/// let other_sub_command = WalletSubCommand::from_iter(&["repl", "custom", "--param1", "20"]);
/// let external_args: Vec<String> = vec!["custom".to_string(), "--param1".to_string(), "20".to_string()];
/// assert!(matches!(
/// other_sub_command,
/// WalletSubCommand::Other(v) if v == external_args
/// ));
/// ```
///
/// To capture wallet sub-commands from a string vector without a preceeding binary name you can
/// create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand
/// enum. See also the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
/// example app.
///
/// # Example
/// ```
/// # use bdk::cli::WalletSubCommand;
/// # use structopt::StructOpt;
/// # use clap::AppSettings;
///
/// #[derive(Debug, StructOpt, Clone, PartialEq)]
/// #[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
/// struct ReplOpt {
/// /// Wallet sub-command
/// #[structopt(subcommand)]
/// pub subcommand: WalletSubCommand,
/// }
/// ```
#[derive(Debug, StructOpt, Clone, PartialEq)]
#[structopt(
rename_all = "snake",
long_about = "A modern, lightweight, descriptor-based wallet"
)]
pub enum WalletSubCommand {
/// Generates a new external address
GetNewAddress,
/// Syncs with the chosen blockchain server
Sync {
/// max addresses to consider
#[structopt(short = "v", long = "max_addresses")]
max_addresses: Option<u32>,
},
/// Lists the available spendable UTXOs
ListUnspent,
/// Lists all the incoming and outgoing transactions of the wallet
ListTransactions,
/// Returns the current wallet balance
GetBalance,
/// Creates a new unsigned transaction
CreateTx {
/// Adds a recipient to the transaction
#[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))]
recipients: Vec<(Script, u64)>,
/// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0
#[structopt(short = "all", long = "send_all")]
send_all: bool,
/// Enables Replace-By-Fee (BIP125)
#[structopt(short = "rbf", long = "enable_rbf")]
enable_rbf: bool,
/// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
#[structopt(long = "offline_signer")]
offline_signer: bool,
/// Selects which utxos *must* be spent
#[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
utxos: Option<Vec<OutPoint>>,
/// Marks a utxo as unspendable
#[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
unspendable: Option<Vec<OutPoint>>,
/// Fee rate to use in sat/vbyte
#[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
fee_rate: Option<f32>,
/// Selects which policy should be used to satisfy the external descriptor
#[structopt(name = "EXT_POLICY", long = "external_policy")]
external_policy: Option<String>,
/// Selects which policy should be used to satisfy the internal descriptor
#[structopt(name = "INT_POLICY", long = "internal_policy")]
internal_policy: Option<String>,
},
/// Bumps the fees of an RBF transaction
BumpFee {
/// TXID of the transaction to update
#[structopt(name = "TXID", short = "txid", long = "txid")]
txid: String,
/// Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`
#[structopt(short = "all", long = "send_all")]
send_all: bool,
/// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
#[structopt(long = "offline_signer")]
offline_signer: bool,
/// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used
#[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
utxos: Option<Vec<OutPoint>>,
/// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees
#[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
unspendable: Option<Vec<OutPoint>>,
/// The new targeted fee rate in sat/vbyte
#[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
fee_rate: f32,
},
/// Returns the available spending policies for the descriptor
Policies,
/// Returns the public version of the wallet's descriptor(s)
PublicDescriptor,
/// Signs and tries to finalize a PSBT
Sign {
/// Sets the PSBT to sign
#[structopt(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
/// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor
#[structopt(name = "HEIGHT", long = "assume_height")]
assume_height: Option<u32>,
},
/// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract
Broadcast {
/// Sets the PSBT to sign
#[structopt(
name = "BASE64_PSBT",
long = "psbt",
required_unless = "RAWTX",
conflicts_with = "RAWTX"
)]
psbt: Option<String>,
/// Sets the raw transaction to broadcast
#[structopt(
name = "RAWTX",
long = "tx",
required_unless = "BASE64_PSBT",
conflicts_with = "BASE64_PSBT"
)]
tx: Option<String>,
},
/// Extracts a raw transaction from a PSBT
ExtractPsbt {
/// Sets the PSBT to extract
#[structopt(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
},
/// Finalizes a PSBT
FinalizePsbt {
/// Sets the PSBT to finalize
#[structopt(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
/// Assume the blockchain has reached a specific height
#[structopt(name = "HEIGHT", long = "assume_height")]
assume_height: Option<u32>,
},
/// Combines multiple PSBTs into one
CombinePsbt {
/// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT
#[structopt(name = "BASE64_PSBT", long = "psbt", required = true)]
psbt: Vec<String>,
},
/// Put any extra arguments into this Vec
#[structopt(external_subcommand)]
Other(Vec<String>),
}
fn parse_recipient(s: &str) -> Result<(Script, u64), String> {
let parts: Vec<_> = s.split(':').collect();
if parts.len() != 2 {
return Err("Invalid format".to_string());
}
let addr = Address::from_str(&parts[0]);
if let Err(e) = addr {
return Err(format!("{:?}", e));
}
let val = u64::from_str(&parts[1]);
if let Err(e) = val {
return Err(format!("{:?}", e));
}
Ok((addr.unwrap().script_pubkey(), val.unwrap()))
}
fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
}
/// Execute a wallet sub-command with a given [`Wallet`].
///
/// Wallet sub-commands are described in [`WalletSubCommand`]. See [`super::cli`] for example usage.
#[maybe_async]
pub fn handle_wallet_subcommand<C, D>(
wallet: &Wallet<C, D>,
wallet_subcommand: WalletSubCommand,
) -> Result<serde_json::Value, Error>
where
C: crate::blockchain::Blockchain,
D: crate::database::BatchDatabase,
{
match wallet_subcommand {
WalletSubCommand::GetNewAddress => Ok(json!({"address": wallet.get_new_address()?})),
WalletSubCommand::Sync { max_addresses } => {
maybe_await!(wallet.sync(log_progress(), max_addresses))?;
Ok(json!({}))
}
WalletSubCommand::ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
WalletSubCommand::ListTransactions => {
Ok(serde_json::to_value(&wallet.list_transactions(false)?)?)
}
WalletSubCommand::GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})),
WalletSubCommand::CreateTx {
recipients,
send_all,
enable_rbf,
offline_signer,
utxos,
unspendable,
fee_rate,
external_policy,
internal_policy,
} => {
let mut tx_builder = TxBuilder::new();
if send_all {
tx_builder = tx_builder
.drain_wallet()
.set_single_recipient(recipients[0].0.clone());
} else {
tx_builder = tx_builder.set_recipients(recipients);
}
if enable_rbf {
tx_builder = tx_builder.enable_rbf();
}
if offline_signer {
tx_builder = tx_builder
.force_non_witness_utxo()
.include_output_redeem_witness_script();
}
if let Some(fee_rate) = fee_rate {
tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
}
if let Some(utxos) = utxos {
tx_builder = tx_builder.utxos(utxos).manually_selected_only();
}
if let Some(unspendable) = unspendable {
tx_builder = tx_builder.unspendable(unspendable);
}
let policies = vec![
external_policy.map(|p| (p, KeychainKind::External)),
internal_policy.map(|p| (p, KeychainKind::Internal)),
];
for (policy, keychain) in policies.into_iter().filter_map(|x| x) {
let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)
.map_err(|s| Error::Generic(s.to_string()))?;
tx_builder = tx_builder.policy_path(policy, keychain);
}
let (psbt, details) = wallet.create_tx(tx_builder)?;
Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
}
WalletSubCommand::BumpFee {
txid,
send_all,
offline_signer,
utxos,
unspendable,
fee_rate,
} => {
let txid = Txid::from_str(txid.as_str()).map_err(|s| Error::Generic(s.to_string()))?;
let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
if send_all {
tx_builder = tx_builder.maintain_single_recipient();
}
if offline_signer {
tx_builder = tx_builder
.force_non_witness_utxo()
.include_output_redeem_witness_script();
}
if let Some(utxos) = utxos {
tx_builder = tx_builder.utxos(utxos);
}
if let Some(unspendable) = unspendable {
tx_builder = tx_builder.unspendable(unspendable);
}
let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?;
Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
}
WalletSubCommand::Policies => Ok(json!({
"external": wallet.policies(KeychainKind::External)?,
"internal": wallet.policies(KeychainKind::Internal)?,
})),
WalletSubCommand::PublicDescriptor => Ok(json!({
"external": wallet.public_descriptor(KeychainKind::External)?.map(|d| d.to_string()),
"internal": wallet.public_descriptor(KeychainKind::Internal)?.map(|d| d.to_string()),
})),
WalletSubCommand::Sign {
psbt,
assume_height,
} => {
let psbt = base64::decode(&psbt).unwrap();
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
let (psbt, finalized) = wallet.sign(psbt, assume_height)?;
Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
}
WalletSubCommand::Broadcast { psbt, tx } => {
let tx = match (psbt, tx) {
(Some(psbt), None) => {
let psbt = base64::decode(&psbt).unwrap();
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
psbt.extract_tx()
}
(None, Some(tx)) => deserialize(&Vec::<u8>::from_hex(&tx).unwrap()).unwrap(),
(Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
(None, None) => panic!("Missing `psbt` and `tx` option"),
};
let txid = maybe_await!(wallet.broadcast(tx))?;
Ok(json!({ "txid": txid }))
}
WalletSubCommand::ExtractPsbt { psbt } => {
let psbt = base64::decode(&psbt).unwrap();
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),}))
}
WalletSubCommand::FinalizePsbt {
psbt,
assume_height,
} => {
let psbt = base64::decode(&psbt).unwrap();
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?;
Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
}
WalletSubCommand::CombinePsbt { psbt } => {
let mut psbts = psbt
.iter()
.map(|s| {
let psbt = base64::decode(&s).unwrap();
let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
psbt
})
.collect::<Vec<_>>();
let init_psbt = psbts.pop().unwrap();
let final_psbt = psbts
.into_iter()
.try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
init_psbt,
|mut acc, x| {
acc.merge(x)?;
Ok(acc)
},
)?;
Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) }))
}
WalletSubCommand::Other(_) => Ok(json!({})),
}
}
#[cfg(test)]
mod test {
use super::{WalletOpt, WalletSubCommand};
use bitcoin::hashes::core::str::FromStr;
use bitcoin::{Address, OutPoint};
use structopt::StructOpt;
#[test]
fn test_get_new_address() {
let cli_args = vec!["repl", "--network", "bitcoin",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--esplora", "https://blockstream.info/api/",
"--esplora_concurrency", "5",
"get_new_address"];
let wallet_opt = WalletOpt::from_iter(&cli_args);
let expected_wallet_opt = WalletOpt {
network: "bitcoin".to_string(),
wallet: "main".to_string(),
proxy: None,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
log_level: "info".to_string(),
#[cfg(feature = "esplora")]
esplora: Some("https://blockstream.info/api/".to_string()),
#[cfg(feature = "esplora")]
esplora_concurrency: 5,
electrum: "ssl://electrum.blockstream.info:60002".to_string(),
subcommand: WalletSubCommand::GetNewAddress,
};
assert_eq!(expected_wallet_opt, wallet_opt);
}
#[test]
fn test_sync() {
let cli_args = vec!["repl", "--network", "testnet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"sync", "--max_addresses", "50"];
let wallet_opt = WalletOpt::from_iter(&cli_args);
let expected_wallet_opt = WalletOpt {
network: "testnet".to_string(),
wallet: "main".to_string(),
proxy: None,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: None,
log_level: "info".to_string(),
#[cfg(feature = "esplora")]
esplora: None,
#[cfg(feature = "esplora")]
esplora_concurrency: 4,
electrum: "ssl://electrum.blockstream.info:60002".to_string(),
subcommand: WalletSubCommand::Sync {
max_addresses: Some(50)
},
};
assert_eq!(expected_wallet_opt, wallet_opt);
}
#[test]
fn test_create_tx() {
let cli_args = vec!["repl", "--network", "testnet", "--proxy", "127.0.0.1:9150",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--server","ssl://electrum.blockstream.info:50002",
"create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
"--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
"--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"];
let wallet_opt = WalletOpt::from_iter(&cli_args);
let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
.unwrap()
.script_pubkey();
let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
.unwrap()
.script_pubkey();
let outpoint1 = OutPoint::from_str(
"87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
)
.unwrap();
let outpoint2 = OutPoint::from_str(
"87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
)
.unwrap();
let expected_wallet_opt = WalletOpt {
network: "testnet".to_string(),
wallet: "main".to_string(),
proxy: Some("127.0.0.1:9150".to_string()),
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
log_level: "info".to_string(),
#[cfg(feature = "esplora")]
esplora: None,
#[cfg(feature = "esplora")]
esplora_concurrency: 4,
electrum: "ssl://electrum.blockstream.info:50002".to_string(),
subcommand: WalletSubCommand::CreateTx {
recipients: vec![(script1, 123456), (script2, 78910)],
send_all: false,
enable_rbf: false,
offline_signer: false,
utxos: Some(vec!(outpoint1, outpoint2)),
unspendable: None,
fee_rate: None,
external_policy: None,
internal_policy: None,
},
};
assert_eq!(expected_wallet_opt, wallet_opt);
}
#[test]
fn test_broadcast() {
let cli_args = vec!["repl", "--network", "testnet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"broadcast",
"--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
let wallet_opt = WalletOpt::from_iter(&cli_args);
let expected_wallet_opt = WalletOpt {
network: "testnet".to_string(),
wallet: "main".to_string(),
proxy: None,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: None,
log_level: "info".to_string(),
#[cfg(feature = "esplora")]
esplora: None,
#[cfg(feature = "esplora")]
esplora_concurrency: 4,
electrum: "ssl://electrum.blockstream.info:60002".to_string(),
subcommand: WalletSubCommand::Broadcast {
psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
tx: None
},
};
assert_eq!(expected_wallet_opt, wallet_opt);
}
}

View File

@@ -29,21 +29,19 @@
//! //!
//! ## Example //! ## Example
//! //!
//! In this example, `wallet_memory` and `wallet_sled` have the same type of `Wallet<OfflineBlockchain, AnyDatabase>`. //! In this example, `wallet_memory` and `wallet_sled` have the same type of `Wallet<(), AnyDatabase>`.
//! //!
//! ```no_run //! ```no_run
//! # use bitcoin::Network; //! # use bitcoin::Network;
//! # use bdk::database::{AnyDatabase, MemoryDatabase}; //! # use bdk::database::{AnyDatabase, MemoryDatabase};
//! # use bdk::{Wallet, OfflineWallet}; //! # use bdk::{Wallet};
//! let memory = MemoryDatabase::default().into(); //! let memory = MemoryDatabase::default();
//! let wallet_memory: OfflineWallet<AnyDatabase> = //! let wallet_memory = Wallet::new_offline("...", None, Network::Testnet, memory)?;
//! Wallet::new_offline("...", None, Network::Testnet, memory)?;
//! //!
//! # #[cfg(feature = "key-value-db")] //! # #[cfg(feature = "key-value-db")]
//! # { //! # {
//! let sled = sled::open("my-database")?.open_tree("default_tree")?.into(); //! let sled = sled::open("my-database")?.open_tree("default_tree")?;
//! let wallet_sled: OfflineWallet<AnyDatabase> = //! let wallet_sled = Wallet::new_offline("...", None, Network::Testnet, sled)?;
//! Wallet::new_offline("...", None, Network::Testnet, sled)?;
//! # } //! # }
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), bdk::Error>(())
//! ``` //! ```
@@ -54,10 +52,10 @@
//! ```no_run //! ```no_run
//! # use bitcoin::Network; //! # use bitcoin::Network;
//! # use bdk::database::*; //! # use bdk::database::*;
//! # use bdk::{Wallet, OfflineWallet}; //! # use bdk::{Wallet};
//! let config = serde_json::from_str("...")?; //! let config = serde_json::from_str("...")?;
//! let database = AnyDatabase::from_config(&config)?; //! let database = AnyDatabase::from_config(&config)?;
//! let wallet: OfflineWallet<_> = Wallet::new_offline("...", None, Network::Testnet, database)?; //! let wallet = Wallet::new_offline("...", None, Network::Testnet, database)?;
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), bdk::Error>(())
//! ``` //! ```

View File

@@ -29,7 +29,7 @@
use std::iter::FromIterator; use std::iter::FromIterator;
use crate::descriptor::Error; use crate::descriptor::DescriptorError;
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
@@ -57,14 +57,14 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
} }
/// Compute the checksum of a descriptor /// Compute the checksum of a descriptor
pub fn get_checksum(desc: &str) -> Result<String, Error> { pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
let mut c = 1; let mut c = 1;
let mut cls = 0; let mut cls = 0;
let mut clscount = 0; let mut clscount = 0;
for ch in desc.chars() { for ch in desc.chars() {
let pos = INPUT_CHARSET let pos = INPUT_CHARSET
.find(ch) .find(ch)
.ok_or(Error::InvalidDescriptorCharacter(ch))? as u64; .ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
c = poly_mod(c, pos & 31); c = poly_mod(c, pos & 31);
cls = cls * 3 + (pos >> 5); cls = cls * 3 + (pos >> 5);
clscount += 1; clscount += 1;
@@ -120,7 +120,7 @@ mod test {
assert!(matches!( assert!(matches!(
get_checksum(&invalid_desc).err(), get_checksum(&invalid_desc).err(),
Some(Error::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
)); ));
} }
} }

View File

@@ -60,6 +60,7 @@ macro_rules! impl_top_level_pk {
$key.to_descriptor_key() $key.to_descriptor_key()
.and_then(|key: DescriptorKey<$ctx>| key.extract(&secp)) .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp))
.map_err($crate::descriptor::DescriptorError::Key)
.map(|(pk, key_map, valid_networks)| { .map(|(pk, key_map, valid_networks)| {
( (
$crate::miniscript::Descriptor::< $crate::miniscript::Descriptor::<
@@ -75,11 +76,17 @@ macro_rules! impl_top_level_pk {
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! impl_leaf_opcode { macro_rules! impl_leaf_opcode {
( $terminal_variant:ident ) => { ( $terminal_variant:ident ) => {{
use $crate::descriptor::CheckMiniscript;
$crate::miniscript::Miniscript::from_ast( $crate::miniscript::Miniscript::from_ast(
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant, $crate::miniscript::miniscript::decode::Terminal::$terminal_variant,
) )
.map_err($crate::Error::Miniscript) .map_err($crate::descriptor::DescriptorError::Miniscript)
.and_then(|minisc| {
minisc.check_minsicript()?;
Ok(minisc)
})
.map(|minisc| { .map(|minisc| {
( (
minisc, minisc,
@@ -87,17 +94,23 @@ macro_rules! impl_leaf_opcode {
$crate::keys::any_network(), $crate::keys::any_network(),
) )
}) })
}; }};
} }
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! impl_leaf_opcode_value { macro_rules! impl_leaf_opcode_value {
( $terminal_variant:ident, $value:expr ) => { ( $terminal_variant:ident, $value:expr ) => {{
use $crate::descriptor::CheckMiniscript;
$crate::miniscript::Miniscript::from_ast( $crate::miniscript::Miniscript::from_ast(
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value), $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value),
) )
.map_err($crate::Error::Miniscript) .map_err($crate::descriptor::DescriptorError::Miniscript)
.and_then(|minisc| {
minisc.check_minsicript()?;
Ok(minisc)
})
.map(|minisc| { .map(|minisc| {
( (
minisc, minisc,
@@ -105,17 +118,23 @@ macro_rules! impl_leaf_opcode_value {
$crate::keys::any_network(), $crate::keys::any_network(),
) )
}) })
}; }};
} }
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! impl_leaf_opcode_value_two { macro_rules! impl_leaf_opcode_value_two {
( $terminal_variant:ident, $one:expr, $two:expr ) => { ( $terminal_variant:ident, $one:expr, $two:expr ) => {{
use $crate::descriptor::CheckMiniscript;
$crate::miniscript::Miniscript::from_ast( $crate::miniscript::Miniscript::from_ast(
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two), $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two),
) )
.map_err($crate::Error::Miniscript) .map_err($crate::descriptor::DescriptorError::Miniscript)
.and_then(|minisc| {
minisc.check_minsicript()?;
Ok(minisc)
})
.map(|minisc| { .map(|minisc| {
( (
minisc, minisc,
@@ -123,13 +142,15 @@ macro_rules! impl_leaf_opcode_value_two {
$crate::keys::any_network(), $crate::keys::any_network(),
) )
}) })
}; }};
} }
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! impl_node_opcode_two { macro_rules! impl_node_opcode_two {
( $terminal_variant:ident, $( $inner:tt )* ) => ({ ( $terminal_variant:ident, $( $inner:tt )* ) => ({
use $crate::descriptor::CheckMiniscript;
let inner = $crate::fragment_internal!( @t $( $inner )* ); let inner = $crate::fragment_internal!( @t $( $inner )* );
let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened(); let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened();
@@ -139,10 +160,14 @@ macro_rules! impl_node_opcode_two {
// join key_maps // join key_maps
a_keymap.extend(b_keymap.into_iter()); a_keymap.extend(b_keymap.into_iter());
Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
std::sync::Arc::new(a_minisc), std::sync::Arc::new(a_minisc),
std::sync::Arc::new(b_minisc), std::sync::Arc::new(b_minisc),
))?, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) ))?;
minisc.check_minsicript()?;
Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
}) })
}); });
} }
@@ -151,6 +176,8 @@ macro_rules! impl_node_opcode_two {
#[macro_export] #[macro_export]
macro_rules! impl_node_opcode_three { macro_rules! impl_node_opcode_three {
( $terminal_variant:ident, $( $inner:tt )* ) => { ( $terminal_variant:ident, $( $inner:tt )* ) => {
use $crate::descriptor::CheckMiniscript;
let inner = $crate::fragment_internal!( @t $( $inner )* ); let inner = $crate::fragment_internal!( @t $( $inner )* );
let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened(); let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened();
@@ -164,11 +191,15 @@ macro_rules! impl_node_opcode_three {
let networks = $crate::keys::merge_networks(&a_networks, &b_networks); let networks = $crate::keys::merge_networks(&a_networks, &b_networks);
let networks = $crate::keys::merge_networks(&networks, &c_networks); let networks = $crate::keys::merge_networks(&networks, &c_networks);
Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
std::sync::Arc::new(a_minisc), std::sync::Arc::new(a_minisc),
std::sync::Arc::new(b_minisc), std::sync::Arc::new(b_minisc),
std::sync::Arc::new(c_minisc), std::sync::Arc::new(c_minisc),
))?, a_keymap, networks)) ))?;
minisc.check_minsicript()?;
Ok((minisc, a_keymap, networks))
}) })
}; };
} }
@@ -190,6 +221,7 @@ macro_rules! impl_sortedmulti {
)* )*
keys.into_iter().collect::<Result<Vec<_>, _>>() keys.into_iter().collect::<Result<Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
.and_then(|keys| $crate::keys::make_sortedmulti_inner($thresh, keys, &secp)) .and_then(|keys| $crate::keys::make_sortedmulti_inner($thresh, keys, &secp))
}); });
@@ -199,18 +231,20 @@ macro_rules! impl_sortedmulti {
#[macro_export] #[macro_export]
macro_rules! apply_modifier { macro_rules! apply_modifier {
( $terminal_variant:ident, $inner:expr ) => {{ ( $terminal_variant:ident, $inner:expr ) => {{
use $crate::descriptor::CheckMiniscript;
$inner $inner
.map_err(|e| -> $crate::Error { e.into() }) .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() })
.and_then(|(minisc, keymap, networks)| { .and_then(|(minisc, keymap, networks)| {
Ok(( let minisc = $crate::miniscript::Miniscript::from_ast(
$crate::miniscript::Miniscript::from_ast( $crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant( std::sync::Arc::new(minisc),
std::sync::Arc::new(minisc), ),
), )?;
)?,
keymap, minisc.check_minsicript()?;
networks,
)) Ok((minisc, keymap, networks))
}) })
}}; }};
@@ -272,7 +306,7 @@ macro_rules! apply_modifier {
/// Macro to write full descriptors with code /// Macro to write full descriptors with code
/// ///
/// This macro expands to a `Result` of /// This macro expands to a `Result` of
/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`Error`](crate::Error) /// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError)
/// ///
/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers /// 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 /// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be
@@ -305,8 +339,11 @@ macro_rules! apply_modifier {
/// ///
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// let my_key_1 = bitcoin::PublicKey::from_str("02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c")?; /// let my_key_1 = bitcoin::PublicKey::from_str(
/// let my_key_2 = bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; /// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c",
/// )?;
/// let my_key_2 =
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
/// let my_timelock = 50; /// let my_timelock = 50;
/// ///
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! { /// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
@@ -315,12 +352,13 @@ macro_rules! apply_modifier {
/// ) /// )
/// }?; /// }?;
/// ///
/// #[rustfmt::skip]
/// let b_items = vec![ /// let b_items = vec![
/// bdk::fragment!(pk(my_key_1))?, /// bdk::fragment!(pk(my_key_1))?,
/// bdk::fragment!(s:pk(my_key_2))?, /// bdk::fragment!(s:pk(my_key_2))?,
/// bdk::fragment!(s:d:v:older(my_timelock))?, /// bdk::fragment!(s:d:v:older(my_timelock))?,
/// ]; /// ];
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2,b_items)))?; /// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
/// ///
/// assert_eq!(descriptor_a, descriptor_b); /// assert_eq!(descriptor_a, descriptor_b);
/// assert_eq!(key_map_a.len(), key_map_b.len()); /// assert_eq!(key_map_a.len(), key_map_b.len());
@@ -496,7 +534,7 @@ macro_rules! fragment_internal {
/// Macro to write descriptor fragments with code /// Macro to write descriptor fragments with code
/// ///
/// This macro will be expanded to an object of type `Result<(Miniscript<DescriptorPublicKey, _>, KeyMap, ValidNetworks), Error>`. It allows writing /// 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, ...))`. /// 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. /// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro.
@@ -599,6 +637,7 @@ macro_rules! fragment {
)* )*
keys.into_iter().collect::<Result<Vec<_>, _>>() keys.into_iter().collect::<Result<Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
.and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp)) .and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp))
}); });
@@ -620,8 +659,8 @@ mod test {
use std::str::FromStr; use std::str::FromStr;
use crate::descriptor::DescriptorMeta; use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::{DescriptorKey, KeyError, ToDescriptorKey, ValidNetworks}; use crate::keys::{DescriptorKey, ToDescriptorKey, ValidNetworks};
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Testnet}; use bitcoin::network::constants::Network::{Bitcoin, Regtest, Testnet};
use bitcoin::util::bip32; use bitcoin::util::bip32;
use bitcoin::util::bip32::ChildNumber; use bitcoin::util::bip32::ChildNumber;
@@ -631,7 +670,7 @@ mod test {
// verify descriptor generates expected script(s) (if bare or pk) or address(es) // verify descriptor generates expected script(s) (if bare or pk) or address(es)
fn check( fn check(
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError>, desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
is_witness: bool, is_witness: bool,
is_fixed: bool, is_fixed: bool,
expected: &[&str], expected: &[&str],
@@ -978,4 +1017,15 @@ mod test {
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))") assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))")
} }
// TODO: uncomment once https://github.com/rust-bitcoin/rust-miniscript/pull/221 is released
//
// #[test]
// #[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
// 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();
// }
} }

View File

@@ -27,26 +27,19 @@
/// Errors related to the parsing and usage of descriptors /// Errors related to the parsing and usage of descriptors
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
//InternalError,
//InvalidPrefix(Vec<u8>),
//HardenedDerivationOnXpub,
//MalformedInput,
/// Invalid HD Key path, such as having a wildcard but a length != 1 /// Invalid HD Key path, such as having a wildcard but a length != 1
InvalidHDKeyPath, InvalidHDKeyPath,
/// The provided descriptor doesn't match its checksum
InvalidDescriptorChecksum,
//KeyParsingError(String),
/// Error thrown while working with [`keys`](crate::keys) /// Error thrown while working with [`keys`](crate::keys)
Key(crate::keys::KeyError), Key(crate::keys::KeyError),
/// Error while extracting and manipulating policies /// Error while extracting and manipulating policies
Policy(crate::descriptor::policy::PolicyError), Policy(crate::descriptor::policy::PolicyError),
//InputIndexDoesntExist,
//MissingPublicKey,
//MissingDetails,
/// Invalid character found in the descriptor checksum /// Invalid character found in the descriptor checksum
InvalidDescriptorCharacter(char), InvalidDescriptorCharacter(char),
//CantDeriveWithMiniscript,
/// BIP32 error /// BIP32 error
BIP32(bitcoin::util::bip32::Error), BIP32(bitcoin::util::bip32::Error),
/// Error during base58 decoding /// Error during base58 decoding

View File

@@ -49,7 +49,7 @@ pub mod policy;
pub mod template; pub mod template;
pub use self::checksum::get_checksum; pub use self::checksum::get_checksum;
use self::error::Error; pub use self::error::Error as DescriptorError;
pub use self::policy::Policy; pub use self::policy::Policy;
use self::template::DescriptorTemplateOut; use self::template::DescriptorTemplateOut;
use crate::keys::{KeyError, ToDescriptorKey}; use crate::keys::{KeyError, ToDescriptorKey};
@@ -72,14 +72,14 @@ pub trait ToWalletDescriptor {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError>; ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>;
} }
impl ToWalletDescriptor for &str { impl ToWalletDescriptor for &str {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let descriptor = if self.contains('#') { let descriptor = if self.contains('#') {
let parts: Vec<&str> = self.splitn(2, '#').collect(); let parts: Vec<&str> = self.splitn(2, '#').collect();
if !get_checksum(parts[0]) if !get_checksum(parts[0])
@@ -87,7 +87,7 @@ impl ToWalletDescriptor for &str {
.map(|computed| computed == parts[1]) .map(|computed| computed == parts[1])
.unwrap_or(false) .unwrap_or(false)
{ {
return Err(KeyError::InvalidChecksum); return Err(DescriptorError::InvalidDescriptorChecksum);
} }
parts[0] parts[0]
@@ -103,7 +103,7 @@ impl ToWalletDescriptor for &String {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
self.as_str().to_wallet_descriptor(network) self.as_str().to_wallet_descriptor(network)
} }
} }
@@ -112,7 +112,7 @@ impl ToWalletDescriptor for ExtendedDescriptor {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
(self, KeyMap::default()).to_wallet_descriptor(network) (self, KeyMap::default()).to_wallet_descriptor(network)
} }
} }
@@ -121,7 +121,7 @@ impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap) {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
use crate::keys::DescriptorKey; use crate::keys::DescriptorKey;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
@@ -140,7 +140,7 @@ impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap) {
if networks.contains(&network) { if networks.contains(&network) {
Ok(pk) Ok(pk)
} else { } else {
Err(KeyError::InvalidNetwork) Err(DescriptorError::Key(KeyError::InvalidNetwork))
} }
}; };
@@ -155,7 +155,7 @@ impl ToWalletDescriptor for DescriptorTemplateOut {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let valid_networks = &self.2; let valid_networks = &self.2;
let fix_key = |pk: &DescriptorPublicKey| { let fix_key = |pk: &DescriptorPublicKey| {
@@ -177,7 +177,7 @@ impl ToWalletDescriptor for DescriptorTemplateOut {
Ok(pk) Ok(pk)
} else { } else {
Err(KeyError::InvalidNetwork) Err(DescriptorError::Key(KeyError::InvalidNetwork))
} }
}; };
@@ -188,6 +188,22 @@ impl ToWalletDescriptor for DescriptorTemplateOut {
} }
} }
#[doc(hidden)]
/// Used internally mainly by the `descriptor!()` and `fragment!()` macros
pub trait CheckMiniscript<Ctx: miniscript::ScriptContext> {
fn check_minsicript(&self) -> Result<(), miniscript::Error>;
}
impl<Ctx: miniscript::ScriptContext, Pk: miniscript::MiniscriptKey> CheckMiniscript<Ctx>
for miniscript::Miniscript<Pk, Ctx>
{
fn check_minsicript(&self) -> Result<(), miniscript::Error> {
Ctx::check_global_validity(self)?;
Ok(())
}
}
/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`] /// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`]
pub trait ExtractPolicy { pub trait ExtractPolicy {
/// Extract the spending [`policy`] /// Extract the spending [`policy`]
@@ -195,7 +211,7 @@ pub trait ExtractPolicy {
&self, &self,
signers: &SignersContainer, signers: &SignersContainer,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<Option<Policy>, Error>; ) -> Result<Option<Policy>, DescriptorError>;
} }
pub(crate) trait XKeyUtils { pub(crate) trait XKeyUtils {
@@ -235,8 +251,8 @@ impl<K: InnerXKey> XKeyUtils for DescriptorXKey<K> {
pub(crate) trait DescriptorMeta: Sized { pub(crate) trait DescriptorMeta: Sized {
fn is_witness(&self) -> bool; fn is_witness(&self) -> bool;
fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, Error>; fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError>;
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, Error>; fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
fn is_fixed(&self) -> bool; fn is_fixed(&self) -> bool;
fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option<Self>; fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option<Self>;
fn derive_from_psbt_input( fn derive_from_psbt_input(
@@ -297,11 +313,11 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
} }
} }
fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, Error> { fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError> {
let translate_key = |key: &DescriptorPublicKey, let translate_key = |key: &DescriptorPublicKey,
index: u32, index: u32,
paths: &mut HDKeyPaths| paths: &mut HDKeyPaths|
-> Result<DummyKey, Error> { -> Result<DummyKey, DescriptorError> {
match key { match key {
DescriptorPublicKey::SinglePub(_) => {} DescriptorPublicKey::SinglePub(_) => {}
DescriptorPublicKey::XPub(xpub) => { DescriptorPublicKey::XPub(xpub) => {
@@ -344,10 +360,10 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
Ok(answer_pk) Ok(answer_pk)
} }
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, Error> { fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
let get_key = |key: &DescriptorPublicKey, let get_key = |key: &DescriptorPublicKey,
keys: &mut Vec<DescriptorXKey<ExtendedPubKey>>| keys: &mut Vec<DescriptorXKey<ExtendedPubKey>>|
-> Result<DummyKey, Error> { -> Result<DummyKey, DescriptorError> {
if let DescriptorPublicKey::XPub(xpub) = key { if let DescriptorPublicKey::XPub(xpub) = key {
keys.push(xpub.clone()) keys.push(xpub.clone())
} }
@@ -369,7 +385,10 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
} }
fn is_fixed(&self) -> bool { fn is_fixed(&self) -> bool {
fn check_key(key: &DescriptorPublicKey, flag: &mut bool) -> Result<DummyKey, Error> { fn check_key(
key: &DescriptorPublicKey,
flag: &mut bool,
) -> Result<DummyKey, DescriptorError> {
match key { match key {
DescriptorPublicKey::SinglePub(_) => {} DescriptorPublicKey::SinglePub(_) => {}
DescriptorPublicKey::XPub(xpub) => { DescriptorPublicKey::XPub(xpub) => {
@@ -398,7 +417,7 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
let try_key = |key: &DescriptorPublicKey, let try_key = |key: &DescriptorPublicKey,
index: &HashMap<Fingerprint, DerivationPath>, index: &HashMap<Fingerprint, DerivationPath>,
found_path: &mut Option<ChildNumber>| found_path: &mut Option<ChildNumber>|
-> Result<DummyKey, Error> { -> Result<DummyKey, DescriptorError> {
if found_path.is_some() { if found_path.is_some() {
// already found a matching path, we are done // already found a matching path, we are done
return Ok(DummyKey::default()); return Ok(DummyKey::default());
@@ -432,7 +451,7 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
Some(path) if !xpub.is_wildcard && path.is_empty() => { Some(path) if !xpub.is_wildcard && path.is_empty() => {
*found_path = Some(ChildNumber::Normal { index: 0 }) *found_path = Some(ChildNumber::Normal { index: 0 })
} }
Some(_) => return Err(Error::InvalidHDKeyPath), Some(_) => return Err(DescriptorError::InvalidHDKeyPath),
_ => {} _ => {}
} }
} }
@@ -713,11 +732,17 @@ mod test {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
.to_wallet_descriptor(Network::Testnet); .to_wallet_descriptor(Network::Testnet);
assert!(matches!(desc.err(), Some(KeyError::InvalidChecksum))); assert!(matches!(
desc.err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
.to_wallet_descriptor(Network::Testnet); .to_wallet_descriptor(Network::Testnet);
assert!(matches!(desc.err(), Some(KeyError::InvalidChecksum))); assert!(matches!(
desc.err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
} }
// test ToWalletDescriptor trait from &str with keys from right and wrong network // test ToWalletDescriptor trait from &str with keys from right and wrong network
@@ -749,11 +774,17 @@ mod test {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
.to_wallet_descriptor(Network::Bitcoin); .to_wallet_descriptor(Network::Bitcoin);
assert!(matches!(desc.err(), Some(KeyError::InvalidNetwork))); assert!(matches!(
desc.err(),
Some(DescriptorError::Key(KeyError::InvalidNetwork))
));
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
.to_wallet_descriptor(Network::Bitcoin); .to_wallet_descriptor(Network::Bitcoin);
assert!(matches!(desc.err(), Some(KeyError::InvalidNetwork))); assert!(matches!(
desc.err(),
Some(DescriptorError::Key(KeyError::InvalidNetwork))
));
} }
// test ToWalletDescriptor trait from the output of the descriptor!() macro // test ToWalletDescriptor trait from the output of the descriptor!() macro

View File

@@ -165,11 +165,13 @@ pub enum SatisfiableItem {
impl SatisfiableItem { impl SatisfiableItem {
/// Returns whether the [`SatisfiableItem`] is a leaf item /// Returns whether the [`SatisfiableItem`] is a leaf item
pub fn is_leaf(&self) -> bool { pub fn is_leaf(&self) -> bool {
!matches!(self, !matches!(
SatisfiableItem::Thresh { self,
items: _, SatisfiableItem::Thresh {
threshold: _, items: _,
}) threshold: _,
}
)
} }
/// Returns a unique id for the [`SatisfiableItem`] /// Returns a unique id for the [`SatisfiableItem`]
@@ -1143,11 +1145,12 @@ mod test {
let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR); let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR);
let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR); let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR);
let sequence = 50; let sequence = 50;
#[rustfmt::skip]
let desc = descriptor!(wsh(thresh( let desc = descriptor!(wsh(thresh(
2, 2,
pk(prvkey0), pk(prvkey0),
s: pk(pubkey1), s:pk(pubkey1),
s: d: v: older(sequence) s:d:v:older(sequence)
))) )))
.unwrap(); .unwrap();

View File

@@ -33,7 +33,8 @@ use bitcoin::Network;
use miniscript::{Legacy, Segwitv0}; use miniscript::{Legacy, Segwitv0};
use super::{ExtendedDescriptor, KeyMap, ToWalletDescriptor}; use super::{ExtendedDescriptor, KeyMap, ToWalletDescriptor};
use crate::keys::{DerivableKey, KeyError, ToDescriptorKey, ValidNetworks}; use crate::descriptor::DescriptorError;
use crate::keys::{DerivableKey, ToDescriptorKey, ValidNetworks};
use crate::{descriptor, KeychainKind}; use crate::{descriptor, KeychainKind};
/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others /// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others
@@ -47,6 +48,7 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// ## Example /// ## Example
/// ///
/// ``` /// ```
/// use bdk::descriptor::error::Error as DescriptorError;
/// use bdk::keys::{KeyError, ToDescriptorKey}; /// use bdk::keys::{KeyError, ToDescriptorKey};
/// use bdk::miniscript::Legacy; /// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut}; /// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
@@ -54,14 +56,14 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// struct MyP2PKH<K: ToDescriptorKey<Legacy>>(K); /// struct MyP2PKH<K: ToDescriptorKey<Legacy>>(K);
/// ///
/// impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> { /// impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
/// fn build(self) -> Result<DescriptorTemplateOut, KeyError> { /// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
/// Ok(bdk::descriptor!(pkh(self.0))?) /// Ok(bdk::descriptor!(pkh(self.0))?)
/// } /// }
/// } /// }
/// ``` /// ```
pub trait DescriptorTemplate { pub trait DescriptorTemplate {
/// Build the complete descriptor /// Build the complete descriptor
fn build(self) -> Result<DescriptorTemplateOut, KeyError>; fn build(self) -> Result<DescriptorTemplateOut, DescriptorError>;
} }
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its /// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
@@ -70,7 +72,7 @@ impl<T: DescriptorTemplate> ToWalletDescriptor for T {
fn to_wallet_descriptor( fn to_wallet_descriptor(
self, self,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
Ok(self.build()?.to_wallet_descriptor(network)?) Ok(self.build()?.to_wallet_descriptor(network)?)
} }
} }
@@ -81,13 +83,13 @@ impl<T: DescriptorTemplate> ToWalletDescriptor for T {
/// ///
/// ``` /// ```
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet}; /// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::P2PKH; /// use bdk::template::P2PKH;
/// ///
/// let key = /// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// P2PKH(key), /// P2PKH(key),
/// None, /// None,
/// Network::Testnet, /// Network::Testnet,
@@ -103,7 +105,7 @@ impl<T: DescriptorTemplate> ToWalletDescriptor for T {
pub struct P2PKH<K: ToDescriptorKey<Legacy>>(pub K); pub struct P2PKH<K: ToDescriptorKey<Legacy>>(pub K);
impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> { impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(descriptor!(pkh(self.0))?) Ok(descriptor!(pkh(self.0))?)
} }
} }
@@ -114,13 +116,13 @@ impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
/// ///
/// ``` /// ```
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet}; /// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::P2WPKH_P2SH; /// use bdk::template::P2WPKH_P2SH;
/// ///
/// let key = /// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// P2WPKH_P2SH(key), /// P2WPKH_P2SH(key),
/// None, /// None,
/// Network::Testnet, /// Network::Testnet,
@@ -137,7 +139,7 @@ impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
pub struct P2WPKH_P2SH<K: ToDescriptorKey<Segwitv0>>(pub K); pub struct P2WPKH_P2SH<K: ToDescriptorKey<Segwitv0>>(pub K);
impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> { impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(descriptor!(sh(wpkh(self.0)))?) Ok(descriptor!(sh(wpkh(self.0)))?)
} }
} }
@@ -148,13 +150,13 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
/// ///
/// ``` /// ```
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet}; /// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::P2WPKH; /// use bdk::template::P2WPKH;
/// ///
/// let key = /// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// P2WPKH(key), /// P2WPKH(key),
/// None, /// None,
/// Network::Testnet, /// Network::Testnet,
@@ -170,7 +172,7 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
pub struct P2WPKH<K: ToDescriptorKey<Segwitv0>>(pub K); pub struct P2WPKH<K: ToDescriptorKey<Segwitv0>>(pub K);
impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> { impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(descriptor!(wpkh(self.0))?) Ok(descriptor!(wpkh(self.0))?)
} }
} }
@@ -186,12 +188,12 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP44; /// use bdk::template::BIP44;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP44(key.clone(), KeychainKind::External), /// BIP44(key.clone(), KeychainKind::External),
/// Some(BIP44(key, KeychainKind::Internal)), /// Some(BIP44(key, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -205,7 +207,7 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
pub struct BIP44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind); pub struct BIP44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> { impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2PKH(legacy::make_bipxx_private(44, self.0, self.1)?).build()?) Ok(P2PKH(legacy::make_bipxx_private(44, self.0, self.1)?).build()?)
} }
} }
@@ -224,13 +226,13 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP44Public; /// use bdk::template::BIP44Public;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; /// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; /// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP44Public(key.clone(), fingerprint, KeychainKind::External), /// BIP44Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(BIP44Public(key, fingerprint, KeychainKind::Internal)), /// Some(BIP44Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -244,7 +246,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
pub struct BIP44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind); pub struct BIP44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> { impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2PKH(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()?) Ok(P2PKH(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()?)
} }
} }
@@ -260,12 +262,12 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP49; /// use bdk::template::BIP49;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP49(key.clone(), KeychainKind::External), /// BIP49(key.clone(), KeychainKind::External),
/// Some(BIP49(key, KeychainKind::Internal)), /// Some(BIP49(key, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -279,7 +281,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
pub struct BIP49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind); pub struct BIP49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> { impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()?) Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()?)
} }
} }
@@ -298,13 +300,13 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP49Public; /// use bdk::template::BIP49Public;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; /// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; /// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP49Public(key.clone(), fingerprint, KeychainKind::External), /// BIP49Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(BIP49Public(key, fingerprint, KeychainKind::Internal)), /// Some(BIP49Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -318,7 +320,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
pub struct BIP49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind); pub struct BIP49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> { impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()?) Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()?)
} }
} }
@@ -334,12 +336,12 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP84; /// use bdk::template::BIP84;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP84(key.clone(), KeychainKind::External), /// BIP84(key.clone(), KeychainKind::External),
/// Some(BIP84(key, KeychainKind::Internal)), /// Some(BIP84(key, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -353,7 +355,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
pub struct BIP84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind); pub struct BIP84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> { impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2WPKH(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()?) Ok(P2WPKH(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()?)
} }
} }
@@ -372,13 +374,13 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network}; /// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, OfflineWallet, KeychainKind}; /// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase; /// # use bdk::database::MemoryDatabase;
/// use bdk::template::BIP84Public; /// use bdk::template::BIP84Public;
/// ///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; /// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?; /// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet: OfflineWallet<_> = Wallet::new_offline( /// let wallet = Wallet::new_offline(
/// BIP84Public(key.clone(), fingerprint, KeychainKind::External), /// BIP84Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(BIP84Public(key, fingerprint, KeychainKind::Internal)), /// Some(BIP84Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet, /// Network::Testnet,
@@ -392,7 +394,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
pub struct BIP84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind); pub struct BIP84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84Public<K> { impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, KeyError> { fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
Ok(P2WPKH(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()?) Ok(P2WPKH(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()?)
} }
} }
@@ -406,7 +408,7 @@ macro_rules! expand_make_bipxx {
bip: u32, bip: u32,
key: K, key: K,
keychain: KeychainKind, keychain: KeychainKind,
) -> Result<impl ToDescriptorKey<$ctx>, KeyError> { ) -> Result<impl ToDescriptorKey<$ctx>, DescriptorError> {
let mut derivation_path = Vec::with_capacity(4); let mut derivation_path = Vec::with_capacity(4);
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?); derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
@@ -430,7 +432,7 @@ macro_rules! expand_make_bipxx {
key: K, key: K,
parent_fingerprint: bip32::Fingerprint, parent_fingerprint: bip32::Fingerprint,
keychain: KeychainKind, keychain: KeychainKind,
) -> Result<impl ToDescriptorKey<$ctx>, KeyError> { ) -> Result<impl ToDescriptorKey<$ctx>, DescriptorError> {
let derivation_path: bip32::DerivationPath = match keychain { let derivation_path: bip32::DerivationPath = match keychain {
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(), KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(), KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(),
@@ -456,8 +458,8 @@ mod test {
// test existing descriptor templates, make sure they are expanded to the right descriptors // test existing descriptor templates, make sure they are expanded to the right descriptors
use super::*; use super::*;
use crate::descriptor::DescriptorMeta; use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::{KeyError, ValidNetworks}; use crate::keys::ValidNetworks;
use bitcoin::hashes::core::str::FromStr; use bitcoin::hashes::core::str::FromStr;
use bitcoin::network::constants::Network::Regtest; use bitcoin::network::constants::Network::Regtest;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
@@ -467,7 +469,7 @@ mod test {
// verify template descriptor generates expected address(es) // verify template descriptor generates expected address(es)
fn check( fn check(
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError>, desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
is_witness: bool, is_witness: bool,
is_fixed: bool, is_fixed: bool,
expected: &[&str], expected: &[&str],

View File

@@ -47,7 +47,12 @@ pub enum Error {
/// Output created is under the dust limit, 546 satoshis /// Output created is under the dust limit, 546 satoshis
OutputBelowDustLimit(usize), OutputBelowDustLimit(usize),
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee /// Wallet's UTXO set is not enough to cover recipient's requested plus fee
InsufficientFunds, InsufficientFunds {
/// Sats needed for some transaction
needed: u64,
/// Sats available for spending
available: u64,
},
/// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow /// 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 /// exponentially, thus a limit is set, and when hit, this error is thrown
BnBTotalTriesExceeded, BnBTotalTriesExceeded,
@@ -89,11 +94,6 @@ pub enum Error {
/// Signing error /// Signing error
Signer(crate::wallet::signer::SignerError), Signer(crate::wallet::signer::SignerError),
// Blockchain interface errors
/// Thrown when trying to call a method that requires a network connection, [`Wallet::sync`](crate::Wallet::sync) and [`Wallet::broadcast`](crate::Wallet::broadcast)
/// This error is thrown when creating the Client for the first time, while recovery attempts are tried
/// during the sync
OfflineClient,
/// Progress value must be between `0.0` (included) and `100.0` (included) /// Progress value must be between `0.0` (included) and `100.0` (included)
InvalidProgressValue(f32), InvalidProgressValue(f32),
/// Progress update error (maybe the channel has been closed) /// Progress update error (maybe the channel has been closed)
@@ -198,7 +198,7 @@ impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
fn from(other: crate::blockchain::compact_filters::CompactFiltersError) -> Self { fn from(other: crate::blockchain::compact_filters::CompactFiltersError) -> Self {
match other { match other {
crate::blockchain::compact_filters::CompactFiltersError::Global(e) => *e, crate::blockchain::compact_filters::CompactFiltersError::Global(e) => *e,
err @ _ => Error::CompactFilters(err), err => Error::CompactFilters(err),
} }
} }
} }

View File

@@ -43,6 +43,7 @@ use miniscript::descriptor::{DescriptorXKey, KeyMap};
pub use miniscript::ScriptContext; pub use miniscript::ScriptContext;
use miniscript::{Miniscript, Terminal}; use miniscript::{Miniscript, Terminal};
use crate::descriptor::{CheckMiniscript, DescriptorError};
use crate::wallet::utils::SecpCtx; use crate::wallet::utils::SecpCtx;
#[cfg(feature = "keys-bip39")] #[cfg(feature = "keys-bip39")]
@@ -420,7 +421,7 @@ where
/// [`ToDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also /// [`ToDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also
/// [`ToDescriptorKey`]. /// [`ToDescriptorKey`].
pub trait GeneratableKey<Ctx: ScriptContext>: Sized { pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
/// Type specifying the amount of entropy required e.g. [u8;32] /// Type specifying the amount of entropy required e.g. `[u8;32]`
type Entropy: AsMut<[u8]> + Default; type Entropy: AsMut<[u8]> + Default;
/// Extra options required by the `generate_with_entropy` /// Extra options required by the `generate_with_entropy`
@@ -572,14 +573,13 @@ fn expand_multi_keys<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
pub fn make_pk<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>( pub fn make_pk<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
descriptor_key: Pk, descriptor_key: Pk,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), KeyError> { ) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
let (key, key_map, valid_networks) = descriptor_key.to_descriptor_key()?.extract(secp)?; let (key, key_map, valid_networks) = descriptor_key.to_descriptor_key()?.extract(secp)?;
let minisc = Miniscript::from_ast(Terminal::PkK(key))?;
Ok(( minisc.check_minsicript()?;
Miniscript::from_ast(Terminal::PkK(key))?,
key_map, Ok((minisc, key_map, valid_networks))
valid_networks,
))
} }
// Used internally by `bdk::fragment!` to build `multi()` fragments // Used internally by `bdk::fragment!` to build `multi()` fragments
@@ -588,14 +588,13 @@ pub fn make_multi<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
thresh: usize, thresh: usize,
pks: Vec<Pk>, pks: Vec<Pk>,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), KeyError> { ) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
let minisc = Miniscript::from_ast(Terminal::Multi(thresh, pks))?;
Ok(( minisc.check_minsicript()?;
Miniscript::from_ast(Terminal::Multi(thresh, pks))?,
key_map, Ok((minisc, key_map, valid_networks))
valid_networks,
))
} }
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments // Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
@@ -610,11 +609,14 @@ pub fn make_sortedmulti_inner<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
KeyMap, KeyMap,
ValidNetworks, ValidNetworks,
), ),
KeyError, DescriptorError,
> { > {
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
let minisc = SortedMultiVec::new(thresh, pks)?;
Ok((SortedMultiVec::new(thresh, pks)?, key_map, valid_networks)) // TODO: should we apply the checks here as well?
Ok((minisc, key_map, valid_networks))
} }
/// The "identity" conversion is used internally by some `bdk::fragment`s /// The "identity" conversion is used internally by some `bdk::fragment`s

View File

@@ -91,11 +91,11 @@
//! //!
//! ### Example //! ### Example
//! ``` //! ```
//! use bdk::{Wallet, OfflineWallet}; //! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase; //! use bdk::database::MemoryDatabase;
//! //!
//! fn main() -> Result<(), bdk::Error> { //! fn main() -> Result<(), bdk::Error> {
//! let wallet: OfflineWallet<_> = Wallet::new_offline( //! let wallet = Wallet::new_offline(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", //! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), //! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
//! bitcoin::Network::Testnet, //! bitcoin::Network::Testnet,
@@ -155,13 +155,13 @@
//! ### Example //! ### Example
//! ```ignore //! ```ignore
//! use base64::decode; //! use base64::decode;
//! use bdk::{Wallet, OfflineWallet}; //! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase; //! use bdk::database::MemoryDatabase;
//! //!
//! use bitcoin::consensus::deserialize; //! use bitcoin::consensus::deserialize;
//! //!
//! fn main() -> Result<(), bdk::Error> { //! fn main() -> Result<(), bdk::Error> {
//! let wallet: OfflineWallet<_> = Wallet::new_offline( //! let wallet = Wallet::new_offline(
//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", //! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), //! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
//! bitcoin::Network::Testnet, //! bitcoin::Network::Testnet,
@@ -191,7 +191,6 @@
//! //!
//! * `all-keys`: all features for working with bitcoin keys //! * `all-keys`: all features for working with bitcoin keys
//! * `async-interface`: async functions in bdk traits //! * `async-interface`: async functions in bdk traits
//! * `cli-utils`: utilities for creating a command line interface wallet
//! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys //! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys
//! //!
//! ## Internal features //! ## Internal features
@@ -233,9 +232,6 @@ pub extern crate reqwest;
#[cfg(feature = "key-value-db")] #[cfg(feature = "key-value-db")]
pub extern crate sled; pub extern crate sled;
#[cfg(feature = "cli-utils")]
pub mod cli;
#[allow(unused_imports)] #[allow(unused_imports)]
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
@@ -268,4 +264,4 @@ pub use types::*;
pub use wallet::address_validator; pub use wallet::address_validator;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::tx_builder::TxBuilder; pub use wallet::tx_builder::TxBuilder;
pub use wallet::{OfflineWallet, Wallet}; pub use wallet::Wallet;

View File

@@ -66,7 +66,7 @@
//! } //! }
//! //!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; //! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue)); //! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
//! //!
//! let address = wallet.get_new_address()?; //! let address = wallet.get_new_address()?;

View File

@@ -71,9 +71,9 @@
//! }) //! })
//! .collect::<Vec<_>>(); //! .collect::<Vec<_>>();
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0; //! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
//! //! let amount_needed_with_fees = (fee_amount + additional_fees).ceil() as u64 + amount_needed;
//! if (fee_amount + additional_fees).ceil() as u64 + amount_needed > selected_amount { //! if amount_needed_with_fees > selected_amount {
//! return Err(bdk::Error::InsufficientFunds); //! return Err(bdk::Error::InsufficientFunds{ needed: amount_needed_with_fees, available: selected_amount });
//! } //! }
//! //!
//! Ok(CoinSelectionResult { //! Ok(CoinSelectionResult {
@@ -84,7 +84,7 @@
//! } //! }
//! } //! }
//! //!
//! # let wallet: OfflineWallet<_> = Wallet::new_offline("", None, Network::Testnet, bdk::database::MemoryDatabase::default())?; //! # let wallet = Wallet::new_offline("", None, Network::Testnet, bdk::database::MemoryDatabase::default())?;
//! // create wallet, sync, ... //! // create wallet, sync, ...
//! //!
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); //! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
@@ -221,8 +221,12 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
) )
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if selected_amount < amount_needed + (fee_amount.ceil() as u64) { let amount_needed_with_fees = amount_needed + (fee_amount.ceil() as u64);
return Err(Error::InsufficientFunds); if selected_amount < amount_needed_with_fees {
return Err(Error::InsufficientFunds {
needed: amount_needed_with_fees,
available: selected_amount,
});
} }
Ok(CoinSelectionResult { Ok(CoinSelectionResult {
@@ -321,7 +325,10 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb(); let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
if curr_available_value + curr_value < actual_target { if curr_available_value + curr_value < actual_target {
return Err(Error::InsufficientFunds); return Err(Error::InsufficientFunds {
needed: actual_target,
available: curr_available_value + curr_value,
});
} }
Ok(self Ok(self

View File

@@ -43,7 +43,7 @@
//! }"#; //! }"#;
//! //!
//! let import = WalletExport::from_str(import)?; //! let import = WalletExport::from_str(import)?;
//! let wallet: OfflineWallet<_> = Wallet::new_offline( //! let wallet = Wallet::new_offline(
//! &import.descriptor(), //! &import.descriptor(),
//! import.change_descriptor().as_ref(), //! import.change_descriptor().as_ref(),
//! Network::Testnet, //! Network::Testnet,
@@ -58,7 +58,7 @@
//! # use bdk::database::*; //! # use bdk::database::*;
//! # use bdk::wallet::export::*; //! # use bdk::wallet::export::*;
//! # use bdk::*; //! # use bdk::*;
//! let wallet: OfflineWallet<_> = Wallet::new_offline( //! let wallet = Wallet::new_offline(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", //! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), //! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
//! Network::Testnet, //! Network::Testnet,
@@ -78,7 +78,6 @@ use serde::{Deserialize, Serialize};
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal}; use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
use crate::blockchain::BlockchainMarker;
use crate::database::BatchDatabase; use crate::database::BatchDatabase;
use crate::wallet::Wallet; use crate::wallet::Wallet;
@@ -120,7 +119,7 @@ impl WalletExport {
/// ///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field /// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`. /// returned will be `0`.
pub fn export_wallet<B: BlockchainMarker, D: BatchDatabase>( pub fn export_wallet<B, D: BatchDatabase>(
wallet: &Wallet<B, D>, wallet: &Wallet<B, D>,
label: &str, label: &str,
include_blockheight: bool, include_blockheight: bool,
@@ -208,7 +207,7 @@ mod test {
use super::*; use super::*;
use crate::database::{memory::MemoryDatabase, BatchOperations}; use crate::database::{memory::MemoryDatabase, BatchOperations};
use crate::types::TransactionDetails; use crate::types::TransactionDetails;
use crate::wallet::{OfflineWallet, Wallet}; use crate::wallet::Wallet;
fn get_test_db() -> MemoryDatabase { fn get_test_db() -> MemoryDatabase {
let mut db = MemoryDatabase::new(); let mut db = MemoryDatabase::new();
@@ -234,7 +233,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
descriptor, descriptor,
Some(change_descriptor), Some(change_descriptor),
Network::Bitcoin, Network::Bitcoin,
@@ -258,7 +257,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let wallet: OfflineWallet<_> = let wallet =
Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap(); Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap(); WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
} }
@@ -272,7 +271,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)"; let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
descriptor, descriptor,
Some(change_descriptor), Some(change_descriptor),
Network::Bitcoin, Network::Bitcoin,
@@ -295,7 +294,7 @@ mod test {
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\ [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))"; ))";
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
descriptor, descriptor,
Some(change_descriptor), Some(change_descriptor),
Network::Testnet, Network::Testnet,
@@ -315,7 +314,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
descriptor, descriptor,
Some(change_descriptor), Some(change_descriptor),
Network::Bitcoin, Network::Bitcoin,

View File

@@ -59,9 +59,12 @@ pub use utils::IsDust;
use address_validator::AddressValidator; use address_validator::AddressValidator;
use signer::{Signer, SignerId, SignerOrdering, SignersContainer}; use signer::{Signer, SignerId, SignerOrdering, SignersContainer};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxBuilderContext}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxBuilderContext};
use utils::{check_nlocktime, check_nsequence_rbf, descriptor_to_pk_ctx, After, Older, SecpCtx}; use utils::{
check_nlocktime, check_nsequence_rbf, descriptor_to_pk_ctx, After, Older, SecpCtx,
DUST_LIMIT_SATOSHI,
};
use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress}; use crate::blockchain::{Blockchain, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::descriptor::{ use crate::descriptor::{
get_checksum, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, Policy, get_checksum, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, Policy,
@@ -73,9 +76,6 @@ use crate::types::*;
const CACHE_ADDR_BATCH_SIZE: u32 = 100; const CACHE_ADDR_BATCH_SIZE: u32 = 100;
/// Type alias for a [`Wallet`] that uses [`OfflineBlockchain`]
pub type OfflineWallet<D> = Wallet<OfflineBlockchain, D>;
/// A Bitcoin wallet /// A Bitcoin wallet
/// ///
/// A wallet takes descriptors, a [`database`](trait@crate::database::Database) and a /// A wallet takes descriptors, a [`database`](trait@crate::database::Database) and a
@@ -84,7 +84,7 @@ pub type OfflineWallet<D> = Wallet<OfflineBlockchain, D>;
/// [creating transactions](Wallet::create_tx), etc. /// [creating transactions](Wallet::create_tx), etc.
/// ///
/// A wallet can be either "online" if the [`blockchain`](crate::blockchain) type provided /// A wallet can be either "online" if the [`blockchain`](crate::blockchain) type provided
/// implements [`Blockchain`], or "offline" [`OfflineBlockchain`] is used. Offline wallets only expose /// implements [`Blockchain`], or "offline" if it is the unit type `()`. Offline wallets only expose
/// methods that don't need any interaction with the blockchain to work. /// methods that don't need any interaction with the blockchain to work.
pub struct Wallet<B, D> { pub struct Wallet<B, D> {
descriptor: ExtendedDescriptor, descriptor: ExtendedDescriptor,
@@ -99,24 +99,38 @@ pub struct Wallet<B, D> {
current_height: Option<u32>, current_height: Option<u32>,
client: Option<B>, client: B,
database: RefCell<D>, database: RefCell<D>,
secp: SecpCtx, secp: SecpCtx,
} }
// offline actions, always available impl<D> Wallet<(), D>
impl<B, D> Wallet<B, D>
where where
B: BlockchainMarker,
D: BatchDatabase, D: BatchDatabase,
{ {
/// Create a new "offline" wallet /// Create a new "offline" wallet
pub fn new_offline<E: ToWalletDescriptor>( pub fn new_offline<E: ToWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
database: D,
) -> Result<Self, Error> {
Self::_new(descriptor, change_descriptor, network, database, (), None)
}
}
impl<B, D> Wallet<B, D>
where
D: BatchDatabase,
{
fn _new<E: ToWalletDescriptor>(
descriptor: E, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
network: Network, network: Network,
mut database: D, mut database: D,
client: B,
current_height: Option<u32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let (descriptor, keymap) = descriptor.to_wallet_descriptor(network)?; let (descriptor, keymap) = descriptor.to_wallet_descriptor(network)?;
database.check_descriptor_checksum( database.check_descriptor_checksum(
@@ -148,18 +162,20 @@ where
signers, signers,
change_signers, change_signers,
address_validators: Vec::new(), address_validators: Vec::new(),
network, network,
current_height,
current_height: None, client,
client: None,
database: RefCell::new(database), database: RefCell::new(database),
secp: Secp256k1::new(), secp: Secp256k1::new(),
}) })
} }
}
// offline actions, always available
impl<B, D> Wallet<B, D>
where
D: BatchDatabase,
{
/// Return a newly generated address using the external descriptor /// Return a newly generated address using the external descriptor
pub fn get_new_address(&self) -> Result<Address, Error> { pub fn get_new_address(&self) -> Result<Address, Error> {
let index = self.fetch_and_increment_index(KeychainKind::External)?; let index = self.fetch_and_increment_index(KeychainKind::External)?;
@@ -241,7 +257,7 @@ where
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::database::*; /// # use bdk::database::*;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (psbt, details) = wallet.create_tx( /// let (psbt, details) = wallet.create_tx(
/// TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) /// TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
@@ -495,11 +511,15 @@ where
match change_output { match change_output {
None if change_val.is_dust() => { None if change_val.is_dust() => {
// single recipient, but the only output would be below dust limit // single recipient, but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit? // TODO: or OutputBelowDustLimit?
return Err(Error::InsufficientFunds {
needed: DUST_LIMIT_SATOSHI,
available: change_val,
});
} }
Some(_) if change_val.is_dust() => { Some(_) if change_val.is_dust() => {
// skip the change output because it's dust, this adds up to the fees // skip the change output because it's dust -- just include it in the fee.
fee_amount += selected_amount - outgoing; fee_amount += change_val;
} }
Some(mut change_output) => { Some(mut change_output) => {
change_output.value = change_val; change_output.value = change_val;
@@ -556,7 +576,7 @@ where
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::database::*; /// # use bdk::database::*;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
/// let txid = Txid::from_str("faff0a466b70f5d5f92bd757a92c1371d4838bdd5bc53a06764e2488e51ce8f8").unwrap(); /// let txid = Txid::from_str("faff0a466b70f5d5f92bd757a92c1371d4838bdd5bc53a06764e2488e51ce8f8").unwrap();
/// let (psbt, details) = wallet.bump_fee( /// let (psbt, details) = wallet.bump_fee(
/// &txid, /// &txid,
@@ -773,7 +793,11 @@ where
} }
Some(_) if change_val_after_add.is_dust() => { Some(_) if change_val_after_add.is_dust() => {
// single_recipient but the only output would be below dust limit // single_recipient but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit? // TODO: or OutputBelowDustLimit?
return Err(Error::InsufficientFunds {
needed: DUST_LIMIT_SATOSHI,
available: change_val_after_add,
});
} }
None => { None => {
removed_updatable_output.value = change_val_after_add; removed_updatable_output.value = change_val_after_add;
@@ -820,7 +844,7 @@ where
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::database::*; /// # use bdk::database::*;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
/// # let (psbt, _) = wallet.create_tx(TxBuilder::new())?; /// # let (psbt, _) = wallet.create_tx(TxBuilder::new())?;
/// let (signed_psbt, finalized) = wallet.sign(psbt, None)?; /// let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), bdk::Error>(())
@@ -1319,12 +1343,15 @@ where
database: D, database: D,
client: B, client: B,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut wallet = Self::new_offline(descriptor, change_descriptor, network, database)?; let current_height = Some(maybe_await!(client.get_height())? as u32);
Self::_new(
wallet.current_height = Some(maybe_await!(client.get_height())? as u32); descriptor,
wallet.client = Some(client); change_descriptor,
network,
Ok(wallet) database,
client,
current_height,
)
} }
/// Sync the internal database with the blockchain /// Sync the internal database with the blockchain
@@ -1372,13 +1399,13 @@ where
// TODO: what if i generate an address first and cache some addresses? // TODO: what if i generate an address first and cache some addresses?
// TODO: we should sync if generating an address triggers a new batch to be stored // TODO: we should sync if generating an address triggers a new batch to be stored
if run_setup { if run_setup {
maybe_await!(self.client.as_ref().ok_or(Error::OfflineClient)?.setup( maybe_await!(self.client.setup(
None, None,
self.database.borrow_mut().deref_mut(), self.database.borrow_mut().deref_mut(),
progress_update, progress_update,
)) ))
} else { } else {
maybe_await!(self.client.as_ref().ok_or(Error::OfflineClient)?.sync( maybe_await!(self.client.sync(
None, None,
self.database.borrow_mut().deref_mut(), self.database.borrow_mut().deref_mut(),
progress_update, progress_update,
@@ -1387,8 +1414,8 @@ where
} }
/// Return a reference to the internal blockchain client /// Return a reference to the internal blockchain client
pub fn client(&self) -> Option<&B> { pub fn client(&self) -> &B {
self.client.as_ref() &self.client
} }
/// Get the Bitcoin network the wallet is using. /// Get the Bitcoin network the wallet is using.
@@ -1399,11 +1426,7 @@ where
/// Broadcast a transaction to the network /// Broadcast a transaction to the network
#[maybe_async] #[maybe_async]
pub fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> { pub fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
maybe_await!(self maybe_await!(self.client.broadcast(&tx))?;
.client
.as_ref()
.ok_or(Error::OfflineClient)?
.broadcast(&tx))?;
Ok(tx.txid()) Ok(tx.txid())
} }
@@ -1424,7 +1447,7 @@ mod test {
#[test] #[test]
fn test_cache_addresses_fixed() { fn test_cache_addresses_fixed() {
let db = MemoryDatabase::new(); let db = MemoryDatabase::new();
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)", "wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
None, None,
Network::Testnet, Network::Testnet,
@@ -1458,7 +1481,7 @@ mod test {
#[test] #[test]
fn test_cache_addresses() { fn test_cache_addresses() {
let db = MemoryDatabase::new(); let db = MemoryDatabase::new();
let wallet: OfflineWallet<_> = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap(); let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!( assert_eq!(
wallet.get_new_address().unwrap().to_string(), wallet.get_new_address().unwrap().to_string(),
@@ -1486,7 +1509,7 @@ mod test {
#[test] #[test]
fn test_cache_addresses_refill() { fn test_cache_addresses_refill() {
let db = MemoryDatabase::new(); let db = MemoryDatabase::new();
let wallet: OfflineWallet<_> = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap(); let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!( assert_eq!(
wallet.get_new_address().unwrap().to_string(), wallet.get_new_address().unwrap().to_string(),
@@ -1533,12 +1556,12 @@ mod test {
pub(crate) fn get_funded_wallet( pub(crate) fn get_funded_wallet(
descriptor: &str, descriptor: &str,
) -> ( ) -> (
OfflineWallet<MemoryDatabase>, Wallet<(), MemoryDatabase>,
(String, Option<String>), (String, Option<String>),
bitcoin::Txid, bitcoin::Txid,
) { ) {
let descriptors = testutils!(@descriptors (descriptor)); let descriptors = testutils!(@descriptors (descriptor));
let wallet: OfflineWallet<_> = Wallet::new_offline( let wallet = Wallet::new_offline(
&descriptors.0, &descriptors.0,
None, None,
Network::Regtest, Network::Regtest,
@@ -1952,7 +1975,7 @@ mod test {
fn test_create_tx_skip_change_dust() { fn test_create_tx_skip_change_dust() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, details) = wallet
.create_tx(TxBuilder::with_recipients(vec![( .create_tx(TxBuilder::with_recipients(vec![(
addr.script_pubkey(), addr.script_pubkey(),
49_800, 49_800,
@@ -1961,6 +1984,7 @@ mod test {
assert_eq!(psbt.global.unsigned_tx.output.len(), 1); assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(psbt.global.unsigned_tx.output[0].value, 49_800); assert_eq!(psbt.global.unsigned_tx.output[0].value, 49_800);
assert_eq!(details.fees, 200);
} }
#[test] #[test]

View File

@@ -79,7 +79,7 @@
//! let custom_signer = CustomSigner::connect(); //! let custom_signer = CustomSigner::connect();
//! //!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; //! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_signer( //! wallet.add_signer(
//! KeychainKind::External, //! KeychainKind::External,
//! Fingerprint::from_str("e30f11b8").unwrap().into(), //! Fingerprint::from_str("e30f11b8").unwrap().into(),

View File

@@ -29,7 +29,7 @@ use miniscript::descriptor::DescriptorPublicKeyCtx;
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
// De-facto standard "dust limit" (even though it should change based on the output type) // De-facto standard "dust limit" (even though it should change based on the output type)
const DUST_LIMIT_SATOSHI: u64 = 546; pub const DUST_LIMIT_SATOSHI: u64 = 546;
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when // MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
// spending using CSV in order to enforce CSV rules // spending using CSV in order to enforce CSV rules

View File

@@ -8,7 +8,7 @@ repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils-macros" documentation = "https://docs.rs/bdk-testutils-macros"
description = "Supporting testing macros for `bdk`" description = "Supporting testing macros for `bdk`"
keywords = ["bdk"] keywords = ["bdk"]
license-file = "../LICENSE" license = "MIT"
[lib] [lib]
proc-macro = true proc-macro = true

View File

@@ -8,7 +8,7 @@ repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils" documentation = "https://docs.rs/bdk-testutils"
description = "Supporting testing utilities for `bdk`" description = "Supporting testing utilities for `bdk`"
keywords = ["bdk"] keywords = ["bdk"]
license-file = "../LICENSE" license = "MIT"
[lib] [lib]
name = "testutils" name = "testutils"