Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd64d12835 | ||
|
|
7e8e9921a9 | ||
|
|
31fa4f723a | ||
|
|
c741df746b | ||
|
|
3205f0c16d | ||
|
|
5f0870a741 | ||
|
|
5a483472c1 | ||
|
|
8d4cc3920a | ||
|
|
14bc9c0e35 | ||
|
|
2451c00268 | ||
|
|
4cad18bbca | ||
|
|
634a0575cb | ||
|
|
d3d07564f2 | ||
|
|
0b768d6f0b | ||
|
|
ec9aefac6b | ||
|
|
d72aa7ebc0 | ||
|
|
99930af12e | ||
|
|
d6e730f18a | ||
|
|
d1e5b87bfc | ||
|
|
c101dea460 | ||
|
|
9ddd502538 | ||
|
|
a5d345fff2 | ||
|
|
11dcc14374 | ||
|
|
4c5ceaff14 | ||
|
|
b5fcddcf1a | ||
|
|
d570ff2c65 | ||
|
|
21c96c9c81 | ||
|
|
c51d544932 | ||
|
|
5e56c3b3c1 | ||
|
|
235961a934 | ||
|
|
df905a8d5e | ||
|
|
8b68cf9546 | ||
|
|
150f4d6f41 | ||
|
|
1c95ca33a8 | ||
|
|
108edc3a6b | ||
|
|
f99a6b9f43 | ||
|
|
aedbc8c97d | ||
|
|
5c42102c79 | ||
|
|
5d5b2fb88c | ||
|
|
9cb6f70fc0 | ||
|
|
5720e38033 | ||
|
|
e9bbb8724f | ||
|
|
648282e602 | ||
|
|
c7a43d941f | ||
|
|
1ffd59d469 | ||
|
|
ae4f4e5416 | ||
|
|
9854fd34ea | ||
|
|
60057a7bf7 | ||
|
|
ea47d7a35b | ||
|
|
f2181f5467 | ||
|
|
34987d58ec | ||
|
|
1c76084db8 | ||
|
|
68dd6d2031 | ||
|
|
1437e1ecfe | ||
|
|
1a71eb1f47 | ||
|
|
0695e9fb3e | ||
|
|
a4a43ea860 | ||
|
|
b627455b8f | ||
|
|
1331193800 | ||
|
|
7de8be46c0 | ||
|
|
55145f57a1 | ||
|
|
e2a4a5884b | ||
|
|
fd34956c29 |
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. -->
|
||||
6
.github/workflows/cont_integration.yml
vendored
6
.github/workflows/cont_integration.yml
vendored
@@ -154,7 +154,7 @@ jobs:
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Check
|
||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async --no-default-features
|
||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
@@ -173,8 +173,8 @@ jobs:
|
||||
- name: Check fmt
|
||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||
|
||||
test_harware_wallet:
|
||||
runs-on: ubuntu-latest
|
||||
test_hardware_wallet:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
target
|
||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2022-01-25
|
||||
run: rustup default nightly-2022-12-14
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
|
||||
57
Cargo.toml
57
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.22.0"
|
||||
version = "0.26.0"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -14,16 +14,16 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
bdk-macros = "^0.6"
|
||||
log = "^0.4"
|
||||
miniscript = { version = "7.0", features = ["use-serde"] }
|
||||
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
|
||||
miniscript = { version = "8.0", features = ["serde"] }
|
||||
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.7"
|
||||
rand = "^0.8"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.11", optional = true }
|
||||
esplora-client = { version = "0.1.1", default-features = false, optional = true }
|
||||
electrum-client = { version = "0.12", optional = true }
|
||||
esplora-client = { version = "0.3", default-features = false, optional = true }
|
||||
rusqlite = { version = "0.27.0", optional = true }
|
||||
ahash = { version = "0.7.6", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
@@ -31,22 +31,22 @@ async-trait = { version = "0.1", optional = true }
|
||||
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
||||
cc = { version = ">=1.0.64", optional = true }
|
||||
socks = { version = "0.3", optional = true }
|
||||
hwi = { version = "0.2.2", optional = true }
|
||||
hwi = { version = "0.4.0", optional = true, features = [ "use-miniscript"] }
|
||||
|
||||
bip39 = { version = "1.0.1", optional = true }
|
||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||
|
||||
# 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
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
async-trait = "0.1"
|
||||
js-sys = "0.3"
|
||||
rand = { version = "^0.7", features = ["wasm-bindgen"] }
|
||||
|
||||
[features]
|
||||
minimal = []
|
||||
@@ -98,13 +98,19 @@ test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoi
|
||||
test-md-docs = ["electrum"]
|
||||
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]
|
||||
lazy_static = "1.4"
|
||||
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"
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
[[example]]
|
||||
name = "compact_filters_balance"
|
||||
required-features = ["compact_filters"]
|
||||
@@ -128,6 +134,31 @@ name = "psbt_signer"
|
||||
path = "examples/psbt_signer.rs"
|
||||
required-features = ["electrum"]
|
||||
|
||||
[[example]]
|
||||
name = "hardware_signer"
|
||||
path = "examples/hardware_signer.rs"
|
||||
required-features = ["electrum", "hardware-signer"]
|
||||
|
||||
[[example]]
|
||||
name = "electrum_backend"
|
||||
path = "examples/electrum_backend.rs"
|
||||
required-features = ["electrum"]
|
||||
|
||||
[[example]]
|
||||
name = "esplora_backend_synchronous"
|
||||
path = "examples/esplora_backend_synchronous.rs"
|
||||
required-features = ["use-esplora-ureq"]
|
||||
|
||||
[[example]]
|
||||
name = "esplora_backend_asynchronous"
|
||||
path = "examples/esplora_backend_asynchronous.rs"
|
||||
required-features = ["use-esplora-reqwest", "reqwest-default-tls", "async-interface"]
|
||||
|
||||
[[example]]
|
||||
name = "mnemonic_to_descriptors"
|
||||
path = "examples/mnemonic_to_descriptors.rs"
|
||||
required-features = ["all-keys"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
30
README.md
30
README.md
@@ -68,12 +68,13 @@ fn main() -> Result<(), bdk::Error> {
|
||||
```rust
|
||||
use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::bitcoin::Network;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
@@ -95,15 +96,16 @@ use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::serialize;
|
||||
use base64;
|
||||
use bdk::bitcoin::consensus::serialize;
|
||||
use bdk::bitcoin::Network;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
@@ -132,21 +134,22 @@ fn main() -> Result<(), bdk::Error> {
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::deserialize;
|
||||
use base64;
|
||||
use bdk::bitcoin::consensus::deserialize;
|
||||
use bdk::bitcoin::Network;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
let psbt = "...";
|
||||
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
let _finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -171,6 +174,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.
|
||||
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
|
||||
|
||||
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(())
|
||||
}
|
||||
87
examples/electrum_backend.rs
Normal file
87
examples/electrum_backend.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::template::Bip84;
|
||||
use bdk::wallet::export::FullyNodedExport;
|
||||
use bdk::{KeychainKind, SyncOptions, Wallet};
|
||||
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bitcoin::util::bip32;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Electrum server.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --example electrum_backend` in the root folder.
|
||||
fn main() {
|
||||
let network = Network::Testnet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let electrum_url = "ssl://electrum.blockstream.info:60002";
|
||||
|
||||
run(&network, electrum_url, xpriv);
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn run(network: &Network, electrum_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
// Apparently it works only with Electrs (not EletrumX)
|
||||
let blockchain = ElectrumBlockchain::from(Client::new(electrum_url).unwrap());
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 6500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 5359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
93
examples/esplora_backend_asynchronous.rs
Normal file
93
examples/esplora_backend_asynchronous.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::blockchain::Blockchain;
|
||||
use bdk::{
|
||||
blockchain::esplora::EsploraBlockchain,
|
||||
database::MemoryDatabase,
|
||||
template::Bip84,
|
||||
wallet::{export::FullyNodedExport, AddressIndex},
|
||||
KeychainKind, SyncOptions, Wallet,
|
||||
};
|
||||
use bitcoin::{
|
||||
util::bip32::{self, ExtendedPrivKey},
|
||||
Network,
|
||||
};
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||
/// using non blocking asynchronous calls with `reqwest`.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --no-default-features --features="use-esplora-reqwest, reqwest-default-tls, async-interface" --example esplora_backend_asynchronous`
|
||||
/// in the root folder.
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let network = Network::Signet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||
|
||||
run(&network, esplora_url, xpriv).await;
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet
|
||||
.sync(&blockchain, SyncOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 10500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 9359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
let _ = blockchain.broadcast(&tx);
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
89
examples/esplora_backend_synchronous.rs
Normal file
89
examples/esplora_backend_synchronous.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::blockchain::Blockchain;
|
||||
use bdk::{
|
||||
blockchain::esplora::EsploraBlockchain,
|
||||
database::MemoryDatabase,
|
||||
template::Bip84,
|
||||
wallet::{export::FullyNodedExport, AddressIndex},
|
||||
KeychainKind, SyncOptions, Wallet,
|
||||
};
|
||||
use bitcoin::{
|
||||
util::bip32::{self, ExtendedPrivKey},
|
||||
Network,
|
||||
};
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||
/// using blocking calls with `ureq`.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --features=use-esplora-ureq --example esplora_backend_synchronous`
|
||||
/// in the root folder.
|
||||
fn main() {
|
||||
let network = Network::Signet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||
|
||||
run(&network, esplora_url, xpriv);
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 10500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 9359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
105
examples/hardware_signer.rs
Normal file
105
examples/hardware_signer.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use bdk::bitcoin::{Address, Network};
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::hwi::{types::HWIChain, HWIClient};
|
||||
use bdk::miniscript::{Descriptor, DescriptorPublicKey};
|
||||
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 mut devices = HWIClient::enumerate()?;
|
||||
if devices.is_empty() {
|
||||
panic!("No devices found. Either plug in a hardware wallet, or start a simulator.");
|
||||
}
|
||||
let first_device = devices.remove(0)?;
|
||||
// ...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::<Descriptor<DescriptorPublicKey>>(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].clone(),
|
||||
Some(descriptors.internal[0].clone()),
|
||||
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(())
|
||||
}
|
||||
60
examples/mnemonic_to_descriptors.rs
Normal file
60
examples/mnemonic_to_descriptors.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::util::bip32::DerivationPath;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor;
|
||||
use bdk::descriptor::IntoWalletDescriptor;
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::Tap;
|
||||
use bdk::Error as BDK_Error;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates how to generate a mnemonic phrase
|
||||
/// using BDK and use that to generate a descriptor string.
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
// In this example we are generating a 12 words mnemonic phrase
|
||||
// but it is also possible generate 15, 18, 21 and 24 words
|
||||
// using their respective `WordCount` variant.
|
||||
let mnemonic: GeneratedKey<_, Tap> =
|
||||
Mnemonic::generate((WordCount::Words12, Language::English))
|
||||
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
|
||||
|
||||
println!("Mnemonic phrase: {}", *mnemonic);
|
||||
let mnemonic_with_passphrase = (mnemonic, None);
|
||||
|
||||
// define external and internal derivation key path
|
||||
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
|
||||
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
|
||||
|
||||
// generate external and internal descriptor from mnemonic
|
||||
let (external_descriptor, ext_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
let (internal_descriptor, int_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
println!("tpub external descriptor: {}", external_descriptor);
|
||||
println!("tpub internal descriptor: {}", internal_descriptor);
|
||||
println!(
|
||||
"tprv external descriptor: {}",
|
||||
external_descriptor.to_string_with_secret(&ext_keymap)
|
||||
);
|
||||
println!(
|
||||
"tprv internal descriptor: {}",
|
||||
internal_descriptor.to_string_with_secret(&int_keymap)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -9,20 +9,38 @@
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::SyncOptions;
|
||||
use bdk::{descriptor, SyncOptions};
|
||||
use bdk::{FeeRate, SignOptions, Wallet};
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::{Address, Network};
|
||||
use electrum_client::Client;
|
||||
use miniscript::descriptor::DescriptorSecretKey;
|
||||
use std::error::Error;
|
||||
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>> {
|
||||
// test keys created with `bdk-cli key generate` and `bdk-cli key derive` commands
|
||||
let signing_external_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*)";
|
||||
let signing_internal_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*)";
|
||||
// test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
|
||||
let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
|
||||
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 watch_only_internal_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/1/*)";
|
||||
let secp = Secp256k1::new();
|
||||
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
|
||||
let blockchain =
|
||||
|
||||
30
examples/utils/mod.rs
Normal file
30
examples/utils/mod.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
pub(crate) mod tx {
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::{database::BatchDatabase, SignOptions, Wallet};
|
||||
use bitcoin::{Address, Transaction};
|
||||
|
||||
pub fn build_signed_tx<D: BatchDatabase>(
|
||||
wallet: &Wallet<D>,
|
||||
recipient_address: &str,
|
||||
amount: u64,
|
||||
) -> Transaction {
|
||||
// Create a transaction builder
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
|
||||
let to_address = Address::from_str(recipient_address).unwrap();
|
||||
|
||||
// Set recipient of the transaction
|
||||
tx_builder.set_recipients(vec![(to_address.script_pubkey(), amount)]);
|
||||
|
||||
// Finalise the transaction and extract PSBT
|
||||
let (mut psbt, _) = tx_builder.finish().unwrap();
|
||||
|
||||
// Sign the above psbt with signing option
|
||||
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
|
||||
// Extract the final transaction
|
||||
psbt.extract_tx()
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,8 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
|
||||
/// "type" : "electrum",
|
||||
/// "url" : "ssl://electrum.blockstream.info:50002",
|
||||
/// "retry": 2,
|
||||
/// "stop_gap": 20
|
||||
/// "stop_gap": 20,
|
||||
/// "validate_domain": true
|
||||
/// }"#,
|
||||
/// )
|
||||
/// .unwrap();
|
||||
@@ -190,11 +191,12 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
|
||||
/// socks5: None,
|
||||
/// timeout: None,
|
||||
/// stop_gap: 20,
|
||||
/// validate_domain: true,
|
||||
/// })
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AnyBlockchainConfig {
|
||||
#[cfg(feature = "electrum")]
|
||||
|
||||
@@ -479,7 +479,7 @@ impl WalletSync for CompactFiltersBlockchain {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Peer address such as 127.0.0.1:18333
|
||||
pub address: String,
|
||||
@@ -490,7 +490,7 @@ pub struct BitcoinPeerConfig {
|
||||
}
|
||||
|
||||
/// Configuration for a [`CompactFiltersBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CompactFiltersBlockchainConfig {
|
||||
/// List of peers to try to connect to for asking headers and filters
|
||||
pub peers: Vec<BitcoinPeerConfig>,
|
||||
|
||||
@@ -75,7 +75,10 @@ impl Mempool {
|
||||
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
||||
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
||||
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::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
|
||||
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),
|
||||
|
||||
@@ -103,42 +103,42 @@ where
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
match self {
|
||||
BundleStatus::Init => {
|
||||
written += 0x00u8.consensus_encode(&mut e)?;
|
||||
written += 0x00u8.consensus_encode(e)?;
|
||||
}
|
||||
BundleStatus::CfHeaders { cf_headers } => {
|
||||
written += 0x01u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x01u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
|
||||
for header in cf_headers {
|
||||
written += header.consensus_encode(&mut e)?;
|
||||
written += header.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::CFilters { cf_filters } => {
|
||||
written += 0x02u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x02u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
for filter in cf_filters {
|
||||
written += filter.consensus_encode(&mut e)?;
|
||||
written += filter.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::Processed { cf_filters } => {
|
||||
written += 0x03u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x03u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
for filter in cf_filters {
|
||||
written += filter.consensus_encode(&mut e)?;
|
||||
written += filter.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::Pruned => {
|
||||
written += 0x04u8.consensus_encode(&mut e)?;
|
||||
written += 0x04u8.consensus_encode(e)?;
|
||||
}
|
||||
BundleStatus::Tip { cf_filters } => {
|
||||
written += 0x05u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x05u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
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 {
|
||||
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
|
||||
let byte_type = u8::consensus_decode(&mut d)?;
|
||||
fn consensus_decode<D: Read + ?Sized>(
|
||||
d: &mut D,
|
||||
) -> Result<Self, bitcoin::consensus::encode::Error> {
|
||||
let byte_type = u8::consensus_decode(d)?;
|
||||
match byte_type {
|
||||
0x00 => Ok(BundleStatus::Init),
|
||||
0x01 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_headers = Vec::with_capacity(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 })
|
||||
}
|
||||
0x02 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(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 })
|
||||
}
|
||||
0x03 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(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 })
|
||||
}
|
||||
0x04 => Ok(BundleStatus::Pruned),
|
||||
0x05 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(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 })
|
||||
@@ -276,7 +278,11 @@ impl ChainStore<Full> {
|
||||
}
|
||||
|
||||
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 mut write_store = self.store.write().unwrap();
|
||||
@@ -647,7 +653,7 @@ impl CfStore {
|
||||
&first_key,
|
||||
(
|
||||
BundleStatus::Init,
|
||||
filter.filter_header(&FilterHeader::from_hash(Default::default())),
|
||||
filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
|
||||
)
|
||||
.serialize(),
|
||||
)?;
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use bitcoin::hash_types::{BlockHash, FilterHeader};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::network::message::NetworkMessage;
|
||||
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
||||
use bitcoin::util::bip158::BlockFilter;
|
||||
@@ -135,7 +136,7 @@ impl CfSync {
|
||||
|
||||
let resp = peer.get_cf_headers(0x00, start_height as u32, stop_hash)?;
|
||||
|
||||
assert!(resp.previous_filter_header == checkpoint);
|
||||
assert_eq!(resp.previous_filter_header, checkpoint);
|
||||
status =
|
||||
self.cf_store
|
||||
.advance_to_cf_headers(index, checkpoint, resp.filter_hashes)?;
|
||||
@@ -254,7 +255,7 @@ where
|
||||
|
||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||
locators_vec,
|
||||
Default::default(),
|
||||
Hash::all_zeros(),
|
||||
)))?;
|
||||
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
|
||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||
@@ -276,7 +277,7 @@ where
|
||||
while sync_height < peer.get_version().start_height as usize {
|
||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||
vec![last_hash],
|
||||
Default::default(),
|
||||
Hash::all_zeros(),
|
||||
)))?;
|
||||
if let NetworkMessage::Headers(headers) = peer
|
||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||
|
||||
@@ -281,9 +281,11 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
|
||||
.client
|
||||
.batch_transaction_get(need_fetch.clone())
|
||||
.map_err(Error::Electrum)?;
|
||||
for (tx, _txid) in txs.into_iter().zip(need_fetch) {
|
||||
debug_assert_eq!(*_txid, tx.txid());
|
||||
self.cache.insert(tx.txid(), tx);
|
||||
let mut txs: HashMap<_, _> = txs.into_iter().map(|tx| (tx.txid(), tx)).collect();
|
||||
for txid in need_fetch {
|
||||
if let Some(tx) = txs.remove(txid) {
|
||||
self.cache.insert(*txid, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +298,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
|
||||
}
|
||||
|
||||
/// Configuration for an [`ElectrumBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct ElectrumBlockchainConfig {
|
||||
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
||||
///
|
||||
@@ -310,6 +312,8 @@ pub struct ElectrumBlockchainConfig {
|
||||
pub timeout: Option<u8>,
|
||||
/// Stop searching addresses for transactions after finding an unused gap of this length
|
||||
pub stop_gap: usize,
|
||||
/// Validate the domain when using SSL
|
||||
pub validate_domain: bool,
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for ElectrumBlockchain {
|
||||
@@ -321,6 +325,7 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
|
||||
.retry(config.retry)
|
||||
.timeout(config.timeout)?
|
||||
.socks5(socks5)?
|
||||
.validate_domain(config.validate_domain)
|
||||
.build();
|
||||
|
||||
Ok(ElectrumBlockchain {
|
||||
@@ -415,6 +420,7 @@ mod test {
|
||||
retry: 0,
|
||||
timeout: None,
|
||||
stop_gap: stop_gap,
|
||||
validate_domain: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,9 @@ impl GetTx for EsploraBlockchain {
|
||||
#[maybe_async]
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
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(block_header.block_hash())
|
||||
Ok(await_or_block!(self
|
||||
.url_client
|
||||
.get_block_hash(height as u32))?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,8 +110,7 @@ impl GetTx for EsploraBlockchain {
|
||||
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
let block_header = self.url_client.get_header(height as u32)?;
|
||||
Ok(block_header.block_hash())
|
||||
Ok(self.url_client.get_block_hash(height as u32)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ mod blocking;
|
||||
pub use self::blocking::*;
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service
|
||||
///
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::bitcoin::hashes::hex::ToHex;
|
||||
use crate::bitcoin::{Network, OutPoint, Transaction, TxOut, Txid};
|
||||
use crate::blockchain::*;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::descriptor::get_checksum;
|
||||
use crate::descriptor::calc_checksum;
|
||||
use crate::error::MissingCachedScripts;
|
||||
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use bitcoin::Script;
|
||||
@@ -77,7 +77,7 @@ impl Deref for RpcBlockchain {
|
||||
}
|
||||
|
||||
/// RpcBlockchain configuration options
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct RpcConfig {
|
||||
/// The bitcoin node url
|
||||
pub url: String,
|
||||
@@ -96,7 +96,7 @@ pub struct RpcConfig {
|
||||
/// 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
|
||||
/// 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 {
|
||||
/// The minimum number of scripts to scan for on initial sync.
|
||||
pub start_script_count: usize,
|
||||
@@ -167,7 +167,7 @@ impl Blockchain for RpcBlockchain {
|
||||
.estimate_smart_fee(target as u16, None)?
|
||||
.fee_rate
|
||||
.ok_or(Error::FeeRateUnavailable)?
|
||||
.as_sat() as f64;
|
||||
.to_sat() as f64;
|
||||
|
||||
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;
|
||||
TransactionDetails {
|
||||
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)
|
||||
if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) {
|
||||
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)
|
||||
@@ -603,7 +608,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
|
||||
LocalUtxo {
|
||||
outpoint: OutPoint::new(entry.txid, entry.vout),
|
||||
txout: TxOut {
|
||||
value: entry.amount.as_sat(),
|
||||
value: entry.amount.to_sat(),
|
||||
script_pubkey: entry.script_pub_key,
|
||||
},
|
||||
keychain,
|
||||
@@ -801,7 +806,7 @@ fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
|
||||
|
||||
fn descriptor_from_script_pubkey(script: &Script) -> String {
|
||||
let desc = format!("raw({})", script.to_hex());
|
||||
format!("{}#{}", desc, get_checksum(&desc).unwrap())
|
||||
format!("{}#{}", desc, calc_checksum(&desc).unwrap())
|
||||
}
|
||||
|
||||
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
|
||||
@@ -873,15 +878,13 @@ impl BlockchainFactory for RpcBlockchainFactory {
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
descriptor::{into_wallet_descriptor_checked, AsDerived},
|
||||
testutils::blockchain_tests::TestClient,
|
||||
descriptor::into_wallet_descriptor_checked, testutils::blockchain_tests::TestClient,
|
||||
wallet::utils::SecpCtx,
|
||||
};
|
||||
|
||||
use bitcoin::{Address, Network};
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
use log::LevelFilter;
|
||||
use miniscript::DescriptorTrait;
|
||||
|
||||
crate::bdk_blockchain_tests! {
|
||||
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
||||
@@ -958,7 +961,7 @@ mod test {
|
||||
|
||||
// generate scripts (1 tx per script)
|
||||
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<_>>();
|
||||
|
||||
// import scripts and wait
|
||||
|
||||
@@ -497,7 +497,7 @@ macro_rules! populate_test_db {
|
||||
}
|
||||
let tx = $crate::bitcoin::Transaction {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
input,
|
||||
output: tx_meta
|
||||
.output
|
||||
|
||||
@@ -57,7 +57,17 @@ static MIGRATIONS: &[&str] = &[
|
||||
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
|
||||
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
|
||||
"DROP TABLE utxos_old;",
|
||||
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
|
||||
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
|
||||
// Fix issue https://github.com/bitcoindevkit/bdk/issues/801: drop duplicated script_pubkeys
|
||||
"ALTER TABLE script_pubkeys RENAME TO script_pubkeys_old;",
|
||||
"DROP INDEX idx_keychain_child;",
|
||||
"DROP INDEX idx_script;",
|
||||
"CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
|
||||
"CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
|
||||
"CREATE INDEX idx_script ON script_pubkeys(script);",
|
||||
"CREATE UNIQUE INDEX idx_script_pks_unique ON script_pubkeys(keychain, child);",
|
||||
"INSERT OR REPLACE INTO script_pubkeys SELECT keychain, child, script FROM script_pubkeys_old;",
|
||||
"DROP TABLE script_pubkeys_old;"
|
||||
];
|
||||
|
||||
/// Sqlite database stored on filesystem
|
||||
@@ -88,7 +98,7 @@ impl SqliteDatabase {
|
||||
child: u32,
|
||||
script: &[u8],
|
||||
) -> Result<i64, Error> {
|
||||
let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
||||
let mut statement = self.connection.prepare_cached("INSERT OR REPLACE INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
||||
statement.execute(named_params! {
|
||||
":keychain": keychain,
|
||||
":child": child,
|
||||
@@ -1096,4 +1106,42 @@ pub mod test {
|
||||
fn test_check_descriptor_checksum() {
|
||||
crate::database::test::test_check_descriptor_checksum(get_database());
|
||||
}
|
||||
|
||||
// Issue 801: https://github.com/bitcoindevkit/bdk/issues/801
|
||||
#[test]
|
||||
fn test_unique_spks() {
|
||||
use crate::bitcoin::hashes::hex::FromHex;
|
||||
use crate::database::*;
|
||||
|
||||
let mut db = get_database();
|
||||
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
for _ in 0..100 {
|
||||
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
}
|
||||
|
||||
let mut statement = db
|
||||
.connection
|
||||
.prepare_cached(
|
||||
"select keychain,child,count(child) from script_pubkeys group by keychain,child;",
|
||||
)
|
||||
.unwrap();
|
||||
let mut rows = statement.query([]).unwrap();
|
||||
while let Some(row) = rows.next().unwrap() {
|
||||
let keychain: String = row.get(0).unwrap();
|
||||
let child: u32 = row.get(1).unwrap();
|
||||
let count: usize = row.get(2).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
count, 1,
|
||||
"keychain={}, child={}, count={}",
|
||||
keychain, child, count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,24 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Computes the checksum bytes of a descriptor
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
/// Computes the checksum bytes of a descriptor.
|
||||
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
|
||||
pub(crate) fn calc_checksum_bytes_internal(
|
||||
mut desc: &str,
|
||||
exclude_hash: bool,
|
||||
) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
|
||||
let mut original_checksum = None;
|
||||
if exclude_hash {
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
let pos = INPUT_CHARSET
|
||||
.iter()
|
||||
@@ -72,38 +84,98 @@ pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||
}
|
||||
|
||||
// if input data already had a checksum, check calculated checksum against original checksum
|
||||
if let Some(original_checksum) = original_checksum {
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
|
||||
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
|
||||
|
||||
/// Compute the checksum bytes of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::descriptor::get_checksum;
|
||||
use crate::descriptor::calc_checksum;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
// test get_checksum() function; it should return the same value as Bitcoin Core
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core
|
||||
#[test]
|
||||
fn test_get_checksum() {
|
||||
fn test_calc_checksum() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
|
||||
assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62");
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
|
||||
assert_eq!(get_checksum(desc).unwrap(), "lasegmfs");
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
}
|
||||
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core even if the
|
||||
// descriptor string includes a checksum hash
|
||||
#[test]
|
||||
fn test_calc_checksum_with_checksum_hash() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_checksum_invalid_character() {
|
||||
fn test_calc_checksum_invalid_character() {
|
||||
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||
|
||||
assert!(matches!(
|
||||
get_checksum(&invalid_desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
));
|
||||
assert_matches!(
|
||||
calc_checksum(&invalid_desc),
|
||||
Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
( 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 ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Older, $value)
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||
});
|
||||
( sha256 ( $hash:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
||||
@@ -795,7 +795,7 @@ macro_rules! fragment {
|
||||
mod test {
|
||||
use bitcoin::hashes::hex::ToHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::{Descriptor, Legacy, Segwitv0};
|
||||
|
||||
use std::str::FromStr;
|
||||
@@ -806,8 +806,6 @@ mod test {
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::PrivateKey;
|
||||
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
|
||||
// test the descriptor!() macro
|
||||
|
||||
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
|
||||
@@ -817,17 +815,15 @@ mod test {
|
||||
is_fixed: bool,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
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() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
desc.at_derivation_index(index)
|
||||
};
|
||||
let address = child_desc.address(Regtest);
|
||||
if let Ok(address) = address {
|
||||
|
||||
@@ -15,32 +15,32 @@
|
||||
//! from [`miniscript`].
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||
use bitcoin::util::{psbt, taproot};
|
||||
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::{Network, Script, TxOut};
|
||||
use bitcoin::{Network, TxOut};
|
||||
|
||||
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
|
||||
use miniscript::descriptor::{
|
||||
DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey,
|
||||
};
|
||||
pub use miniscript::{
|
||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
|
||||
pub mod checksum;
|
||||
pub mod derived;
|
||||
#[doc(hidden)]
|
||||
pub mod dsl;
|
||||
pub mod error;
|
||||
pub mod policy;
|
||||
pub mod template;
|
||||
|
||||
pub use self::checksum::get_checksum;
|
||||
pub use self::derived::{AsDerived, DerivedDescriptorKey};
|
||||
pub use self::checksum::calc_checksum;
|
||||
use self::checksum::calc_checksum_bytes;
|
||||
pub use self::error::Error as DescriptorError;
|
||||
pub use self::policy::Policy;
|
||||
use self::template::DescriptorTemplateOut;
|
||||
@@ -52,7 +52,7 @@ use crate::wallet::utils::SecpCtx;
|
||||
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
||||
|
||||
/// 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
|
||||
/// [`psbt::Output`]
|
||||
@@ -84,19 +84,15 @@ impl IntoWalletDescriptor for &str {
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let descriptor = if self.contains('#') {
|
||||
let parts: Vec<&str> = self.splitn(2, '#').collect();
|
||||
if !get_checksum(parts[0])
|
||||
.ok()
|
||||
.map(|computed| computed == parts[1])
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
let descriptor = match self.split_once('#') {
|
||||
Some((desc, original_checksum)) => {
|
||||
let checksum = calc_checksum_bytes(desc)?;
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
desc
|
||||
}
|
||||
|
||||
parts[0]
|
||||
} else {
|
||||
self
|
||||
None => self,
|
||||
};
|
||||
|
||||
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
|
||||
@@ -132,28 +128,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
use crate::keys::DescriptorKey;
|
||||
|
||||
let check_key = |pk: &DescriptorPublicKey| {
|
||||
let (pk, _, networks) = if self.0.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)?
|
||||
};
|
||||
struct Translator<'s, 'd> {
|
||||
secp: &'s SecpCtx,
|
||||
descriptor: &'d ExtendedDescriptor,
|
||||
network: Network,
|
||||
}
|
||||
|
||||
if networks.contains(&network) {
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
impl<'s, 'd>
|
||||
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
|
||||
for Translator<'s, 'd>
|
||||
{
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +207,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
_secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let valid_networks = &self.2;
|
||||
struct Translator {
|
||||
network: Network,
|
||||
}
|
||||
|
||||
let fix_key = |pk: &DescriptorPublicKey| {
|
||||
if valid_networks.contains(&network) {
|
||||
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
|
||||
for Translator
|
||||
{
|
||||
fn pk(
|
||||
&mut self,
|
||||
pk: &DescriptorPublicKey,
|
||||
) -> Result<DescriptorPublicKey, DescriptorError> {
|
||||
// workaround for xpubs generated by other key types, like bip39: since when the
|
||||
// conversion is made one network has to be chosen, what we generally choose
|
||||
// "mainnet", but then override the set of valid networks to specify that all of
|
||||
@@ -175,7 +226,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
let pk = match pk {
|
||||
DescriptorPublicKey::XPub(ref xpub) => {
|
||||
let mut xpub = xpub.clone();
|
||||
xpub.xkey.network = network;
|
||||
xpub.xkey.network = self.network;
|
||||
|
||||
DescriptorPublicKey::XPub(xpub)
|
||||
}
|
||||
@@ -183,15 +234,42 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
};
|
||||
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
};
|
||||
miniscript::translate_hash_clone!(
|
||||
DescriptorPublicKey,
|
||||
DescriptorPublicKey,
|
||||
DescriptorError
|
||||
);
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it
|
||||
let translated = self.0.translate_pk(fix_key, fix_key)?;
|
||||
let (desc, keymap, networks) = self;
|
||||
|
||||
Ok((translated, self.1))
|
||||
if !networks.contains(&network) {
|
||||
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it in the descriptor
|
||||
let translated = desc.translate_pk(&mut Translator { network })?;
|
||||
// ...and in the key map
|
||||
let fixed_keymap = keymap
|
||||
.into_iter()
|
||||
.map(|(mut k, mut v)| {
|
||||
match (&mut k, &mut v) {
|
||||
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
|
||||
xpub.xkey.network = network;
|
||||
xprv.xkey.network = network;
|
||||
}
|
||||
(_, DescriptorSecretKey::Single(key)) => {
|
||||
key.key.network = network;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(k, v)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((translated, fixed_keymap))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +288,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
derivation_path,
|
||||
wildcard,
|
||||
..
|
||||
}) = k.as_key()
|
||||
}) = k
|
||||
{
|
||||
return *wildcard == Wildcard::Hardened
|
||||
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
||||
@@ -257,7 +335,6 @@ pub trait ExtractPolicy {
|
||||
}
|
||||
|
||||
pub(crate) trait XKeyUtils {
|
||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||
}
|
||||
|
||||
@@ -265,27 +342,6 @@ impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
where
|
||||
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 {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
@@ -294,11 +350,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 {
|
||||
fn is_witness(&self) -> bool;
|
||||
fn is_taproot(&self) -> bool;
|
||||
@@ -307,63 +358,23 @@ pub(crate) trait DescriptorMeta {
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_tap_key_origins<'s>(
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_input<'s>(
|
||||
&self,
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
) -> Option<DerivedDescriptor>;
|
||||
}
|
||||
|
||||
impl DescriptorMeta for ExtendedDescriptor {
|
||||
@@ -387,7 +398,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
self.for_each_key(|pk| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk {
|
||||
answer.push(xpub.clone());
|
||||
}
|
||||
|
||||
@@ -401,7 +412,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||
path: &DerivationPath,
|
||||
@@ -423,7 +434,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
|
||||
// using `for_any_key` should make this stop as soon as we return `true`
|
||||
self.for_any_key(|key| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.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
|
||||
// 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
|
||||
@@ -481,14 +492,14 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
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>(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
||||
let key_origins = hd_keypaths
|
||||
.iter()
|
||||
@@ -506,7 +517,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
|
||||
let key_origins = tap_key_origins
|
||||
.iter()
|
||||
@@ -520,19 +531,19 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
) -> Option<DerivedDescriptor> {
|
||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if self.is_deriveable() {
|
||||
if self.has_wildcard() {
|
||||
// We can't try to bruteforce the derivation index, exit here
|
||||
return None;
|
||||
}
|
||||
|
||||
let descriptor = self.as_derived_fixed(secp);
|
||||
let descriptor = self.at_derivation_index(0);
|
||||
match descriptor.desc_type() {
|
||||
// TODO: add pk() here
|
||||
DescriptorType::Pkh
|
||||
@@ -566,90 +577,11 @@ 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)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
@@ -773,23 +705,40 @@ mod test {
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||
// we are using an "xpub"
|
||||
let key = (xpub, path).into_descriptor_key().unwrap();
|
||||
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
// override it with any. this happens in some key conversions, like bip39
|
||||
let key = key.override_valid_networks(any_network());
|
||||
|
||||
// make a descriptor out of it
|
||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||
// this should convert the key that supports "any_network" to the right network (testnet)
|
||||
let (wallet_desc, _) = desc
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
|
||||
let mut xprv_testnet = xprv;
|
||||
xprv_testnet.network = Network::Testnet;
|
||||
|
||||
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
||||
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||
xkey: xpub_testnet,
|
||||
origin: None,
|
||||
derivation_path: path,
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
|
||||
assert_eq!(
|
||||
keymap
|
||||
.get(&desc_pubkey)
|
||||
.map(|key| key.to_public(&secp).unwrap()),
|
||||
Some(desc_pubkey)
|
||||
);
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
||||
@@ -815,17 +764,11 @@ mod test {
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from &str with keys from right and wrong network
|
||||
@@ -859,17 +802,11 @@ mod test {
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
|
||||
|
||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from the output of the descriptor!() macro
|
||||
@@ -903,11 +840,7 @@ mod test {
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
DescriptorError::HardenedDerivationXpub
|
||||
));
|
||||
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
|
||||
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
@@ -917,7 +850,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_sh_wsh_sortedmulti_redeemscript() {
|
||||
use super::{AsDerived, DescriptorScripts};
|
||||
use miniscript::psbt::PsbtInputExt;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
@@ -925,11 +858,16 @@ mod test {
|
||||
let (descriptor, _) =
|
||||
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();
|
||||
|
||||
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
|
||||
assert_eq!(descriptor.psbt_witness_script(), Some(script));
|
||||
let mut psbt_input = psbt::Input::default();
|
||||
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::{Serialize, Serializer};
|
||||
|
||||
use bitcoin::hashes::*;
|
||||
use bitcoin::hashes::{hash160, ripemd160, sha256};
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::{PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
|
||||
|
||||
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)]
|
||||
use log::{debug, error, info, trace};
|
||||
@@ -58,9 +61,9 @@ use log::{debug, error, info, trace};
|
||||
use crate::descriptor::ExtractPolicy;
|
||||
use crate::keys::ExtScriptContext;
|
||||
use crate::wallet::signer::{SignerId, SignersContainer};
|
||||
use crate::wallet::utils::{self, After, Older, SecpCtx};
|
||||
use crate::wallet::utils::{After, Older, SecpCtx};
|
||||
|
||||
use super::checksum::get_checksum;
|
||||
use super::checksum::calc_checksum;
|
||||
use super::error::Error;
|
||||
use super::XKeyUtils;
|
||||
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
|
||||
@@ -81,11 +84,11 @@ pub enum PkOrF {
|
||||
impl PkOrF {
|
||||
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
||||
match k {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(pk),
|
||||
..
|
||||
}) => PkOrF::Pubkey(*pk),
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(pk),
|
||||
..
|
||||
}) => PkOrF::XOnlyPubkey(*pk),
|
||||
@@ -111,7 +114,7 @@ pub enum SatisfiableItem {
|
||||
/// Double SHA256 preimage hash
|
||||
Hash256Preimage {
|
||||
/// The digest value
|
||||
hash: sha256d::Hash,
|
||||
hash: hash256::Hash,
|
||||
},
|
||||
/// RIPEMD160 preimage hash
|
||||
Ripemd160Preimage {
|
||||
@@ -125,13 +128,13 @@ pub enum SatisfiableItem {
|
||||
},
|
||||
/// Absolute timeclock timestamp
|
||||
AbsoluteTimelock {
|
||||
/// The timestamp value
|
||||
value: u32,
|
||||
/// The timelock value
|
||||
value: LockTime,
|
||||
},
|
||||
/// Relative timelock locktime
|
||||
RelativeTimelock {
|
||||
/// The locktime value
|
||||
value: u32,
|
||||
/// The timelock value
|
||||
value: Sequence,
|
||||
},
|
||||
/// Multi-signature public keys with threshold count
|
||||
Multisig {
|
||||
@@ -165,7 +168,7 @@ impl SatisfiableItem {
|
||||
|
||||
/// Returns a unique id for the [`SatisfiableItem`]
|
||||
pub fn id(&self) -> String {
|
||||
get_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
|
||||
calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
|
||||
.expect("Failed to compute a SatisfiableItem id")
|
||||
}
|
||||
}
|
||||
@@ -438,32 +441,30 @@ pub struct Policy {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Optional CheckSequenceVerify condition
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub csv: Option<u32>,
|
||||
pub csv: Option<Sequence>,
|
||||
/// Optional timelock condition
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timelock: Option<u32>,
|
||||
pub timelock: Option<LockTime>,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> {
|
||||
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) {
|
||||
fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
|
||||
if !a.is_same_unit(b) {
|
||||
Err(PolicyError::MixedTimelockUnits)
|
||||
} else if a > b {
|
||||
Ok(a)
|
||||
} else {
|
||||
Ok(max(a, b))
|
||||
Ok(b)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> {
|
||||
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK;
|
||||
|
||||
let a = a & mask;
|
||||
let b = b & mask;
|
||||
|
||||
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
||||
fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
|
||||
if a.is_time_locked() != b.is_time_locked() {
|
||||
Err(PolicyError::MixedTimelockUnits)
|
||||
} else {
|
||||
Ok(max(a, b))
|
||||
@@ -720,15 +721,18 @@ impl From<SatisfiableItem> for Policy {
|
||||
}
|
||||
|
||||
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
||||
// For consistency we always compute the key hash in "ecdsa" form (with the leading sign
|
||||
// prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
|
||||
// for a key, so it doesn't really matter how the identifier is computed.
|
||||
match key {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(pk),
|
||||
..
|
||||
}) => pk.to_pubkeyhash().into(),
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(pk),
|
||||
..
|
||||
}) => pk.to_pubkeyhash().into(),
|
||||
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||
}
|
||||
}
|
||||
@@ -779,7 +783,7 @@ fn generic_sig_in_psbt<
|
||||
) -> bool {
|
||||
//TODO check signature validity
|
||||
psbt.inputs.iter().all(|input| match key {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
|
||||
DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
|
||||
DescriptorPublicKey::XPub(xpub) => {
|
||||
//TODO check actual derivation matches
|
||||
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))
|
||||
}
|
||||
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 {
|
||||
condition: Condition {
|
||||
timelock: Some(*value),
|
||||
timelock: Some(value.into()),
|
||||
csv: None,
|
||||
},
|
||||
};
|
||||
@@ -905,9 +912,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
} = build_sat
|
||||
{
|
||||
let after = After::new(Some(current_height), false);
|
||||
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
|
||||
let inputs_sat = psbt_inputs_sat(psbt)
|
||||
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
|
||||
let after_sat =
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
|
||||
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&sat, value.into())
|
||||
});
|
||||
if after_sat && inputs_sat {
|
||||
policy.satisfaction = policy.contribution.clone();
|
||||
}
|
||||
@@ -999,6 +1008,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
|
||||
Policy::make_thresh(mapped, threshold)?
|
||||
}
|
||||
|
||||
// Unsupported
|
||||
Terminal::RawPkH(_) => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1124,14 +1136,13 @@ mod test {
|
||||
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||
use crate::wallet::signer::SignersContainer;
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::Network;
|
||||
use miniscript::DescriptorTrait;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1172,8 +1183,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::None);
|
||||
|
||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||
let (wallet_desc, keymap) = desc
|
||||
@@ -1185,10 +1196,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||
);
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None);
|
||||
}
|
||||
|
||||
// 2 pub keys descriptor, required 2 prv keys
|
||||
@@ -1207,19 +1216,16 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
// TODO should this be "Satisfaction::None" since we have no prv keys?
|
||||
// TODO should items and conditions not be empty?
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
&& m == &2usize
|
||||
&& items.is_empty()
|
||||
&& conditions.is_empty()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1238,18 +1244,15 @@ mod test {
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
&& m == &2usize
|
||||
&& items.len() == 1
|
||||
&& conditions.contains_key(&0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1271,18 +1274,15 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &1
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0])
|
||||
&& conditions.contains_key(&vec![1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1303,18 +1303,15 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0,1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1329,31 +1326,27 @@ mod test {
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
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)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::None);
|
||||
|
||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
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)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||
);
|
||||
assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint);
|
||||
assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None);
|
||||
}
|
||||
|
||||
// single key, 1 prv and 1 pub key descriptor, required 1 prv keys
|
||||
@@ -1368,25 +1361,21 @@ mod test {
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
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)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &1
|
||||
assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
|
||||
&& m == 1
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0])
|
||||
&& conditions.contains_key(&vec![1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1418,18 +1407,14 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2)
|
||||
);
|
||||
assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items.len() == 3
|
||||
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
|
||||
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
|
||||
&& conditions.get(&vec![1,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(sequence))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1574,7 +1559,7 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
let addr = wallet_desc
|
||||
.as_derived(0, &secp)
|
||||
.at_derivation_index(0)
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1592,11 +1577,9 @@ mod test {
|
||||
.unwrap();
|
||||
//println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
|
||||
|
||||
assert!(
|
||||
matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![0]
|
||||
)
|
||||
);
|
||||
|
||||
let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
|
||||
@@ -1606,11 +1589,9 @@ mod test {
|
||||
.unwrap();
|
||||
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
|
||||
|
||||
assert!(
|
||||
matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![1]
|
||||
)
|
||||
);
|
||||
|
||||
let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
|
||||
@@ -1618,11 +1599,9 @@ mod test {
|
||||
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![0, 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1646,7 +1625,7 @@ mod test {
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
|
||||
let addr = wallet_desc
|
||||
.as_derived(0, &secp)
|
||||
.at_derivation_index(0)
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1666,11 +1645,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items.is_empty()
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy).unwrap());
|
||||
|
||||
@@ -1684,11 +1661,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat_expired, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items == &vec![0]
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
|
||||
|
||||
@@ -1704,11 +1679,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat_expired_signed, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items == &vec![0, 1]
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
|
||||
}
|
||||
@@ -1783,12 +1756,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1])
|
||||
);
|
||||
assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]);
|
||||
|
||||
let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
|
||||
let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
|
||||
@@ -1880,19 +1849,11 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty())
|
||||
);
|
||||
assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty());
|
||||
|
||||
assert!(
|
||||
matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1])
|
||||
);
|
||||
assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]);
|
||||
|
||||
let satisfied_items = match policy_signed.item {
|
||||
SatisfiableItem::Thresh { items, .. } => items,
|
||||
|
||||
@@ -235,14 +235,17 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
|
||||
P2Pkh(legacy::make_bipxx_public(
|
||||
44, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,14 +314,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(
|
||||
49, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,14 +393,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(
|
||||
84, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +449,7 @@ macro_rules! expand_make_bipxx {
|
||||
key: K,
|
||||
parent_fingerprint: bip32::Fingerprint,
|
||||
keychain: KeychainKind,
|
||||
network: Network,
|
||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||
let derivation_path: bip32::DerivationPath = match keychain {
|
||||
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
|
||||
@@ -448,7 +458,10 @@ macro_rules! expand_make_bipxx {
|
||||
|
||||
let source_path = bip32::DerivationPath::from(vec![
|
||||
bip32::ChildNumber::from_hardened_idx(bip)?,
|
||||
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
match network {
|
||||
Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
_ => bip32::ChildNumber::from_hardened_idx(1)?,
|
||||
},
|
||||
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
]);
|
||||
|
||||
@@ -468,12 +481,11 @@ mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
@@ -490,9 +502,9 @@ mod test {
|
||||
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 0 }));
|
||||
assert_matches!(coin_type, Hardened { index: 0 });
|
||||
}
|
||||
|
||||
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
@@ -504,9 +516,9 @@ mod test {
|
||||
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 1 }));
|
||||
assert_matches!(coin_type, Hardened { index: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,17 +529,15 @@ mod test {
|
||||
is_fixed: bool,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
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() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
desc.at_derivation_index(index)
|
||||
};
|
||||
let address = child_desc.address(Regtest).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 crate::bitcoin::Network;
|
||||
use crate::{descriptor, wallet, wallet::address_validator};
|
||||
use crate::{descriptor, wallet};
|
||||
use bitcoin::{OutPoint, Txid};
|
||||
|
||||
/// 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
|
||||
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
|
||||
Encode(bitcoin::consensus::encode::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
/// An ECDSA error
|
||||
@@ -149,6 +149,14 @@ pub enum 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
|
||||
/// on cached `scriptPubKey`s.
|
||||
#[derive(Debug)]
|
||||
@@ -181,7 +189,6 @@ macro_rules! impl_error {
|
||||
}
|
||||
|
||||
impl_error!(descriptor::error::Error, Descriptor);
|
||||
impl_error!(address_validator::AddressValidatorError, AddressValidator);
|
||||
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
|
||||
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!(miniscript::Error, Miniscript);
|
||||
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||
impl_error!(serde_json::Error, Json);
|
||||
|
||||
@@ -24,8 +24,8 @@ use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
|
||||
|
||||
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||
pub use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
||||
SinglePubKey, SortedMultiVec,
|
||||
DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
|
||||
SortedMultiVec,
|
||||
};
|
||||
pub use miniscript::ScriptContext;
|
||||
use miniscript::{Miniscript, Terminal};
|
||||
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
let mut key_map = KeyMap::with_capacity(1);
|
||||
|
||||
let public = secret
|
||||
.as_public(secp)
|
||||
.to_public(secp)
|
||||
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
|
||||
key_map.insert(public.clone(), secret);
|
||||
|
||||
@@ -224,8 +224,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||
/// ScriptContext, SinglePub, SinglePubKey,
|
||||
/// };
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
@@ -235,7 +235,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
/// Ok(DescriptorKey::from_public(
|
||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
/// DescriptorPublicKey::Single(SinglePub {
|
||||
/// origin: None,
|
||||
/// key: SinglePubKey::FullKey(self.pubkey),
|
||||
/// }),
|
||||
@@ -842,7 +842,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match self {
|
||||
DescriptorPublicKey::SinglePub(_) => any_network(),
|
||||
DescriptorPublicKey::Single(_) => any_network(),
|
||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||
if xkey.network == Network::Bitcoin =>
|
||||
{
|
||||
@@ -857,7 +857,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(self),
|
||||
origin: None,
|
||||
})
|
||||
@@ -867,7 +867,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(self),
|
||||
origin: None,
|
||||
})
|
||||
@@ -878,7 +878,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match &self {
|
||||
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
||||
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
|
||||
mainnet_network()
|
||||
}
|
||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
||||
@@ -903,7 +903,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self,
|
||||
origin: None,
|
||||
})
|
||||
|
||||
@@ -203,6 +203,8 @@ pub extern crate miniscript;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
pub extern crate hwi;
|
||||
|
||||
#[cfg(all(feature = "reqwest", feature = "ureq"))]
|
||||
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
|
||||
@@ -254,6 +256,9 @@ pub extern crate rusqlite;
|
||||
#[macro_use]
|
||||
pub mod testutils;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate assert_matches;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
pub(crate) mod error;
|
||||
@@ -263,7 +268,7 @@ pub mod descriptor;
|
||||
#[cfg(feature = "test-md-docs")]
|
||||
mod doctest;
|
||||
pub mod keys;
|
||||
pub(crate) mod psbt;
|
||||
pub mod psbt;
|
||||
pub(crate) mod types;
|
||||
pub mod wallet;
|
||||
|
||||
@@ -271,7 +276,6 @@ pub use descriptor::template;
|
||||
pub use descriptor::HdKeyPaths;
|
||||
pub use error::Error;
|
||||
pub use types::*;
|
||||
pub use wallet::address_validator;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
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
|
||||
// licenses.
|
||||
|
||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||
|
||||
use crate::FeeRate;
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
// TODO upstream the functions here to `rust-bitcoin`?
|
||||
|
||||
/// Trait to add functions to extract utxos and calculate fees.
|
||||
pub trait PsbtUtils {
|
||||
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||
|
||||
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
|
||||
|
||||
@@ -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 bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
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::{Auth, Client as RpcClient, RpcApi};
|
||||
use core::str::FromStr;
|
||||
@@ -110,7 +120,7 @@ impl TestClient {
|
||||
if let Some(true) = meta_tx.replaceable {
|
||||
// for some reason core doesn't set this field right
|
||||
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::transaction::{OutPoint, TxIn, TxOut};
|
||||
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
let block_template: serde_json::Value = self
|
||||
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
||||
@@ -176,7 +187,7 @@ impl TestClient {
|
||||
block_template["previousblockhash"].as_str().unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
merkle_root: TxMerkleNode::default(),
|
||||
merkle_root: TxMerkleNode::all_zeros(),
|
||||
time: block_template["curtime"].as_u64().unwrap() as u32,
|
||||
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
||||
nonce: 0,
|
||||
@@ -184,15 +195,15 @@ impl TestClient {
|
||||
debug!("header: {:#?}", header);
|
||||
|
||||
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
|
||||
let mut coinbase_tx = Transaction {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
lock_time: PackedLockTime(0),
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
script_sig: Builder::new().push_int(height).into_script(),
|
||||
sequence: 0xFFFFFFFF,
|
||||
sequence: Sequence(0xFFFFFFFF),
|
||||
witness: Witness::from_vec(vec![witness_reserved_value]),
|
||||
}],
|
||||
output: vec![],
|
||||
@@ -1184,7 +1195,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
|
||||
|
||||
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]
|
||||
|
||||
@@ -23,7 +23,7 @@ pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
||||
None
|
||||
}
|
||||
|
||||
/// Runs all avaliable tests.
|
||||
/// Runs all available tests.
|
||||
fn run(&self) {
|
||||
let test_client = &mut TestClient::default();
|
||||
|
||||
|
||||
@@ -101,25 +101,21 @@ impl TestIncomingTx {
|
||||
macro_rules! testutils {
|
||||
( @external $descriptors:expr, $child:expr ) => ({
|
||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::descriptor::AsDerived;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
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 ) => ({
|
||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::descriptor::AsDerived;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
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;
|
||||
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) });
|
||||
( @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 )* ) )* ) => ({
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use $crate::miniscript::descriptor::Descriptor;
|
||||
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)]
|
||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
||||
let mut keys = HashMap::new();
|
||||
$(
|
||||
keys = testutils!{ @keys $( $keys )* };
|
||||
)*
|
||||
|
||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||
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 mut translator = Translator { keys, is_internal: false };
|
||||
|
||||
});
|
||||
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 internal = None::<String>$(.or({
|
||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
translator.is_internal = true;
|
||||
|
||||
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
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())
|
||||
let internal = None::<String>$(.or({
|
||||
let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
|
||||
Some(internal.to_string())
|
||||
}))?;
|
||||
|
||||
(external, internal)
|
||||
|
||||
123
src/types.rs
123
src/types.rs
@@ -166,7 +166,7 @@ pub struct LocalUtxo {
|
||||
}
|
||||
|
||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WeightedUtxo {
|
||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
@@ -177,7 +177,7 @@ pub struct WeightedUtxo {
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
@@ -224,7 +224,7 @@ impl Utxo {
|
||||
}
|
||||
|
||||
/// A wallet transaction
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TransactionDetails {
|
||||
/// Optional transaction
|
||||
pub transaction: Option<Transaction>,
|
||||
@@ -247,6 +247,20 @@ pub struct TransactionDetails {
|
||||
pub confirmation_time: Option<BlockTime>,
|
||||
}
|
||||
|
||||
impl PartialOrd for TransactionDetails {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TransactionDetails {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.confirmation_time
|
||||
.cmp(&other.confirmation_time)
|
||||
.then_with(|| self.txid.cmp(&other.txid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Block height and timestamp of a block
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct BlockTime {
|
||||
@@ -256,6 +270,20 @@ pub struct BlockTime {
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl PartialOrd for BlockTime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for BlockTime {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.height
|
||||
.cmp(&other.height)
|
||||
.then_with(|| self.timestamp.cmp(&other.timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
/// **DEPRECATED**: Confirmation time of a transaction
|
||||
///
|
||||
/// The structure has been renamed to `BlockTime`
|
||||
@@ -334,6 +362,95 @@ impl std::iter::Sum for Balance {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
#[test]
|
||||
fn sort_block_time() {
|
||||
let block_time_a = BlockTime {
|
||||
height: 100,
|
||||
timestamp: 100,
|
||||
};
|
||||
|
||||
let block_time_b = BlockTime {
|
||||
height: 100,
|
||||
timestamp: 110,
|
||||
};
|
||||
|
||||
let block_time_c = BlockTime {
|
||||
height: 0,
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
let mut vec = vec![
|
||||
block_time_a.clone(),
|
||||
block_time_b.clone(),
|
||||
block_time_c.clone(),
|
||||
];
|
||||
vec.sort();
|
||||
let expected = vec![block_time_c, block_time_a, block_time_b];
|
||||
|
||||
assert_eq!(vec, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_tx_details() {
|
||||
let block_time_a = BlockTime {
|
||||
height: 100,
|
||||
timestamp: 100,
|
||||
};
|
||||
|
||||
let block_time_b = BlockTime {
|
||||
height: 0,
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
let tx_details_a = TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_inner([0; 32]),
|
||||
received: 0,
|
||||
sent: 0,
|
||||
fee: None,
|
||||
confirmation_time: None,
|
||||
};
|
||||
|
||||
let tx_details_b = TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_inner([0; 32]),
|
||||
received: 0,
|
||||
sent: 0,
|
||||
fee: None,
|
||||
confirmation_time: Some(block_time_a),
|
||||
};
|
||||
|
||||
let tx_details_c = TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_inner([0; 32]),
|
||||
received: 0,
|
||||
sent: 0,
|
||||
fee: None,
|
||||
confirmation_time: Some(block_time_b.clone()),
|
||||
};
|
||||
|
||||
let tx_details_d = TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_inner([1; 32]),
|
||||
received: 0,
|
||||
sent: 0,
|
||||
fee: None,
|
||||
confirmation_time: Some(block_time_b),
|
||||
};
|
||||
|
||||
let mut vec = vec![
|
||||
tx_details_a.clone(),
|
||||
tx_details_b.clone(),
|
||||
tx_details_c.clone(),
|
||||
tx_details_d.clone(),
|
||||
];
|
||||
vec.sort();
|
||||
let expected = vec![tx_details_a, tx_details_c, tx_details_d, tx_details_b];
|
||||
|
||||
assert_eq!(vec, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_store_feerate_in_const() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -102,11 +102,11 @@ use crate::{error::Error, Utxo};
|
||||
use bitcoin::consensus::encode::serialize;
|
||||
use bitcoin::Script;
|
||||
|
||||
#[cfg(test)]
|
||||
use assert_matches::assert_matches;
|
||||
use rand::seq::SliceRandom;
|
||||
#[cfg(not(test))]
|
||||
use rand::thread_rng;
|
||||
#[cfg(test)]
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 {
|
||||
dust_threshold,
|
||||
change_fee,
|
||||
@@ -671,6 +671,7 @@ impl BranchAndBoundCoinSelection {
|
||||
optional_utxos.shuffle(&mut thread_rng());
|
||||
#[cfg(test)]
|
||||
{
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
optional_utxos.shuffle(&mut rng);
|
||||
@@ -835,7 +836,7 @@ mod test {
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: rng.gen_range(0, 200000000),
|
||||
value: rng.gen_range(0..200000000),
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
@@ -866,7 +867,7 @@ mod test {
|
||||
}
|
||||
|
||||
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[..utxos_picked_len]
|
||||
.iter()
|
||||
@@ -1226,6 +1227,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_bnb_coin_selection_required_not_enough() {
|
||||
let utxos = get_test_utxos();
|
||||
let database = MemoryDatabase::default();
|
||||
@@ -1521,24 +1523,22 @@ mod test {
|
||||
let database = MemoryDatabase::default();
|
||||
let drain_script = Script::default();
|
||||
|
||||
let err = BranchAndBoundCoinSelection::default()
|
||||
.coin_select(
|
||||
&database,
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
)
|
||||
.unwrap_err();
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
&database,
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InsufficientFunds {
|
||||
assert_matches!(
|
||||
selection,
|
||||
Err(Error::InsufficientFunds {
|
||||
available: 300_000,
|
||||
..
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1551,24 +1551,22 @@ mod test {
|
||||
.into_iter()
|
||||
.partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
|
||||
|
||||
let err = BranchAndBoundCoinSelection::default()
|
||||
.coin_select(
|
||||
&database,
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
)
|
||||
.unwrap_err();
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
&database,
|
||||
required,
|
||||
optional,
|
||||
FeeRate::from_sat_per_vb(10.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InsufficientFunds {
|
||||
assert_matches!(
|
||||
selection,
|
||||
Err(Error::InsufficientFunds {
|
||||
available: 300_010,
|
||||
..
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1577,23 +1575,21 @@ mod test {
|
||||
let database = MemoryDatabase::default();
|
||||
let drain_script = Script::default();
|
||||
|
||||
let err = BranchAndBoundCoinSelection::default()
|
||||
.coin_select(
|
||||
&database,
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(10_000.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
)
|
||||
.unwrap_err();
|
||||
let selection = BranchAndBoundCoinSelection::default().coin_select(
|
||||
&database,
|
||||
utxos,
|
||||
vec![],
|
||||
FeeRate::from_sat_per_vb(10_000.0),
|
||||
500_000,
|
||||
&drain_script,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InsufficientFunds {
|
||||
assert_matches!(
|
||||
selection,
|
||||
Err(Error::InsufficientFunds {
|
||||
available: 300_010,
|
||||
..
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,15 +134,11 @@ impl FullyNodedExport {
|
||||
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
||||
_ if !include_blockheight => 0,
|
||||
Err(_) => 0,
|
||||
Ok(txs) => {
|
||||
let mut heights = txs
|
||||
.into_iter()
|
||||
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
||||
.collect::<Vec<_>>();
|
||||
heights.sort_unstable();
|
||||
|
||||
*heights.last().unwrap_or(&0)
|
||||
}
|
||||
Ok(txs) => txs
|
||||
.into_iter()
|
||||
.filter_map(|tx| tx.confirmation_time.map(|c| c.height))
|
||||
.min()
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
let export = FullyNodedExport {
|
||||
@@ -249,6 +245,22 @@ mod test {
|
||||
fee: Some(500),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345678,
|
||||
height: 5001,
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
db.set_tx(&TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_str(
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
)
|
||||
.unwrap(),
|
||||
received: 25_000,
|
||||
sent: 0,
|
||||
fee: Some(300),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345677,
|
||||
height: 5000,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -11,7 +11,43 @@
|
||||
|
||||
//! 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 mut devices = HWIClient::enumerate()?;
|
||||
//! if devices.is_empty() {
|
||||
//! panic!("No devices found!");
|
||||
//! }
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, 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::secp256k1::{All, Secp256k1};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,10 +96,10 @@ use bitcoin::{secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
|
||||
KeyMap, SinglePubKey,
|
||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
|
||||
SinglePubKey,
|
||||
};
|
||||
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
|
||||
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
|
||||
|
||||
use super::utils::SecpCtx;
|
||||
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||
@@ -369,11 +369,11 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
|
||||
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
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> {
|
||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
Some(DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self.signer,
|
||||
origin: None,
|
||||
}))
|
||||
@@ -472,6 +472,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
sign_options.allow_grinding,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -485,9 +486,14 @@ fn sign_psbt_ecdsa(
|
||||
hash: bitcoin::Sighash,
|
||||
hash_ty: EcdsaSighashType,
|
||||
secp: &SecpCtx,
|
||||
allow_grinding: bool,
|
||||
) {
|
||||
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)
|
||||
.expect("invalid or corrupted ecdsa signature");
|
||||
|
||||
@@ -511,13 +517,13 @@ fn sign_psbt_schnorr(
|
||||
let keypair = match leaf_hash {
|
||||
None => keypair
|
||||
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||
.into_inner(),
|
||||
.to_inner(),
|
||||
Some(_) => keypair, // no tweak for script spend
|
||||
};
|
||||
|
||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||
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");
|
||||
|
||||
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
||||
@@ -570,7 +576,7 @@ impl SignersContainer {
|
||||
self.0
|
||||
.values()
|
||||
.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()
|
||||
}
|
||||
|
||||
@@ -595,8 +601,13 @@ impl SignersContainer {
|
||||
};
|
||||
|
||||
match secret {
|
||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
||||
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
|
||||
DescriptorSecretKey::Single(private_key) => container.add_external(
|
||||
SignerId::from(
|
||||
private_key
|
||||
.key
|
||||
.public_key(secp)
|
||||
.to_pubkeyhash(SigType::Ecdsa),
|
||||
),
|
||||
SignerOrdering::default(),
|
||||
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
||||
),
|
||||
@@ -718,10 +729,15 @@ pub struct SignOptions {
|
||||
///
|
||||
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||
pub sign_with_tap_internal_key: bool,
|
||||
|
||||
/// Whether we should grind ECDSA signature to ensure signing with low r
|
||||
/// or not.
|
||||
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
|
||||
pub allow_grinding: bool,
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TapLeavesOptions {
|
||||
/// The signer will sign all the leaves it has a key for.
|
||||
All,
|
||||
@@ -751,6 +767,7 @@ impl Default for SignOptions {
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
allow_grinding: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,6 +998,7 @@ mod signers_container_tests {
|
||||
use crate::descriptor;
|
||||
use crate::descriptor::IntoWalletDescriptor;
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::Network;
|
||||
@@ -1050,17 +1068,17 @@ mod signers_container_tests {
|
||||
signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
|
||||
signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
|
||||
|
||||
assert!(matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)));
|
||||
assert!(matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)));
|
||||
assert!(matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)));
|
||||
assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1));
|
||||
assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2));
|
||||
assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3));
|
||||
|
||||
// The `signer4` has the same ID as `signer3` but lower ordering.
|
||||
// It should be found by `id3` instead of `signer3`.
|
||||
signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
|
||||
assert!(matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)));
|
||||
assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4));
|
||||
|
||||
// Can't find anything with ID that doesn't exist
|
||||
assert!(matches!(signers.find(id_nonexistent), None));
|
||||
assert_matches!(signers.find(id_nonexistent), None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
||||
@@ -42,9 +42,7 @@ use std::default::Default;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{OutPoint, Script, Transaction};
|
||||
|
||||
use miniscript::descriptor::DescriptorTrait;
|
||||
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||
@@ -139,7 +137,7 @@ pub(crate) struct TxParams {
|
||||
pub(crate) manually_selected_only: bool,
|
||||
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
||||
pub(crate) ordering: TxOrdering,
|
||||
pub(crate) locktime: Option<u32>,
|
||||
pub(crate) locktime: Option<LockTime>,
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
@@ -147,7 +145,7 @@ pub(crate) struct TxParams {
|
||||
pub(crate) add_global_xpubs: bool,
|
||||
pub(crate) include_output_redeem_witness_script: bool,
|
||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||
pub(crate) current_height: Option<u32>,
|
||||
pub(crate) current_height: Option<LockTime>,
|
||||
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
|
||||
///
|
||||
/// 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
|
||||
}
|
||||
@@ -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
|
||||
/// 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
|
||||
}
|
||||
@@ -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.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -703,7 +701,7 @@ impl TxOrdering {
|
||||
#[cfg(not(test))]
|
||||
let mut rng = rand::thread_rng();
|
||||
#[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);
|
||||
}
|
||||
@@ -736,13 +734,13 @@ impl Default for Version {
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub(crate) enum RbfValue {
|
||||
Default,
|
||||
Value(u32),
|
||||
Value(Sequence),
|
||||
}
|
||||
|
||||
impl RbfValue {
|
||||
pub(crate) fn get_value(&self) -> u32 {
|
||||
pub(crate) fn get_value(&self) -> Sequence {
|
||||
match self {
|
||||
RbfValue::Default => 0xFFFFFFFD,
|
||||
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
RbfValue::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
@@ -858,10 +856,12 @@ mod test {
|
||||
}
|
||||
|
||||
fn get_test_utxos() -> Vec<LocalUtxo> {
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
vec![
|
||||
LocalUtxo {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||
vout: 0,
|
||||
},
|
||||
txout: Default::default(),
|
||||
@@ -870,7 +870,7 @@ mod test {
|
||||
},
|
||||
LocalUtxo {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||
vout: 1,
|
||||
},
|
||||
txout: Default::default(),
|
||||
|
||||
@@ -9,23 +9,11 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bitcoin::blockdata::script::Script;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::{LockTime, Script, Sequence};
|
||||
|
||||
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.
|
||||
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
||||
/// keep it compatible with network dust rate
|
||||
@@ -38,7 +26,7 @@ pub trait IsDust {
|
||||
|
||||
impl IsDust for u64 {
|
||||
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 {
|
||||
// This flag cannot be set in the nSequence when spending using OP_CSV
|
||||
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
||||
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
|
||||
// The RBF value must enable relative timelocks
|
||||
if !rbf.is_relative_lock_time() {
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
||||
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 {
|
||||
fn check_after(&self, n: u32) -> bool {
|
||||
fn check_after(&self, n: LockTime) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
current_height >= n
|
||||
current_height >= n.to_consensus_u32()
|
||||
} else {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -125,10 +95,15 @@ impl 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 {
|
||||
// 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 {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -139,11 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
||||
};
|
||||
use crate::bitcoin::Address;
|
||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
|
||||
use super::{check_nsequence_rbf, IsDust};
|
||||
use crate::bitcoin::{Address, Sequence};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
@@ -165,66 +141,40 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_time() {
|
||||
let result = check_nsequence_rbf(
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
||||
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,
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::database::{BatchOperations, MemoryDatabase};
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::{Transaction, Txid};
|
||||
@@ -137,9 +138,7 @@ mod test {
|
||||
}
|
||||
|
||||
let result = verify_tx(&signed_tx, &database, &blockchain);
|
||||
assert!(result.is_err(), "Should fail with missing input tx");
|
||||
assert!(
|
||||
matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid()),
|
||||
assert_matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid(),
|
||||
"Error should be a `MissingInputTx` error"
|
||||
);
|
||||
|
||||
@@ -147,9 +146,9 @@ mod test {
|
||||
database.set_raw_tx(&prev_tx).unwrap();
|
||||
|
||||
let result = verify_tx(&unsigned_tx, &database, &blockchain);
|
||||
assert!(result.is_err(), "Should fail since the TX is unsigned");
|
||||
assert!(
|
||||
matches!(result, Err(VerifyError::Consensus(_))),
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(VerifyError::Consensus(_)),
|
||||
"Error should be a `Consensus` error"
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user