Merge branch 'master' into fix_wallet_checksum
This commit is contained in:
commit
9cb6f70fc0
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement request
|
||||||
|
about: Request a new feature or change to an existing feature
|
||||||
|
title: ''
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the enhancement**
|
||||||
|
<!-- A clear and concise description of what you would like added or changed. -->
|
||||||
|
|
||||||
|
**Use case**
|
||||||
|
<!-- Tell us how you or others will use this new feature or change to an existing feature. -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
<!-- Add any other context about the enhancement here. -->
|
12
.github/workflows/cont_integration.yml
vendored
12
.github/workflows/cont_integration.yml
vendored
@ -17,16 +17,16 @@ jobs:
|
|||||||
- default
|
- default
|
||||||
- minimal
|
- minimal
|
||||||
- all-keys
|
- all-keys
|
||||||
- minimal,use-esplora-ureq
|
- minimal,use-esplora-blocking
|
||||||
- key-value-db
|
- key-value-db
|
||||||
- electrum
|
- electrum
|
||||||
- compact_filters
|
- compact_filters
|
||||||
- esplora,ureq,key-value-db,electrum
|
- use-esplora-blocking,key-value-db,electrum
|
||||||
- compiler
|
- compiler
|
||||||
- rpc
|
- rpc
|
||||||
- verify
|
- verify
|
||||||
- async-interface
|
- async-interface
|
||||||
- use-esplora-reqwest
|
- use-esplora-async
|
||||||
- sqlite
|
- sqlite
|
||||||
- sqlite-bundled
|
- sqlite-bundled
|
||||||
steps:
|
steps:
|
||||||
@ -100,10 +100,10 @@ jobs:
|
|||||||
features: test-rpc-legacy
|
features: test-rpc-legacy
|
||||||
- name: esplora
|
- name: esplora
|
||||||
testprefix: esplora
|
testprefix: esplora
|
||||||
features: test-esplora,use-esplora-reqwest,verify
|
features: test-esplora,use-esplora-async,verify
|
||||||
- name: esplora
|
- name: esplora
|
||||||
testprefix: esplora
|
testprefix: esplora
|
||||||
features: test-esplora,use-esplora-ureq,verify
|
features: test-esplora,use-esplora-blocking,verify
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -154,7 +154,7 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
|
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: Rust fmt
|
name: Rust fmt
|
||||||
|
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
|
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,use-esplora-blocking,compact_filters,rpc,key-value-db,sqlite,all-keys,verify,hardware-signer -- --cfg docsrs -Dwarnings
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
/.vscode
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
.idea
|
.idea
|
||||||
|
52
Cargo.toml
52
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.22.0"
|
version = "0.23.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"
|
||||||
@ -14,40 +14,39 @@ license = "MIT OR Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bdk-macros = "^0.6"
|
bdk-macros = "^0.6"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
miniscript = { version = "7.0", features = ["use-serde"] }
|
miniscript = { version = "8.0", features = ["serde"] }
|
||||||
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
|
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
rand = "^0.7"
|
rand = "^0.8"
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.11", optional = true }
|
electrum-client = { version = "0.12", optional = true }
|
||||||
|
esplora-client = { version = "0.2", default-features = false, optional = true }
|
||||||
rusqlite = { version = "0.27.0", optional = true }
|
rusqlite = { version = "0.27.0", optional = true }
|
||||||
ahash = { version = "0.7.6", optional = true }
|
ahash = { version = "0.7.6", optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
|
||||||
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
async-trait = { version = "0.1", optional = true }
|
async-trait = { version = "0.1", optional = true }
|
||||||
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
||||||
cc = { version = ">=1.0.64", optional = true }
|
cc = { version = ">=1.0.64", optional = true }
|
||||||
socks = { version = "0.3", optional = true }
|
socks = { version = "0.3", optional = true }
|
||||||
hwi = { version = "0.2.2", optional = true }
|
hwi = { version = "0.3.0", optional = true }
|
||||||
|
|
||||||
bip39 = { version = "1.0.1", optional = true }
|
bip39 = { version = "1.0.1", optional = true }
|
||||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||||
|
|
||||||
# Needed by bdk_blockchain_tests macro and the `rpc` feature
|
# Needed by bdk_blockchain_tests macro and the `rpc` feature
|
||||||
bitcoincore-rpc = { version = "0.15", optional = true }
|
bitcoincore-rpc = { version = "0.16", optional = true }
|
||||||
|
|
||||||
# Platform-specific dependencies
|
# Platform-specific dependencies
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
getrandom = "0.2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
rand = { version = "^0.7", features = ["wasm-bindgen"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
minimal = []
|
minimal = []
|
||||||
@ -69,23 +68,26 @@ hardware-signer = ["hwi"]
|
|||||||
#
|
#
|
||||||
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
|
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
|
||||||
# access to the asynchronous method implementations. Then, if Esplora is wanted,
|
# access to the asynchronous method implementations. Then, if Esplora is wanted,
|
||||||
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
|
# enable the `use-esplora-async` feature.
|
||||||
# - Users wanting blocking HTTP calls can use any of the other blockchain
|
# - Users wanting blocking HTTP calls can use any of the other blockchain
|
||||||
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
|
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
|
||||||
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
|
# use Esplora should enable the `use-esplora-blocking` feature.
|
||||||
#
|
#
|
||||||
# WARNING: Please take care with the features below, various combinations will
|
# WARNING: Please take care with the features below, various combinations will
|
||||||
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
||||||
async-interface = ["async-trait"]
|
async-interface = ["async-trait"]
|
||||||
electrum = ["electrum-client"]
|
electrum = ["electrum-client"]
|
||||||
# MUST ALSO USE `--no-default-features`.
|
# MUST ALSO USE `--no-default-features`.
|
||||||
use-esplora-reqwest = ["esplora", "reqwest", "reqwest/socks", "futures"]
|
use-esplora-async = ["esplora", "esplora-client/async", "futures"]
|
||||||
use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
|
use-esplora-blocking = ["esplora", "esplora-client/blocking"]
|
||||||
|
# Deprecated aliases
|
||||||
|
use-esplora-reqwest = ["use-esplora-async"]
|
||||||
|
use-esplora-ureq = ["use-esplora-blocking"]
|
||||||
# Typical configurations will not need to use `esplora` feature directly.
|
# Typical configurations will not need to use `esplora` feature directly.
|
||||||
esplora = []
|
esplora = []
|
||||||
|
|
||||||
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
|
# Use below feature with `use-esplora-async` to enable reqwest default TLS support
|
||||||
reqwest-default-tls = ["reqwest/default-tls"]
|
reqwest-default-tls = ["esplora-client/async-https"]
|
||||||
|
|
||||||
# Debug/Test features
|
# Debug/Test features
|
||||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||||
@ -96,13 +98,18 @@ test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoi
|
|||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
test-hardware-signer = ["hardware-signer"]
|
test-hardware-signer = ["hardware-signer"]
|
||||||
|
|
||||||
|
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
|
||||||
|
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||||
|
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
|
||||||
|
dev-getrandom-wasm = ["getrandom/js"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
electrsd = "0.20"
|
electrsd = "0.21"
|
||||||
|
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
|
||||||
|
base64 = "^0.13"
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "address_validator"
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "compact_filters_balance"
|
name = "compact_filters_balance"
|
||||||
required-features = ["compact_filters"]
|
required-features = ["compact_filters"]
|
||||||
@ -126,9 +133,14 @@ name = "psbt_signer"
|
|||||||
path = "examples/psbt_signer.rs"
|
path = "examples/psbt_signer.rs"
|
||||||
required-features = ["electrum"]
|
required-features = ["electrum"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "hardware_signer"
|
||||||
|
path = "examples/hardware_signer.rs"
|
||||||
|
required-features = ["electrum", "hardware-signer"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["compiler", "electrum", "esplora", "use-esplora-ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
features = ["compiler", "electrum", "esplora", "use-esplora-blocking", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
||||||
# defines the configuration attribute `docsrs`
|
# defines the configuration attribute `docsrs`
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
15
README.md
15
README.md
@ -95,7 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
|
|||||||
use bdk::electrum_client::Client;
|
use bdk::electrum_client::Client;
|
||||||
use bdk::wallet::AddressIndex::New;
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
use bitcoin::base64;
|
use base64;
|
||||||
use bitcoin::consensus::serialize;
|
use bitcoin::consensus::serialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@ -132,7 +132,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
```rust,no_run
|
```rust,no_run
|
||||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||||
|
|
||||||
use bitcoin::base64;
|
use base64;
|
||||||
use bitcoin::consensus::deserialize;
|
use bitcoin::consensus::deserialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@ -171,6 +171,17 @@ cargo test --features test-electrum
|
|||||||
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
|
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
|
||||||
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
||||||
|
|
||||||
|
## Running under WASM
|
||||||
|
|
||||||
|
If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under either of
|
Licensed under either of
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
// Bitcoin Dev Kit
|
|
||||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
|
||||||
//
|
|
||||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
|
||||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
|
||||||
// You may not use this file except in accordance with one or both of these
|
|
||||||
// licenses.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use bdk::bitcoin;
|
|
||||||
use bdk::database::MemoryDatabase;
|
|
||||||
use bdk::descriptor::HdKeyPaths;
|
|
||||||
#[allow(deprecated)]
|
|
||||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
|
||||||
use bdk::KeychainKind;
|
|
||||||
use bdk::Wallet;
|
|
||||||
|
|
||||||
use bdk::wallet::AddressIndex::New;
|
|
||||||
use bitcoin::hashes::hex::FromHex;
|
|
||||||
use bitcoin::util::bip32::Fingerprint;
|
|
||||||
use bitcoin::{Network, Script};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct DummyValidator;
|
|
||||||
#[allow(deprecated)]
|
|
||||||
impl AddressValidator for DummyValidator {
|
|
||||||
fn validate(
|
|
||||||
&self,
|
|
||||||
keychain: KeychainKind,
|
|
||||||
hd_keypaths: &HdKeyPaths,
|
|
||||||
script: &Script,
|
|
||||||
) -> Result<(), AddressValidatorError> {
|
|
||||||
let (_, path) = hd_keypaths
|
|
||||||
.values()
|
|
||||||
.find(|(fing, _)| fing == &Fingerprint::from_hex("bc123c3e").unwrap())
|
|
||||||
.ok_or(AddressValidatorError::InvalidScript)?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Validating `{:?}` {} address, script: {}",
|
|
||||||
keychain, path, script
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
|
||||||
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
|
||||||
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
|
||||||
|
|
||||||
wallet.get_address(New)?;
|
|
||||||
wallet.get_address(New)?;
|
|
||||||
wallet.get_address(New)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
103
examples/hardware_signer.rs
Normal file
103
examples/hardware_signer.rs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
use bdk::bitcoin::{Address, Network};
|
||||||
|
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||||
|
use bdk::database::MemoryDatabase;
|
||||||
|
use bdk::hwi::{types::HWIChain, HWIClient};
|
||||||
|
use bdk::signer::SignerOrdering;
|
||||||
|
use bdk::wallet::{hardwaresigner::HWISigner, AddressIndex};
|
||||||
|
use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
|
||||||
|
use electrum_client::Client;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// This example shows how to sync a wallet, create a transaction, sign it
|
||||||
|
// and broadcast it using an external hardware wallet.
|
||||||
|
// The hardware wallet must be connected to the computer and unlocked before
|
||||||
|
// running the example. Also, the `hwi` python package should be installed
|
||||||
|
// and available in the environment.
|
||||||
|
//
|
||||||
|
// To avoid loss of funds, consider using an hardware wallet simulator:
|
||||||
|
// * Coldcard: https://github.com/Coldcard/firmware
|
||||||
|
// * Ledger: https://github.com/LedgerHQ/speculos
|
||||||
|
// * Trezor: https://docs.trezor.io/trezor-firmware/core/emulator/index.html
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Hold tight, I'm connecting to your hardware wallet...");
|
||||||
|
|
||||||
|
// Listing all the available hardware wallet devices...
|
||||||
|
let devices = HWIClient::enumerate()?;
|
||||||
|
let first_device = devices
|
||||||
|
.first()
|
||||||
|
.expect("No devices found. Either plug in a hardware wallet, or start a simulator.");
|
||||||
|
// ...and creating a client out of the first one
|
||||||
|
let client = HWIClient::get_client(first_device, true, HWIChain::Test)?;
|
||||||
|
println!("Look what I found, a {}!", first_device.model);
|
||||||
|
|
||||||
|
// Getting the HW's public descriptors
|
||||||
|
let descriptors = client.get_descriptors(None)?;
|
||||||
|
println!(
|
||||||
|
"The hardware wallet's descriptor is: {}",
|
||||||
|
descriptors.receive[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Creating a custom signer from the device
|
||||||
|
let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
|
||||||
|
let mut wallet = Wallet::new(
|
||||||
|
&descriptors.receive[0],
|
||||||
|
Some(&descriptors.internal[0]),
|
||||||
|
Network::Testnet,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Adding the hardware signer to the BDK wallet
|
||||||
|
wallet.add_signer(
|
||||||
|
KeychainKind::External,
|
||||||
|
SignerOrdering(200),
|
||||||
|
Arc::new(custom_signer),
|
||||||
|
);
|
||||||
|
|
||||||
|
// create client for Blockstream's testnet electrum server
|
||||||
|
let blockchain =
|
||||||
|
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||||
|
|
||||||
|
println!("Syncing the wallet...");
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||||
|
|
||||||
|
// get deposit address
|
||||||
|
let deposit_address = wallet.get_address(AddressIndex::New)?;
|
||||||
|
|
||||||
|
let balance = wallet.get_balance()?;
|
||||||
|
println!("Wallet balances in SATs: {}", balance);
|
||||||
|
|
||||||
|
if balance.get_total() < 10000 {
|
||||||
|
println!(
|
||||||
|
"Send some sats from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
|
||||||
|
addr = deposit_address.address
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
|
||||||
|
let (mut psbt, _details) = {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder
|
||||||
|
.drain_wallet()
|
||||||
|
.drain_to(return_address.script_pubkey())
|
||||||
|
.enable_rbf()
|
||||||
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
|
builder.finish()?
|
||||||
|
};
|
||||||
|
|
||||||
|
// `sign` will call the hardware wallet asking for a signature
|
||||||
|
assert!(
|
||||||
|
wallet.sign(&mut psbt, SignOptions::default())?,
|
||||||
|
"The hardware wallet couldn't finalize the transaction :("
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Let's broadcast your tx...");
|
||||||
|
let raw_transaction = psbt.extract_tx();
|
||||||
|
let txid = raw_transaction.txid();
|
||||||
|
|
||||||
|
blockchain.broadcast(&raw_transaction)?;
|
||||||
|
println!("Transaction broadcasted! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -9,20 +9,38 @@
|
|||||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||||
use bdk::database::MemoryDatabase;
|
use bdk::database::MemoryDatabase;
|
||||||
use bdk::wallet::AddressIndex;
|
use bdk::wallet::AddressIndex;
|
||||||
use bdk::SyncOptions;
|
use bdk::{descriptor, SyncOptions};
|
||||||
use bdk::{FeeRate, SignOptions, Wallet};
|
use bdk::{FeeRate, SignOptions, Wallet};
|
||||||
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::{Address, Network};
|
use bitcoin::{Address, Network};
|
||||||
use electrum_client::Client;
|
use electrum_client::Client;
|
||||||
|
use miniscript::descriptor::DescriptorSecretKey;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// This example shows how to sign and broadcast the transaction for a PSBT (Partially Signed
|
||||||
|
/// Bitcoin Transaction) for a single key, witness public key hash (WPKH) based descriptor wallet.
|
||||||
|
/// The electrum protocol is used to sync blockchain data from the testnet bitcoin network and
|
||||||
|
/// wallet data is stored in an ephemeral in-memory database. The process steps are:
|
||||||
|
/// 1. Create a "signing" wallet and a "watch-only" wallet based on the same private keys.
|
||||||
|
/// 2. Deposit testnet funds into the watch only wallet.
|
||||||
|
/// 3. Sync the watch only wallet and create a spending transaction to return all funds to the testnet faucet.
|
||||||
|
/// 4. Sync the signing wallet and sign and finalize the PSBT created by the watch only wallet.
|
||||||
|
/// 5. Broadcast the transactions from the finalized PSBT.
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// test keys created with `bdk-cli key generate` and `bdk-cli key derive` commands
|
// test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
|
||||||
let signing_external_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*)";
|
let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
|
||||||
let signing_internal_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*)";
|
let internal_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*").unwrap();
|
||||||
|
|
||||||
let watch_only_external_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/0/*)";
|
let secp = Secp256k1::new();
|
||||||
let watch_only_internal_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/1/*)";
|
let external_public_xkey = external_secret_xkey.to_public(&secp).unwrap();
|
||||||
|
let internal_public_xkey = internal_secret_xkey.to_public(&secp).unwrap();
|
||||||
|
|
||||||
|
let signing_external_descriptor = descriptor!(wpkh(external_secret_xkey)).unwrap();
|
||||||
|
let signing_internal_descriptor = descriptor!(wpkh(internal_secret_xkey)).unwrap();
|
||||||
|
|
||||||
|
let watch_only_external_descriptor = descriptor!(wpkh(external_public_xkey)).unwrap();
|
||||||
|
let watch_only_internal_descriptor = descriptor!(wpkh(internal_public_xkey)).unwrap();
|
||||||
|
|
||||||
// create client for Blockstream's testnet electrum server
|
// create client for Blockstream's testnet electrum server
|
||||||
let blockchain =
|
let blockchain =
|
||||||
|
@ -194,7 +194,7 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
|
|||||||
/// );
|
/// );
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum AnyBlockchainConfig {
|
pub enum AnyBlockchainConfig {
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
|
@ -479,7 +479,7 @@ impl WalletSync for CompactFiltersBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Data to connect to a Bitcoin P2P peer
|
/// Data to connect to a Bitcoin P2P peer
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||||
pub struct BitcoinPeerConfig {
|
pub struct BitcoinPeerConfig {
|
||||||
/// Peer address such as 127.0.0.1:18333
|
/// Peer address such as 127.0.0.1:18333
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@ -490,7 +490,7 @@ pub struct BitcoinPeerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for a [`CompactFiltersBlockchain`]
|
/// Configuration for a [`CompactFiltersBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||||
pub struct CompactFiltersBlockchainConfig {
|
pub struct CompactFiltersBlockchainConfig {
|
||||||
/// List of peers to try to connect to for asking headers and filters
|
/// List of peers to try to connect to for asking headers and filters
|
||||||
pub peers: Vec<BitcoinPeerConfig>,
|
pub peers: Vec<BitcoinPeerConfig>,
|
||||||
|
@ -75,7 +75,10 @@ impl Mempool {
|
|||||||
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
||||||
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
||||||
let identifer = match inventory {
|
let identifer = match inventory {
|
||||||
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None,
|
Inventory::Error
|
||||||
|
| Inventory::Block(_)
|
||||||
|
| Inventory::WitnessBlock(_)
|
||||||
|
| Inventory::CompactBlock(_) => return None,
|
||||||
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
|
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
|
||||||
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
|
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
|
||||||
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),
|
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),
|
||||||
|
@ -103,42 +103,42 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Encodable for BundleStatus {
|
impl Encodable for BundleStatus {
|
||||||
fn consensus_encode<W: Write>(&self, mut e: W) -> Result<usize, std::io::Error> {
|
fn consensus_encode<W: Write + ?Sized>(&self, e: &mut W) -> Result<usize, std::io::Error> {
|
||||||
let mut written = 0;
|
let mut written = 0;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
BundleStatus::Init => {
|
BundleStatus::Init => {
|
||||||
written += 0x00u8.consensus_encode(&mut e)?;
|
written += 0x00u8.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
BundleStatus::CfHeaders { cf_headers } => {
|
BundleStatus::CfHeaders { cf_headers } => {
|
||||||
written += 0x01u8.consensus_encode(&mut e)?;
|
written += 0x01u8.consensus_encode(e)?;
|
||||||
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
|
||||||
for header in cf_headers {
|
for header in cf_headers {
|
||||||
written += header.consensus_encode(&mut e)?;
|
written += header.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BundleStatus::CFilters { cf_filters } => {
|
BundleStatus::CFilters { cf_filters } => {
|
||||||
written += 0x02u8.consensus_encode(&mut e)?;
|
written += 0x02u8.consensus_encode(e)?;
|
||||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||||
for filter in cf_filters {
|
for filter in cf_filters {
|
||||||
written += filter.consensus_encode(&mut e)?;
|
written += filter.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BundleStatus::Processed { cf_filters } => {
|
BundleStatus::Processed { cf_filters } => {
|
||||||
written += 0x03u8.consensus_encode(&mut e)?;
|
written += 0x03u8.consensus_encode(e)?;
|
||||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||||
for filter in cf_filters {
|
for filter in cf_filters {
|
||||||
written += filter.consensus_encode(&mut e)?;
|
written += filter.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BundleStatus::Pruned => {
|
BundleStatus::Pruned => {
|
||||||
written += 0x04u8.consensus_encode(&mut e)?;
|
written += 0x04u8.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
BundleStatus::Tip { cf_filters } => {
|
BundleStatus::Tip { cf_filters } => {
|
||||||
written += 0x05u8.consensus_encode(&mut e)?;
|
written += 0x05u8.consensus_encode(e)?;
|
||||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||||
for filter in cf_filters {
|
for filter in cf_filters {
|
||||||
written += filter.consensus_encode(&mut e)?;
|
written += filter.consensus_encode(e)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,51 +148,53 @@ impl Encodable for BundleStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Decodable for BundleStatus {
|
impl Decodable for BundleStatus {
|
||||||
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
|
fn consensus_decode<D: Read + ?Sized>(
|
||||||
let byte_type = u8::consensus_decode(&mut d)?;
|
d: &mut D,
|
||||||
|
) -> Result<Self, bitcoin::consensus::encode::Error> {
|
||||||
|
let byte_type = u8::consensus_decode(d)?;
|
||||||
match byte_type {
|
match byte_type {
|
||||||
0x00 => Ok(BundleStatus::Init),
|
0x00 => Ok(BundleStatus::Init),
|
||||||
0x01 => {
|
0x01 => {
|
||||||
let num = VarInt::consensus_decode(&mut d)?;
|
let num = VarInt::consensus_decode(d)?;
|
||||||
let num = num.0 as usize;
|
let num = num.0 as usize;
|
||||||
|
|
||||||
let mut cf_headers = Vec::with_capacity(num);
|
let mut cf_headers = Vec::with_capacity(num);
|
||||||
for _ in 0..num {
|
for _ in 0..num {
|
||||||
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
|
cf_headers.push(FilterHeader::consensus_decode(d)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BundleStatus::CfHeaders { cf_headers })
|
Ok(BundleStatus::CfHeaders { cf_headers })
|
||||||
}
|
}
|
||||||
0x02 => {
|
0x02 => {
|
||||||
let num = VarInt::consensus_decode(&mut d)?;
|
let num = VarInt::consensus_decode(d)?;
|
||||||
let num = num.0 as usize;
|
let num = num.0 as usize;
|
||||||
|
|
||||||
let mut cf_filters = Vec::with_capacity(num);
|
let mut cf_filters = Vec::with_capacity(num);
|
||||||
for _ in 0..num {
|
for _ in 0..num {
|
||||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BundleStatus::CFilters { cf_filters })
|
Ok(BundleStatus::CFilters { cf_filters })
|
||||||
}
|
}
|
||||||
0x03 => {
|
0x03 => {
|
||||||
let num = VarInt::consensus_decode(&mut d)?;
|
let num = VarInt::consensus_decode(d)?;
|
||||||
let num = num.0 as usize;
|
let num = num.0 as usize;
|
||||||
|
|
||||||
let mut cf_filters = Vec::with_capacity(num);
|
let mut cf_filters = Vec::with_capacity(num);
|
||||||
for _ in 0..num {
|
for _ in 0..num {
|
||||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BundleStatus::Processed { cf_filters })
|
Ok(BundleStatus::Processed { cf_filters })
|
||||||
}
|
}
|
||||||
0x04 => Ok(BundleStatus::Pruned),
|
0x04 => Ok(BundleStatus::Pruned),
|
||||||
0x05 => {
|
0x05 => {
|
||||||
let num = VarInt::consensus_decode(&mut d)?;
|
let num = VarInt::consensus_decode(d)?;
|
||||||
let num = num.0 as usize;
|
let num = num.0 as usize;
|
||||||
|
|
||||||
let mut cf_filters = Vec::with_capacity(num);
|
let mut cf_filters = Vec::with_capacity(num);
|
||||||
for _ in 0..num {
|
for _ in 0..num {
|
||||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BundleStatus::Tip { cf_filters })
|
Ok(BundleStatus::Tip { cf_filters })
|
||||||
@ -276,7 +278,11 @@ impl ChainStore<Full> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> {
|
pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> {
|
||||||
let new_cf_name: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect();
|
let new_cf_name: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.map(|byte| byte as char)
|
||||||
|
.take(16)
|
||||||
|
.collect();
|
||||||
let new_cf_name = format!("_headers:{}", new_cf_name);
|
let new_cf_name = format!("_headers:{}", new_cf_name);
|
||||||
|
|
||||||
let mut write_store = self.store.write().unwrap();
|
let mut write_store = self.store.write().unwrap();
|
||||||
@ -647,7 +653,7 @@ impl CfStore {
|
|||||||
&first_key,
|
&first_key,
|
||||||
(
|
(
|
||||||
BundleStatus::Init,
|
BundleStatus::Init,
|
||||||
filter.filter_header(&FilterHeader::from_hash(Default::default())),
|
filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
|
||||||
)
|
)
|
||||||
.serialize(),
|
.serialize(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bitcoin::hash_types::{BlockHash, FilterHeader};
|
use bitcoin::hash_types::{BlockHash, FilterHeader};
|
||||||
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::network::message::NetworkMessage;
|
use bitcoin::network::message::NetworkMessage;
|
||||||
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
||||||
use bitcoin::util::bip158::BlockFilter;
|
use bitcoin::util::bip158::BlockFilter;
|
||||||
@ -254,7 +255,7 @@ where
|
|||||||
|
|
||||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||||
locators_vec,
|
locators_vec,
|
||||||
Default::default(),
|
Hash::all_zeros(),
|
||||||
)))?;
|
)))?;
|
||||||
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
|
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
|
||||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||||
@ -276,7 +277,7 @@ where
|
|||||||
while sync_height < peer.get_version().start_height as usize {
|
while sync_height < peer.get_version().start_height as usize {
|
||||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||||
vec![last_hash],
|
vec![last_hash],
|
||||||
Default::default(),
|
Hash::all_zeros(),
|
||||||
)))?;
|
)))?;
|
||||||
if let NetworkMessage::Headers(headers) = peer
|
if let NetworkMessage::Headers(headers) = peer
|
||||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||||
|
@ -296,7 +296,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for an [`ElectrumBlockchain`]
|
/// Configuration for an [`ElectrumBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||||
pub struct ElectrumBlockchainConfig {
|
pub struct ElectrumBlockchainConfig {
|
||||||
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
||||||
///
|
///
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
//! structs from the esplora API
|
|
||||||
//!
|
|
||||||
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
|
|
||||||
use crate::BlockTime;
|
|
||||||
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness};
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
|
||||||
pub struct PrevOut {
|
|
||||||
pub value: u64,
|
|
||||||
pub scriptpubkey: Script,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
|
||||||
pub struct Vin {
|
|
||||||
pub txid: Txid,
|
|
||||||
pub vout: u32,
|
|
||||||
// None if coinbase
|
|
||||||
pub prevout: Option<PrevOut>,
|
|
||||||
pub scriptsig: Script,
|
|
||||||
#[serde(deserialize_with = "deserialize_witness", default)]
|
|
||||||
pub witness: Vec<Vec<u8>>,
|
|
||||||
pub sequence: u32,
|
|
||||||
pub is_coinbase: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
|
||||||
pub struct Vout {
|
|
||||||
pub value: u64,
|
|
||||||
pub scriptpubkey: Script,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
|
||||||
pub struct TxStatus {
|
|
||||||
pub confirmed: bool,
|
|
||||||
pub block_height: Option<u32>,
|
|
||||||
pub block_time: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
|
||||||
pub struct Tx {
|
|
||||||
pub txid: Txid,
|
|
||||||
pub version: i32,
|
|
||||||
pub locktime: u32,
|
|
||||||
pub vin: Vec<Vin>,
|
|
||||||
pub vout: Vec<Vout>,
|
|
||||||
pub status: TxStatus,
|
|
||||||
pub fee: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tx {
|
|
||||||
pub fn to_tx(&self) -> Transaction {
|
|
||||||
Transaction {
|
|
||||||
version: self.version,
|
|
||||||
lock_time: self.locktime,
|
|
||||||
input: self
|
|
||||||
.vin
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|vin| TxIn {
|
|
||||||
previous_output: OutPoint {
|
|
||||||
txid: vin.txid,
|
|
||||||
vout: vin.vout,
|
|
||||||
},
|
|
||||||
script_sig: vin.scriptsig,
|
|
||||||
sequence: vin.sequence,
|
|
||||||
witness: Witness::from_vec(vin.witness),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
output: self
|
|
||||||
.vout
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|vout| TxOut {
|
|
||||||
value: vout.value,
|
|
||||||
script_pubkey: vout.scriptpubkey,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn confirmation_time(&self) -> Option<BlockTime> {
|
|
||||||
match self.status {
|
|
||||||
TxStatus {
|
|
||||||
confirmed: true,
|
|
||||||
block_height: Some(height),
|
|
||||||
block_time: Some(timestamp),
|
|
||||||
} => Some(BlockTime { timestamp, height }),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
|
|
||||||
self.vin
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|vin| {
|
|
||||||
vin.prevout.map(|po| TxOut {
|
|
||||||
script_pubkey: po.scriptpubkey,
|
|
||||||
value: po.value,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_witness<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::de::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use crate::serde::Deserialize;
|
|
||||||
use bitcoin::hashes::hex::FromHex;
|
|
||||||
let list = Vec::<String>::deserialize(d)?;
|
|
||||||
list.into_iter()
|
|
||||||
.map(|hex_str| Vec::<u8>::from_hex(&hex_str))
|
|
||||||
.collect::<Result<Vec<Vec<u8>>, _>>()
|
|
||||||
.map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
@ -14,49 +14,36 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::consensus::{deserialize, serialize};
|
use bitcoin::{Transaction, Txid};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
|
||||||
use bitcoin::hashes::{sha256, Hash};
|
|
||||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
use ::reqwest::{Client, StatusCode};
|
use esplora_client::{convert_fee_rate, AsyncClient, Builder, Tx};
|
||||||
use futures::stream::{FuturesOrdered, TryStreamExt};
|
use futures::stream::{FuturesOrdered, TryStreamExt};
|
||||||
|
|
||||||
use super::api::Tx;
|
|
||||||
use crate::blockchain::esplora::EsploraError;
|
|
||||||
use crate::blockchain::*;
|
use crate::blockchain::*;
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
/// Structure encapsulates Esplora client
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UrlClient {
|
|
||||||
url: String,
|
|
||||||
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
|
||||||
// when the target platform is wasm32.
|
|
||||||
client: Client,
|
|
||||||
concurrency: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure that implements the logic to sync with Esplora
|
/// Structure that implements the logic to sync with Esplora
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EsploraBlockchain {
|
pub struct EsploraBlockchain {
|
||||||
url_client: UrlClient,
|
url_client: AsyncClient,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
|
concurrency: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
impl std::convert::From<AsyncClient> for EsploraBlockchain {
|
||||||
fn from(url_client: UrlClient) -> Self {
|
fn from(url_client: AsyncClient) -> Self {
|
||||||
EsploraBlockchain {
|
EsploraBlockchain {
|
||||||
url_client,
|
url_client,
|
||||||
stop_gap: 20,
|
stop_gap: 20,
|
||||||
|
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,19 +51,25 @@ impl std::convert::From<UrlClient> for EsploraBlockchain {
|
|||||||
impl EsploraBlockchain {
|
impl EsploraBlockchain {
|
||||||
/// Create a new instance of the client from a base URL and `stop_gap`.
|
/// Create a new instance of the client from a base URL and `stop_gap`.
|
||||||
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||||
|
let url_client = Builder::new(base_url)
|
||||||
|
.build_async()
|
||||||
|
.expect("Should never fail with no proxy and timeout");
|
||||||
|
|
||||||
|
Self::from_client(url_client, stop_gap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a new instance given a client
|
||||||
|
pub fn from_client(url_client: AsyncClient, stop_gap: usize) -> Self {
|
||||||
EsploraBlockchain {
|
EsploraBlockchain {
|
||||||
url_client: UrlClient {
|
url_client,
|
||||||
url: base_url.to_string(),
|
|
||||||
client: Client::new(),
|
|
||||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
|
||||||
},
|
|
||||||
stop_gap,
|
stop_gap,
|
||||||
|
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the concurrency to use when doing batch queries against the Esplora instance.
|
/// Set the concurrency to use when doing batch queries against the Esplora instance.
|
||||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||||
self.url_client.concurrency = concurrency;
|
self.concurrency = concurrency;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,17 +87,19 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(await_or_block!(self.url_client._broadcast(tx))?)
|
Ok(await_or_block!(self.url_client.broadcast(tx))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
|
let estimates = await_or_block!(self.url_client.get_fee_estimates())?;
|
||||||
super::into_fee_rate(target, estimates)
|
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
|
||||||
|
target, estimates,
|
||||||
|
)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for EsploraBlockchain {
|
impl Deref for EsploraBlockchain {
|
||||||
type Target = UrlClient;
|
type Target = AsyncClient;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.url_client
|
&self.url_client
|
||||||
@ -116,22 +111,23 @@ impl StatelessBlockchain for EsploraBlockchain {}
|
|||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(await_or_block!(self.url_client._get_height())?)
|
Ok(await_or_block!(self.url_client.get_height())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl GetTx for EsploraBlockchain {
|
impl GetTx for EsploraBlockchain {
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
Ok(await_or_block!(self.url_client._get_tx(txid))?)
|
Ok(await_or_block!(self.url_client.get_tx(txid))?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl GetBlockHash for EsploraBlockchain {
|
impl GetBlockHash for EsploraBlockchain {
|
||||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
let block_header = await_or_block!(self.url_client._get_header(height as u32))?;
|
Ok(await_or_block!(self
|
||||||
Ok(block_header.block_hash())
|
.url_client
|
||||||
|
.get_block_hash(height as u32))?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,10 +147,10 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
Request::Script(script_req) => {
|
Request::Script(script_req) => {
|
||||||
let futures: FuturesOrdered<_> = script_req
|
let futures: FuturesOrdered<_> = script_req
|
||||||
.request()
|
.request()
|
||||||
.take(self.url_client.concurrency as usize)
|
.take(self.concurrency as usize)
|
||||||
.map(|script| async move {
|
.map(|script| async move {
|
||||||
let mut related_txs: Vec<Tx> =
|
let mut related_txs: Vec<Tx> =
|
||||||
self.url_client._scripthash_txs(script, None).await?;
|
self.url_client.scripthash_txs(script, None).await?;
|
||||||
|
|
||||||
let n_confirmed =
|
let n_confirmed =
|
||||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||||
@ -164,7 +160,7 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
loop {
|
loop {
|
||||||
let new_related_txs: Vec<Tx> = self
|
let new_related_txs: Vec<Tx> = self
|
||||||
.url_client
|
.url_client
|
||||||
._scripthash_txs(
|
.scripthash_txs(
|
||||||
script,
|
script,
|
||||||
Some(related_txs.last().unwrap().txid),
|
Some(related_txs.last().unwrap().txid),
|
||||||
)
|
)
|
||||||
@ -204,6 +200,7 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.get(txid)
|
.get(txid)
|
||||||
.expect("must be in index")
|
.expect("must be in index")
|
||||||
.confirmation_time()
|
.confirmation_time()
|
||||||
|
.map(Into::into)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
conftime_req.satisfy(conftimes)?
|
conftime_req.satisfy(conftimes)?
|
||||||
@ -227,132 +224,26 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UrlClient {
|
|
||||||
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.client
|
|
||||||
.get(&format!("{}/tx/{}/raw", self.url, txid))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let StatusCode::NOT_FOUND = resp.status() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
|
|
||||||
match self._get_tx(txid).await {
|
|
||||||
Ok(Some(tx)) => Ok(tx),
|
|
||||||
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.client
|
|
||||||
.get(&format!("{}/block-height/{}", self.url, block_height))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let StatusCode::NOT_FOUND = resp.status() {
|
|
||||||
return Err(EsploraError::HeaderHeightNotFound(block_height));
|
|
||||||
}
|
|
||||||
let bytes = resp.bytes().await?;
|
|
||||||
let hash = std::str::from_utf8(&bytes)
|
|
||||||
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
|
|
||||||
|
|
||||||
let resp = self
|
|
||||||
.client
|
|
||||||
.get(&format!("{}/block/{}/header", self.url, hash))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
|
|
||||||
|
|
||||||
Ok(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
|
||||||
self.client
|
|
||||||
.post(&format!("{}/tx", self.url))
|
|
||||||
.body(serialize(transaction).to_hex())
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get_height(&self) -> Result<u32, EsploraError> {
|
|
||||||
let req = self
|
|
||||||
.client
|
|
||||||
.get(&format!("{}/blocks/tip/height", self.url))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(req.error_for_status()?.text().await?.parse()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _scripthash_txs(
|
|
||||||
&self,
|
|
||||||
script: &Script,
|
|
||||||
last_seen: Option<Txid>,
|
|
||||||
) -> Result<Vec<Tx>, EsploraError> {
|
|
||||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
|
||||||
let url = match last_seen {
|
|
||||||
Some(last_seen) => format!(
|
|
||||||
"{}/scripthash/{}/txs/chain/{}",
|
|
||||||
self.url, script_hash, last_seen
|
|
||||||
),
|
|
||||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
|
||||||
};
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json::<Vec<Tx>>()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.get(&format!("{}/fee-estimates", self.url,))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json::<HashMap<String, f64>>()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||||
type Config = super::EsploraBlockchainConfig;
|
type Config = super::EsploraBlockchainConfig;
|
||||||
|
|
||||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||||
let map_e = |e: reqwest::Error| Error::Esplora(Box::new(e.into()));
|
let mut builder = Builder::new(config.base_url.as_str());
|
||||||
|
|
||||||
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
|
|
||||||
if let Some(concurrency) = config.concurrency {
|
|
||||||
blockchain.url_client.concurrency = concurrency;
|
|
||||||
}
|
|
||||||
let mut builder = Client::builder();
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(proxy) = &config.proxy {
|
|
||||||
builder = builder.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(timeout) = config.timeout {
|
if let Some(timeout) = config.timeout {
|
||||||
builder = builder.timeout(core::time::Duration::from_secs(timeout));
|
builder = builder.timeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchain.url_client.client = builder.build().map_err(map_e)?;
|
if let Some(proxy) = &config.proxy {
|
||||||
|
builder = builder.proxy(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut blockchain =
|
||||||
|
EsploraBlockchain::from_client(builder.build_async()?, config.stop_gap);
|
||||||
|
|
||||||
|
if let Some(concurrency) = config.concurrency {
|
||||||
|
blockchain = blockchain.with_concurrency(concurrency);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(blockchain)
|
Ok(blockchain)
|
||||||
}
|
}
|
@ -12,42 +12,26 @@
|
|||||||
//! Esplora by way of `ureq` HTTP client.
|
//! Esplora by way of `ureq` HTTP client.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
use ureq::{Agent, Proxy, Response};
|
use bitcoin::{Transaction, Txid};
|
||||||
|
|
||||||
use bitcoin::consensus::{deserialize, serialize};
|
use esplora_client::{convert_fee_rate, BlockingClient, Builder, Tx};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
|
||||||
use bitcoin::hashes::{sha256, Hash};
|
|
||||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
|
||||||
|
|
||||||
use super::api::Tx;
|
|
||||||
use crate::blockchain::esplora::EsploraError;
|
|
||||||
use crate::blockchain::*;
|
use crate::blockchain::*;
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
/// Structure encapsulates ureq Esplora client
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UrlClient {
|
|
||||||
url: String,
|
|
||||||
agent: Agent,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure that implements the logic to sync with Esplora
|
/// Structure that implements the logic to sync with Esplora
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EsploraBlockchain {
|
pub struct EsploraBlockchain {
|
||||||
url_client: UrlClient,
|
url_client: BlockingClient,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
concurrency: u8,
|
concurrency: u8,
|
||||||
}
|
}
|
||||||
@ -55,22 +39,22 @@ pub struct EsploraBlockchain {
|
|||||||
impl EsploraBlockchain {
|
impl EsploraBlockchain {
|
||||||
/// Create a new instance of the client from a base URL and the `stop_gap`.
|
/// Create a new instance of the client from a base URL and the `stop_gap`.
|
||||||
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||||
|
let url_client = Builder::new(base_url)
|
||||||
|
.build_blocking()
|
||||||
|
.expect("Should never fail with no proxy and timeout");
|
||||||
|
|
||||||
|
Self::from_client(url_client, stop_gap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a new instance given a client
|
||||||
|
pub fn from_client(url_client: BlockingClient, stop_gap: usize) -> Self {
|
||||||
EsploraBlockchain {
|
EsploraBlockchain {
|
||||||
url_client: UrlClient {
|
url_client,
|
||||||
url: base_url.to_string(),
|
|
||||||
agent: Agent::new(),
|
|
||||||
},
|
|
||||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||||
stop_gap,
|
stop_gap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the inner `ureq` agent.
|
|
||||||
pub fn with_agent(mut self, agent: Agent) -> Self {
|
|
||||||
self.url_client.agent = agent;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the number of parallel requests the client can make.
|
/// Set the number of parallel requests the client can make.
|
||||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||||
self.concurrency = concurrency;
|
self.concurrency = concurrency;
|
||||||
@ -90,18 +74,20 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
self.url_client._broadcast(tx)?;
|
self.url_client.broadcast(tx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
let estimates = self.url_client._get_fee_estimates()?;
|
let estimates = self.url_client.get_fee_estimates()?;
|
||||||
super::into_fee_rate(target, estimates)
|
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
|
||||||
|
target, estimates,
|
||||||
|
)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for EsploraBlockchain {
|
impl Deref for EsploraBlockchain {
|
||||||
type Target = UrlClient;
|
type Target = BlockingClient;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.url_client
|
&self.url_client
|
||||||
@ -112,20 +98,19 @@ impl StatelessBlockchain for EsploraBlockchain {}
|
|||||||
|
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(self.url_client._get_height()?)
|
Ok(self.url_client.get_height()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTx for EsploraBlockchain {
|
impl GetTx for EsploraBlockchain {
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
Ok(self.url_client._get_tx(txid)?)
|
Ok(self.url_client.get_tx(txid)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetBlockHash for EsploraBlockchain {
|
impl GetBlockHash for EsploraBlockchain {
|
||||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
let block_header = self.url_client._get_header(height as u32)?;
|
Ok(self.url_client.get_block_hash(height as u32)?)
|
||||||
Ok(block_header.block_hash())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +136,7 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
let client = self.url_client.clone();
|
let client = self.url_client.clone();
|
||||||
// make each request in its own thread.
|
// make each request in its own thread.
|
||||||
handles.push(std::thread::spawn(move || {
|
handles.push(std::thread::spawn(move || {
|
||||||
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
|
let mut related_txs: Vec<Tx> = client.scripthash_txs(&script, None)?;
|
||||||
|
|
||||||
let n_confirmed =
|
let n_confirmed =
|
||||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||||
@ -159,7 +144,7 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
// keep requesting to see if there's more.
|
// keep requesting to see if there's more.
|
||||||
if n_confirmed >= 25 {
|
if n_confirmed >= 25 {
|
||||||
loop {
|
loop {
|
||||||
let new_related_txs: Vec<Tx> = client._scripthash_txs(
|
let new_related_txs: Vec<Tx> = client.scripthash_txs(
|
||||||
&script,
|
&script,
|
||||||
Some(related_txs.last().unwrap().txid),
|
Some(related_txs.last().unwrap().txid),
|
||||||
)?;
|
)?;
|
||||||
@ -202,6 +187,7 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.get(txid)
|
.get(txid)
|
||||||
.expect("must be in index")
|
.expect("must be in index")
|
||||||
.confirmation_time()
|
.confirmation_time()
|
||||||
|
.map(Into::into)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
conftime_req.satisfy(conftimes)?
|
conftime_req.satisfy(conftimes)?
|
||||||
@ -226,159 +212,22 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UrlClient {
|
|
||||||
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.get(&format!("{}/tx/{}/raw", self.url, txid))
|
|
||||||
.call();
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
|
|
||||||
Err(ureq::Error::Status(code, _)) => {
|
|
||||||
if is_status_not_found(code) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(EsploraError::HttpResponse(code))
|
|
||||||
}
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
|
|
||||||
match self._get_tx(txid) {
|
|
||||||
Ok(Some(tx)) => Ok(tx),
|
|
||||||
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.get(&format!("{}/block-height/{}", self.url, block_height))
|
|
||||||
.call();
|
|
||||||
|
|
||||||
let bytes = match resp {
|
|
||||||
Ok(resp) => Ok(into_bytes(resp)?),
|
|
||||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let hash = std::str::from_utf8(&bytes)
|
|
||||||
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
|
|
||||||
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.get(&format!("{}/block/{}/header", self.url, hash))
|
|
||||||
.call();
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
|
|
||||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.post(&format!("{}/tx", self.url))
|
|
||||||
.send_string(&serialize(transaction).to_hex());
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
Ok(_) => Ok(()), // We do not return the txid?
|
|
||||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_height(&self) -> Result<u32, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.get(&format!("{}/blocks/tip/height", self.url))
|
|
||||||
.call();
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
Ok(resp) => Ok(resp.into_string()?.parse()?),
|
|
||||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
|
||||||
let resp = self
|
|
||||||
.agent
|
|
||||||
.get(&format!("{}/fee-estimates", self.url,))
|
|
||||||
.call();
|
|
||||||
|
|
||||||
let map = match resp {
|
|
||||||
Ok(resp) => {
|
|
||||||
let map: HashMap<String, f64> = resp.into_json()?;
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
|
||||||
Err(e) => Err(EsploraError::Ureq(e)),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _scripthash_txs(
|
|
||||||
&self,
|
|
||||||
script: &Script,
|
|
||||||
last_seen: Option<Txid>,
|
|
||||||
) -> Result<Vec<Tx>, EsploraError> {
|
|
||||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
|
||||||
let url = match last_seen {
|
|
||||||
Some(last_seen) => format!(
|
|
||||||
"{}/scripthash/{}/txs/chain/{}",
|
|
||||||
self.url, script_hash, last_seen
|
|
||||||
),
|
|
||||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
|
||||||
};
|
|
||||||
Ok(self.agent.get(&url).call()?.into_json()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_status_not_found(status: u16) -> bool {
|
|
||||||
status == 404
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
|
|
||||||
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
|
|
||||||
|
|
||||||
let mut buf: Vec<u8> = vec![];
|
|
||||||
resp.into_reader()
|
|
||||||
.take((BYTES_LIMIT + 1) as u64)
|
|
||||||
.read_to_end(&mut buf)?;
|
|
||||||
if buf.len() > BYTES_LIMIT {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
"response too big for into_bytes",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||||
type Config = super::EsploraBlockchainConfig;
|
type Config = super::EsploraBlockchainConfig;
|
||||||
|
|
||||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||||
let mut agent_builder = ureq::AgentBuilder::new();
|
let mut builder = Builder::new(config.base_url.as_str());
|
||||||
|
|
||||||
if let Some(timeout) = config.timeout {
|
if let Some(timeout) = config.timeout {
|
||||||
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
|
builder = builder.timeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(proxy) = &config.proxy {
|
if let Some(proxy) = &config.proxy {
|
||||||
agent_builder = agent_builder
|
builder = builder.proxy(proxy);
|
||||||
.proxy(Proxy::new(proxy).map_err(|e| Error::Esplora(Box::new(e.into())))?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
|
let mut blockchain =
|
||||||
.with_agent(agent_builder.build());
|
EsploraBlockchain::from_client(builder.build_blocking()?, config.stop_gap);
|
||||||
|
|
||||||
if let Some(concurrency) = config.concurrency {
|
if let Some(concurrency) = config.concurrency {
|
||||||
blockchain = blockchain.with_concurrency(concurrency);
|
blockchain = blockchain.with_concurrency(concurrency);
|
||||||
@ -387,12 +236,3 @@ impl ConfigurableBlockchain for EsploraBlockchain {
|
|||||||
Ok(blockchain)
|
Ok(blockchain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ureq::Error> for EsploraError {
|
|
||||||
fn from(e: ureq::Error) -> Self {
|
|
||||||
match e {
|
|
||||||
ureq::Error::Status(code, _) => EsploraError::HttpResponse(code),
|
|
||||||
e => EsploraError::Ureq(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,89 +15,25 @@
|
|||||||
//! depending on your needs (blocking or async respectively).
|
//! depending on your needs (blocking or async respectively).
|
||||||
//!
|
//!
|
||||||
//! Please note, to configure the Esplora HTTP client correctly use one of:
|
//! Please note, to configure the Esplora HTTP client correctly use one of:
|
||||||
//! Blocking: --features='esplora,ureq'
|
//! Blocking: --features='use-esplora-blocking'
|
||||||
//! Async: --features='async-interface,esplora,reqwest' --no-default-features
|
//! Async: --features='async-interface,use-esplora-async' --no-default-features
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use bitcoin::consensus;
|
pub use esplora_client::Error as EsploraError;
|
||||||
use bitcoin::{BlockHash, Txid};
|
|
||||||
|
|
||||||
use crate::error::Error;
|
#[cfg(feature = "use-esplora-async")]
|
||||||
use crate::FeeRate;
|
mod r#async;
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "use-esplora-async")]
|
||||||
mod reqwest;
|
pub use self::r#async::*;
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "use-esplora-blocking")]
|
||||||
pub use self::reqwest::*;
|
mod blocking;
|
||||||
|
|
||||||
#[cfg(feature = "ureq")]
|
#[cfg(feature = "use-esplora-blocking")]
|
||||||
mod ureq;
|
pub use self::blocking::*;
|
||||||
|
|
||||||
#[cfg(feature = "ureq")]
|
|
||||||
pub use self::ureq::*;
|
|
||||||
|
|
||||||
mod api;
|
|
||||||
|
|
||||||
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
|
|
||||||
let fee_val = {
|
|
||||||
let mut pairs = estimates
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
pairs.sort_unstable_by_key(|(k, _)| std::cmp::Reverse(*k));
|
|
||||||
pairs
|
|
||||||
.into_iter()
|
|
||||||
.find(|(k, _)| k <= &target)
|
|
||||||
.map(|(_, v)| v)
|
|
||||||
.unwrap_or(1.0)
|
|
||||||
};
|
|
||||||
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors that can happen during a sync with [`EsploraBlockchain`]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum EsploraError {
|
|
||||||
/// Error during ureq HTTP request
|
|
||||||
#[cfg(feature = "ureq")]
|
|
||||||
Ureq(::ureq::Error),
|
|
||||||
/// Transport error during the ureq HTTP call
|
|
||||||
#[cfg(feature = "ureq")]
|
|
||||||
UreqTransport(::ureq::Transport),
|
|
||||||
/// Error during reqwest HTTP request
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
Reqwest(::reqwest::Error),
|
|
||||||
/// HTTP response error
|
|
||||||
HttpResponse(u16),
|
|
||||||
/// IO error during ureq response read
|
|
||||||
Io(io::Error),
|
|
||||||
/// No header found in ureq response
|
|
||||||
NoHeader,
|
|
||||||
/// Invalid number returned
|
|
||||||
Parsing(std::num::ParseIntError),
|
|
||||||
/// Invalid Bitcoin data returned
|
|
||||||
BitcoinEncoding(bitcoin::consensus::encode::Error),
|
|
||||||
/// Invalid Hex data returned
|
|
||||||
Hex(bitcoin::hashes::hex::Error),
|
|
||||||
|
|
||||||
/// Transaction not found
|
|
||||||
TransactionNotFound(Txid),
|
|
||||||
/// Header height not found
|
|
||||||
HeaderHeightNotFound(u32),
|
|
||||||
/// Header hash not found
|
|
||||||
HeaderHashNotFound(BlockHash),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for EsploraError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for an [`EsploraBlockchain`]
|
/// Configuration for an [`EsploraBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||||
pub struct EsploraBlockchainConfig {
|
pub struct EsploraBlockchainConfig {
|
||||||
/// Base URL of the esplora service
|
/// Base URL of the esplora service
|
||||||
///
|
///
|
||||||
@ -138,16 +74,11 @@ impl EsploraBlockchainConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for EsploraError {}
|
impl From<esplora_client::BlockTime> for crate::BlockTime {
|
||||||
|
fn from(esplora_client::BlockTime { timestamp, height }: esplora_client::BlockTime) -> Self {
|
||||||
#[cfg(feature = "ureq")]
|
Self { timestamp, height }
|
||||||
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
|
}
|
||||||
#[cfg(feature = "reqwest")]
|
}
|
||||||
impl_error!(::reqwest::Error, Reqwest, EsploraError);
|
|
||||||
impl_error!(io::Error, Io, EsploraError);
|
|
||||||
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
|
|
||||||
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
|
|
||||||
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "test-esplora")]
|
#[cfg(feature = "test-esplora")]
|
||||||
@ -161,58 +92,11 @@ const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn feerate_parsing() {
|
|
||||||
let esplora_fees = serde_json::from_str::<HashMap<String, f64>>(
|
|
||||||
r#"{
|
|
||||||
"25": 1.015,
|
|
||||||
"5": 2.3280000000000003,
|
|
||||||
"12": 2.0109999999999997,
|
|
||||||
"15": 1.018,
|
|
||||||
"17": 1.018,
|
|
||||||
"11": 2.0109999999999997,
|
|
||||||
"3": 3.01,
|
|
||||||
"2": 4.9830000000000005,
|
|
||||||
"6": 2.2359999999999998,
|
|
||||||
"21": 1.018,
|
|
||||||
"13": 1.081,
|
|
||||||
"7": 2.2359999999999998,
|
|
||||||
"8": 2.2359999999999998,
|
|
||||||
"16": 1.018,
|
|
||||||
"20": 1.018,
|
|
||||||
"22": 1.017,
|
|
||||||
"23": 1.017,
|
|
||||||
"504": 1,
|
|
||||||
"9": 2.2359999999999998,
|
|
||||||
"14": 1.018,
|
|
||||||
"10": 2.0109999999999997,
|
|
||||||
"24": 1.017,
|
|
||||||
"1008": 1,
|
|
||||||
"1": 4.9830000000000005,
|
|
||||||
"4": 2.3280000000000003,
|
|
||||||
"19": 1.018,
|
|
||||||
"144": 1,
|
|
||||||
"18": 1.018
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
into_fee_rate(6, esplora_fees.clone()).unwrap(),
|
|
||||||
FeeRate::from_sat_per_vb(2.236)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
into_fee_rate(26, esplora_fees).unwrap(),
|
|
||||||
FeeRate::from_sat_per_vb(1.015),
|
|
||||||
"should inherit from value for 25"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "test-esplora")]
|
#[cfg(feature = "test-esplora")]
|
||||||
fn test_esplora_with_variable_configs() {
|
fn test_esplora_with_variable_configs() {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
use crate::testutils::{
|
use crate::testutils::{
|
||||||
blockchain_tests::TestClient,
|
blockchain_tests::TestClient,
|
||||||
configurable_blockchain_tests::ConfigurableBlockchainTester,
|
configurable_blockchain_tests::ConfigurableBlockchainTester,
|
||||||
|
@ -77,7 +77,7 @@ impl Deref for RpcBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RpcBlockchain configuration options
|
/// RpcBlockchain configuration options
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub struct RpcConfig {
|
pub struct RpcConfig {
|
||||||
/// The bitcoin node url
|
/// The bitcoin node url
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@ -96,7 +96,7 @@ pub struct RpcConfig {
|
|||||||
/// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with
|
/// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with
|
||||||
/// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining
|
/// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining
|
||||||
/// how the `importdescriptors` RPC calls are to be made.
|
/// how the `importdescriptors` RPC calls are to be made.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub struct RpcSyncParams {
|
pub struct RpcSyncParams {
|
||||||
/// The minimum number of scripts to scan for on initial sync.
|
/// The minimum number of scripts to scan for on initial sync.
|
||||||
pub start_script_count: usize,
|
pub start_script_count: usize,
|
||||||
@ -167,7 +167,7 @@ impl Blockchain for RpcBlockchain {
|
|||||||
.estimate_smart_fee(target as u16, None)?
|
.estimate_smart_fee(target as u16, None)?
|
||||||
.fee_rate
|
.fee_rate
|
||||||
.ok_or(Error::FeeRateUnavailable)?
|
.ok_or(Error::FeeRateUnavailable)?
|
||||||
.as_sat() as f64;
|
.to_sat() as f64;
|
||||||
|
|
||||||
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
|
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
|
||||||
}
|
}
|
||||||
@ -410,7 +410,12 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
|
|||||||
updated = true;
|
updated = true;
|
||||||
TransactionDetails {
|
TransactionDetails {
|
||||||
txid: tx_res.info.txid,
|
txid: tx_res.info.txid,
|
||||||
..Default::default()
|
transaction: None,
|
||||||
|
|
||||||
|
received: 0,
|
||||||
|
sent: 0,
|
||||||
|
fee: None,
|
||||||
|
confirmation_time: None,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -430,7 +435,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
|
|||||||
// update fee (if needed)
|
// update fee (if needed)
|
||||||
if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) {
|
if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) {
|
||||||
updated = true;
|
updated = true;
|
||||||
db_tx.fee = Some(new_fee.as_sat().unsigned_abs());
|
db_tx.fee = Some(new_fee.to_sat().unsigned_abs());
|
||||||
}
|
}
|
||||||
|
|
||||||
// update confirmation time (if needed)
|
// update confirmation time (if needed)
|
||||||
@ -603,7 +608,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
|
|||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: OutPoint::new(entry.txid, entry.vout),
|
outpoint: OutPoint::new(entry.txid, entry.vout),
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value: entry.amount.as_sat(),
|
value: entry.amount.to_sat(),
|
||||||
script_pubkey: entry.script_pub_key,
|
script_pubkey: entry.script_pub_key,
|
||||||
},
|
},
|
||||||
keychain,
|
keychain,
|
||||||
@ -873,15 +878,13 @@ impl BlockchainFactory for RpcBlockchainFactory {
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
descriptor::{into_wallet_descriptor_checked, AsDerived},
|
descriptor::into_wallet_descriptor_checked, testutils::blockchain_tests::TestClient,
|
||||||
testutils::blockchain_tests::TestClient,
|
|
||||||
wallet::utils::SecpCtx,
|
wallet::utils::SecpCtx,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitcoin::{Address, Network};
|
use bitcoin::{Address, Network};
|
||||||
use bitcoincore_rpc::RpcApi;
|
use bitcoincore_rpc::RpcApi;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use miniscript::DescriptorTrait;
|
|
||||||
|
|
||||||
crate::bdk_blockchain_tests! {
|
crate::bdk_blockchain_tests! {
|
||||||
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
||||||
@ -958,7 +961,7 @@ mod test {
|
|||||||
|
|
||||||
// generate scripts (1 tx per script)
|
// generate scripts (1 tx per script)
|
||||||
let scripts = (0..TX_COUNT)
|
let scripts = (0..TX_COUNT)
|
||||||
.map(|index| desc.as_derived(index, &secp).script_pubkey())
|
.map(|index| desc.at_derivation_index(index).script_pubkey())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// import scripts and wait
|
// import scripts and wait
|
||||||
|
@ -497,7 +497,7 @@ macro_rules! populate_test_db {
|
|||||||
}
|
}
|
||||||
let tx = $crate::bitcoin::Transaction {
|
let tx = $crate::bitcoin::Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: bitcoin::PackedLockTime(0),
|
||||||
input,
|
input,
|
||||||
output: tx_meta
|
output: tx_meta
|
||||||
.output
|
.output
|
||||||
|
@ -1,210 +0,0 @@
|
|||||||
// Bitcoin Dev Kit
|
|
||||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
|
||||||
//
|
|
||||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
|
||||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
|
||||||
// You may not use this file except in accordance with one or both of these
|
|
||||||
// licenses.
|
|
||||||
|
|
||||||
//! Derived descriptor keys
|
|
||||||
//!
|
|
||||||
//! The [`DerivedDescriptorKey`] type is a wrapper over the standard [`DescriptorPublicKey`] which
|
|
||||||
//! guarantees that all the extended keys have a fixed derivation path, i.e. all the wildcards have
|
|
||||||
//! been replaced by actual derivation indexes.
|
|
||||||
//!
|
|
||||||
//! The [`AsDerived`] trait provides a quick way to derive descriptors to obtain a
|
|
||||||
//! `Descriptor<DerivedDescriptorKey>` type. This, in turn, can be used to derive public
|
|
||||||
//! keys for arbitrary derivation indexes.
|
|
||||||
//!
|
|
||||||
//! Combining this with [`Wallet::get_signers`], secret keys can also be derived.
|
|
||||||
//!
|
|
||||||
//! # Example
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! # use std::str::FromStr;
|
|
||||||
//! # use bitcoin::secp256k1::Secp256k1;
|
|
||||||
//! use bdk::descriptor::{AsDerived, DescriptorPublicKey};
|
|
||||||
//! use bdk::miniscript::{ToPublicKey, TranslatePk, MiniscriptKey};
|
|
||||||
//!
|
|
||||||
//! let secp = Secp256k1::gen_new();
|
|
||||||
//!
|
|
||||||
//! let key = DescriptorPublicKey::from_str("[aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/*")?;
|
|
||||||
//! let (descriptor, _, _) = bdk::descriptor!(wpkh(key))?;
|
|
||||||
//!
|
|
||||||
//! // derived: wpkh([aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/42)#3ladd0t2
|
|
||||||
//! let derived = descriptor.as_derived(42, &secp);
|
|
||||||
//! println!("derived: {}", derived);
|
|
||||||
//!
|
|
||||||
//! // with_pks: wpkh(02373ecb54c5e83bd7e0d40adf78b65efaf12fafb13571f0261fc90364eee22e1e)#p4jjgvll
|
|
||||||
//! let with_pks = derived.translate_pk_infallible(|pk| pk.to_public_key(), |pkh| pkh.to_public_key().to_pubkeyhash());
|
|
||||||
//! println!("with_pks: {}", with_pks);
|
|
||||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::fmt;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use bitcoin::hashes::hash160;
|
|
||||||
use bitcoin::{PublicKey, XOnlyPublicKey};
|
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard};
|
|
||||||
use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk};
|
|
||||||
|
|
||||||
use crate::wallet::utils::SecpCtx;
|
|
||||||
|
|
||||||
/// Extended [`DescriptorPublicKey`] that has been derived
|
|
||||||
///
|
|
||||||
/// Derived keys are guaranteed to never contain wildcards of any kind
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx);
|
|
||||||
|
|
||||||
impl<'s> DerivedDescriptorKey<'s> {
|
|
||||||
/// Construct a new derived key
|
|
||||||
///
|
|
||||||
/// Panics if the key is wildcard
|
|
||||||
pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> {
|
|
||||||
if let DescriptorPublicKey::XPub(xpub) = &key {
|
|
||||||
assert!(xpub.wildcard == Wildcard::None)
|
|
||||||
}
|
|
||||||
|
|
||||||
DerivedDescriptorKey(key, secp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Deref for DerivedDescriptorKey<'s> {
|
|
||||||
type Target = DescriptorPublicKey;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> PartialEq for DerivedDescriptorKey<'s> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.0 == other.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Eq for DerivedDescriptorKey<'s> {}
|
|
||||||
|
|
||||||
impl<'s> PartialOrd for DerivedDescriptorKey<'s> {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
self.0.partial_cmp(&other.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Ord for DerivedDescriptorKey<'s> {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.0.cmp(&other.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> fmt::Display for DerivedDescriptorKey<'s> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Hash for DerivedDescriptorKey<'s> {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.0.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
|
|
||||||
type Hash = Self;
|
|
||||||
|
|
||||||
fn to_pubkeyhash(&self) -> Self::Hash {
|
|
||||||
DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_uncompressed(&self) -> bool {
|
|
||||||
self.0.is_uncompressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
|
||||||
fn to_public_key(&self) -> PublicKey {
|
|
||||||
match &self.0 {
|
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
|
||||||
key: SinglePubKey::XOnly(_),
|
|
||||||
..
|
|
||||||
}) => panic!("Found x-only public key in non-tr descriptor"),
|
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
|
||||||
key: SinglePubKey::FullKey(ref pk),
|
|
||||||
..
|
|
||||||
}) => *pk,
|
|
||||||
DescriptorPublicKey::XPub(ref xpub) => PublicKey::new(
|
|
||||||
xpub.xkey
|
|
||||||
.derive_pub(self.1, &xpub.derivation_path)
|
|
||||||
.expect("Shouldn't fail, only normal derivations")
|
|
||||||
.public_key,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_x_only_pubkey(&self) -> XOnlyPublicKey {
|
|
||||||
match &self.0 {
|
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
|
||||||
key: SinglePubKey::XOnly(ref pk),
|
|
||||||
..
|
|
||||||
}) => *pk,
|
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
|
||||||
key: SinglePubKey::FullKey(ref pk),
|
|
||||||
..
|
|
||||||
}) => XOnlyPublicKey::from(pk.inner),
|
|
||||||
DescriptorPublicKey::XPub(ref xpub) => XOnlyPublicKey::from(
|
|
||||||
xpub.xkey
|
|
||||||
.derive_pub(self.1, &xpub.derivation_path)
|
|
||||||
.expect("Shouldn't fail, only normal derivations")
|
|
||||||
.public_key,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash {
|
|
||||||
hash.to_public_key().to_pubkeyhash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utilities to derive descriptors
|
|
||||||
///
|
|
||||||
/// Check out the [module level] documentation for more.
|
|
||||||
///
|
|
||||||
/// [module level]: crate::descriptor::derived
|
|
||||||
pub trait AsDerived {
|
|
||||||
/// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
|
|
||||||
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
|
|
||||||
-> Descriptor<DerivedDescriptorKey<'s>>;
|
|
||||||
|
|
||||||
/// Transform the keys into `DerivedDescriptorKey`.
|
|
||||||
///
|
|
||||||
/// Panics if the descriptor is not "fixed", i.e. if it's derivable
|
|
||||||
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsDerived for Descriptor<DescriptorPublicKey> {
|
|
||||||
fn as_derived<'s>(
|
|
||||||
&self,
|
|
||||||
index: u32,
|
|
||||||
secp: &'s SecpCtx,
|
|
||||||
) -> Descriptor<DerivedDescriptorKey<'s>> {
|
|
||||||
self.derive(index).translate_pk_infallible(
|
|
||||||
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
|
||||||
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>> {
|
|
||||||
assert!(!self.is_deriveable());
|
|
||||||
|
|
||||||
self.as_derived(0, secp)
|
|
||||||
}
|
|
||||||
}
|
|
@ -700,10 +700,10 @@ macro_rules! fragment {
|
|||||||
$crate::keys::make_pkh($key, &secp)
|
$crate::keys::make_pkh($key, &secp)
|
||||||
});
|
});
|
||||||
( after ( $value:expr ) ) => ({
|
( after ( $value:expr ) ) => ({
|
||||||
$crate::impl_leaf_opcode_value!(After, $value)
|
$crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302
|
||||||
});
|
});
|
||||||
( older ( $value:expr ) ) => ({
|
( older ( $value:expr ) ) => ({
|
||||||
$crate::impl_leaf_opcode_value!(Older, $value)
|
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||||
});
|
});
|
||||||
( sha256 ( $hash:expr ) ) => ({
|
( sha256 ( $hash:expr ) ) => ({
|
||||||
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
||||||
@ -795,7 +795,7 @@ macro_rules! fragment {
|
|||||||
mod test {
|
mod test {
|
||||||
use bitcoin::hashes::hex::ToHex;
|
use bitcoin::hashes::hex::ToHex;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||||
use miniscript::{Descriptor, Legacy, Segwitv0};
|
use miniscript::{Descriptor, Legacy, Segwitv0};
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@ -806,8 +806,6 @@ mod test {
|
|||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::PrivateKey;
|
use bitcoin::PrivateKey;
|
||||||
|
|
||||||
use crate::descriptor::derived::AsDerived;
|
|
||||||
|
|
||||||
// test the descriptor!() macro
|
// test the descriptor!() macro
|
||||||
|
|
||||||
// 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)
|
||||||
@ -817,17 +815,15 @@ mod test {
|
|||||||
is_fixed: bool,
|
is_fixed: bool,
|
||||||
expected: &[&str],
|
expected: &[&str],
|
||||||
) {
|
) {
|
||||||
let secp = Secp256k1::new();
|
|
||||||
|
|
||||||
let (desc, _key_map, _networks) = desc.unwrap();
|
let (desc, _key_map, _networks) = desc.unwrap();
|
||||||
assert_eq!(desc.is_witness(), is_witness);
|
assert_eq!(desc.is_witness(), is_witness);
|
||||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||||
for i in 0..expected.len() {
|
for i in 0..expected.len() {
|
||||||
let index = i as u32;
|
let index = i as u32;
|
||||||
let child_desc = if !desc.is_deriveable() {
|
let child_desc = if !desc.has_wildcard() {
|
||||||
desc.as_derived_fixed(&secp)
|
desc.at_derivation_index(0)
|
||||||
} else {
|
} else {
|
||||||
desc.as_derived(index, &secp)
|
desc.at_derivation_index(index)
|
||||||
};
|
};
|
||||||
let address = child_desc.address(Regtest);
|
let address = child_desc.address(Regtest);
|
||||||
if let Ok(address) = address {
|
if let Ok(address) = address {
|
||||||
|
@ -15,24 +15,22 @@
|
|||||||
//! from [`miniscript`].
|
//! from [`miniscript`].
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||||
use bitcoin::util::{psbt, taproot};
|
use bitcoin::util::{psbt, taproot};
|
||||||
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||||
use bitcoin::{Network, Script, TxOut};
|
use bitcoin::{Network, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
|
use miniscript::descriptor::{DefiniteDescriptorKey, DescriptorType, InnerXKey, SinglePubKey};
|
||||||
pub use miniscript::{
|
pub use miniscript::{
|
||||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||||
};
|
};
|
||||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
|
||||||
|
|
||||||
use crate::descriptor::policy::BuildSatisfaction;
|
use crate::descriptor::policy::BuildSatisfaction;
|
||||||
|
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
pub mod derived;
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod dsl;
|
pub mod dsl;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@ -41,7 +39,6 @@ pub mod template;
|
|||||||
|
|
||||||
pub use self::checksum::calc_checksum;
|
pub use self::checksum::calc_checksum;
|
||||||
use self::checksum::calc_checksum_bytes;
|
use self::checksum::calc_checksum_bytes;
|
||||||
pub use self::derived::{AsDerived, DerivedDescriptorKey};
|
|
||||||
pub use self::error::Error as DescriptorError;
|
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;
|
||||||
@ -53,7 +50,7 @@ use crate::wallet::utils::SecpCtx;
|
|||||||
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
||||||
|
|
||||||
/// Alias for a [`Descriptor`] that contains extended **derived** keys
|
/// Alias for a [`Descriptor`] that contains extended **derived** keys
|
||||||
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
|
||||||
|
|
||||||
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
||||||
/// [`psbt::Output`]
|
/// [`psbt::Output`]
|
||||||
@ -129,28 +126,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
|||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
use crate::keys::DescriptorKey;
|
use crate::keys::DescriptorKey;
|
||||||
|
|
||||||
let check_key = |pk: &DescriptorPublicKey| {
|
struct Translator<'s, 'd> {
|
||||||
let (pk, _, networks) = if self.0.is_witness() {
|
secp: &'s SecpCtx,
|
||||||
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
descriptor: &'d ExtendedDescriptor,
|
||||||
pk.clone().into_descriptor_key()?;
|
network: Network,
|
||||||
descriptor_key.extract(secp)?
|
}
|
||||||
} else {
|
|
||||||
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
|
||||||
pk.clone().into_descriptor_key()?;
|
|
||||||
descriptor_key.extract(secp)?
|
|
||||||
};
|
|
||||||
|
|
||||||
if networks.contains(&network) {
|
impl<'s, 'd>
|
||||||
Ok(pk)
|
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
|
||||||
} else {
|
for Translator<'s, 'd>
|
||||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
{
|
||||||
|
fn pk(
|
||||||
|
&mut self,
|
||||||
|
pk: &DescriptorPublicKey,
|
||||||
|
) -> Result<miniscript::DummyKey, DescriptorError> {
|
||||||
|
let secp = &self.secp;
|
||||||
|
|
||||||
|
let (_, _, networks) = if self.descriptor.is_taproot() {
|
||||||
|
let descriptor_key: DescriptorKey<miniscript::Tap> =
|
||||||
|
pk.clone().into_descriptor_key()?;
|
||||||
|
descriptor_key.extract(secp)?
|
||||||
|
} else if self.descriptor.is_witness() {
|
||||||
|
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||||
|
pk.clone().into_descriptor_key()?;
|
||||||
|
descriptor_key.extract(secp)?
|
||||||
|
} else {
|
||||||
|
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||||
|
pk.clone().into_descriptor_key()?;
|
||||||
|
descriptor_key.extract(secp)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if networks.contains(&self.network) {
|
||||||
|
Ok(miniscript::DummyKey)
|
||||||
|
} else {
|
||||||
|
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
fn sha256(
|
||||||
|
&mut self,
|
||||||
|
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
|
||||||
|
) -> Result<miniscript::DummySha256Hash, DescriptorError> {
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
fn hash256(
|
||||||
|
&mut self,
|
||||||
|
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
|
||||||
|
) -> Result<miniscript::DummyHash256Hash, DescriptorError> {
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
fn ripemd160(
|
||||||
|
&mut self,
|
||||||
|
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
|
||||||
|
) -> Result<miniscript::DummyRipemd160Hash, DescriptorError> {
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
fn hash160(
|
||||||
|
&mut self,
|
||||||
|
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
|
||||||
|
) -> Result<miniscript::DummyHash160Hash, DescriptorError> {
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check the network for the keys
|
// check the network for the keys
|
||||||
let translated = self.0.translate_pk(check_key, check_key)?;
|
self.0.translate_pk(&mut Translator {
|
||||||
|
secp,
|
||||||
|
network,
|
||||||
|
descriptor: &self.0,
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok((translated, self.1))
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +205,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
|||||||
_secp: &SecpCtx,
|
_secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
let valid_networks = &self.2;
|
struct Translator {
|
||||||
|
network: Network,
|
||||||
|
}
|
||||||
|
|
||||||
let fix_key = |pk: &DescriptorPublicKey| {
|
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
|
||||||
if valid_networks.contains(&network) {
|
for Translator
|
||||||
|
{
|
||||||
|
fn pk(
|
||||||
|
&mut self,
|
||||||
|
pk: &DescriptorPublicKey,
|
||||||
|
) -> Result<DescriptorPublicKey, DescriptorError> {
|
||||||
// workaround for xpubs generated by other key types, like bip39: since when the
|
// workaround for xpubs generated by other key types, like bip39: since when the
|
||||||
// conversion is made one network has to be chosen, what we generally choose
|
// conversion is made one network has to be chosen, what we generally choose
|
||||||
// "mainnet", but then override the set of valid networks to specify that all of
|
// "mainnet", but then override the set of valid networks to specify that all of
|
||||||
@ -172,7 +224,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
|||||||
let pk = match pk {
|
let pk = match pk {
|
||||||
DescriptorPublicKey::XPub(ref xpub) => {
|
DescriptorPublicKey::XPub(ref xpub) => {
|
||||||
let mut xpub = xpub.clone();
|
let mut xpub = xpub.clone();
|
||||||
xpub.xkey.network = network;
|
xpub.xkey.network = self.network;
|
||||||
|
|
||||||
DescriptorPublicKey::XPub(xpub)
|
DescriptorPublicKey::XPub(xpub)
|
||||||
}
|
}
|
||||||
@ -180,13 +232,20 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(pk)
|
Ok(pk)
|
||||||
} else {
|
|
||||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
|
||||||
}
|
}
|
||||||
};
|
miniscript::translate_hash_clone!(
|
||||||
|
DescriptorPublicKey,
|
||||||
|
DescriptorPublicKey,
|
||||||
|
DescriptorError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.2.contains(&network) {
|
||||||
|
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||||
|
}
|
||||||
|
|
||||||
// fixup the network for keys that need it
|
// fixup the network for keys that need it
|
||||||
let translated = self.0.translate_pk(fix_key, fix_key)?;
|
let translated = self.0.translate_pk(&mut Translator { network })?;
|
||||||
|
|
||||||
Ok((translated, self.1))
|
Ok((translated, self.1))
|
||||||
}
|
}
|
||||||
@ -207,7 +266,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
|||||||
derivation_path,
|
derivation_path,
|
||||||
wildcard,
|
wildcard,
|
||||||
..
|
..
|
||||||
}) = k.as_key()
|
}) = k
|
||||||
{
|
{
|
||||||
return *wildcard == Wildcard::Hardened
|
return *wildcard == Wildcard::Hardened
|
||||||
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
||||||
@ -254,7 +313,6 @@ pub trait ExtractPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait XKeyUtils {
|
pub(crate) trait XKeyUtils {
|
||||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
|
|
||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,27 +320,6 @@ impl<T> XKeyUtils for DescriptorXKey<T>
|
|||||||
where
|
where
|
||||||
T: InnerXKey,
|
T: InnerXKey,
|
||||||
{
|
{
|
||||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
|
||||||
let full_path = match self.origin {
|
|
||||||
Some((_, ref path)) => path
|
|
||||||
.into_iter()
|
|
||||||
.chain(self.derivation_path.into_iter())
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
None => self.derivation_path.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.wildcard != Wildcard::None {
|
|
||||||
full_path
|
|
||||||
.into_iter()
|
|
||||||
.chain(append.iter())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
full_path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||||
match self.origin {
|
match self.origin {
|
||||||
Some((fingerprint, _)) => fingerprint,
|
Some((fingerprint, _)) => fingerprint,
|
||||||
@ -291,11 +328,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DerivedDescriptorMeta {
|
|
||||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths;
|
|
||||||
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait DescriptorMeta {
|
pub(crate) trait DescriptorMeta {
|
||||||
fn is_witness(&self) -> bool;
|
fn is_witness(&self) -> bool;
|
||||||
fn is_taproot(&self) -> bool;
|
fn is_taproot(&self) -> bool;
|
||||||
@ -304,63 +336,23 @@ pub(crate) trait DescriptorMeta {
|
|||||||
&self,
|
&self,
|
||||||
hd_keypaths: &HdKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>>;
|
) -> Option<DerivedDescriptor>;
|
||||||
fn derive_from_tap_key_origins<'s>(
|
fn derive_from_tap_key_origins<'s>(
|
||||||
&self,
|
&self,
|
||||||
tap_key_origins: &TapKeyOrigins,
|
tap_key_origins: &TapKeyOrigins,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>>;
|
) -> Option<DerivedDescriptor>;
|
||||||
fn derive_from_psbt_key_origins<'s>(
|
fn derive_from_psbt_key_origins<'s>(
|
||||||
&self,
|
&self,
|
||||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>>;
|
) -> Option<DerivedDescriptor>;
|
||||||
fn derive_from_psbt_input<'s>(
|
fn derive_from_psbt_input<'s>(
|
||||||
&self,
|
&self,
|
||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
utxo: Option<TxOut>,
|
utxo: Option<TxOut>,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>>;
|
) -> Option<DerivedDescriptor>;
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait DescriptorScripts {
|
|
||||||
fn psbt_redeem_script(&self) -> Option<Script>;
|
|
||||||
fn psbt_witness_script(&self) -> Option<Script>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
|
||||||
fn psbt_redeem_script(&self) -> Option<Script> {
|
|
||||||
match self.desc_type() {
|
|
||||||
DescriptorType::ShWpkh => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::ShWsh => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
|
||||||
DescriptorType::Sh => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::Bare => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::ShSortedMulti => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::ShWshSortedMulti => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
|
||||||
DescriptorType::Pkh
|
|
||||||
| DescriptorType::Wpkh
|
|
||||||
| DescriptorType::Tr
|
|
||||||
| DescriptorType::Wsh
|
|
||||||
| DescriptorType::WshSortedMulti => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn psbt_witness_script(&self) -> Option<Script> {
|
|
||||||
match self.desc_type() {
|
|
||||||
DescriptorType::Wsh => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
|
|
||||||
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
|
||||||
Some(self.explicit_script().unwrap())
|
|
||||||
}
|
|
||||||
DescriptorType::Bare
|
|
||||||
| DescriptorType::Sh
|
|
||||||
| DescriptorType::Pkh
|
|
||||||
| DescriptorType::Wpkh
|
|
||||||
| DescriptorType::ShSortedMulti
|
|
||||||
| DescriptorType::Tr
|
|
||||||
| DescriptorType::ShWpkh => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescriptorMeta for ExtendedDescriptor {
|
impl DescriptorMeta for ExtendedDescriptor {
|
||||||
@ -384,7 +376,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
let mut answer = Vec::new();
|
let mut answer = Vec::new();
|
||||||
|
|
||||||
self.for_each_key(|pk| {
|
self.for_each_key(|pk| {
|
||||||
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
|
if let DescriptorPublicKey::XPub(xpub) = pk {
|
||||||
answer.push(xpub.clone());
|
answer.push(xpub.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,7 +390,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
&self,
|
&self,
|
||||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>> {
|
) -> Option<DerivedDescriptor> {
|
||||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||||
path: &DerivationPath,
|
path: &DerivationPath,
|
||||||
@ -420,7 +412,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
|
|
||||||
// using `for_any_key` should make this stop as soon as we return `true`
|
// using `for_any_key` should make this stop as soon as we return `true`
|
||||||
self.for_any_key(|key| {
|
self.for_any_key(|key| {
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
if let DescriptorPublicKey::XPub(xpub) = key {
|
||||||
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
|
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
|
||||||
// return the "prefix" that matched, so we remove that prefix from the full path
|
// return the "prefix" that matched, so we remove that prefix from the full path
|
||||||
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
|
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
|
||||||
@ -478,14 +470,14 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
false
|
false
|
||||||
});
|
});
|
||||||
|
|
||||||
path_found.map(|path| self.as_derived(path, secp))
|
path_found.map(|path| self.at_derivation_index(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_from_hd_keypaths<'s>(
|
fn derive_from_hd_keypaths<'s>(
|
||||||
&self,
|
&self,
|
||||||
hd_keypaths: &HdKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>> {
|
) -> Option<DerivedDescriptor> {
|
||||||
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
||||||
let key_origins = hd_keypaths
|
let key_origins = hd_keypaths
|
||||||
.iter()
|
.iter()
|
||||||
@ -503,7 +495,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
&self,
|
&self,
|
||||||
tap_key_origins: &TapKeyOrigins,
|
tap_key_origins: &TapKeyOrigins,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>> {
|
) -> Option<DerivedDescriptor> {
|
||||||
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
|
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
|
||||||
let key_origins = tap_key_origins
|
let key_origins = tap_key_origins
|
||||||
.iter()
|
.iter()
|
||||||
@ -517,19 +509,19 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
utxo: Option<TxOut>,
|
utxo: Option<TxOut>,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>> {
|
) -> Option<DerivedDescriptor> {
|
||||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||||
return Some(derived);
|
return Some(derived);
|
||||||
}
|
}
|
||||||
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
||||||
return Some(derived);
|
return Some(derived);
|
||||||
}
|
}
|
||||||
if self.is_deriveable() {
|
if self.has_wildcard() {
|
||||||
// We can't try to bruteforce the derivation index, exit here
|
// We can't try to bruteforce the derivation index, exit here
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let descriptor = self.as_derived_fixed(secp);
|
let descriptor = self.at_derivation_index(0);
|
||||||
match descriptor.desc_type() {
|
match descriptor.desc_type() {
|
||||||
// TODO: add pk() here
|
// TODO: add pk() here
|
||||||
DescriptorType::Pkh
|
DescriptorType::Pkh
|
||||||
@ -563,86 +555,6 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
|
||||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths {
|
|
||||||
let mut answer = BTreeMap::new();
|
|
||||||
self.for_each_key(|key| {
|
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
|
||||||
let derived_pubkey = xpub
|
|
||||||
.xkey
|
|
||||||
.derive_pub(secp, &xpub.derivation_path)
|
|
||||||
.expect("Derivation can't fail");
|
|
||||||
|
|
||||||
answer.insert(
|
|
||||||
derived_pubkey.public_key,
|
|
||||||
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
});
|
|
||||||
|
|
||||||
answer
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins {
|
|
||||||
use miniscript::ToPublicKey;
|
|
||||||
|
|
||||||
let mut answer = BTreeMap::new();
|
|
||||||
let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| {
|
|
||||||
let key_origin = match pk.deref() {
|
|
||||||
DescriptorPublicKey::XPub(xpub) => {
|
|
||||||
Some((xpub.root_fingerprint(secp), xpub.full_path(&[])))
|
|
||||||
}
|
|
||||||
DescriptorPublicKey::SinglePub(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If this is the internal key, we only insert the key origin if it's not None.
|
|
||||||
// For keys found in the tap tree we always insert a key origin (because the signer
|
|
||||||
// looks for it to know which leaves to sign for), even though it may be None
|
|
||||||
match (lh, key_origin) {
|
|
||||||
(None, Some(ko)) => {
|
|
||||||
answer
|
|
||||||
.entry(pk.to_x_only_pubkey())
|
|
||||||
.or_insert_with(|| (vec![], ko));
|
|
||||||
}
|
|
||||||
(Some(lh), origin) => {
|
|
||||||
answer
|
|
||||||
.entry(pk.to_x_only_pubkey())
|
|
||||||
.or_insert_with(|| (vec![], origin.unwrap_or_default()))
|
|
||||||
.0
|
|
||||||
.push(lh);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Descriptor::Tr(tr) = &self {
|
|
||||||
// Internal key first, then iterate the scripts
|
|
||||||
insert_path(tr.internal_key(), None);
|
|
||||||
|
|
||||||
for (_, ms) in tr.iter_scripts() {
|
|
||||||
// Assume always the same leaf version
|
|
||||||
let leaf_hash = taproot::TapLeafHash::from_script(
|
|
||||||
&ms.encode(),
|
|
||||||
taproot::LeafVersion::TapScript,
|
|
||||||
);
|
|
||||||
|
|
||||||
for key in ms.iter_pk_pkh() {
|
|
||||||
let key = match key {
|
|
||||||
miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk,
|
|
||||||
miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk,
|
|
||||||
};
|
|
||||||
|
|
||||||
insert_path(&key, Some(leaf_hash));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
answer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@ -914,7 +826,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sh_wsh_sortedmulti_redeemscript() {
|
fn test_sh_wsh_sortedmulti_redeemscript() {
|
||||||
use super::{AsDerived, DescriptorScripts};
|
use miniscript::psbt::PsbtInputExt;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
@ -922,11 +834,16 @@ mod test {
|
|||||||
let (descriptor, _) =
|
let (descriptor, _) =
|
||||||
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
||||||
|
|
||||||
let descriptor = descriptor.as_derived(0, &secp);
|
let descriptor = descriptor.at_derivation_index(0);
|
||||||
|
|
||||||
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||||
|
|
||||||
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
|
let mut psbt_input = psbt::Input::default();
|
||||||
assert_eq!(descriptor.psbt_witness_script(), Some(script));
|
psbt_input
|
||||||
|
.update_with_descriptor_unchecked(&descriptor)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
|
||||||
|
assert_eq!(psbt_input.witness_script, Some(script));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,14 +43,17 @@ use std::fmt;
|
|||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
use bitcoin::hashes::*;
|
use bitcoin::hashes::{hash160, ripemd160, sha256};
|
||||||
use bitcoin::util::bip32::Fingerprint;
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
use bitcoin::{PublicKey, XOnlyPublicKey};
|
use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
|
||||||
|
|
||||||
use miniscript::descriptor::{
|
use miniscript::descriptor::{
|
||||||
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner,
|
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
|
||||||
|
};
|
||||||
|
use miniscript::hash256;
|
||||||
|
use miniscript::{
|
||||||
|
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
|
||||||
};
|
};
|
||||||
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
@ -58,7 +61,7 @@ use log::{debug, error, info, trace};
|
|||||||
use crate::descriptor::ExtractPolicy;
|
use crate::descriptor::ExtractPolicy;
|
||||||
use crate::keys::ExtScriptContext;
|
use crate::keys::ExtScriptContext;
|
||||||
use crate::wallet::signer::{SignerId, SignersContainer};
|
use crate::wallet::signer::{SignerId, SignersContainer};
|
||||||
use crate::wallet::utils::{self, After, Older, SecpCtx};
|
use crate::wallet::utils::{After, Older, SecpCtx};
|
||||||
|
|
||||||
use super::checksum::calc_checksum;
|
use super::checksum::calc_checksum;
|
||||||
use super::error::Error;
|
use super::error::Error;
|
||||||
@ -81,11 +84,11 @@ pub enum PkOrF {
|
|||||||
impl PkOrF {
|
impl PkOrF {
|
||||||
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
||||||
match k {
|
match k {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::FullKey(pk),
|
key: SinglePubKey::FullKey(pk),
|
||||||
..
|
..
|
||||||
}) => PkOrF::Pubkey(*pk),
|
}) => PkOrF::Pubkey(*pk),
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::XOnly(pk),
|
key: SinglePubKey::XOnly(pk),
|
||||||
..
|
..
|
||||||
}) => PkOrF::XOnlyPubkey(*pk),
|
}) => PkOrF::XOnlyPubkey(*pk),
|
||||||
@ -111,7 +114,7 @@ pub enum SatisfiableItem {
|
|||||||
/// Double SHA256 preimage hash
|
/// Double SHA256 preimage hash
|
||||||
Hash256Preimage {
|
Hash256Preimage {
|
||||||
/// The digest value
|
/// The digest value
|
||||||
hash: sha256d::Hash,
|
hash: hash256::Hash,
|
||||||
},
|
},
|
||||||
/// RIPEMD160 preimage hash
|
/// RIPEMD160 preimage hash
|
||||||
Ripemd160Preimage {
|
Ripemd160Preimage {
|
||||||
@ -125,13 +128,13 @@ pub enum SatisfiableItem {
|
|||||||
},
|
},
|
||||||
/// Absolute timeclock timestamp
|
/// Absolute timeclock timestamp
|
||||||
AbsoluteTimelock {
|
AbsoluteTimelock {
|
||||||
/// The timestamp value
|
/// The timelock value
|
||||||
value: u32,
|
value: LockTime,
|
||||||
},
|
},
|
||||||
/// Relative timelock locktime
|
/// Relative timelock locktime
|
||||||
RelativeTimelock {
|
RelativeTimelock {
|
||||||
/// The locktime value
|
/// The timelock value
|
||||||
value: u32,
|
value: Sequence,
|
||||||
},
|
},
|
||||||
/// Multi-signature public keys with threshold count
|
/// Multi-signature public keys with threshold count
|
||||||
Multisig {
|
Multisig {
|
||||||
@ -438,32 +441,30 @@ pub struct Policy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An extra condition that must be satisfied but that is out of control of the user
|
/// An extra condition that must be satisfied but that is out of control of the user
|
||||||
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)]
|
/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
|
||||||
|
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
|
||||||
pub struct Condition {
|
pub struct Condition {
|
||||||
/// Optional CheckSequenceVerify condition
|
/// Optional CheckSequenceVerify condition
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub csv: Option<u32>,
|
pub csv: Option<Sequence>,
|
||||||
/// Optional timelock condition
|
/// Optional timelock condition
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timelock: Option<u32>,
|
pub timelock: Option<LockTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Condition {
|
impl Condition {
|
||||||
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> {
|
fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
|
||||||
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) {
|
if !a.is_same_unit(b) {
|
||||||
Err(PolicyError::MixedTimelockUnits)
|
Err(PolicyError::MixedTimelockUnits)
|
||||||
|
} else if a > b {
|
||||||
|
Ok(a)
|
||||||
} else {
|
} else {
|
||||||
Ok(max(a, b))
|
Ok(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> {
|
fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
|
||||||
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK;
|
if a.is_time_locked() != b.is_time_locked() {
|
||||||
|
|
||||||
let a = a & mask;
|
|
||||||
let b = b & mask;
|
|
||||||
|
|
||||||
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
|
||||||
Err(PolicyError::MixedTimelockUnits)
|
Err(PolicyError::MixedTimelockUnits)
|
||||||
} else {
|
} else {
|
||||||
Ok(max(a, b))
|
Ok(max(a, b))
|
||||||
@ -720,15 +721,18 @@ impl From<SatisfiableItem> for Policy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
||||||
|
// For consistency we always compute the key hash in "ecdsa" form (with the leading sign
|
||||||
|
// prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
|
||||||
|
// for a key, so it doesn't really matter how the identifier is computed.
|
||||||
match key {
|
match key {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::FullKey(pk),
|
key: SinglePubKey::FullKey(pk),
|
||||||
..
|
..
|
||||||
}) => pk.to_pubkeyhash().into(),
|
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::XOnly(pk),
|
key: SinglePubKey::XOnly(pk),
|
||||||
..
|
..
|
||||||
}) => pk.to_pubkeyhash().into(),
|
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||||
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -779,7 +783,7 @@ fn generic_sig_in_psbt<
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
//TODO check signature validity
|
//TODO check signature validity
|
||||||
psbt.inputs.iter().all(|input| match key {
|
psbt.inputs.iter().all(|input| match key {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
|
DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
|
||||||
DescriptorPublicKey::XPub(xpub) => {
|
DescriptorPublicKey::XPub(xpub) => {
|
||||||
//TODO check actual derivation matches
|
//TODO check actual derivation matches
|
||||||
match extract(input, xpub.root_fingerprint(secp)) {
|
match extract(input, xpub.root_fingerprint(secp)) {
|
||||||
@ -891,10 +895,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
|||||||
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
|
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
|
||||||
}
|
}
|
||||||
Terminal::After(value) => {
|
Terminal::After(value) => {
|
||||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
|
||||||
|
value: value.into(),
|
||||||
|
}
|
||||||
|
.into();
|
||||||
policy.contribution = Satisfaction::Complete {
|
policy.contribution = Satisfaction::Complete {
|
||||||
condition: Condition {
|
condition: Condition {
|
||||||
timelock: Some(*value),
|
timelock: Some(value.into()),
|
||||||
csv: None,
|
csv: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -905,9 +912,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
|||||||
} = build_sat
|
} = build_sat
|
||||||
{
|
{
|
||||||
let after = After::new(Some(current_height), false);
|
let after = After::new(Some(current_height), false);
|
||||||
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
|
let after_sat =
|
||||||
let inputs_sat = psbt_inputs_sat(psbt)
|
Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
|
||||||
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
|
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
|
||||||
|
Satisfier::<bitcoin::PublicKey>::check_after(&sat, value.into())
|
||||||
|
});
|
||||||
if after_sat && inputs_sat {
|
if after_sat && inputs_sat {
|
||||||
policy.satisfaction = policy.contribution.clone();
|
policy.satisfaction = policy.contribution.clone();
|
||||||
}
|
}
|
||||||
@ -999,6 +1008,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
|||||||
|
|
||||||
Policy::make_thresh(mapped, threshold)?
|
Policy::make_thresh(mapped, threshold)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unsupported
|
||||||
|
Terminal::RawPkH(_) => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1124,14 +1136,12 @@ mod test {
|
|||||||
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
|
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::descriptor::derived::AsDerived;
|
|
||||||
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
|
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
|
||||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||||
use crate::wallet::signer::SignersContainer;
|
use crate::wallet::signer::SignersContainer;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::Network;
|
use bitcoin::Network;
|
||||||
use miniscript::DescriptorTrait;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -1329,9 +1339,8 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
|
||||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -1343,16 +1352,15 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
|
||||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
assert!(matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint));
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1368,21 +1376,20 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
|
||||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
matches!(policy.item, Multisig { keys, threshold } if threshold == 1
|
||||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
|
||||||
&& m == &1
|
&& m == 1
|
||||||
&& items.len() == 2
|
&& items.len() == 2
|
||||||
&& conditions.contains_key(&vec![0])
|
&& conditions.contains_key(&vec![0])
|
||||||
&& conditions.contains_key(&vec![1])
|
&& conditions.contains_key(&vec![1])
|
||||||
@ -1427,8 +1434,8 @@ mod test {
|
|||||||
&& m == &2
|
&& m == &2
|
||||||
&& items.len() == 3
|
&& items.len() == 3
|
||||||
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
|
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
|
||||||
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
|
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
|
||||||
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
|
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1574,7 +1581,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let addr = wallet_desc
|
let addr = wallet_desc
|
||||||
.as_derived(0, &secp)
|
.at_derivation_index(0)
|
||||||
.address(Network::Testnet)
|
.address(Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -1646,7 +1653,7 @@ mod test {
|
|||||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
let addr = wallet_desc
|
let addr = wallet_desc
|
||||||
.as_derived(0, &secp)
|
.at_derivation_index(0)
|
||||||
.address(Network::Testnet)
|
.address(Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -468,12 +468,10 @@ mod test {
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::descriptor::derived::AsDerived;
|
|
||||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||||
use crate::keys::ValidNetworks;
|
use crate::keys::ValidNetworks;
|
||||||
use bitcoin::network::constants::Network::Regtest;
|
use bitcoin::network::constants::Network::Regtest;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||||
@ -517,17 +515,15 @@ mod test {
|
|||||||
is_fixed: bool,
|
is_fixed: bool,
|
||||||
expected: &[&str],
|
expected: &[&str],
|
||||||
) {
|
) {
|
||||||
let secp = Secp256k1::new();
|
|
||||||
|
|
||||||
let (desc, _key_map, _networks) = desc.unwrap();
|
let (desc, _key_map, _networks) = desc.unwrap();
|
||||||
assert_eq!(desc.is_witness(), is_witness);
|
assert_eq!(desc.is_witness(), is_witness);
|
||||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||||
for i in 0..expected.len() {
|
for i in 0..expected.len() {
|
||||||
let index = i as u32;
|
let index = i as u32;
|
||||||
let child_desc = if !desc.is_deriveable() {
|
let child_desc = if !desc.has_wildcard() {
|
||||||
desc.as_derived_fixed(&secp)
|
desc.at_derivation_index(0)
|
||||||
} else {
|
} else {
|
||||||
desc.as_derived(index, &secp)
|
desc.at_derivation_index(index)
|
||||||
};
|
};
|
||||||
let address = child_desc.address(Regtest).unwrap();
|
let address = child_desc.address(Regtest).unwrap();
|
||||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||||
|
16
src/error.rs
16
src/error.rs
@ -12,7 +12,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::bitcoin::Network;
|
use crate::bitcoin::Network;
|
||||||
use crate::{descriptor, wallet, wallet::address_validator};
|
use crate::{descriptor, wallet};
|
||||||
use bitcoin::{OutPoint, Txid};
|
use bitcoin::{OutPoint, Txid};
|
||||||
|
|
||||||
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||||
@ -99,12 +99,12 @@ pub enum Error {
|
|||||||
|
|
||||||
/// Error related to the parsing and usage of descriptors
|
/// Error related to the parsing and usage of descriptors
|
||||||
Descriptor(crate::descriptor::error::Error),
|
Descriptor(crate::descriptor::error::Error),
|
||||||
/// Error that can be returned to fail the validation of an address
|
|
||||||
AddressValidator(crate::wallet::address_validator::AddressValidatorError),
|
|
||||||
/// Encoding error
|
/// Encoding error
|
||||||
Encode(bitcoin::consensus::encode::Error),
|
Encode(bitcoin::consensus::encode::Error),
|
||||||
/// Miniscript error
|
/// Miniscript error
|
||||||
Miniscript(miniscript::Error),
|
Miniscript(miniscript::Error),
|
||||||
|
/// Miniscript PSBT error
|
||||||
|
MiniscriptPsbt(MiniscriptPsbtError),
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
Bip32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
/// An ECDSA error
|
/// An ECDSA error
|
||||||
@ -149,6 +149,14 @@ pub enum Error {
|
|||||||
Rusqlite(rusqlite::Error),
|
Rusqlite(rusqlite::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Errors returned by miniscript when updating inconsistent PSBTs
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum MiniscriptPsbtError {
|
||||||
|
Conversion(miniscript::descriptor::ConversionError),
|
||||||
|
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
|
||||||
|
OutputUpdate(miniscript::psbt::OutputUpdateError),
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
||||||
/// on cached `scriptPubKey`s.
|
/// on cached `scriptPubKey`s.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -181,7 +189,6 @@ macro_rules! impl_error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_error!(descriptor::error::Error, Descriptor);
|
impl_error!(descriptor::error::Error, Descriptor);
|
||||||
impl_error!(address_validator::AddressValidatorError, AddressValidator);
|
|
||||||
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
|
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
|
||||||
impl_error!(wallet::signer::SignerError, Signer);
|
impl_error!(wallet::signer::SignerError, Signer);
|
||||||
|
|
||||||
@ -198,6 +205,7 @@ impl From<crate::keys::KeyError> for Error {
|
|||||||
|
|
||||||
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
||||||
impl_error!(miniscript::Error, Miniscript);
|
impl_error!(miniscript::Error, Miniscript);
|
||||||
|
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
|
||||||
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||||
impl_error!(serde_json::Error, Json);
|
impl_error!(serde_json::Error, Json);
|
||||||
|
@ -24,8 +24,8 @@ use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
|
|||||||
|
|
||||||
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||||
pub use miniscript::descriptor::{
|
pub use miniscript::descriptor::{
|
||||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
|
||||||
SinglePubKey, SortedMultiVec,
|
SortedMultiVec,
|
||||||
};
|
};
|
||||||
pub use miniscript::ScriptContext;
|
pub use miniscript::ScriptContext;
|
||||||
use miniscript::{Miniscript, Terminal};
|
use miniscript::{Miniscript, Terminal};
|
||||||
@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
|||||||
let mut key_map = KeyMap::with_capacity(1);
|
let mut key_map = KeyMap::with_capacity(1);
|
||||||
|
|
||||||
let public = secret
|
let public = secret
|
||||||
.as_public(secp)
|
.to_public(secp)
|
||||||
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
|
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
|
||||||
key_map.insert(public.clone(), secret);
|
key_map.insert(public.clone(), secret);
|
||||||
|
|
||||||
@ -224,8 +224,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// use bdk::bitcoin::PublicKey;
|
/// use bdk::bitcoin::PublicKey;
|
||||||
///
|
///
|
||||||
/// use bdk::keys::{
|
/// use bdk::keys::{
|
||||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||||
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
|
/// ScriptContext, SinglePub, SinglePubKey,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// pub struct MyKeyType {
|
/// pub struct MyKeyType {
|
||||||
@ -235,7 +235,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
/// Ok(DescriptorKey::from_public(
|
/// Ok(DescriptorKey::from_public(
|
||||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
/// DescriptorPublicKey::Single(SinglePub {
|
||||||
/// origin: None,
|
/// origin: None,
|
||||||
/// key: SinglePubKey::FullKey(self.pubkey),
|
/// key: SinglePubKey::FullKey(self.pubkey),
|
||||||
/// }),
|
/// }),
|
||||||
@ -842,7 +842,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
|||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let networks = match self {
|
let networks = match self {
|
||||||
DescriptorPublicKey::SinglePub(_) => any_network(),
|
DescriptorPublicKey::Single(_) => any_network(),
|
||||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||||
if xkey.network == Network::Bitcoin =>
|
if xkey.network == Network::Bitcoin =>
|
||||||
{
|
{
|
||||||
@ -857,7 +857,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
|||||||
|
|
||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::FullKey(self),
|
key: SinglePubKey::FullKey(self),
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
@ -867,7 +867,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
|||||||
|
|
||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::Single(SinglePub {
|
||||||
key: SinglePubKey::XOnly(self),
|
key: SinglePubKey::XOnly(self),
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
@ -878,7 +878,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
|||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let networks = match &self {
|
let networks = match &self {
|
||||||
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
|
||||||
mainnet_network()
|
mainnet_network()
|
||||||
}
|
}
|
||||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
||||||
@ -903,7 +903,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
|
|||||||
|
|
||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
DescriptorSecretKey::Single(SinglePriv {
|
||||||
key: self,
|
key: self,
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
|
@ -203,6 +203,8 @@ pub extern crate miniscript;
|
|||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
#[cfg(feature = "hardware-signer")]
|
||||||
|
pub extern crate hwi;
|
||||||
|
|
||||||
#[cfg(all(feature = "reqwest", feature = "ureq"))]
|
#[cfg(all(feature = "reqwest", feature = "ureq"))]
|
||||||
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
|
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
|
||||||
@ -237,6 +239,9 @@ pub extern crate bitcoincore_rpc;
|
|||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
pub extern crate electrum_client;
|
pub extern crate electrum_client;
|
||||||
|
|
||||||
|
#[cfg(feature = "esplora")]
|
||||||
|
pub extern crate esplora_client;
|
||||||
|
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
pub extern crate sled;
|
pub extern crate sled;
|
||||||
|
|
||||||
@ -260,7 +265,7 @@ pub mod descriptor;
|
|||||||
#[cfg(feature = "test-md-docs")]
|
#[cfg(feature = "test-md-docs")]
|
||||||
mod doctest;
|
mod doctest;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub(crate) mod psbt;
|
pub mod psbt;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|
||||||
@ -268,7 +273,6 @@ pub use descriptor::template;
|
|||||||
pub use descriptor::HdKeyPaths;
|
pub use descriptor::HdKeyPaths;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub use wallet::address_validator;
|
|
||||||
pub use wallet::signer;
|
pub use wallet::signer;
|
||||||
pub use wallet::signer::SignOptions;
|
pub use wallet::signer::SignOptions;
|
||||||
pub use wallet::tx_builder::TxBuilder;
|
pub use wallet::tx_builder::TxBuilder;
|
||||||
|
@ -9,11 +9,17 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
|
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||||
|
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||||
use bitcoin::TxOut;
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
|
// TODO upstream the functions here to `rust-bitcoin`?
|
||||||
|
|
||||||
|
/// Trait to add functions to extract utxos and calculate fees.
|
||||||
pub trait PsbtUtils {
|
pub trait PsbtUtils {
|
||||||
|
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
|
||||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||||
|
|
||||||
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
|
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
use crate::testutils::TestIncomingTx;
|
use crate::testutils::TestIncomingTx;
|
||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
use bitcoin::hashes::sha256d;
|
use bitcoin::hashes::sha256d;
|
||||||
use bitcoin::{Address, Amount, Script, Transaction, Txid, Witness};
|
use bitcoin::{Address, Amount, PackedLockTime, Script, Sequence, Transaction, Txid, Witness};
|
||||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
@ -110,7 +120,7 @@ impl TestClient {
|
|||||||
if let Some(true) = meta_tx.replaceable {
|
if let Some(true) = meta_tx.replaceable {
|
||||||
// for some reason core doesn't set this field right
|
// for some reason core doesn't set this field right
|
||||||
for input in &mut tx.input {
|
for input in &mut tx.input {
|
||||||
input.sequence = 0xFFFFFFFD;
|
input.sequence = Sequence(0xFFFFFFFD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +174,7 @@ impl TestClient {
|
|||||||
use bitcoin::blockdata::script::Builder;
|
use bitcoin::blockdata::script::Builder;
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
||||||
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
||||||
|
use bitcoin::hashes::Hash;
|
||||||
|
|
||||||
let block_template: serde_json::Value = self
|
let block_template: serde_json::Value = self
|
||||||
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
||||||
@ -176,7 +187,7 @@ impl TestClient {
|
|||||||
block_template["previousblockhash"].as_str().unwrap(),
|
block_template["previousblockhash"].as_str().unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
merkle_root: TxMerkleNode::default(),
|
merkle_root: TxMerkleNode::all_zeros(),
|
||||||
time: block_template["curtime"].as_u64().unwrap() as u32,
|
time: block_template["curtime"].as_u64().unwrap() as u32,
|
||||||
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
@ -184,15 +195,15 @@ impl TestClient {
|
|||||||
debug!("header: {:#?}", header);
|
debug!("header: {:#?}", header);
|
||||||
|
|
||||||
let height = block_template["height"].as_u64().unwrap() as i64;
|
let height = block_template["height"].as_u64().unwrap() as i64;
|
||||||
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
|
let witness_reserved_value: Vec<u8> = sha256d::Hash::all_zeros().as_ref().into();
|
||||||
// burn block subsidy and fees, not a big deal
|
// burn block subsidy and fees, not a big deal
|
||||||
let mut coinbase_tx = Transaction {
|
let mut coinbase_tx = Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: PackedLockTime(0),
|
||||||
input: vec![TxIn {
|
input: vec![TxIn {
|
||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
script_sig: Builder::new().push_int(height).into_script(),
|
script_sig: Builder::new().push_int(height).into_script(),
|
||||||
sequence: 0xFFFFFFFF,
|
sequence: Sequence(0xFFFFFFFF),
|
||||||
witness: Witness::from_vec(vec![witness_reserved_value]),
|
witness: Witness::from_vec(vec![witness_reserved_value]),
|
||||||
}],
|
}],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
@ -1184,7 +1195,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
|
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
|
||||||
|
|
||||||
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
|
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
|
||||||
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
|
assert_eq!(taproot_balance.to_sat(), 25_000, "node has incorrect taproot wallet balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -101,25 +101,21 @@ impl TestIncomingTx {
|
|||||||
macro_rules! testutils {
|
macro_rules! testutils {
|
||||||
( @external $descriptors:expr, $child:expr ) => ({
|
( @external $descriptors:expr, $child:expr ) => ({
|
||||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
use $crate::descriptor::AsDerived;
|
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||||
parsed.as_derived($child, &secp).address(bitcoin::Network::Regtest).expect("No address form")
|
parsed.at_derivation_index($child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||||
});
|
});
|
||||||
( @internal $descriptors:expr, $child:expr ) => ({
|
( @internal $descriptors:expr, $child:expr ) => ({
|
||||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
use $crate::descriptor::AsDerived;
|
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||||
parsed.as_derived($child, &secp).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
parsed.at_derivation_index($child).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
||||||
});
|
});
|
||||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||||
@ -186,49 +182,50 @@ macro_rules! testutils {
|
|||||||
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
|
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use $crate::miniscript::descriptor::Descriptor;
|
use $crate::miniscript::descriptor::Descriptor;
|
||||||
use $crate::miniscript::TranslatePk;
|
use $crate::miniscript::TranslatePk;
|
||||||
|
|
||||||
|
struct Translator {
|
||||||
|
keys: HashMap<&'static str, (String, Option<String>, Option<String>)>,
|
||||||
|
is_internal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $crate::miniscript::Translator<String, String, Infallible> for Translator {
|
||||||
|
fn pk(&mut self, pk: &String) -> Result<String, Infallible> {
|
||||||
|
match self.keys.get(pk.as_str()) {
|
||||||
|
Some((key, ext_path, int_path)) => {
|
||||||
|
let path = if self.is_internal { int_path } else { ext_path };
|
||||||
|
Ok(format!("{}{}", key, path.clone().unwrap_or_default()))
|
||||||
|
}
|
||||||
|
None => Ok(pk.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn sha256(&mut self, sha256: &String) -> Result<String, Infallible> { Ok(sha256.clone()) }
|
||||||
|
fn hash256(&mut self, hash256: &String) -> Result<String, Infallible> { Ok(hash256.clone()) }
|
||||||
|
fn ripemd160(&mut self, ripemd160: &String) -> Result<String, Infallible> { Ok(ripemd160.clone()) }
|
||||||
|
fn hash160(&mut self, hash160: &String) -> Result<String, Infallible> { Ok(hash160.clone()) }
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused_assignments, unused_mut)]
|
#[allow(unused_assignments, unused_mut)]
|
||||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
let mut keys = HashMap::new();
|
||||||
$(
|
$(
|
||||||
keys = testutils!{ @keys $( $keys )* };
|
keys = testutils!{ @keys $( $keys )* };
|
||||||
)*
|
)*
|
||||||
|
|
||||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
let mut translator = Translator { keys, is_internal: false };
|
||||||
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
|
|
||||||
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
|
||||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
|
||||||
} else {
|
|
||||||
k.clone()
|
|
||||||
}
|
|
||||||
}, |kh| {
|
|
||||||
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
|
||||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
|
||||||
} else {
|
|
||||||
kh.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||||
|
let external = external.translate_pk(&mut translator).expect("Infallible conversion");
|
||||||
let external = external.to_string();
|
let external = external.to_string();
|
||||||
|
|
||||||
let internal = None::<String>$(.or({
|
translator.is_internal = true;
|
||||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
|
||||||
|
|
||||||
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
let internal = None::<String>$(.or({
|
||||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
|
||||||
} else {
|
Some(internal.to_string())
|
||||||
k.clone()
|
|
||||||
}
|
|
||||||
}, |kh| {
|
|
||||||
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
|
||||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
|
||||||
} else {
|
|
||||||
kh.clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Some(string_internal.to_string())
|
|
||||||
}))?;
|
}))?;
|
||||||
|
|
||||||
(external, internal)
|
(external, internal)
|
||||||
|
@ -166,7 +166,7 @@ pub struct LocalUtxo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct WeightedUtxo {
|
pub struct WeightedUtxo {
|
||||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||||
@ -177,7 +177,7 @@ pub struct WeightedUtxo {
|
|||||||
pub utxo: Utxo,
|
pub utxo: Utxo,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// An unspent transaction output (UTXO).
|
/// An unspent transaction output (UTXO).
|
||||||
pub enum Utxo {
|
pub enum Utxo {
|
||||||
/// A UTXO owned by the local wallet.
|
/// A UTXO owned by the local wallet.
|
||||||
@ -224,7 +224,7 @@ impl Utxo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A wallet transaction
|
/// A wallet transaction
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TransactionDetails {
|
pub struct TransactionDetails {
|
||||||
/// Optional transaction
|
/// Optional transaction
|
||||||
pub transaction: Option<Transaction>,
|
pub transaction: Option<Transaction>,
|
||||||
|
@ -1,158 +0,0 @@
|
|||||||
// Bitcoin Dev Kit
|
|
||||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
|
||||||
//
|
|
||||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
|
||||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
|
||||||
// You may not use this file except in accordance with one or both of these
|
|
||||||
// licenses.
|
|
||||||
|
|
||||||
//! Address validation callbacks
|
|
||||||
//!
|
|
||||||
//! The typical usage of those callbacks is for displaying the newly-generated address on a
|
|
||||||
//! hardware wallet, so that the user can cross-check its correctness.
|
|
||||||
//!
|
|
||||||
//! More generally speaking though, these callbacks can also be used to "do something" every time
|
|
||||||
//! an address is generated, without necessarily checking or validating it.
|
|
||||||
//!
|
|
||||||
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
|
|
||||||
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
|
|
||||||
//! whenever a new address is generated (either explicitly by the user with
|
|
||||||
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
|
|
||||||
//! address) all the attached validators will be polled, in sequence. All of them must complete
|
|
||||||
//! successfully to continue.
|
|
||||||
//!
|
|
||||||
//! ## Example
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! # use std::sync::Arc;
|
|
||||||
//! # use bitcoin::*;
|
|
||||||
//! # use bdk::address_validator::*;
|
|
||||||
//! # use bdk::database::*;
|
|
||||||
//! # use bdk::*;
|
|
||||||
//! # use bdk::wallet::AddressIndex::New;
|
|
||||||
//! #[derive(Debug)]
|
|
||||||
//! struct PrintAddressAndContinue;
|
|
||||||
//!
|
|
||||||
//! impl AddressValidator for PrintAddressAndContinue {
|
|
||||||
//! fn validate(
|
|
||||||
//! &self,
|
|
||||||
//! keychain: KeychainKind,
|
|
||||||
//! hd_keypaths: &HdKeyPaths,
|
|
||||||
//! script: &Script
|
|
||||||
//! ) -> Result<(), AddressValidatorError> {
|
|
||||||
//! let address = Address::from_script(script, Network::Testnet)
|
|
||||||
//! .as_ref()
|
|
||||||
//! .map(Address::to_string)
|
|
||||||
//! .unwrap_or(script.to_string());
|
|
||||||
//! println!("New address of type {:?}: {}", keychain, address);
|
|
||||||
//! println!("HD keypaths: {:#?}", hd_keypaths);
|
|
||||||
//!
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
|
||||||
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
|
||||||
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
|
|
||||||
//!
|
|
||||||
//! let address = wallet.get_address(New)?;
|
|
||||||
//! println!("Address: {}", address);
|
|
||||||
//! # Ok::<(), bdk::Error>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use bitcoin::Script;
|
|
||||||
|
|
||||||
use crate::descriptor::HdKeyPaths;
|
|
||||||
use crate::types::KeychainKind;
|
|
||||||
|
|
||||||
/// Errors that can be returned to fail the validation of an address
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum AddressValidatorError {
|
|
||||||
/// User rejected the address
|
|
||||||
UserRejected,
|
|
||||||
/// Network connection error
|
|
||||||
ConnectionError,
|
|
||||||
/// Network request timeout error
|
|
||||||
TimeoutError,
|
|
||||||
/// Invalid script
|
|
||||||
InvalidScript,
|
|
||||||
/// A custom error message
|
|
||||||
Message(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for AddressValidatorError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for AddressValidatorError {}
|
|
||||||
|
|
||||||
/// Trait to build address validators
|
|
||||||
///
|
|
||||||
/// All the address validators attached to a wallet with [`Wallet::add_address_validator`](super::Wallet::add_address_validator) will be polled
|
|
||||||
/// every time an address (external or internal) is generated by the wallet. Errors returned in the
|
|
||||||
/// validator will be propagated up to the original caller that triggered the address generation.
|
|
||||||
///
|
|
||||||
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
|
||||||
#[deprecated = "AddressValidator was rarely used. Address validation can occur outside of BDK"]
|
|
||||||
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
|
||||||
/// Validate or inspect an address
|
|
||||||
fn validate(
|
|
||||||
&self,
|
|
||||||
keychain: KeychainKind,
|
|
||||||
hd_keypaths: &HdKeyPaths,
|
|
||||||
script: &Script,
|
|
||||||
) -> Result<(), AddressValidatorError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::wallet::AddressIndex::New;
|
|
||||||
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct TestValidator;
|
|
||||||
#[allow(deprecated)]
|
|
||||||
impl AddressValidator for TestValidator {
|
|
||||||
fn validate(
|
|
||||||
&self,
|
|
||||||
_keychain: KeychainKind,
|
|
||||||
_hd_keypaths: &HdKeyPaths,
|
|
||||||
_script: &bitcoin::Script,
|
|
||||||
) -> Result<(), AddressValidatorError> {
|
|
||||||
Err(AddressValidatorError::InvalidScript)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic(expected = "InvalidScript")]
|
|
||||||
fn test_address_validator_external() {
|
|
||||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
|
||||||
#[allow(deprecated)]
|
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
|
||||||
|
|
||||||
wallet.get_address(New).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic(expected = "InvalidScript")]
|
|
||||||
fn test_address_validator_internal() {
|
|
||||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
|
||||||
#[allow(deprecated)]
|
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
|
||||||
|
|
||||||
let addr = crate::testutils!(@external descriptors, 10);
|
|
||||||
let mut builder = wallet.build_tx();
|
|
||||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
|
||||||
builder.finish().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
@ -310,7 +310,7 @@ pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Sc
|
|||||||
let drain_val = remaining_amount.saturating_sub(change_fee);
|
let drain_val = remaining_amount.saturating_sub(change_fee);
|
||||||
|
|
||||||
if drain_val.is_dust(drain_script) {
|
if drain_val.is_dust(drain_script) {
|
||||||
let dust_threshold = drain_script.dust_value().as_sat();
|
let dust_threshold = drain_script.dust_value().to_sat();
|
||||||
Excess::NoChange {
|
Excess::NoChange {
|
||||||
dust_threshold,
|
dust_threshold,
|
||||||
change_fee,
|
change_fee,
|
||||||
@ -835,7 +835,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value: rng.gen_range(0, 200000000),
|
value: rng.gen_range(0..200000000),
|
||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
@ -866,7 +866,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
||||||
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
|
let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
|
||||||
utxos.shuffle(&mut rng);
|
utxos.shuffle(&mut rng);
|
||||||
utxos[..utxos_picked_len]
|
utxos[..utxos_picked_len]
|
||||||
.iter()
|
.iter()
|
||||||
@ -1226,6 +1226,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
fn test_bnb_coin_selection_required_not_enough() {
|
fn test_bnb_coin_selection_required_not_enough() {
|
||||||
let utxos = get_test_utxos();
|
let utxos = get_test_utxos();
|
||||||
let database = MemoryDatabase::default();
|
let database = MemoryDatabase::default();
|
||||||
|
@ -11,7 +11,40 @@
|
|||||||
|
|
||||||
//! HWI Signer
|
//! HWI Signer
|
||||||
//!
|
//!
|
||||||
//! This module contains a simple implementation of a Custom signer for rust-hwi
|
//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
|
||||||
|
//! used with hardware wallets.
|
||||||
|
//! ```no_run
|
||||||
|
//! # use bdk::bitcoin::Network;
|
||||||
|
//! # use bdk::database::MemoryDatabase;
|
||||||
|
//! # use bdk::signer::SignerOrdering;
|
||||||
|
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||||
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
|
//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
|
||||||
|
//! # use hwi::{types::HWIChain, HWIClient};
|
||||||
|
//! # use std::sync::Arc;
|
||||||
|
//! #
|
||||||
|
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let devices = HWIClient::enumerate()?;
|
||||||
|
//! let first_device = devices.first().expect("No devices found!");
|
||||||
|
//! let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
|
||||||
|
//!
|
||||||
|
//! # let mut wallet = Wallet::new(
|
||||||
|
//! # "",
|
||||||
|
//! # None,
|
||||||
|
//! # Network::Testnet,
|
||||||
|
//! # MemoryDatabase::default(),
|
||||||
|
//! # )?;
|
||||||
|
//! #
|
||||||
|
//! // Adding the hardware signer to the BDK wallet
|
||||||
|
//! wallet.add_signer(
|
||||||
|
//! KeychainKind::External,
|
||||||
|
//! SignerOrdering(200),
|
||||||
|
//! Arc::new(custom_signer),
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
use bitcoin::psbt::PartiallySignedTransaction;
|
use bitcoin::psbt::PartiallySignedTransaction;
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
@ -24,20 +24,17 @@ use std::sync::Arc;
|
|||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
|
|
||||||
use bitcoin::consensus::encode::serialize;
|
use bitcoin::consensus::encode::serialize;
|
||||||
use bitcoin::util::{psbt, taproot};
|
use bitcoin::util::psbt;
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
Address, EcdsaSighashType, Network, OutPoint, SchnorrSighashType, Script, Transaction, TxOut,
|
Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
|
||||||
Txid, Witness,
|
Transaction, TxOut, Txid, Witness,
|
||||||
};
|
};
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorTrait;
|
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
|
||||||
use miniscript::psbt::PsbtInputSatisfier;
|
|
||||||
use miniscript::ToPublicKey;
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
pub mod address_validator;
|
|
||||||
pub mod coin_selection;
|
pub mod coin_selection;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod signer;
|
pub mod signer;
|
||||||
@ -54,25 +51,21 @@ pub mod hardwaresigner;
|
|||||||
|
|
||||||
pub use utils::IsDust;
|
pub use utils::IsDust;
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
use address_validator::AddressValidator;
|
|
||||||
use coin_selection::DefaultCoinSelectionAlgorithm;
|
use coin_selection::DefaultCoinSelectionAlgorithm;
|
||||||
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
||||||
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
||||||
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx};
|
use utils::{check_nsequence_rbf, After, Older, SecpCtx};
|
||||||
|
|
||||||
use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync};
|
use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync};
|
||||||
use crate::database::memory::MemoryDatabase;
|
use crate::database::memory::MemoryDatabase;
|
||||||
use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
|
use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
|
||||||
use crate::descriptor::checksum::calc_checksum_bytes_internal;
|
use crate::descriptor::checksum::calc_checksum_bytes_internal;
|
||||||
use crate::descriptor::derived::AsDerived;
|
|
||||||
use crate::descriptor::policy::BuildSatisfaction;
|
use crate::descriptor::policy::BuildSatisfaction;
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta,
|
calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
|
||||||
DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor,
|
ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
|
||||||
Policy, XKeyUtils,
|
|
||||||
};
|
};
|
||||||
use crate::error::Error;
|
use crate::error::{Error, MiniscriptPsbtError};
|
||||||
use crate::psbt::PsbtUtils;
|
use crate::psbt::PsbtUtils;
|
||||||
use crate::signer::SignerError;
|
use crate::signer::SignerError;
|
||||||
use crate::testutils;
|
use crate::testutils;
|
||||||
@ -101,9 +94,6 @@ pub struct Wallet<D> {
|
|||||||
signers: Arc<SignersContainer>,
|
signers: Arc<SignersContainer>,
|
||||||
change_signers: Arc<SignersContainer>,
|
change_signers: Arc<SignersContainer>,
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
address_validators: Vec<Arc<dyn AddressValidator>>,
|
|
||||||
|
|
||||||
network: Network,
|
network: Network,
|
||||||
|
|
||||||
database: RefCell<D>,
|
database: RefCell<D>,
|
||||||
@ -144,7 +134,7 @@ pub enum AddressIndex {
|
|||||||
|
|
||||||
/// A derived address and the index it was found at
|
/// A derived address and the index it was found at
|
||||||
/// For convenience this automatically derefs to `Address`
|
/// For convenience this automatically derefs to `Address`
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct AddressInfo {
|
pub struct AddressInfo {
|
||||||
/// Child index of this address
|
/// Child index of this address
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
@ -236,7 +226,6 @@ where
|
|||||||
change_descriptor,
|
change_descriptor,
|
||||||
signers,
|
signers,
|
||||||
change_signers,
|
change_signers,
|
||||||
address_validators: Vec::new(),
|
|
||||||
network,
|
network,
|
||||||
database: RefCell::new(database),
|
database: RefCell::new(database),
|
||||||
secp,
|
secp,
|
||||||
@ -267,7 +256,7 @@ where
|
|||||||
|
|
||||||
let address_result = self
|
let address_result = self
|
||||||
.get_descriptor_for_keychain(keychain)
|
.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(incremented_index, &self.secp)
|
.at_derivation_index(incremented_index)
|
||||||
.address(self.network);
|
.address(self.network);
|
||||||
|
|
||||||
address_result
|
address_result
|
||||||
@ -286,7 +275,7 @@ where
|
|||||||
|
|
||||||
let derived_key = self
|
let derived_key = self
|
||||||
.get_descriptor_for_keychain(keychain)
|
.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(current_index, &self.secp);
|
.at_derivation_index(current_index);
|
||||||
|
|
||||||
let script_pubkey = derived_key.script_pubkey();
|
let script_pubkey = derived_key.script_pubkey();
|
||||||
|
|
||||||
@ -314,7 +303,7 @@ where
|
|||||||
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index
|
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index
|
||||||
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
||||||
self.get_descriptor_for_keychain(keychain)
|
self.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(index, &self.secp)
|
.at_derivation_index(index)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
index,
|
index,
|
||||||
@ -330,7 +319,7 @@ where
|
|||||||
self.set_index(keychain, index)?;
|
self.set_index(keychain, index)?;
|
||||||
|
|
||||||
self.get_descriptor_for_keychain(keychain)
|
self.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(index, &self.secp)
|
.at_derivation_index(index)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
index,
|
index,
|
||||||
@ -379,7 +368,7 @@ where
|
|||||||
/// transaction output scripts.
|
/// transaction output scripts.
|
||||||
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
|
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
|
||||||
let mut new_addresses_cached = false;
|
let mut new_addresses_cached = false;
|
||||||
let max_address = match self.descriptor.is_deriveable() {
|
let max_address = match self.descriptor.has_wildcard() {
|
||||||
false => 0,
|
false => 0,
|
||||||
true => max_addresses,
|
true => max_addresses,
|
||||||
};
|
};
|
||||||
@ -396,7 +385,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(change_descriptor) = &self.change_descriptor {
|
if let Some(change_descriptor) = &self.change_descriptor {
|
||||||
let max_address = match change_descriptor.is_deriveable() {
|
let max_address = match change_descriptor.has_wildcard() {
|
||||||
false => 0,
|
false => 0,
|
||||||
true => max_addresses,
|
true => max_addresses,
|
||||||
};
|
};
|
||||||
@ -565,24 +554,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an address validator
|
|
||||||
///
|
|
||||||
/// See [the `address_validator` module](address_validator) for an example.
|
|
||||||
#[deprecated]
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub fn add_address_validator(&mut self, validator: Arc<dyn AddressValidator>) {
|
|
||||||
self.address_validators.push(validator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the address validators
|
|
||||||
///
|
|
||||||
/// See [the `address_validator` module](address_validator).
|
|
||||||
#[deprecated]
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub fn get_address_validators(&self) -> &[Arc<dyn AddressValidator>] {
|
|
||||||
&self.address_validators
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start building a transaction.
|
/// Start building a transaction.
|
||||||
///
|
///
|
||||||
/// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
|
/// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
|
||||||
@ -697,10 +668,9 @@ where
|
|||||||
// We use a match here instead of a map_or_else as it's way more readable :)
|
// We use a match here instead of a map_or_else as it's way more readable :)
|
||||||
let current_height = match params.current_height {
|
let current_height = match params.current_height {
|
||||||
// If they didn't tell us the current height, we assume it's the latest sync height.
|
// If they didn't tell us the current height, we assume it's the latest sync height.
|
||||||
None => self
|
None => self.database().get_sync_time()?.map(|sync_time| {
|
||||||
.database()
|
LockTime::from_height(sync_time.block_time.height).expect("Invalid height")
|
||||||
.get_sync_time()?
|
}),
|
||||||
.map(|sync_time| sync_time.block_time.height),
|
|
||||||
h => h,
|
h => h,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -710,24 +680,33 @@ where
|
|||||||
// Fee sniping can be partially prevented by setting the timelock
|
// Fee sniping can be partially prevented by setting the timelock
|
||||||
// to current_height. If we don't know the current_height,
|
// to current_height. If we don't know the current_height,
|
||||||
// we default to 0.
|
// we default to 0.
|
||||||
let fee_sniping_height = current_height.unwrap_or(0);
|
let fee_sniping_height = current_height.unwrap_or(LockTime::ZERO);
|
||||||
|
|
||||||
// We choose the biggest between the required nlocktime and the fee sniping
|
// We choose the biggest between the required nlocktime and the fee sniping
|
||||||
// height
|
// height
|
||||||
std::cmp::max(requirements.timelock.unwrap_or(0), fee_sniping_height)
|
match requirements.timelock {
|
||||||
|
// No requirement, just use the fee_sniping_height
|
||||||
|
None => fee_sniping_height,
|
||||||
|
// There's a block-based requirement, but the value is lower than the fee_sniping_height
|
||||||
|
Some(value @ LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height,
|
||||||
|
// There's a time-based requirement or a block-based requirement greater
|
||||||
|
// than the fee_sniping_height use that value
|
||||||
|
Some(value) => value,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Specific nLockTime required and we have no constraints, so just set to that value
|
// Specific nLockTime required and we have no constraints, so just set to that value
|
||||||
Some(x) if requirements.timelock.is_none() => x,
|
Some(x) if requirements.timelock.is_none() => x,
|
||||||
// Specific nLockTime required and it's compatible with the constraints
|
// Specific nLockTime required and it's compatible with the constraints
|
||||||
Some(x) if check_nlocktime(x, requirements.timelock.unwrap()) => x,
|
Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x,
|
||||||
// Invalid nLockTime required
|
// Invalid nLockTime required
|
||||||
Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{}`, but at least `{}` is required to spend from this script", x, requirements.timelock.unwrap())))
|
Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap())))
|
||||||
};
|
};
|
||||||
|
|
||||||
let n_sequence = match (params.rbf, requirements.csv) {
|
let n_sequence = match (params.rbf, requirements.csv) {
|
||||||
// No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
|
// No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
|
||||||
(None, None) if lock_time != 0 => 0xFFFFFFFE,
|
(None, None) if lock_time != LockTime::ZERO => Sequence::ENABLE_LOCKTIME_NO_RBF,
|
||||||
// No RBF, CSV or nLockTime, make the transaction final
|
// No RBF, CSV or nLockTime, make the transaction final
|
||||||
(None, None) => 0xFFFFFFFF,
|
(None, None) => Sequence::MAX,
|
||||||
|
|
||||||
// No RBF requested, use the value from CSV. Note that this value is by definition
|
// No RBF requested, use the value from CSV. Note that this value is by definition
|
||||||
// non-final, so even if a timelock is enabled this nSequence is fine, hence why we
|
// non-final, so even if a timelock is enabled this nSequence is fine, hence why we
|
||||||
@ -735,7 +714,7 @@ where
|
|||||||
(None, Some(csv)) => csv,
|
(None, Some(csv)) => csv,
|
||||||
|
|
||||||
// RBF with a specific value but that value is too high
|
// RBF with a specific value but that value is too high
|
||||||
(Some(tx_builder::RbfValue::Value(rbf)), _) if rbf >= 0xFFFFFFFE => {
|
(Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
|
||||||
return Err(Error::Generic(
|
return Err(Error::Generic(
|
||||||
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
|
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
|
||||||
))
|
))
|
||||||
@ -745,7 +724,7 @@ where
|
|||||||
if !check_nsequence_rbf(rbf, csv) =>
|
if !check_nsequence_rbf(rbf, csv) =>
|
||||||
{
|
{
|
||||||
return Err(Error::Generic(format!(
|
return Err(Error::Generic(format!(
|
||||||
"Cannot enable RBF with nSequence `{}` given a required OP_CSV of `{}`",
|
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
|
||||||
rbf, csv
|
rbf, csv
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
@ -788,7 +767,7 @@ where
|
|||||||
|
|
||||||
let mut tx = Transaction {
|
let mut tx = Transaction {
|
||||||
version,
|
version,
|
||||||
lock_time,
|
lock_time: lock_time.into(),
|
||||||
input: vec![],
|
input: vec![],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
};
|
};
|
||||||
@ -853,7 +832,7 @@ where
|
|||||||
params.drain_wallet,
|
params.drain_wallet,
|
||||||
params.manually_selected_only,
|
params.manually_selected_only,
|
||||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||||
current_height,
|
current_height.map(LockTime::to_consensus_u32),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// get drain script
|
// get drain script
|
||||||
@ -1005,7 +984,11 @@ where
|
|||||||
Some(tx) => tx,
|
Some(tx) => tx,
|
||||||
};
|
};
|
||||||
let mut tx = details.transaction.take().unwrap();
|
let mut tx = details.transaction.take().unwrap();
|
||||||
if !tx.input.iter().any(|txin| txin.sequence <= 0xFFFFFFFD) {
|
if !tx
|
||||||
|
.input
|
||||||
|
.iter()
|
||||||
|
.any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
|
||||||
|
{
|
||||||
return Err(Error::IrreplaceableTransaction);
|
return Err(Error::IrreplaceableTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1131,8 +1114,9 @@ where
|
|||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
sign_options: SignOptions,
|
sign_options: SignOptions,
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
// this helps us doing our job later
|
// This adds all the PSBT metadata for the inputs, which will help us later figure out how
|
||||||
self.add_input_hd_keypaths(psbt)?;
|
// to derive our keys
|
||||||
|
self.update_psbt_with_descriptor(psbt)?;
|
||||||
|
|
||||||
// If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
|
// If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
|
||||||
// has the `non_witness_utxo`
|
// has the `non_witness_utxo`
|
||||||
@ -1333,21 +1317,18 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_descriptor_for_txout(
|
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Result<Option<DerivedDescriptor>, Error> {
|
||||||
&self,
|
|
||||||
txout: &TxOut,
|
|
||||||
) -> Result<Option<DerivedDescriptor<'_>>, Error> {
|
|
||||||
Ok(self
|
Ok(self
|
||||||
.database
|
.database
|
||||||
.borrow()
|
.borrow()
|
||||||
.get_path_from_script_pubkey(&txout.script_pubkey)?
|
.get_path_from_script_pubkey(&txout.script_pubkey)?
|
||||||
.map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child))
|
.map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child))
|
||||||
.map(|(desc, child)| desc.as_derived(child, &self.secp)))
|
.map(|(desc, child)| desc.at_derivation_index(child)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||||
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
||||||
let index = match descriptor.is_deriveable() {
|
let index = match descriptor.has_wildcard() {
|
||||||
false => 0,
|
false => 0,
|
||||||
true => self.database.borrow_mut().increment_last_index(keychain)?,
|
true => self.database.borrow_mut().increment_last_index(keychain)?,
|
||||||
};
|
};
|
||||||
@ -1361,22 +1342,12 @@ where
|
|||||||
self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?;
|
self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let derived_descriptor = descriptor.as_derived(index, &self.secp);
|
|
||||||
|
|
||||||
let hd_keypaths = derived_descriptor.get_hd_keypaths(&self.secp);
|
|
||||||
let script = derived_descriptor.script_pubkey();
|
|
||||||
|
|
||||||
for validator in &self.address_validators {
|
|
||||||
#[allow(deprecated)]
|
|
||||||
validator.validate(keychain, &hd_keypaths, &script)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(index)
|
Ok(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||||
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
||||||
let index = match descriptor.is_deriveable() {
|
let index = match descriptor.has_wildcard() {
|
||||||
false => Some(0),
|
false => Some(0),
|
||||||
true => self.database.borrow_mut().get_last_index(keychain)?,
|
true => self.database.borrow_mut().get_last_index(keychain)?,
|
||||||
};
|
};
|
||||||
@ -1400,7 +1371,7 @@ where
|
|||||||
mut count: u32,
|
mut count: u32,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
||||||
if !descriptor.is_deriveable() {
|
if !descriptor.has_wildcard() {
|
||||||
if from > 0 {
|
if from > 0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -1413,7 +1384,7 @@ where
|
|||||||
let start_time = time::Instant::new();
|
let start_time = time::Instant::new();
|
||||||
for i in from..(from + count) {
|
for i in from..(from + count) {
|
||||||
address_batch.set_script_pubkey(
|
address_batch.set_script_pubkey(
|
||||||
&descriptor.as_derived(i, &self.secp).script_pubkey(),
|
&descriptor.at_derivation_index(i).script_pubkey(),
|
||||||
keychain,
|
keychain,
|
||||||
i,
|
i,
|
||||||
)?;
|
)?;
|
||||||
@ -1617,52 +1588,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// probably redundant but it doesn't hurt...
|
self.update_psbt_with_descriptor(&mut psbt)?;
|
||||||
self.add_input_hd_keypaths(&mut psbt)?;
|
|
||||||
|
|
||||||
// add metadata for the outputs
|
|
||||||
for (psbt_output, tx_output) in psbt.outputs.iter_mut().zip(psbt.unsigned_tx.output.iter())
|
|
||||||
{
|
|
||||||
if let Some((keychain, child)) = self
|
|
||||||
.database
|
|
||||||
.borrow()
|
|
||||||
.get_path_from_script_pubkey(&tx_output.script_pubkey)?
|
|
||||||
{
|
|
||||||
let (desc, _) = self._get_descriptor_for_keychain(keychain);
|
|
||||||
let derived_descriptor = desc.as_derived(child, &self.secp);
|
|
||||||
|
|
||||||
if let miniscript::Descriptor::Tr(tr) = &derived_descriptor {
|
|
||||||
let tap_tree = if tr.taptree().is_some() {
|
|
||||||
let mut builder = taproot::TaprootBuilder::new();
|
|
||||||
for (depth, ms) in tr.iter_scripts() {
|
|
||||||
let script = ms.encode();
|
|
||||||
builder = builder.add_leaf(depth, script).expect(
|
|
||||||
"Computing spend data on a valid Tree should always succeed",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(
|
|
||||||
psbt::TapTree::from_builder(builder)
|
|
||||||
.expect("The tree should always be valid"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
psbt_output.tap_tree = tap_tree;
|
|
||||||
psbt_output
|
|
||||||
.tap_key_origins
|
|
||||||
.append(&mut derived_descriptor.get_tap_key_origins(&self.secp));
|
|
||||||
psbt_output.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey());
|
|
||||||
} else {
|
|
||||||
psbt_output
|
|
||||||
.bip32_derivation
|
|
||||||
.append(&mut derived_descriptor.get_hd_keypaths(&self.secp));
|
|
||||||
}
|
|
||||||
if params.include_output_redeem_witness_script {
|
|
||||||
psbt_output.witness_script = derived_descriptor.psbt_witness_script();
|
|
||||||
psbt_output.redeem_script = derived_descriptor.psbt_redeem_script();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(psbt)
|
Ok(psbt)
|
||||||
}
|
}
|
||||||
@ -1688,29 +1614,11 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let desc = self.get_descriptor_for_keychain(keychain);
|
let desc = self.get_descriptor_for_keychain(keychain);
|
||||||
let derived_descriptor = desc.as_derived(child, &self.secp);
|
let derived_descriptor = desc.at_derivation_index(child);
|
||||||
|
|
||||||
if let miniscript::Descriptor::Tr(tr) = &derived_descriptor {
|
psbt_input
|
||||||
psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp);
|
.update_with_descriptor_unchecked(&derived_descriptor)
|
||||||
psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey());
|
.map_err(MiniscriptPsbtError::Conversion)?;
|
||||||
|
|
||||||
let spend_info = tr.spend_info();
|
|
||||||
psbt_input.tap_merkle_root = spend_info.merkle_root();
|
|
||||||
psbt_input.tap_scripts = spend_info
|
|
||||||
.as_script_map()
|
|
||||||
.keys()
|
|
||||||
.filter_map(|script_ver| {
|
|
||||||
spend_info
|
|
||||||
.control_block(script_ver)
|
|
||||||
.map(|cb| (cb, script_ver.clone()))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
} else {
|
|
||||||
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp);
|
|
||||||
}
|
|
||||||
|
|
||||||
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
|
|
||||||
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
|
|
||||||
|
|
||||||
let prev_output = utxo.outpoint;
|
let prev_output = utxo.outpoint;
|
||||||
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
||||||
@ -1724,38 +1632,47 @@ where
|
|||||||
Ok(psbt_input)
|
Ok(psbt_input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_input_hd_keypaths(
|
fn update_psbt_with_descriptor(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut input_utxos = Vec::with_capacity(psbt.inputs.len());
|
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
|
||||||
for n in 0..psbt.inputs.len() {
|
// the input utxos and outputs
|
||||||
input_utxos.push(psbt.get_utxo_for(n).clone());
|
//
|
||||||
}
|
// Clippy complains that the collect is not required, but that's wrong
|
||||||
|
#[allow(clippy::needless_collect)]
|
||||||
|
let utxos = (0..psbt.inputs.len())
|
||||||
|
.filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
|
||||||
|
.chain(
|
||||||
|
psbt.unsigned_tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, out)| (false, i, out.clone())),
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// try to add hd_keypaths if we've already seen the output
|
// Try to figure out the keychain and derivation for every input and output
|
||||||
for (psbt_input, out) in psbt.inputs.iter_mut().zip(input_utxos.iter()) {
|
for (is_input, index, out) in utxos.into_iter() {
|
||||||
if let Some(out) = out {
|
if let Some((keychain, child)) = self
|
||||||
if let Some((keychain, child)) = self
|
.database
|
||||||
.database
|
.borrow()
|
||||||
.borrow()
|
.get_path_from_script_pubkey(&out.script_pubkey)?
|
||||||
.get_path_from_script_pubkey(&out.script_pubkey)?
|
{
|
||||||
{
|
debug!(
|
||||||
debug!("Found descriptor {:?}/{}", keychain, child);
|
"Found descriptor for input #{} {:?}/{}",
|
||||||
|
index, keychain, child
|
||||||
|
);
|
||||||
|
|
||||||
// merge hd_keypaths or tap_key_origins
|
let desc = self.get_descriptor_for_keychain(keychain);
|
||||||
let desc = self.get_descriptor_for_keychain(keychain);
|
let desc = desc.at_derivation_index(child);
|
||||||
if desc.is_taproot() {
|
|
||||||
let mut tap_key_origins = desc
|
if is_input {
|
||||||
.as_derived(child, &self.secp)
|
psbt.update_input_with_descriptor(index, &desc)
|
||||||
.get_tap_key_origins(&self.secp);
|
.map_err(MiniscriptPsbtError::UtxoUpdate)?;
|
||||||
psbt_input.tap_key_origins.append(&mut tap_key_origins);
|
} else {
|
||||||
} else {
|
psbt.update_output_with_descriptor(index, &desc)
|
||||||
let mut hd_keypaths = desc
|
.map_err(MiniscriptPsbtError::OutputUpdate)?;
|
||||||
.as_derived(child, &self.secp)
|
|
||||||
.get_hd_keypaths(&self.secp);
|
|
||||||
psbt_input.bip32_derivation.append(&mut hd_keypaths);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1794,12 +1711,12 @@ where
|
|||||||
|
|
||||||
// We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will
|
// We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will
|
||||||
// end up with an infinite loop
|
// end up with an infinite loop
|
||||||
let is_deriveable = self.descriptor.is_deriveable()
|
let has_wildcard = self.descriptor.has_wildcard()
|
||||||
&& (self.change_descriptor.is_none()
|
&& (self.change_descriptor.is_none()
|
||||||
|| self.change_descriptor.as_ref().unwrap().is_deriveable());
|
|| self.change_descriptor.as_ref().unwrap().has_wildcard());
|
||||||
|
|
||||||
// Restrict max rounds in case of faulty "missing cache" implementation by blockchain
|
// Restrict max rounds in case of faulty "missing cache" implementation by blockchain
|
||||||
let max_rounds = if is_deriveable { 100 } else { 1 };
|
let max_rounds = if has_wildcard { 100 } else { 1 };
|
||||||
|
|
||||||
for _ in 0..max_rounds {
|
for _ in 0..max_rounds {
|
||||||
let sync_res =
|
let sync_res =
|
||||||
@ -1934,7 +1851,7 @@ pub fn get_funded_wallet(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod test {
|
pub(crate) mod test {
|
||||||
use bitcoin::{util::psbt, Network};
|
use bitcoin::{util::psbt, Network, PackedLockTime, Sequence};
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::types::KeychainKind;
|
use crate::types::KeychainKind;
|
||||||
@ -2270,7 +2187,7 @@ pub(crate) mod test {
|
|||||||
|
|
||||||
// Since we never synced the wallet we don't have a last_sync_height
|
// Since we never synced the wallet we don't have a last_sync_height
|
||||||
// we could use to try to prevent fee sniping. We default to 0.
|
// we could use to try to prevent fee sniping. We default to 0.
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, 0);
|
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2295,7 +2212,7 @@ pub(crate) mod test {
|
|||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
// current_height will override the last sync height
|
// current_height will override the last sync height
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, current_height);
|
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(current_height));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2318,7 +2235,10 @@ pub(crate) mod test {
|
|||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
// If there's no current_height we're left with using the last sync height
|
// If there's no current_height we're left with using the last sync height
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, sync_time.block_time.height);
|
assert_eq!(
|
||||||
|
psbt.unsigned_tx.lock_time,
|
||||||
|
PackedLockTime(sync_time.block_time.height)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2329,7 +2249,7 @@ pub(crate) mod test {
|
|||||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, 100_000);
|
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(100_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2340,13 +2260,13 @@ pub(crate) mod test {
|
|||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.current_height(630_001)
|
.current_height(630_001)
|
||||||
.nlocktime(630_000);
|
.nlocktime(LockTime::from_height(630_000).unwrap());
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
// When we explicitly specify a nlocktime
|
// When we explicitly specify a nlocktime
|
||||||
// we don't try any fee sniping prevention trick
|
// we don't try any fee sniping prevention trick
|
||||||
// (we ignore the current_height)
|
// (we ignore the current_height)
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, 630_000);
|
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(630_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2356,15 +2276,15 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.nlocktime(630_000);
|
.nlocktime(LockTime::from_height(630_000).unwrap());
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.lock_time, 630_000);
|
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(630_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(
|
#[should_panic(
|
||||||
expected = "TxBuilder requested timelock of `50000`, but at least `100000` is required to spend from this script"
|
expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script"
|
||||||
)]
|
)]
|
||||||
fn test_create_tx_custom_locktime_incompatible_with_cltv() {
|
fn test_create_tx_custom_locktime_incompatible_with_cltv() {
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||||
@ -2372,7 +2292,7 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.nlocktime(50000);
|
.nlocktime(LockTime::from_height(50000).unwrap());
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2384,7 +2304,7 @@ pub(crate) mod test {
|
|||||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 6);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2398,12 +2318,12 @@ pub(crate) mod test {
|
|||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
// When CSV is enabled it takes precedence over the rbf value (unless forced by the user).
|
// When CSV is enabled it takes precedence over the rbf value (unless forced by the user).
|
||||||
// It will be set to the OP_CSV value, in this case 6
|
// It will be set to the OP_CSV value, in this case 6
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 6);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(
|
#[should_panic(
|
||||||
expected = "Cannot enable RBF with nSequence `3` given a required OP_CSV of `6`"
|
expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`"
|
||||||
)]
|
)]
|
||||||
fn test_create_tx_with_custom_rbf_csv() {
|
fn test_create_tx_with_custom_rbf_csv() {
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
|
||||||
@ -2411,7 +2331,7 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.enable_rbf_with_sequence(3);
|
.enable_rbf_with_sequence(Sequence(3));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2423,7 +2343,7 @@ pub(crate) mod test {
|
|||||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFE);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2434,7 +2354,7 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.enable_rbf_with_sequence(0xFFFFFFFE);
|
.enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2445,10 +2365,10 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), 25_000)
|
.add_recipient(addr.script_pubkey(), 25_000)
|
||||||
.enable_rbf_with_sequence(0xDEADBEEF);
|
.enable_rbf_with_sequence(Sequence(0xDEADBEEF));
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xDEADBEEF);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2475,7 +2395,7 @@ pub(crate) mod test {
|
|||||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFF);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2996,7 +2916,7 @@ pub(crate) mod test {
|
|||||||
.policy_path(path, KeychainKind::External);
|
.policy_path(path, KeychainKind::External);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFF);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -3015,7 +2935,7 @@ pub(crate) mod test {
|
|||||||
.policy_path(path, KeychainKind::External);
|
.policy_path(path, KeychainKind::External);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.unsigned_tx.input[0].sequence, 144);
|
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -4868,7 +4788,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key());
|
let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key());
|
||||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
|
||||||
let path = vec![("rn4nre9c".to_string(), vec![0])]
|
let path = vec![("e5mmg3xh".to_string(), vec![0])]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -4878,48 +4798,50 @@ pub(crate) mod test {
|
|||||||
.policy_path(path, KeychainKind::External);
|
.policy_path(path, KeychainKind::External);
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
|
let mut input_key_origins = psbt.inputs[0]
|
||||||
|
.tap_key_origins
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
input_key_origins.sort();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
psbt.inputs[0]
|
input_key_origins,
|
||||||
.tap_key_origins
|
vec![
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![(
|
|
||||||
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
|
|
||||||
(
|
(
|
||||||
vec![
|
from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
|
||||||
from_str!(
|
(
|
||||||
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
|
vec![],
|
||||||
),
|
(FromStr::from_str("871fd295").unwrap(), vec![].into())
|
||||||
from_str!(
|
)
|
||||||
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
|
),
|
||||||
)
|
(
|
||||||
],
|
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
|
||||||
(Default::default(), Default::default())
|
(
|
||||||
|
vec![
|
||||||
|
from_str!(
|
||||||
|
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
|
||||||
|
),
|
||||||
|
from_str!(
|
||||||
|
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
(FromStr::from_str("ece52657").unwrap(), vec![].into())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)],
|
],
|
||||||
"Wrong input tap_key_origins"
|
"Wrong input tap_key_origins"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut output_key_origins = psbt.outputs[0]
|
||||||
|
.tap_key_origins
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
output_key_origins.sort();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
psbt.outputs[0]
|
input_key_origins, output_key_origins,
|
||||||
.tap_key_origins
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![(
|
|
||||||
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
|
|
||||||
(
|
|
||||||
vec![
|
|
||||||
from_str!(
|
|
||||||
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
|
|
||||||
),
|
|
||||||
from_str!(
|
|
||||||
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
(Default::default(), Default::default())
|
|
||||||
)
|
|
||||||
)],
|
|
||||||
"Wrong output tap_key_origins"
|
"Wrong output tap_key_origins"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -5171,7 +5093,7 @@ pub(crate) mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_taproot_script_spend_sign_include_some_leaves() {
|
fn test_taproot_script_spend_sign_include_some_leaves() {
|
||||||
use crate::signer::TapLeavesOptions;
|
use crate::signer::TapLeavesOptions;
|
||||||
use crate::wallet::taproot::TapLeafHash;
|
use bitcoin::util::taproot::TapLeafHash;
|
||||||
|
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
@ -5213,7 +5135,7 @@ pub(crate) mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
||||||
use crate::signer::TapLeavesOptions;
|
use crate::signer::TapLeavesOptions;
|
||||||
use crate::wallet::taproot::TapLeafHash;
|
use bitcoin::util::taproot::TapLeafHash;
|
||||||
|
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
@ -5560,6 +5482,7 @@ pub(crate) mod test {
|
|||||||
SignOptions {
|
SignOptions {
|
||||||
remove_partial_sigs: false,
|
remove_partial_sigs: false,
|
||||||
try_finalize: false,
|
try_finalize: false,
|
||||||
|
allow_grinding: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -5574,6 +5497,7 @@ pub(crate) mod test {
|
|||||||
&mut psbt,
|
&mut psbt,
|
||||||
SignOptions {
|
SignOptions {
|
||||||
remove_partial_sigs: false,
|
remove_partial_sigs: false,
|
||||||
|
allow_grinding: false,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -5582,6 +5506,39 @@ pub(crate) mod test {
|
|||||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
|
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_rate_sign_grinding_low_r() {
|
||||||
|
// Our goal is to obtain a transaction with a signature with low-R (70 bytes)
|
||||||
|
// by setting the `allow_grinding` signing option as true.
|
||||||
|
// We then check that our fee rate and fee calculation is alright and that our
|
||||||
|
// signature is 70 bytes.
|
||||||
|
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
|
let addr = wallet.get_address(New).unwrap();
|
||||||
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder
|
||||||
|
.drain_to(addr.script_pubkey())
|
||||||
|
.drain_wallet()
|
||||||
|
.fee_rate(fee_rate);
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.sign(
|
||||||
|
&mut psbt,
|
||||||
|
SignOptions {
|
||||||
|
remove_partial_sigs: false,
|
||||||
|
allow_grinding: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
|
||||||
|
let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
|
||||||
|
assert_eq!(sig_len, 70);
|
||||||
|
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-hardware-signer")]
|
#[cfg(feature = "test-hardware-signer")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_signer() {
|
fn test_create_signer() {
|
||||||
|
@ -96,10 +96,10 @@ use bitcoin::{secp256k1, XOnlyPublicKey};
|
|||||||
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
||||||
|
|
||||||
use miniscript::descriptor::{
|
use miniscript::descriptor::{
|
||||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
|
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
|
||||||
KeyMap, SinglePubKey,
|
SinglePubKey,
|
||||||
};
|
};
|
||||||
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
|
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
|
||||||
|
|
||||||
use super::utils::SecpCtx;
|
use super::utils::SecpCtx;
|
||||||
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||||
@ -369,11 +369,11 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
|||||||
|
|
||||||
impl SignerCommon for SignerWrapper<PrivateKey> {
|
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
Some(DescriptorSecretKey::Single(SinglePriv {
|
||||||
key: self.signer,
|
key: self.signer,
|
||||||
origin: None,
|
origin: None,
|
||||||
}))
|
}))
|
||||||
@ -472,6 +472,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
|||||||
hash,
|
hash,
|
||||||
hash_ty,
|
hash_ty,
|
||||||
secp,
|
secp,
|
||||||
|
sign_options.allow_grinding,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -485,9 +486,14 @@ fn sign_psbt_ecdsa(
|
|||||||
hash: bitcoin::Sighash,
|
hash: bitcoin::Sighash,
|
||||||
hash_ty: EcdsaSighashType,
|
hash_ty: EcdsaSighashType,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
|
allow_grinding: bool,
|
||||||
) {
|
) {
|
||||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
let sig = secp.sign_ecdsa(msg, secret_key);
|
let sig = if allow_grinding {
|
||||||
|
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||||
|
} else {
|
||||||
|
secp.sign_ecdsa(msg, secret_key)
|
||||||
|
};
|
||||||
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||||
.expect("invalid or corrupted ecdsa signature");
|
.expect("invalid or corrupted ecdsa signature");
|
||||||
|
|
||||||
@ -511,13 +517,13 @@ fn sign_psbt_schnorr(
|
|||||||
let keypair = match leaf_hash {
|
let keypair = match leaf_hash {
|
||||||
None => keypair
|
None => keypair
|
||||||
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||||
.into_inner(),
|
.to_inner(),
|
||||||
Some(_) => keypair, // no tweak for script spend
|
Some(_) => keypair, // no tweak for script spend
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
let sig = secp.sign_schnorr(msg, &keypair);
|
let sig = secp.sign_schnorr(msg, &keypair);
|
||||||
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair))
|
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||||
.expect("invalid or corrupted schnorr signature");
|
.expect("invalid or corrupted schnorr signature");
|
||||||
|
|
||||||
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
||||||
@ -570,7 +576,7 @@ impl SignersContainer {
|
|||||||
self.0
|
self.0
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|signer| signer.descriptor_secret_key())
|
.filter_map(|signer| signer.descriptor_secret_key())
|
||||||
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
|
.filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,8 +601,13 @@ impl SignersContainer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match secret {
|
match secret {
|
||||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
DescriptorSecretKey::Single(private_key) => container.add_external(
|
||||||
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
|
SignerId::from(
|
||||||
|
private_key
|
||||||
|
.key
|
||||||
|
.public_key(secp)
|
||||||
|
.to_pubkeyhash(SigType::Ecdsa),
|
||||||
|
),
|
||||||
SignerOrdering::default(),
|
SignerOrdering::default(),
|
||||||
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
||||||
),
|
),
|
||||||
@ -718,10 +729,15 @@ pub struct SignOptions {
|
|||||||
///
|
///
|
||||||
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||||
pub sign_with_tap_internal_key: bool,
|
pub sign_with_tap_internal_key: bool,
|
||||||
|
|
||||||
|
/// Whether we should grind ECDSA signature to ensure signing with low r
|
||||||
|
/// or not.
|
||||||
|
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
|
||||||
|
pub allow_grinding: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Customize which taproot script-path leaves the signer should sign.
|
/// Customize which taproot script-path leaves the signer should sign.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum TapLeavesOptions {
|
pub enum TapLeavesOptions {
|
||||||
/// The signer will sign all the leaves it has a key for.
|
/// The signer will sign all the leaves it has a key for.
|
||||||
All,
|
All,
|
||||||
@ -751,6 +767,7 @@ impl Default for SignOptions {
|
|||||||
try_finalize: true,
|
try_finalize: true,
|
||||||
tap_leaves_options: TapLeavesOptions::default(),
|
tap_leaves_options: TapLeavesOptions::default(),
|
||||||
sign_with_tap_internal_key: true,
|
sign_with_tap_internal_key: true,
|
||||||
|
allow_grinding: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,7 @@ use std::default::Default;
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||||
use bitcoin::{OutPoint, Script, Transaction};
|
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorTrait;
|
|
||||||
|
|
||||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||||
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||||
@ -139,7 +137,7 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) manually_selected_only: bool,
|
pub(crate) manually_selected_only: bool,
|
||||||
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
||||||
pub(crate) ordering: TxOrdering,
|
pub(crate) ordering: TxOrdering,
|
||||||
pub(crate) locktime: Option<u32>,
|
pub(crate) locktime: Option<LockTime>,
|
||||||
pub(crate) rbf: Option<RbfValue>,
|
pub(crate) rbf: Option<RbfValue>,
|
||||||
pub(crate) version: Option<Version>,
|
pub(crate) version: Option<Version>,
|
||||||
pub(crate) change_policy: ChangeSpendPolicy,
|
pub(crate) change_policy: ChangeSpendPolicy,
|
||||||
@ -147,7 +145,7 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) add_global_xpubs: bool,
|
pub(crate) add_global_xpubs: bool,
|
||||||
pub(crate) include_output_redeem_witness_script: bool,
|
pub(crate) include_output_redeem_witness_script: bool,
|
||||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||||
pub(crate) current_height: Option<u32>,
|
pub(crate) current_height: Option<LockTime>,
|
||||||
pub(crate) allow_dust: bool,
|
pub(crate) allow_dust: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,7 +424,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
/// Use a specific nLockTime while creating the transaction
|
/// Use a specific nLockTime while creating the transaction
|
||||||
///
|
///
|
||||||
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
||||||
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self {
|
pub fn nlocktime(&mut self, locktime: LockTime) -> &mut Self {
|
||||||
self.params.locktime = Some(locktime);
|
self.params.locktime = Some(locktime);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -541,7 +539,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
///
|
///
|
||||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||||
/// be a valid nSequence to signal RBF.
|
/// be a valid nSequence to signal RBF.
|
||||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
|
||||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -558,7 +556,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
///
|
///
|
||||||
/// In both cases, if you don't provide a current height, we use the last sync height.
|
/// In both cases, if you don't provide a current height, we use the last sync height.
|
||||||
pub fn current_height(&mut self, height: u32) -> &mut Self {
|
pub fn current_height(&mut self, height: u32) -> &mut Self {
|
||||||
self.params.current_height = Some(height);
|
self.params.current_height = Some(LockTime::from_height(height).expect("Invalid height"));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,7 +701,7 @@ impl TxOrdering {
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
|
||||||
|
|
||||||
tx.output.shuffle(&mut rng);
|
tx.output.shuffle(&mut rng);
|
||||||
}
|
}
|
||||||
@ -736,13 +734,13 @@ impl Default for Version {
|
|||||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
pub(crate) enum RbfValue {
|
pub(crate) enum RbfValue {
|
||||||
Default,
|
Default,
|
||||||
Value(u32),
|
Value(Sequence),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RbfValue {
|
impl RbfValue {
|
||||||
pub(crate) fn get_value(&self) -> u32 {
|
pub(crate) fn get_value(&self) -> Sequence {
|
||||||
match self {
|
match self {
|
||||||
RbfValue::Default => 0xFFFFFFFD,
|
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||||
RbfValue::Value(v) => *v,
|
RbfValue::Value(v) => *v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -858,10 +856,12 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_test_utxos() -> Vec<LocalUtxo> {
|
fn get_test_utxos() -> Vec<LocalUtxo> {
|
||||||
|
use bitcoin::hashes::Hash;
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
txid: Default::default(),
|
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||||
vout: 0,
|
vout: 0,
|
||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
@ -870,7 +870,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
txid: Default::default(),
|
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||||
vout: 1,
|
vout: 1,
|
||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
|
@ -9,23 +9,11 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
use bitcoin::blockdata::script::Script;
|
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
use bitcoin::{LockTime, Script, Sequence};
|
||||||
|
|
||||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||||
|
|
||||||
// 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
|
|
||||||
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
|
|
||||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
|
||||||
// otherwise it's time-based
|
|
||||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
|
||||||
// Mask for the bits used to express the timelock
|
|
||||||
pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
|
|
||||||
|
|
||||||
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
|
|
||||||
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
|
|
||||||
|
|
||||||
/// Trait to check if a value is below the dust limit.
|
/// Trait to check if a value is below the dust limit.
|
||||||
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
||||||
/// keep it compatible with network dust rate
|
/// keep it compatible with network dust rate
|
||||||
@ -38,7 +26,7 @@ pub trait IsDust {
|
|||||||
|
|
||||||
impl IsDust for u64 {
|
impl IsDust for u64 {
|
||||||
fn is_dust(&self, script: &Script) -> bool {
|
fn is_dust(&self, script: &Script) -> bool {
|
||||||
*self < script.dust_value().as_sat()
|
*self < script.dust_value().to_sat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,19 +44,15 @@ impl After {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
|
||||||
// This flag cannot be set in the nSequence when spending using OP_CSV
|
// The RBF value must enable relative timelocks
|
||||||
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
if !rbf.is_relative_lock_time() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mask = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK;
|
|
||||||
let rbf = rbf & mask;
|
|
||||||
let csv = csv & mask;
|
|
||||||
|
|
||||||
// Both values should be represented in the same unit (either time-based or
|
// Both values should be represented in the same unit (either time-based or
|
||||||
// block-height based)
|
// block-height based)
|
||||||
if (rbf < SEQUENCE_LOCKTIME_TYPE_FLAG) != (csv < SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
if rbf.is_time_locked() != csv.is_time_locked() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
|
|
||||||
// Both values should be expressed in the same unit
|
|
||||||
if (nlocktime < BLOCKS_TIMELOCK_THRESHOLD) != (required < BLOCKS_TIMELOCK_THRESHOLD) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The value should be at least `required`
|
|
||||||
if nlocktime < required {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
||||||
fn check_after(&self, n: u32) -> bool {
|
fn check_after(&self, n: LockTime) -> bool {
|
||||||
if let Some(current_height) = self.current_height {
|
if let Some(current_height) = self.current_height {
|
||||||
current_height >= n
|
current_height >= n.to_consensus_u32()
|
||||||
} else {
|
} else {
|
||||||
self.assume_height_reached
|
self.assume_height_reached
|
||||||
}
|
}
|
||||||
@ -125,10 +95,15 @@ impl Older {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||||
fn check_older(&self, n: u32) -> bool {
|
fn check_older(&self, n: Sequence) -> bool {
|
||||||
if let Some(current_height) = self.current_height {
|
if let Some(current_height) = self.current_height {
|
||||||
// TODO: test >= / >
|
// TODO: test >= / >
|
||||||
current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64
|
current_height
|
||||||
|
>= self
|
||||||
|
.create_height
|
||||||
|
.unwrap_or(0)
|
||||||
|
.checked_add(n.to_consensus_u32())
|
||||||
|
.expect("Overflowing addition")
|
||||||
} else {
|
} else {
|
||||||
self.assume_height_reached
|
self.assume_height_reached
|
||||||
}
|
}
|
||||||
@ -139,11 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{
|
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||||
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
|
// otherwise it's time-based
|
||||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||||
};
|
|
||||||
use crate::bitcoin::Address;
|
use super::{check_nsequence_rbf, IsDust};
|
||||||
|
use crate::bitcoin::{Address, Sequence};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -165,66 +141,40 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_msb_set() {
|
fn test_check_nsequence_rbf_msb_set() {
|
||||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
|
||||||
assert!(!result);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_lt_csv() {
|
fn test_check_nsequence_rbf_lt_csv() {
|
||||||
let result = check_nsequence_rbf(4000, 5000);
|
let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
|
||||||
assert!(!result);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_different_unit() {
|
fn test_check_nsequence_rbf_different_unit() {
|
||||||
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
let result =
|
||||||
|
check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
|
||||||
assert!(!result);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_mask() {
|
fn test_check_nsequence_rbf_mask() {
|
||||||
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
|
||||||
assert!(result);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_same_unit_blocks() {
|
fn test_check_nsequence_rbf_same_unit_blocks() {
|
||||||
let result = check_nsequence_rbf(10_000, 5000);
|
let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
|
||||||
assert!(result);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_same_unit_time() {
|
fn test_check_nsequence_rbf_same_unit_time() {
|
||||||
let result = check_nsequence_rbf(
|
let result = check_nsequence_rbf(
|
||||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
|
||||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
|
||||||
);
|
|
||||||
assert!(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_nlocktime_lt_cltv() {
|
|
||||||
let result = check_nlocktime(4000, 5000);
|
|
||||||
assert!(!result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_nlocktime_different_unit() {
|
|
||||||
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
|
||||||
assert!(!result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_nlocktime_same_unit_blocks() {
|
|
||||||
let result = check_nlocktime(10_000, 5000);
|
|
||||||
assert!(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_nlocktime_same_unit_time() {
|
|
||||||
let result = check_nlocktime(
|
|
||||||
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
|
||||||
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
|
||||||
);
|
);
|
||||||
assert!(result);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ use crate::error::Error;
|
|||||||
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
||||||
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
||||||
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
||||||
|
///
|
||||||
|
/// [`Blockchain`]: crate::blockchain::Blockchain
|
||||||
pub fn verify_tx<D: Database, B: GetTx>(
|
pub fn verify_tx<D: Database, B: GetTx>(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
database: &D,
|
database: &D,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user