Compare commits

...

104 Commits

Author SHA1 Message Date
William Casarin
476fa3fd7d add Copy trait to Progress types 2021-06-23 08:31:55 -07:00
Alekos Filini
2755b09e7b Bump CI stable version to 1.53
Fixes #374
2021-06-21 12:16:54 +02:00
Alekos Filini
5e6286a493 Fix clippy warnings on 1.53
Fix `clippy::inconsistent_struct_constructor`: the constructor field
order was inconsistent with the struct declaration.
2021-06-21 12:16:45 +02:00
Alekos Filini
67714adc80 Fix CHANGELOG
The `Rpc` backend is not part of the release but it accidentally ended
up there during the merge
2021-06-21 09:07:15 +02:00
Alekos Filini
9ff86ea37c Merge commit 'refs/pull/370/head' of github.com:bitcoindevkit/bdk 2021-06-18 12:54:11 +02:00
Steve Myers
ceeb3a40cf [ci] Revert change to run_blockchain_tests.sh back to using container id 2021-06-15 15:57:14 -07:00
Steve Myers
e3316aee4c [ci] Change blockchain tests to use bitcoind rpc cookie authentication 2021-06-15 15:39:54 -07:00
Steve Myers
c2567b61aa Merge branch 'release/0.8.0' 2021-06-14 11:47:39 -07:00
Steve Myers
e1a77b87ab Fix CHANGELOG unreleased link 2021-06-14 11:43:48 -07:00
Steve Myers
5bf758b03a Add CHANGELOG v0.8.0 link 2021-06-14 11:40:50 -07:00
Riccardo Casatta
0bbfa5f989 make fee in TransactionDetails Option, add confirmation_time field as Option
confirmation_time contains both a block height and block timestamp and is
Some only for confirmed transaction
2021-06-14 15:29:24 +02:00
Alekos Filini
18254110c6 Merge commit 'refs/pull/348/head' of github.com:bitcoindevkit/bdk 2021-06-11 11:41:23 +02:00
Alekos Filini
44217539e5 Bump version to 0.8.1-dev 2021-06-11 11:29:42 +02:00
Alekos Filini
33b45ebe82 Bump version to 0.8.0 2021-06-10 16:00:01 +02:00
Alekos Filini
2faed425ed Update CHANGELOG 2021-06-10 15:59:24 +02:00
Alekos Filini
2cc05c07a5 Bump version in src/lib.rs 2021-06-10 15:59:08 +02:00
Riccardo Casatta
fe371f9d92 Use bitcoin's base64 feature for Psbts 2021-06-10 15:50:44 +02:00
Tobin Harding
12de13b95c Remove redundant borrows
Clippy emits:

  warning: this expression borrows a reference

As suggested remove the borrows from the front of vars that are already references.
2021-06-10 13:16:07 +10:00
Alekos Filini
9205295332 Merge commit 'refs/pull/365/head' of github.com:bitcoindevkit/bdk into release/0.8.0 2021-06-09 16:05:16 +02:00
Tobin Harding
3b446c9e14 Use no_run instead of ignore
We have an attribute `no_run` that builds but does not run example code
in Rustdocs, this keeps the examples building as the codebase evolves.

use `no_run` and fix example code so it builds cleanly during test run.

Some examples that require the `electrum` feature to be available have
been feature-gated to make sure they aren't accidentally compiled when
that feature is not enabled.

Co-authored-by: Alekos Filini <alekos.filini@gmail.com>
2021-06-09 11:29:57 +02:00
Alekos Filini
378167efca Remove explicit feature(external_doc)
It looks like this is now enabled by default as of `cargo 1.54.0-nightly (0cecbd673 2021-06-01)`
2021-06-09 11:27:25 +02:00
Alekos Filini
224be27aa8 Fix example/doctests format 2021-06-04 15:53:15 +02:00
Alekos Filini
4a23070cc8 [ci] Check fmt for examples/doctests 2021-06-04 15:07:02 +02:00
Riccardo Casatta
ba2e3042cc add details to TODO, format doc example 2021-06-04 15:05:35 +02:00
Alekos Filini
f8117c0f9f Bump version to 0.8.0-rc.1 2021-06-04 09:42:14 +02:00
Riccardo Casatta
1639984b56 move scan in setup 2021-06-03 15:26:47 +02:00
Riccardo Casatta
ab54a17eb7 update bitcoind dep 2021-06-03 11:07:39 +02:00
Riccardo Casatta
ae5aa06586 use storage address instead of satoshi's 2021-06-03 11:06:24 +02:00
Riccardo Casatta
ab98283159 always ask node for tx no matter capabilities 2021-06-03 10:56:02 +02:00
Riccardo Casatta
81851190f0 correctly initialize UTXO keychain kind 2021-06-03 10:56:02 +02:00
Riccardo Casatta
e1b037a921 change feature to execute sync from rpc to test-rpc 2021-06-03 10:56:01 +02:00
Riccardo Casatta
9b7ed08891 rename struct to CallResult 2021-06-03 10:56:01 +02:00
Riccardo Casatta
dffb753ce3 match also on signet 2021-06-03 10:56:00 +02:00
Riccardo Casatta
0b969657cd update changelog with rpc feature 2021-06-03 10:55:59 +02:00
Riccardo Casatta
bfef2e3cfe Implements RPC Backend 2021-06-03 10:55:58 +02:00
Paul Miller
0ec064ef13 Use AddressInfo in private methods 2021-05-27 17:11:16 -04:00
Paul Miller
6b60914ca1 return AddressInfo from get_address 2021-05-27 17:11:16 -04:00
Alekos Filini
881ca8d1e3 [signer] Add an option to explicitly allow using non-ALL sighashes
Instead of blindly using the `sighash_type` set in a psbt input, we
now only sign `SIGHASH_ALL` inputs by default, and require the user to
explicitly opt-in to using other sighashes if they desire to do so.

Fixes #350
2021-05-26 10:38:15 +02:00
Alekos Filini
5633475ce8 Merge commit 'refs/pull/347/head' of github.com:bitcoindevkit/bdk 2021-05-26 08:56:38 +02:00
LLFourn
ea8488b2a7 Initialize env_logger at start of blockchain tests 2021-05-21 13:21:59 +10:00
LLFourn
d2a981efee run_blockchain_tests.sh improvements 2021-05-21 13:21:41 +10:00
LLFourn
4c92daf517 Uppercase 'Test' so that github can see what's up
It is expecting something named 'Test electrum'
2021-05-20 14:33:02 +10:00
LLFourn
aba2a05d83 Add script for running the blockchain tests locally 2021-05-19 16:45:48 +10:00
LLFourn
5b194c268d Fix clippy warnings inside testutils macro
Now that it's inside the main repo clippy is having a go at me.
2021-05-19 16:45:48 +10:00
LLFourn
00bdf08f2a Remove testutils feature so doctests worka again
I wanted to only conditionally compile testutils but it's needed in
doctests which we can't conditionally compile for:

https://github.com/rust-lang/rust/issues/67295
2021-05-19 16:45:48 +10:00
LLFourn
38b0470b14 Move blockchain related stuff to blockchain_tests 2021-05-19 16:45:48 +10:00
LLFourn
d60c5003bf Merge testutils crate into the main crate
This avoids having to keep the apis in sync between the macros and the
main project.
2021-05-19 16:45:48 +10:00
LLFourn
fcae5adabd Run blockchain tests on esplora
They were only being run on electrum before.
2021-05-19 15:47:44 +10:00
Steve Myers
9f04a9d82d Merge commit 'refs/pull/338/head' of github.com:bitcoindevkit/bdk 2021-05-18 16:41:45 -07:00
LLFourn
465ef6e674 Roll blockchain tests proc macro into normal macro
This means one less crate in the repo. Had to do a Default on TestClient
to satisfy clippy.
2021-05-18 20:02:33 +10:00
Steve Myers
aaa9943a5f Merge commit 'refs/pull/346/head' of github.com:bitcoindevkit/bdk 2021-05-14 10:49:30 -07:00
Alekos Filini
3897e29740 Fix changelog 2021-05-12 15:11:20 +02:00
Alekos Filini
8f06e45872 Bump version to 0.7.1-dev 2021-05-12 15:10:28 +02:00
Alekos Filini
766570abfd Bump version to 0.7.0 2021-05-12 14:20:58 +02:00
Alekos Filini
934ec366d9 Use the released testutils-macros 2021-05-12 14:20:23 +02:00
Alekos Filini
d0733e9496 Bump version in src/lib.rs 2021-05-12 14:19:58 +02:00
Alekos Filini
3c7a1f5918 Bump testutils-macros to v0.6.0 2021-05-12 14:19:00 +02:00
Alekos Filini
85aadaccd2 Update changelog in preparation of v0.7.0 2021-05-12 14:17:46 +02:00
Tobin Harding
fad0fe9f30 Update create transaction example code
The transaction builder changed a while ago, looks like some of the
example code did not get updated.

Update the transaction creation code to use a mutable builder.
2021-05-12 14:13:23 +02:00
Tobin Harding
6546b77c08 Remove stale comments
The two fields this comment references are not `Option` type. This
comment seems to be stale.
2021-05-11 13:29:22 +10:00
Tobin Harding
e1066e955c Remove unneeded unit expression
Clippy emits:

  warning: unneeded unit expression

As suggested, remove the unneeded unit expression.
2021-05-11 10:52:08 +10:00
Tobin Harding
7f06dc3330 Clear clippy manual_map warning
The lint `manual_map` is new so we cannot explicitly allow it and
maintain backwards comparability. Instead, allow all lints for
`get_utxo_for` with a comment explaining why.
2021-05-11 10:52:07 +10:00
Tobin Harding
de40351710 Use consistent field ordering
Clippy emits:

  warning: struct constructor field order is inconsistent with struct
  definition field order

As suggested, re-order the fields to be consistent with the struct
definition.
2021-05-11 10:51:44 +10:00
Tobin Harding
de811bea30 Use !any() instead of find()...is_none()
Clippy emits:

  warning: called `is_none()` after searching an `Iterator` with `find`

As suggested, use the construct: `!foo.iter().any(...)`
2021-05-11 10:51:44 +10:00
Tobin Harding
74cc80d127 Remove unnecessary clone
Clippy emits:

  warning: using `clone` on type `descriptor::policy::Condition` which
  implements the `Copy` trait

Remove the clone and rely on `Copy`.
2021-05-11 10:51:44 +10:00
Tobin Harding
009f68a06a Use assert!(foo) instead of assert_eq!(foo, true)
It is redundant to pass true/false to `assert_eq!` since `assert!`
already asserts true/false.

This may, however, be controversial if someone thinks that

```
    assert_eq!(foo, false);
```

is more clear than

```
    assert!(!foo);
```

Use `assert!` directly instead of `assert_eq!` with true/false argument.
2021-05-11 10:51:44 +10:00
Riccardo Casatta
47f26447da continue signing when finding already finalized inputs 2021-05-07 16:32:24 +02:00
Tobin Harding
12641b9e8f Use PsbtKey instead of PSBT
We recently converted uses of `PSBT` -> `Psbt` inline with idiomatic
Rust acronym identifiers. Do the same to `PSBTKey`.

Use `PsbtKey` instead of `PSBTKey` when aliasing the import of
`psbt::raw::Key` from `bitcoin` library.
2021-05-07 16:29:53 +02:00
Tobin Harding
aa3707b5b4 Use Psbt instead of PSBT
Idiomatic Rust uses lowercase for acronyms for all characters after the
first e.g. `std::net::TcpStream`. PSBT (Partially Signed Bitcoin
Transaction) should be rendered `Psbt` in Rust code if we want to write
idiomatic Rust.

Use `Psbt` instead of `PSBT` when aliasing the import of
`PartiallySignedTransaction` from `bitcoin` library.
2021-05-07 16:29:50 +02:00
Riccardo Casatta
f6631e35b8 continue signing when finding already finalized inputs 2021-05-07 13:52:20 +02:00
Alekos Filini
3608ff9f14 Merge commit 'refs/pull/341/head' of github.com:bitcoindevkit/bdk into release/0.7.0 2021-05-07 11:00:00 +02:00
Alekos Filini
7fdb98e147 Merge commit 'refs/pull/341/head' of github.com:bitcoindevkit/bdk 2021-05-07 10:59:32 +02:00
Tobin Harding
9aea90bd81 Use default: D mirroring Rust documentation
Currently we use `F: f` for the argument that is the default function
passed to `map_or_else` and pass a closure for the second argument. This
bent my brain while reading the documentation because the docs use
`default: D` for the first and `f: F` for the second. Although this is
totally trivial it makes deciphering the combinator chain easier if we
name the arguments the same way the Rust docs do.

Use `default: D` for the identifier of the default function passed into `map_or_else`.
2021-05-07 09:08:49 +10:00
Riccardo Casatta
898dfe6cf1 get psbt inputs with bounds check 2021-05-06 16:10:11 +02:00
Riccardo Casatta
7961ae7f8e Check index out of bound also for tx inputs not only for psbt inputs 2021-05-06 15:13:25 +02:00
Alekos Filini
8bf77c8f07 Bump version to 0.7.0-rc.1 2021-05-06 13:56:38 +02:00
Alekos Filini
3c7bae9ce9 Rewrite the non_witness_utxo check 2021-05-06 11:39:01 +02:00
Alekos Filini
17bcd8ed7d [signer] Replace force_non_witness_utxo with only_witness_utxo
Instead of providing an opt-in option to force the addition of the
`non_witness_utxo`, we will now add them by default and provide the
option to disable them when they aren't considered necessary.
2021-05-06 08:58:39 +02:00
Alekos Filini
b5e9589803 [signer] Adjust signing behavior with SignOptions 2021-05-06 08:58:38 +02:00
Alekos Filini
1d628d84b5 [signer] Fix error variant 2021-05-05 16:59:59 +02:00
Alekos Filini
b84fd6ea5c Fix import for FromStr 2021-05-05 16:59:57 +02:00
Alekos Filini
8fe4222c33 Merge commit 'refs/pull/336/head' of github.com:bitcoindevkit/bdk 2021-05-05 14:51:36 +02:00
codeShark149
e626f2e255 Added grcov based code coverage reporting in github action 2021-04-30 17:20:20 +05:30
LLFourn
5a0c150ff9 Make wallet methods take &mut psbt
Rather than consuming it because that is unergonomic.
2021-04-28 15:34:25 +10:00
Alekos Filini
00f07818f9 Merge commit 'refs/pull/321/head' of github.com:bitcoindevkit/bdk 2021-04-16 14:08:26 +02:00
Riccardo Casatta
136a4bddb2 Verify PSBT input satisfaction 2021-04-16 12:22:49 +02:00
Riccardo Casatta
ff7b74ec27 destructure tuple to improve clarity 2021-04-16 12:22:47 +02:00
Steve Myers
8c00326990 [ci] Revert fixed nightly-2021-03-23, use actual nightly 2021-04-15 10:15:13 -07:00
Riccardo Casatta
afcd26032d comment out println in tests, fix doc 2021-04-15 16:48:42 +02:00
Riccardo Casatta
8f422a1bf9 Add timelocks to policy satisfaction results
Also for signature the logic has been refactored to handle appropriately nested cases.
2021-04-15 15:57:35 +02:00
Alekos Filini
45983d2166 Merge commit 'refs/pull/322/head' of github.com:bitcoindevkit/bdk 2021-04-15 11:40:34 +02:00
Steve Myers
89cb4de7f6 [ci] Update 'test-readme-examples' job to use nightly-2021-03-23 2021-04-14 20:34:32 -07:00
Steve Myers
7ca0e0e2bd [ci] Update 'Tarpaulin to codecov.io' job to use nightly-2021-03-23 2021-04-14 20:33:42 -07:00
Alekos Filini
2bddd9baed Update CHANGELOG for v0.6.0 2021-04-14 18:49:52 +02:00
Alekos Filini
0135ba29c5 Bump version to 0.6.1-dev 2021-04-14 18:47:31 +02:00
Steve Myers
3ed44ce8cf Remove unneeded script 2021-04-09 09:19:19 -07:00
Steve Myers
8e7d8312a9 [ci] Update 'build-test' job to clippy check all-targets 2021-04-08 14:44:35 -07:00
Steve Myers
4da7488dc4 Update 'cargo-check.sh' to not check +nightly 2021-04-08 14:36:07 -07:00
Steve Myers
e37680af96 Use .flatten() instead of .filter_map(|x| x), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_identity
2021-04-08 14:18:07 -07:00
Steve Myers
5f873ae500 Use .any() instead of .find().is_some(), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#search_is_some
2021-04-08 14:18:07 -07:00
Steve Myers
2380634496 Use .get(0) instead of .iter().next(), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_slice
2021-04-08 14:18:07 -07:00
Steve Myers
af98b8da06 Compare float equality using error margin EPSILON, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#float_cmp
2021-04-08 14:17:59 -07:00
Steve Myers
b68ec050e2 Remove redundant clone, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone
2021-04-08 11:41:58 -07:00
Steve Myers
ac7df09200 Remove needlessly taken reference of both operands, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
2021-04-08 11:39:38 -07:00
43 changed files with 3123 additions and 1770 deletions

View File

@@ -3,25 +3,35 @@ on: [push]
name: Code Coverage
jobs:
tarpaulin-codecov:
name: Tarpaulin to codecov.io
Codecov:
name: Code Coverage
runs-on: ubuntu-latest
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
- id: coverage
name: Generate coverage
uses: actions-rs/grcov@v0.1.5
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Tarpaulin
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
- name: Publish to codecov.io
uses: codecov/codecov-action@v1.0.15
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
file: ./cobertura.xml
file: ${{ steps.coverage.outputs.report }}
directory: ./coverage/reports/

View File

@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
rust:
- 1.51.0 # STABLE
- 1.53.0 # STABLE
- 1.46.0 # MSRV
features:
- default
@@ -22,6 +22,7 @@ jobs:
- compact_filters
- esplora,key-value-db,electrum
- compiler
- rpc
steps:
- name: checkout
uses: actions/checkout@v2
@@ -46,7 +47,7 @@ jobs:
- name: Build
run: cargo build --features ${{ matrix.features }} --no-default-features
- name: Clippy
run: cargo clippy --features ${{ matrix.features }} --no-default-features -- -D warnings
run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
- name: Test
run: cargo test --features ${{ matrix.features }} --no-default-features
@@ -73,17 +74,30 @@ jobs:
- name: Test
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
test-electrum:
name: Test electrum
test-blockchains:
name: Test ${{ matrix.blockchain.name }}
runs-on: ubuntu-16.04
container: bitcoindevkit/electrs:0.2.0
strategy:
fail-fast: false
matrix:
blockchain:
- name: electrum
container: bitcoindevkit/electrs:0.4.0
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
- name: esplora
container: bitcoindevkit/esplora:0.4.0
start: /root/electrs --network regtest -vvv --daemon-dir $GITHUB_WORKSPACE/.bitcoin --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002
- name: rpc
container: bitcoindevkit/electrs:0.4.0
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
container: ${{ matrix.blockchain.container }}
env:
BDK_RPC_AUTH: USER_PASS
BDK_RPC_USER: admin
BDK_RPC_PASS: passw
BDK_RPC_AUTH: COOKIEFILE
BDK_RPC_COOKIEFILE: ${{ github.workspace }}/.bitcoin/regtest/.cookie
BDK_RPC_URL: 127.0.0.1:18443
BDK_RPC_WALLET: bdk-test
BDK_ELECTRUM_URL: tcp://127.0.0.1:60401
BDK_ESPLORA_URL: http://127.0.0.1:3002
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -95,18 +109,22 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: get pkg-config # running esplora tests seems to need this
run: apt update && apt install -y --fix-missing pkg-config libssl-dev
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain
run: $HOME/.cargo/bin/rustup default 1.51.0 # STABLE
run: $HOME/.cargo/bin/rustup default 1.53.0 # STABLE
- name: Set profile
run: $HOME/.cargo/bin/rustup set profile minimal
- name: Update toolchain
run: $HOME/.cargo/bin/rustup update
- name: Start core
run: ./ci/start-core.sh
- name: start ${{ matrix.blockchain.name }}
run: nohup ${{ matrix.blockchain.start }} & sleep 5
- name: Test
run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features
run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
check-wasm:
name: Check WASM
@@ -131,7 +149,7 @@ jobs:
- run: sudo apt-get update || exit 1
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
- name: Set default toolchain
run: rustup default 1.51.0 # STABLE
run: rustup default 1.53.0 # STABLE
- name: Set profile
run: rustup set profile minimal
- name: Add target wasm32
@@ -148,12 +166,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Set default toolchain
run: rustup default 1.51.0 # STABLE
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Add clippy
- name: Add rustfmt
run: rustup component add rustfmt
- name: Update toolchain
run: rustup update
- name: Check fmt
run: cargo fmt --all -- --check
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check

View File

@@ -18,7 +18,7 @@ jobs:
target
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain
run: rustup default nightly-2021-03-23
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain

View File

@@ -6,6 +6,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Wallet
#### Added
- Bitcoin core RPC added as blockchain backend
## [v0.8.0] - [v0.7.0]
### Wallet
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
#### Changed
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
## [v0.7.0] - [v0.6.0]
### Policy
#### Changed
Removed `fill_satisfaction` method in favor of enum parameter in `extract_policy` method
#### Added
Timelocks are considered (optionally) in building the `satisfaction` field
### Wallet
- Changed `Wallet::{sign, finalize_psbt}` now take a `&mut psbt` rather than consuming it.
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
## [v0.6.0] - [v0.5.1]
### Misc
#### Changed
- New minimum supported rust version is 1.46.0
@@ -315,10 +343,13 @@ final transaction is created by calling `finish` on the builder.
- Use `MemoryDatabase` in the compiler example
- Make the REPL return JSON
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...HEAD
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...HEAD
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
[v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0
[v0.5.0]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...v0.5.0
[v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.6.0"
version = "0.8.1-dev"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0"
bdk-macros = "^0.4"
log = "^0.4"
miniscript = "5.1"
bitcoin = { version = "^0.26", features = ["use-serde"] }
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.7"
@@ -32,6 +32,10 @@ socks = { version = "0.3", optional = true }
lazy_static = { version = "1.4", optional = true }
tiny-bip39 = { version = "^0.8", optional = true }
# Needed by bdk_blockchain_tests macro
bitcoincore-rpc = { version = "0.13", optional = true }
serial_test = { version = "0.4", optional = true }
# Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] }
@@ -52,20 +56,22 @@ key-value-db = ["sled"]
async-interface = ["async-trait"]
all-keys = ["keys-bip39"]
keys-bip39 = ["tiny-bip39"]
rpc = ["bitcoincore-rpc"]
# Debug/Test features
debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"]
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum"]
test-rpc = ["rpc"]
test-esplora = ["esplora"]
test-md-docs = ["electrum"]
[dev-dependencies]
bdk-testutils = "0.4"
bdk-testutils-macros = "0.5"
serial_test = "0.4"
lazy_static = "1.4"
env_logger = "0.7"
base64 = "^0.11"
clap = "2.33"
serial_test = "0.4"
bitcoind = "0.10.0"
[[example]]
name = "address_validator"
@@ -79,10 +85,7 @@ path = "examples/compiler.rs"
required-features = ["compiler"]
[workspace]
members = ["macros", "testutils", "testutils-macros"]
# Generate docs with nightly to add the "features required" badge
# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
members = ["macros"]
[package.metadata.docs.rs]
features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
# defines the configuration attribute `docsrs`

View File

@@ -130,7 +130,7 @@ fn main() -> Result<(), bdk::Error> {
### Sign a transaction
```rust,no_run
use bdk::{Wallet, database::MemoryDatabase};
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::consensus::deserialize;
@@ -143,9 +143,9 @@ fn main() -> Result<(), bdk::Error> {
)?;
let psbt = "...";
let psbt = deserialize(&base64::decode(psbt).unwrap())?;
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
Ok(())
}
@@ -166,4 +166,4 @@ at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
dual licensed as above, without any additional terms or conditions.

View File

@@ -1,17 +1,14 @@
#!/usr/bin/env sh
echo "Starting bitcoin node."
/root/bitcoind -regtest -server -daemon -fallbackfee=0.0002 -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
mkdir $GITHUB_WORKSPACE/.bitcoin
/root/bitcoind -regtest -server -daemon -datadir=$GITHUB_WORKSPACE/.bitcoin -fallbackfee=0.0002 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
echo "Waiting for bitcoin node."
until /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS getblockchaininfo; do
until /root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin getblockchaininfo; do
sleep 1
done
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS createwallet $BDK_RPC_WALLET
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin createwallet $BDK_RPC_WALLET
echo "Generating 150 bitcoin blocks."
ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress)
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR
echo "Starting electrs node."
nohup /root/electrs --network regtest --jsonrpc-import &
sleep 5
ADDR=$(/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin -rpcwallet=$BDK_RPC_WALLET getnewaddress)
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin generatetoaddress 150 $ADDR

13
codecov.yaml Normal file
View File

@@ -0,0 +1,13 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
base: auto
informational: false
patch:
default:
target: auto
threshold: 100%
base: auto

71
run_blockchain_tests.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/sh
usage() {
cat <<'EOF'
Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker.
Usage: ./run_blockchain_tests.sh [esplora|electrum|rpc] [test name].
EOF
}
eprintln(){
echo "$@" >&2
}
cleanup() {
if test "$id"; then
eprintln "cleaning up $blockchain docker container $id";
docker rm -fv "$id" > /dev/null;
rm /tmp/regtest-"$id".cookie;
fi
trap - EXIT INT
}
# Makes sure we clean up the container at the end or if ^C
trap 'rc=$?; cleanup; exit $rc' EXIT INT
blockchain="$1"
test_name="$2"
case "$blockchain" in
electrum)
eprintln "starting electrs docker container"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
;;
esplora)
eprintln "starting esplora docker container"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora:0.4.0)"
export BDK_ESPLORA_URL=http://127.0.0.1:3002
;;
rpc)
eprintln "starting bitcoind docker container (via electrs container)"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
;;
*)
usage;
exit 1;
;;
esac
# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box
export BDK_RPC_AUTH=COOKIEFILE
export BDK_RPC_COOKIEFILE=/tmp/regtest-"$id".cookie
export BDK_RPC_URL=127.0.0.1:18443
export BDK_RPC_WALLET=bdk-test
export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401
cli(){
docker exec -it "$id" /root/bitcoin-cli -regtest -datadir=/root/.bitcoin $@
}
#eprintln "running getwalletinfo until bitcoind seems to be alive"
while ! cli getwalletinfo >/dev/null; do sleep 1; done
# sleep again for good measure!
sleep 1;
# copy bitcoind cookie file to /tmp
docker cp "$id":/root/.bitcoin/regtest/.cookie /tmp/regtest-"$id".cookie
cargo test --features "test-blockchains,test-$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name"

View File

@@ -1,31 +0,0 @@
#!/bin/bash
#
# Run various invocations of cargo check
features=( "default" "compiler" "electrum" "esplora" "compact_filters" "key-value-db" "async-interface" "all-keys" "keys-bip39" )
toolchains=( "+stable" "+1.46" "+nightly" )
main() {
check_src
check_all_targets
}
# Check with all features, with various toolchains.
check_src() {
for toolchain in "${toolchains[@]}"; do
cmd="cargo $toolchain clippy --all-targets --no-default-features"
for feature in "${features[@]}"; do
touch_files
$cmd --features "$feature"
done
done
}
# Touch files to prevent cached warnings from not showing up.
touch_files() {
touch $(find . -name *.rs)
}
main
exit 0

View File

@@ -71,7 +71,7 @@ use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
use crate::FeeRate;
use crate::{ConfirmationTime, FeeRate};
use peer::*;
use store::*;
@@ -146,7 +146,7 @@ impl CompactFiltersBlockchain {
database: &mut D,
tx: &Transaction,
height: Option<u32>,
timestamp: u64,
timestamp: Option<u64>,
internal_max_deriv: &mut Option<u32>,
external_max_deriv: &mut Option<u32>,
) -> Result<(), Error> {
@@ -206,9 +206,8 @@ impl CompactFiltersBlockchain {
transaction: Some(tx.clone()),
received: incoming,
sent: outgoing,
height,
timestamp,
fees: inputs_sum.saturating_sub(outputs_sum),
confirmation_time: ConfirmationTime::new(height, timestamp),
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
};
info!("Saving tx {}", tx.txid);
@@ -364,8 +363,8 @@ impl Blockchain for CompactFiltersBlockchain {
);
let mut updates = database.begin_batch();
for details in database.iter_txs(false)? {
match details.height {
Some(height) if (height as usize) < last_synced_block => continue,
match details.confirmation_time {
Some(c) if (c.height as usize) < last_synced_block => continue,
_ => updates.del_tx(&details.txid, false)?,
};
}
@@ -387,7 +386,7 @@ impl Blockchain for CompactFiltersBlockchain {
database,
tx,
Some(height as u32),
0,
None,
&mut internal_max_deriv,
&mut external_max_deriv,
)?;
@@ -398,7 +397,7 @@ impl Blockchain for CompactFiltersBlockchain {
database,
tx,
None,
0,
None,
&mut internal_max_deriv,
&mut external_max_deriv,
)?;

View File

@@ -227,12 +227,12 @@ impl Peer {
Ok(Peer {
writer,
reader_thread,
responses,
reader_thread,
connected,
mempool,
network,
version,
network,
})
}

View File

@@ -45,13 +45,6 @@ use crate::FeeRate;
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
pub struct ElectrumBlockchain(Client);
#[cfg(test)]
#[cfg(feature = "test-electrum")]
#[bdk_blockchain_tests(crate)]
fn local_electrs() -> ElectrumBlockchain {
ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap())
}
impl std::convert::From<Client> for ElectrumBlockchain {
fn from(client: Client) -> Self {
ElectrumBlockchain(client)
@@ -175,3 +168,10 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
)?))
}
}
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> ElectrumBlockchain {
ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap())
}
}

View File

@@ -414,3 +414,10 @@ impl_error!(reqwest::Error, Reqwest, EsploraError);
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> EsploraBlockchain {
EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None)
}
}

View File

@@ -43,6 +43,13 @@ pub use self::electrum::ElectrumBlockchain;
#[cfg(feature = "electrum")]
pub use self::electrum::ElectrumBlockchainConfig;
#[cfg(feature = "rpc")]
pub mod rpc;
#[cfg(feature = "rpc")]
pub use self::rpc::RpcBlockchain;
#[cfg(feature = "rpc")]
pub use self::rpc::RpcConfig;
#[cfg(feature = "esplora")]
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
pub mod esplora;
@@ -52,6 +59,7 @@ pub use self::esplora::EsploraBlockchain;
#[cfg(feature = "compact_filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
pub mod compact_filters;
#[cfg(feature = "compact_filters")]
pub use self::compact_filters::CompactFiltersBlockchain;
@@ -166,7 +174,7 @@ impl Progress for Sender<ProgressData> {
}
/// Type that implements [`Progress`] and drops every update received
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct NoopProgress;
/// Create a new instance of [`NoopProgress`]
@@ -181,7 +189,7 @@ impl Progress for NoopProgress {
}
/// Type that implements [`Progress`] and logs at level `INFO` every update received
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct LogProgress;
/// Create a nwe instance of [`LogProgress`]

673
src/blockchain/rpc.rs Normal file
View File

@@ -0,0 +1,673 @@
// Bitcoin Dev Kit
// Written in 2021 by Riccardo Casatta <riccardo@casatta.it>
//
// 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.
//! Rpc Blockchain
//!
//! Backend that gets blockchain data from Bitcoin Core RPC
//!
//! ## Example
//!
//! ```no_run
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain};
//! let config = RpcConfig {
//! url: "127.0.0.1:18332".to_string(),
//! auth: bitcoincore_rpc::Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
//! network: bdk::bitcoin::Network::Testnet,
//! wallet_name: "wallet_name".to_string(),
//! skip_blocks: None,
//! };
//! let blockchain = RpcBlockchain::from_config(&config);
//! ```
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::database::{BatchDatabase, DatabaseUtils};
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
use crate::wallet::utils::SecpCtx;
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoincore_rpc::json::{
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
};
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{Auth, Client, RpcApi};
use log::debug;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
#[derive(Debug)]
pub struct RpcBlockchain {
/// Rpc client to the node, includes the wallet name
client: Client,
/// Network used
network: Network,
/// Blockchain capabilities, cached here at startup
capabilities: HashSet<Capability>,
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
skip_blocks: Option<u32>,
/// This is a fixed Address used as a hack key to store information on the node
_storage_address: Address,
}
/// RpcBlockchain configuration options
#[derive(Debug)]
pub struct RpcConfig {
/// The bitcoin node url
pub url: String,
/// The bitcoin node authentication mechanism
pub auth: Auth,
/// The network we are using (it will be checked the bitcoin node network matches this)
pub network: Network,
/// The wallet name in the bitcoin node, consider using [wallet_name_from_descriptor] for this
pub wallet_name: String,
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
pub skip_blocks: Option<u32>,
}
impl RpcBlockchain {
fn get_node_synced_height(&self) -> Result<u32, Error> {
let info = self.client.get_address_info(&self._storage_address)?;
if let Some(GetAddressInfoResultLabel::Simple(label)) = info.labels.first() {
Ok(label
.parse::<u32>()
.unwrap_or_else(|_| self.skip_blocks.unwrap_or(0)))
} else {
Ok(self.skip_blocks.unwrap_or(0))
}
}
/// Set the synced height in the core node by using a label of a fixed address so that
/// another client with the same descriptor doesn't rescan the blockchain
fn set_node_synced_height(&self, height: u32) -> Result<(), Error> {
Ok(self
.client
.set_label(&self._storage_address, &height.to_string())?)
}
}
impl Blockchain for RpcBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
self.capabilities.clone()
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
stop_gap: Option<usize>,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
debug!(
"importing {} script_pubkeys (some maybe already imported)",
scripts_pubkeys.len()
);
let requests: Vec<_> = scripts_pubkeys
.iter()
.map(|s| ImportMultiRequest {
timestamp: ImportMultiRescanSince::Timestamp(0),
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(&s)),
watchonly: Some(true),
..Default::default()
})
.collect();
let options = ImportMultiOptions {
rescan: Some(false),
};
// Note we use import_multi because as of bitcoin core 0.21.0 many descriptors are not supported
// https://bitcoindevkit.org/descriptors/#compatibility-matrix
//TODO maybe convenient using import_descriptor for compatible descriptor and import_multi as fallback
self.client.import_multi(&requests, Some(&options))?;
let current_height = self.get_height()?;
// min because block invalidate may cause height to go down
let node_synced = self.get_node_synced_height()?.min(current_height);
//TODO call rescan in chunks (updating node_synced_height) so that in case of
// interruption work can be partially recovered
debug!(
"rescan_blockchain from:{} to:{}",
node_synced, current_height
);
self.client
.rescan_blockchain(Some(node_synced as usize), Some(current_height as usize))?;
progress_update.update(1.0, None)?;
self.set_node_synced_height(current_height)?;
self.sync(stop_gap, database, progress_update)
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
_stop_gap: Option<usize>,
db: &mut D,
_progress_update: P,
) -> Result<(), Error> {
let mut indexes = HashMap::new();
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
indexes.insert(*keykind, db.get_last_index(*keykind)?.unwrap_or(0));
}
let mut known_txs: HashMap<_, _> = db
.iter_txs(true)?
.into_iter()
.map(|tx| (tx.txid, tx))
.collect();
let known_utxos: HashSet<_> = db.iter_utxos()?.into_iter().collect();
//TODO list_since_blocks would be more efficient
let current_utxo = self
.client
.list_unspent(Some(0), None, None, Some(true), None)?;
debug!("current_utxo len {}", current_utxo.len());
//TODO supported up to 1_000 txs, should use since_blocks or do paging
let list_txs = self
.client
.list_transactions(None, Some(1_000), None, Some(true))?;
let mut list_txs_ids = HashSet::new();
for tx_result in list_txs.iter().filter(|t| {
// list_txs returns all conflicting tx we want to
// filter out replaced tx => unconfirmed and not in the mempool
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
}) {
let txid = tx_result.info.txid;
list_txs_ids.insert(txid);
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
let confirmation_time =
ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
if confirmation_time != known_tx.confirmation_time {
// reorg may change tx height
debug!(
"updating tx({}) confirmation time to: {:?}",
txid, confirmation_time
);
known_tx.confirmation_time = confirmation_time;
db.set_tx(&known_tx)?;
}
} else {
//TODO check there is already the raw tx in db?
let tx_result = self.client.get_transaction(&txid, Some(true))?;
let tx: Transaction = deserialize(&tx_result.hex)?;
let mut received = 0u64;
let mut sent = 0u64;
for output in tx.output.iter() {
if let Ok(Some((kind, index))) =
db.get_path_from_script_pubkey(&output.script_pubkey)
{
if index > *indexes.get(&kind).unwrap() {
indexes.insert(kind, index);
}
received += output.value;
}
}
for input in tx.input.iter() {
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
sent += previous_output.value;
}
}
let td = TransactionDetails {
transaction: Some(tx),
txid: tx_result.info.txid,
confirmation_time: ConfirmationTime::new(
tx_result.info.blockheight,
tx_result.info.blocktime,
),
received,
sent,
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
};
debug!(
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
td.txid, tx_result.fee, td.fee
);
db.set_tx(&td)?;
}
}
for known_txid in known_txs.keys() {
if !list_txs_ids.contains(known_txid) {
debug!("removing tx: {}", known_txid);
db.del_tx(known_txid, false)?;
}
}
let current_utxos: HashSet<_> = current_utxo
.into_iter()
.map(|u| {
Ok(LocalUtxo {
outpoint: OutPoint::new(u.txid, u.vout),
keychain: db
.get_path_from_script_pubkey(&u.script_pub_key)?
.ok_or(Error::TransactionNotFound)?
.0,
txout: TxOut {
value: u.amount.as_sat(),
script_pubkey: u.script_pub_key,
},
})
})
.collect::<Result<_, Error>>()?;
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
for s in spent {
debug!("removing utxo: {:?}", s);
db.del_utxo(&s.outpoint)?;
}
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
for s in received {
debug!("adding utxo: {:?}", s);
db.set_utxo(s)?;
}
for (keykind, index) in indexes {
debug!("{:?} max {}", keykind, index);
db.set_last_index(keykind, index)?;
}
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let sat_per_kb = self
.client
.estimate_smart_fee(target as u16, None)?
.fee_rate
.ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64;
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
}
}
impl ConfigurableBlockchain for RpcBlockchain {
type Config = RpcConfig;
/// Returns RpcBlockchain backend creating an RPC client to a specific wallet named as the descriptor's checksum
/// if it's the first time it creates the wallet in the node and upon return is granted the wallet is loaded
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let wallet_name = config.wallet_name.clone();
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
let client = Client::new(wallet_url, config.auth.clone())?;
let loaded_wallets = client.list_wallets()?;
if loaded_wallets.contains(&wallet_name) {
debug!("wallet already loaded {:?}", wallet_name);
} else {
let existing_wallets = list_wallet_dir(&client)?;
if existing_wallets.contains(&wallet_name) {
client.load_wallet(&wallet_name)?;
debug!("wallet loaded {:?}", wallet_name);
} else {
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
debug!("wallet created {:?}", wallet_name);
}
}
let blockchain_info = client.get_blockchain_info()?;
let network = match blockchain_info.chain.as_str() {
"main" => Network::Bitcoin,
"test" => Network::Testnet,
"regtest" => Network::Regtest,
"signet" => Network::Signet,
_ => return Err(Error::Generic("Invalid network".to_string())),
};
if network != config.network {
return Err(Error::InvalidNetwork {
requested: config.network,
found: network,
});
}
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
let rpc_version = client.version()?;
if rpc_version >= 210_000 {
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
if info.contains_key("txindex") {
capabilities.insert(Capability::GetAnyTx);
capabilities.insert(Capability::AccurateFees);
}
}
// this is just a fixed address used only to store a label containing the synced height in the node
let mut storage_address =
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
storage_address.network = network;
Ok(RpcBlockchain {
client,
network,
capabilities,
_storage_address: storage_address,
skip_blocks: config.skip_blocks,
})
}
}
/// Deterministically generate a unique name given the descriptors defining the wallet
pub fn wallet_name_from_descriptor<T>(
descriptor: T,
change_descriptor: Option<T>,
network: Network,
secp: &SecpCtx,
) -> Result<String, Error>
where
T: IntoWalletDescriptor,
{
//TODO check descriptors contains only public keys
let descriptor = descriptor
.into_wallet_descriptor(&secp, network)?
.0
.to_string();
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
if let Some(change_descriptor) = change_descriptor {
let change_descriptor = change_descriptor
.into_wallet_descriptor(&secp, network)?
.0
.to_string();
wallet_name.push_str(
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
);
}
Ok(wallet_name)
}
/// return the wallets available in default wallet directory
//TODO use bitcoincore_rpc method when PR #179 lands
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
#[derive(Deserialize)]
struct Name {
name: String,
}
#[derive(Deserialize)]
struct CallResult {
wallets: Vec<Name>,
}
let result: CallResult = client.call("listwalletdir", &[])?;
Ok(result.wallets.into_iter().map(|n| n.name).collect())
}
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> RpcBlockchain {
let url = std::env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let url = format!("http://{}", url);
// TODO same code in `fn get_auth` in testutils, make it public there
let auth = match std::env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
std::env::var("BDK_RPC_USER").unwrap(),
std::env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(std::path::PathBuf::from(
std::env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
};
let config = RpcConfig {
url,
auth,
network: Network::Regtest,
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
skip_blocks: None,
};
RpcBlockchain::from_config(&config).unwrap()
}
}
#[cfg(feature = "test-rpc")]
#[cfg(test)]
mod test {
use super::{RpcBlockchain, RpcConfig};
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Amount, Network, Transaction};
use crate::blockchain::rpc::wallet_name_from_descriptor;
use crate::blockchain::{noop_progress, Blockchain, Capability, ConfigurableBlockchain};
use crate::database::MemoryDatabase;
use crate::wallet::AddressIndex;
use crate::Wallet;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::Txid;
use bitcoincore_rpc::json::CreateRawTransactionInput;
use bitcoincore_rpc::RawTx;
use bitcoincore_rpc::{Auth, RpcApi};
use bitcoind::BitcoinD;
use std::collections::HashMap;
fn create_rpc(
bitcoind: &BitcoinD,
desc: &str,
network: Network,
) -> Result<RpcBlockchain, crate::Error> {
let secp = Secp256k1::new();
let wallet_name = wallet_name_from_descriptor(desc, None, network, &secp).unwrap();
let config = RpcConfig {
url: bitcoind.rpc_url(),
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
network,
wallet_name,
skip_blocks: None,
};
RpcBlockchain::from_config(&config)
}
fn create_bitcoind(args: Vec<String>) -> BitcoinD {
let exe = std::env::var("BITCOIND_EXE").unwrap();
bitcoind::BitcoinD::with_args(exe, args, false, bitcoind::P2P::No).unwrap()
}
const DESCRIPTOR_PUB: &'static str = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
const DESCRIPTOR_PRIV: &'static str = "wpkh(tprv8ZgxMBicQKsPdZxBDUcvTSMEaLwCTzTc6gmw8KBKwa3BJzWzec4g6VUbQBHJcutDH6mMEmBeVyN27H1NF3Nu8isZ1Sts4SufWyfLE6Mf1MB/*)";
#[test]
fn test_rpc_wallet_setup() {
env_logger::try_init().unwrap();
let bitcoind = create_bitcoind(vec![]);
let node_address = bitcoind.client.get_new_address(None, None).unwrap();
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let db = MemoryDatabase::new();
let wallet = Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain).unwrap();
wallet.sync(noop_progress(), None).unwrap();
generate(&bitcoind, 101);
wallet.sync(noop_progress(), None).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap();
let expected_address = "bcrt1q8dyvgt4vhr8ald4xuwewcxhdjha9a5k78wxm5t";
assert_eq!(expected_address, address.to_string());
send_to_address(&bitcoind, &address, 100_000);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_address.script_pubkey(), 50_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(
wallet.get_balance().unwrap(),
100_000 - 50_000 - details.fee.unwrap_or(0)
);
drop(wallet);
// test skip_blocks
generate(&bitcoind, 5);
let config = RpcConfig {
url: bitcoind.rpc_url(),
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
network: Network::Regtest,
wallet_name: "another-name".to_string(),
skip_blocks: Some(103),
};
let blockchain_skip = RpcBlockchain::from_config(&config).unwrap();
let db = MemoryDatabase::new();
let wallet_skip =
Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain_skip).unwrap();
wallet_skip.sync(noop_progress(), None).unwrap();
send_to_address(&bitcoind, &address, 100_000);
wallet_skip.sync(noop_progress(), None).unwrap();
assert_eq!(wallet_skip.get_balance().unwrap(), 100_000);
}
#[test]
fn test_rpc_from_config() {
let bitcoind = create_bitcoind(vec![]);
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest);
assert!(blockchain.is_ok());
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Testnet);
assert!(blockchain.is_err(), "wrong network doesn't error");
}
#[test]
fn test_rpc_capabilities_get_tx() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let capabilities = rpc.get_capabilities();
assert!(capabilities.contains(&Capability::FullHistory) && capabilities.len() == 1);
let bitcoind_indexed = create_bitcoind(vec!["-txindex".to_string()]);
let rpc_indexed = create_rpc(&bitcoind_indexed, DESCRIPTOR_PUB, Network::Regtest).unwrap();
assert_eq!(rpc_indexed.get_capabilities().len(), 3);
let address = generate(&bitcoind_indexed, 101);
let txid = send_to_address(&bitcoind_indexed, &address, 100_000);
assert!(rpc_indexed.get_tx(&txid).unwrap().is_some());
assert!(rpc.get_tx(&txid).is_err());
}
#[test]
fn test_rpc_estimate_fee_get_height() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let result = rpc.estimate_fee(2);
assert!(result.is_err());
let address = generate(&bitcoind, 100);
// create enough tx so that core give some fee estimation
for _ in 0..15 {
let _ = bitcoind.client.generate_to_address(1, &address).unwrap();
for _ in 0..2 {
send_to_address(&bitcoind, &address, 100_000);
}
}
let result = rpc.estimate_fee(2);
assert!(result.is_ok());
assert_eq!(rpc.get_height().unwrap(), 115);
}
#[test]
fn test_rpc_node_synced_height() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let synced_height = rpc.get_node_synced_height().unwrap();
assert_eq!(synced_height, 0);
rpc.set_node_synced_height(1).unwrap();
let synced_height = rpc.get_node_synced_height().unwrap();
assert_eq!(synced_height, 1);
}
#[test]
fn test_rpc_broadcast() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let address = generate(&bitcoind, 101);
let utxo = bitcoind
.client
.list_unspent(None, None, None, None, None)
.unwrap();
let input = CreateRawTransactionInput {
txid: utxo[0].txid,
vout: utxo[0].vout,
sequence: None,
};
let out: HashMap<_, _> = vec![(
address.to_string(),
utxo[0].amount - Amount::from_sat(100_000),
)]
.into_iter()
.collect();
let tx = bitcoind
.client
.create_raw_transaction(&[input], &out, None, None)
.unwrap();
let signed_tx = bitcoind
.client
.sign_raw_transaction_with_wallet(tx.raw_hex(), None, None)
.unwrap();
let parsed_tx: Transaction = deserialize(&signed_tx.hex).unwrap();
rpc.broadcast(&parsed_tx).unwrap();
assert!(bitcoind
.client
.get_raw_mempool()
.unwrap()
.contains(&tx.txid()));
}
#[test]
fn test_rpc_wallet_name() {
let secp = Secp256k1::new();
let name =
wallet_name_from_descriptor(DESCRIPTOR_PUB, None, Network::Regtest, &secp).unwrap();
assert_eq!("tmg7aqay", name);
}
fn generate(bitcoind: &BitcoinD, blocks: u64) -> Address {
let address = bitcoind.client.get_new_address(None, None).unwrap();
bitcoind
.client
.generate_to_address(blocks, &address)
.unwrap();
address
}
fn send_to_address(bitcoind: &BitcoinD, address: &Address, amount: u64) -> Txid {
bitcoind
.client
.send_to_address(
&address,
Amount::from_sat(amount),
None,
None,
None,
None,
None,
None,
)
.unwrap()
}
}

View File

@@ -21,7 +21,7 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid};
use super::*;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
use crate::types::{ConfirmationTime, KeychainKind, LocalUtxo, TransactionDetails};
use crate::wallet::time::Instant;
use crate::wallet::utils::ChunksIterator;
@@ -147,18 +147,19 @@ pub trait ElectrumLikeSync {
// save any tx details not in db but in history_txs_id or with different height/timestamp
for txid in history_txs_id.iter() {
let height = txid_height.get(txid).cloned().flatten();
let timestamp = *new_timestamps.get(txid).unwrap_or(&0u64);
let timestamp = new_timestamps.get(txid).cloned();
if let Some(tx_details) = txs_details_in_db.get(txid) {
// check if height matches, otherwise updates it
if tx_details.height != height {
// check if tx height matches, otherwise updates it. timestamp is not in the if clause
// because we are not asking headers for confirmed tx we know about
if tx_details.confirmation_time.as_ref().map(|c| c.height) != height {
let confirmation_time = ConfirmationTime::new(height, timestamp);
let mut new_tx_details = tx_details.clone();
new_tx_details.height = height;
new_tx_details.timestamp = timestamp;
new_tx_details.confirmation_time = confirmation_time;
batch.set_tx(&new_tx_details)?;
}
} else {
save_transaction_details_and_utxos(
&txid,
txid,
db,
timestamp,
height,
@@ -171,7 +172,7 @@ pub trait ElectrumLikeSync {
// remove any tx details in db but not in history_txs_id
for txid in txs_details_in_db.keys() {
if !history_txs_id.contains(txid) {
batch.del_tx(&txid, false)?;
batch.del_tx(txid, false)?;
}
}
@@ -238,9 +239,13 @@ pub trait ElectrumLikeSync {
chunk_size: usize,
) -> Result<HashMap<Txid, u64>, Error> {
let mut txid_timestamp = HashMap::new();
let txid_in_db_with_conf: HashSet<_> = txs_details_in_db
.values()
.filter_map(|details| details.confirmation_time.as_ref().map(|_| details.txid))
.collect();
let needed_txid_height: HashMap<&Txid, u32> = txid_height
.iter()
.filter(|(t, _)| txs_details_in_db.get(*t).is_none())
.filter(|(t, _)| !txid_in_db_with_conf.contains(*t))
.filter_map(|(t, o)| o.map(|h| (t, h)))
.collect();
let needed_heights: HashSet<u32> = needed_txid_height.values().cloned().collect();
@@ -292,7 +297,7 @@ pub trait ElectrumLikeSync {
fn save_transaction_details_and_utxos<D: BatchDatabase>(
txid: &Txid,
db: &mut D,
timestamp: u64,
timestamp: Option<u64>,
height: Option<u32>,
updates: &mut dyn BatchOperations,
utxo_deps: &HashMap<OutPoint, OutPoint>,
@@ -329,7 +334,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
// removes conflicting UTXO if any (generated from same inputs, like for example RBF)
if let Some(outpoint) = utxo_deps.get(&input.previous_output) {
updates.del_utxo(&outpoint)?;
updates.del_utxo(outpoint)?;
}
}
@@ -355,9 +360,8 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
transaction: Some(tx),
received: incoming,
sent: outgoing,
height,
timestamp,
fees: inputs_sum.saturating_sub(outputs_sum), /* if the tx is a coinbase, fees would be negative */
confirmation_time: ConfirmationTime::new(height, timestamp),
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
};
updates.set_tx(&tx_details)?;

View File

@@ -320,7 +320,7 @@ impl Database for Tree {
.map(|b| -> Result<_, Error> {
let mut txdetails: TransactionDetails = serde_json::from_slice(&b)?;
if include_raw {
txdetails.transaction = self.get_raw_tx(&txid)?;
txdetails.transaction = self.get_raw_tx(txid)?;
}
Ok(txdetails)

View File

@@ -394,7 +394,7 @@ impl Database for MemoryDatabase {
Ok(self.map.get(&key).map(|b| {
let mut txdetails: TransactionDetails = b.downcast_ref().cloned().unwrap();
if include_raw {
txdetails.transaction = self.get_raw_tx(&txid).unwrap();
txdetails.transaction = self.get_raw_tx(txid).unwrap();
}
txdetails
@@ -429,8 +429,8 @@ impl BatchDatabase for MemoryDatabase {
}
fn commit_batch(&mut self, mut batch: Self::Batch) -> Result<(), Error> {
for key in batch.deleted_keys {
self.map.remove(&key);
for key in batch.deleted_keys.iter() {
self.map.remove(key);
}
self.map.append(&mut batch.map);
Ok(())
@@ -473,18 +473,18 @@ macro_rules! populate_test_db {
};
let txid = tx.txid();
let height = tx_meta
.min_confirmations
.map(|conf| current_height.unwrap().checked_sub(conf as u32).unwrap());
let confirmation_time = tx_meta.min_confirmations.map(|conf| ConfirmationTime {
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
timestamp: 0,
});
let tx_details = TransactionDetails {
transaction: Some(tx.clone()),
txid,
timestamp: 0,
height,
fee: Some(0),
received: 0,
sent: 0,
fees: 0,
confirmation_time,
};
db.set_tx(&tx_details).unwrap();
@@ -511,7 +511,7 @@ macro_rules! doctest_wallet {
() => {{
use $crate::bitcoin::Network;
use $crate::database::MemoryDatabase;
use testutils::testutils;
use $crate::testutils;
let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
let descriptors = testutils!(@descriptors (descriptor) (descriptor));

View File

@@ -164,14 +164,14 @@ pub(crate) trait DatabaseUtils: Database {
.map(|o| o.is_some())
}
fn get_raw_tx_or<F>(&self, txid: &Txid, f: F) -> Result<Option<Transaction>, Error>
fn get_raw_tx_or<D>(&self, txid: &Txid, default: D) -> Result<Option<Transaction>, Error>
where
F: FnOnce() -> Result<Option<Transaction>, Error>,
D: FnOnce() -> Result<Option<Transaction>, Error>,
{
self.get_tx(txid, true)?
.map(|t| t.transaction)
.flatten()
.map_or_else(f, |t| Ok(Some(t)))
.map_or_else(default, |t| Ok(Some(t)))
}
fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {
@@ -314,11 +314,13 @@ pub mod test {
let mut tx_details = TransactionDetails {
transaction: Some(tx),
txid,
timestamp: 123456,
received: 1337,
sent: 420420,
fees: 140,
height: Some(1000),
fee: Some(140),
confirmation_time: Some(ConfirmationTime {
timestamp: 123456,
height: 1000,
}),
};
tree.set_tx(&tx_details).unwrap();

View File

@@ -535,9 +535,7 @@ macro_rules! fragment_internal {
( @t , $( $tail:tt )* ) => ({
$crate::fragment_internal!( @t $( $tail )* )
});
( @t ) => ({
()
});
( @t ) => ({});
// Fallback to calling `fragment!()`
( $( $tokens:tt )* ) => ({

View File

@@ -27,6 +27,8 @@ use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum;
pub(crate) mod derived;
#[doc(hidden)]
@@ -126,11 +128,11 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
let (pk, _, networks) = if self.0.is_witness() {
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(&secp)?
desciptor_key.extract(secp)?
} else {
let desciptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(&secp)?
desciptor_key.extract(secp)?
};
if networks.contains(&network) {
@@ -255,6 +257,7 @@ pub trait ExtractPolicy {
fn extract_policy(
&self,
signers: &SignersContainer,
psbt: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, DescriptorError>;
}

View File

@@ -22,6 +22,7 @@
//! # use std::sync::Arc;
//! # use bdk::descriptor::*;
//! # use bdk::bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new();
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
//!
@@ -29,7 +30,7 @@
//! println!("{:?}", extended_desc);
//!
//! let signers = Arc::new(key_map.into());
//! let policy = extended_desc.extract_policy(&signers, &secp)?;
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy)?);
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -47,24 +48,20 @@ use bitcoin::PublicKey;
use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner};
use miniscript::{
Descriptor, ForEachKey, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal,
ToPublicKey,
Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal, ToPublicKey,
};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use crate::descriptor::{
DerivedDescriptor, DerivedDescriptorKey, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
};
use crate::psbt::PsbtUtils;
use crate::descriptor::{DerivedDescriptorKey, ExtractPolicy};
use crate::wallet::signer::{SignerId, SignersContainer};
use crate::wallet::utils::{self, SecpCtx};
use crate::wallet::utils::{self, After, Older, SecpCtx};
use super::checksum::get_checksum;
use super::error::Error;
use super::XKeyUtils;
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use miniscript::psbt::PsbtInputSatisfier;
/// Raw public key or extended key fingerprint
@@ -339,7 +336,7 @@ impl Satisfaction {
items.push(inner_index);
let conditions_set = other_conditions
.values()
.fold(HashSet::new(), |set, i| set.union(&i).cloned().collect());
.fold(HashSet::new(), |set, i| set.union(i).cloned().collect());
conditions.insert(inner_index, conditions_set);
}
}
@@ -563,13 +560,18 @@ impl Policy {
conditions: Default::default(),
sorted: None,
};
let mut satisfaction = contribution.clone();
for (index, item) in items.iter().enumerate() {
contribution.add(&item.contribution, index)?;
satisfaction.add(&item.satisfaction, index)?;
}
contribution.finalize();
satisfaction.finalize();
let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
policy.contribution = contribution;
policy.satisfaction = satisfaction;
Ok(Some(policy))
}
@@ -577,6 +579,7 @@ impl Policy {
fn make_multisig(
keys: &[DescriptorPublicKey],
signers: &SignersContainer,
build_sat: BuildSatisfaction,
threshold: usize,
sorted: bool,
secp: &SecpCtx,
@@ -594,6 +597,8 @@ impl Policy {
conditions: Default::default(),
sorted: Some(sorted),
};
let mut satisfaction = contribution.clone();
for (index, key) in keys.iter().enumerate() {
if signers.find(signer_id(key, secp)).is_some() {
contribution.add(
@@ -603,7 +608,19 @@ impl Policy {
index,
)?;
}
if let Some(psbt) = build_sat.psbt() {
if signature_in_psbt(psbt, key, secp) {
satisfaction.add(
&Satisfaction::Complete {
condition: Default::default(),
},
index,
)?;
}
}
}
satisfaction.finalize();
contribution.finalize();
let mut policy: Policy = SatisfiableItem::Multisig {
@@ -612,6 +629,7 @@ impl Policy {
}
.into();
policy.contribution = contribution;
policy.satisfaction = satisfaction;
Ok(Some(policy))
}
@@ -698,52 +716,6 @@ impl Policy {
_ => Ok(Condition::default()),
}
}
/// fill `self.satisfaction` with the signatures we already have in the PSBT
pub fn fill_satisfactions(
&mut self,
psbt: &PSBT,
desc: &ExtendedDescriptor, // can't put in self because non Serialize
secp: &SecpCtx,
) -> Result<(), Error> {
// Start from an empty Satisfaction (like a contribution without signers)
let policy = desc.extract_policy(&SignersContainer::default(), &secp)?;
if let Some(policy) = policy {
self.satisfaction = policy.contribution;
for (i, input) in psbt.inputs.iter().enumerate() {
let s = PsbtInputSatisfier::new(psbt, i);
let derived_desc = desc.derive_from_psbt_input(input, psbt.get_utxo_for(i), secp);
self.add_satisfaction(s, derived_desc);
}
self.satisfaction.finalize();
}
Ok(())
}
fn add_satisfaction<S: Satisfier<bitcoin::PublicKey>>(
&mut self,
satisfier: S,
derived_desc: Option<DerivedDescriptor>,
) {
if let Some(derived_desc) = derived_desc {
let mut index = 0;
derived_desc.for_each_key(|k| {
if satisfier.lookup_sig(&k.as_key().to_public_key()).is_some() {
//TODO check signature verifies
let _ = self.satisfaction.add(
&Satisfaction::Complete {
condition: Default::default(),
},
index,
);
}
index += 1;
true
});
}
}
}
impl From<SatisfiableItem> for Policy {
@@ -759,7 +731,12 @@ fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
}
}
fn signature(key: &DescriptorPublicKey, signers: &SignersContainer, secp: &SecpCtx) -> Policy {
fn signature(
key: &DescriptorPublicKey,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Policy {
let mut policy: Policy = SatisfiableItem::Signature(PkOrF::from_key(key, secp)).into();
policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
@@ -770,9 +747,38 @@ fn signature(key: &DescriptorPublicKey, signers: &SignersContainer, secp: &SecpC
Satisfaction::None
};
if let Some(psbt) = build_sat.psbt() {
policy.satisfaction = if signature_in_psbt(psbt, key, secp) {
Satisfaction::Complete {
condition: Default::default(),
}
} else {
Satisfaction::None
};
}
policy
}
fn signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
//TODO check signature validity
psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(key) => input.partial_sigs.contains_key(&key.key),
DescriptorPublicKey::XPub(xpub) => {
let pubkey = input
.bip32_derivation
.iter()
.find(|(_, (f, _))| *f == xpub.root_fingerprint(secp))
.map(|(p, _)| p);
//TODO check actual derivation matches
match pubkey {
Some(pubkey) => input.partial_sigs.contains_key(pubkey),
None => false,
}
}
})
}
fn signature_key(
key: &<DescriptorPublicKey as MiniscriptKey>::Hash,
signers: &SignersContainer,
@@ -796,12 +802,13 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
fn extract_policy(
&self,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
Ok(match &self.node {
// Leaves
Terminal::True | Terminal::False => None,
Terminal::PkK(pubkey) => Some(signature(pubkey, signers, secp)),
Terminal::PkK(pubkey) => Some(signature(pubkey, signers, build_sat, secp)),
Terminal::PkH(pubkey_hash) => Some(signature_key(pubkey_hash, signers, secp)),
Terminal::After(value) => {
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
@@ -811,6 +818,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
csv: None,
},
};
if let BuildSatisfaction::PsbtTimelocks {
current_height,
psbt,
..
} = build_sat
{
let after = After::new(Some(current_height), false);
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
if after_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
}
Some(policy)
}
@@ -822,6 +843,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
csv: Some(*value),
},
};
if let BuildSatisfaction::PsbtTimelocks {
current_height,
input_max_height,
psbt,
} = build_sat
{
let older = Older::new(Some(current_height), Some(input_max_height), false);
let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
if older_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
}
Some(policy)
}
@@ -835,7 +870,9 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
Terminal::Hash160(hash) => {
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
}
Terminal::Multi(k, pks) => Policy::make_multisig(pks, signers, *k, false, secp)?,
Terminal::Multi(k, pks) => {
Policy::make_multisig(pks, signers, build_sat, *k, false, secp)?
}
// Identities
Terminal::Alt(inner)
| Terminal::Swap(inner)
@@ -843,34 +880,34 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
| Terminal::DupIf(inner)
| Terminal::Verify(inner)
| Terminal::NonZero(inner)
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, secp)?,
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?,
// Complex policies
Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and(
a.extract_policy(signers, secp)?,
b.extract_policy(signers, secp)?,
a.extract_policy(signers, build_sat, secp)?,
b.extract_policy(signers, build_sat, secp)?,
)?,
Terminal::AndOr(x, y, z) => Policy::make_or(
Policy::make_and(
x.extract_policy(signers, secp)?,
y.extract_policy(signers, secp)?,
x.extract_policy(signers, build_sat, secp)?,
y.extract_policy(signers, build_sat, secp)?,
)?,
z.extract_policy(signers, secp)?,
z.extract_policy(signers, build_sat, secp)?,
)?,
Terminal::OrB(a, b)
| Terminal::OrD(a, b)
| Terminal::OrC(a, b)
| Terminal::OrI(a, b) => Policy::make_or(
a.extract_policy(signers, secp)?,
b.extract_policy(signers, secp)?,
a.extract_policy(signers, build_sat, secp)?,
b.extract_policy(signers, build_sat, secp)?,
)?,
Terminal::Thresh(k, nodes) => {
let mut threshold = *k;
let mapped: Vec<_> = nodes
.iter()
.map(|n| n.extract_policy(signers, secp))
.map(|n| n.extract_policy(signers, build_sat, secp))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter_map(|x| x)
.flatten()
.collect();
if mapped.len() < nodes.len() {
@@ -886,20 +923,55 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
}
}
fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator<Item = PsbtInputSatisfier> {
(0..psbt.inputs.len()).map(move |i| PsbtInputSatisfier::new(psbt, i))
}
/// Options to build the satisfaction field in the policy
#[derive(Debug, Clone, Copy)]
pub enum BuildSatisfaction<'a> {
/// Don't generate `satisfaction` field
None,
/// Analyze the given PSBT to check for existing signatures
Psbt(&'a Psbt),
/// Like `Psbt` variant and also check for expired timelocks
PsbtTimelocks {
/// Given PSBT
psbt: &'a Psbt,
/// Current blockchain height
current_height: u32,
/// The highest confirmation height between the inputs
/// CSV should consider different inputs, but we consider the worst condition for the tx as whole
input_max_height: u32,
},
}
impl<'a> BuildSatisfaction<'a> {
fn psbt(&self) -> Option<&'a Psbt> {
match self {
BuildSatisfaction::None => None,
BuildSatisfaction::Psbt(psbt) => Some(psbt),
BuildSatisfaction::PsbtTimelocks { psbt, .. } => Some(psbt),
}
}
}
impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
fn extract_policy(
&self,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
fn make_sortedmulti<Ctx: ScriptContext>(
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
Ok(Policy::make_multisig(
keys.pks.as_ref(),
signers,
build_sat,
keys.k,
true,
secp,
@@ -907,22 +979,24 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
}
match self {
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))),
Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))),
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Sh(sh) => match sh.as_inner() {
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))),
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
ShInner::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
WshInner::SortedMulti(ref keys) => {
make_sortedmulti(keys, signers, build_sat, secp)
}
},
},
Descriptor::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
},
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, secp)?),
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
}
}
}
@@ -933,7 +1007,6 @@ mod test {
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
use super::*;
use crate::bitcoin::consensus::deserialize;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{Multisig, Signature, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
@@ -957,8 +1030,8 @@ mod test {
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
let path = bip32::DerivationPath::from_str(path).unwrap();
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
let fingerprint = tprv.fingerprint(&secp);
let tpub = bip32::ExtendedPubKey::from_private(secp, &tprv);
let fingerprint = tprv.fingerprint(secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();
@@ -978,7 +1051,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -993,7 +1066,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1017,14 +1090,14 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& &keys[0].fingerprint.unwrap() == &fingerprint0
&& &keys[1].fingerprint.unwrap() == &fingerprint1)
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
);
// TODO should this be "Satisfaction::None" since we have no prv keys?
// TODO should items and conditions not be empty?
@@ -1049,13 +1122,13 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& &keys[0].fingerprint.unwrap() == &fingerprint0
&& &keys[1].fingerprint.unwrap() == &fingerprint1)
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
);
assert!(
@@ -1081,7 +1154,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1113,7 +1186,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1146,7 +1219,7 @@ mod test {
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1162,7 +1235,7 @@ mod test {
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1189,7 +1262,7 @@ mod test {
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1232,7 +1305,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1271,7 +1344,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
println!("desc policy = {:?}", policy); // TODO remove
@@ -1296,7 +1369,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
println!("desc policy = {:?}", policy); // TODO remove
@@ -1314,7 +1387,7 @@ mod test {
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc
.extract_policy(&signers_container, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1337,7 +1410,7 @@ mod test {
let signers = keymap.into();
let policy = wallet_desc
.extract_policy(&signers, &secp)
.extract_policy(&signers, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1369,11 +1442,12 @@ mod test {
assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5)));
}
const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
const ALICE_BOB_PATH: &str = "m/0'";
#[test]
fn test_extract_satisfaction() {
const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
const ALICE_BOB_PATH: &str = "m/0'";
const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA==";
const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA";
@@ -1400,45 +1474,132 @@ mod test {
let signers_container = Arc::new(SignersContainer::from(keymap));
let original_policy = wallet_desc
.extract_policy(&signers_container, &secp)
let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
let policy_alice_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
.unwrap();
//println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
let psbt: PSBT = deserialize(&base64::decode(ALICE_SIGNED_PSBT).unwrap()).unwrap();
let mut policy_clone = original_policy.clone();
policy_clone
.fill_satisfactions(&psbt, &wallet_desc, &secp)
.unwrap();
assert!(
matches!(&policy_clone.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![0]
)
);
let mut policy_clone = original_policy.clone();
let psbt: PSBT = deserialize(&base64::decode(BOB_SIGNED_PSBT).unwrap()).unwrap();
policy_clone
.fill_satisfactions(&psbt, &wallet_desc, &secp)
let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
let policy_bob_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
.unwrap();
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
assert!(
matches!(&policy_clone.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![1]
)
);
let mut policy_clone = original_policy.clone();
let psbt: PSBT = deserialize(&base64::decode(ALICE_BOB_SIGNED_PSBT).unwrap()).unwrap();
policy_clone
.fill_satisfactions(&psbt, &wallet_desc, &secp)
let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
let policy_alice_bob_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_clone.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![0, 1]
)
);
}
#[test]
fn test_extract_satisfaction_timelock() {
//const PSBT_POLICY_CONSIDER_TIMELOCK_NOT_EXPIRED: &str = "cHNidP8BAFMBAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAD/////ATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED: &str = "cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED: &str ="cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstIMEUCIQCtZxNm6H3Ux3pnc64DSpgohMdBj+57xhFHcURYt2BpPAIgG3OnI7bcj/3GtWX1HHyYGSI7QGa/zq5YnsmK1Cw29NABAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEIoAQASDBFAiEArWcTZuh91Md6Z3OuA0qYKITHQY/ue8YRR3FEWLdgaTwCIBtzpyO23I/9xrVl9Rx8mBkiO0Bmv86uWJ7JitQsNvTQAQEBUnZjUrJpaHwhA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLrJN8IQL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiKyTUocAAA==";
let secp = Secp256k1::new();
let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc =
descriptor!(wsh(thresh(2,d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let addr = wallet_desc
.as_derived(0, &secp)
.address(Network::Testnet)
.unwrap();
assert_eq!(
"tb1qhpemaacpeu8ajlnh8k9v55ftg0px58r8630fz8t5mypxcwdk5d8sum522g",
addr.to_string()
);
let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap();
let build_sat = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt,
current_height: 10,
input_max_height: 9,
};
let policy = wallet_desc
.extract_policy(&signers_container, build_sat, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items.is_empty()
)
);
//println!("{}", serde_json::to_string(&policy).unwrap());
let build_sat_expired = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt,
current_height: 12,
input_max_height: 9,
};
let policy_expired = wallet_desc
.extract_policy(&signers_container, build_sat_expired, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0]
)
);
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
let psbt_signed = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap();
let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt_signed,
current_height: 12,
input_max_height: 9,
};
let policy_expired_signed = wallet_desc
.extract_policy(&signers_container, build_sat_expired_signed, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0, 1]
)
);
//println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
}
}

View File

@@ -37,7 +37,7 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
///
/// ```
/// use bdk::descriptor::error::Error as DescriptorError;
/// use bdk::keys::{KeyError, IntoDescriptorKey};
/// use bdk::keys::{IntoDescriptorKey, KeyError};
/// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
///
@@ -455,11 +455,12 @@ expand_make_bipxx!(segwit_v0, Segwitv0);
mod test {
// test existing descriptor templates, make sure they are expanded to the right descriptors
use std::str::FromStr;
use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::ValidNetworks;
use bitcoin::hashes::core::str::FromStr;
use bitcoin::network::constants::Network::Regtest;
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};

View File

@@ -11,6 +11,7 @@
use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator};
use bitcoin::OutPoint;
@@ -64,6 +65,8 @@ pub enum Error {
/// Required fee absolute value (satoshi)
required: u64,
},
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
@@ -80,7 +83,13 @@ pub enum Error {
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
/// Signing error
Signer(crate::wallet::signer::SignerError),
/// Invalid network
InvalidNetwork {
/// requested network, for example what is given as bdk-cli option
requested: Network,
/// found network, for example the network of the bitcoin node
found: Network,
},
/// Progress value must be between `0.0` (included) and `100.0` (included)
InvalidProgressValue(f32),
/// Progress update error (maybe the channel has been closed)
@@ -106,6 +115,8 @@ pub enum Error {
Hex(bitcoin::hashes::hex::Error),
/// Partially signed bitcoin transaction error
Psbt(bitcoin::util::psbt::Error),
/// Partially signed bitcoin transaction parseerror
PsbtParse(bitcoin::util::psbt::PsbtParseError),
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
//MissingInputUTXO(usize),
@@ -126,6 +137,9 @@ pub enum Error {
#[cfg(feature = "key-value-db")]
/// Sled database error
Sled(sled::Error),
#[cfg(feature = "rpc")]
/// Rpc client error
Rpc(bitcoincore_rpc::Error),
}
impl fmt::Display for Error {
@@ -172,6 +186,7 @@ impl_error!(bitcoin::secp256k1::Error, Secp256k1);
impl_error!(serde_json::Error, Json);
impl_error!(bitcoin::hashes::hex::Error, Hex);
impl_error!(bitcoin::util::psbt::Error, Psbt);
impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
#[cfg(feature = "electrum")]
impl_error!(electrum_client::Error, Electrum);
@@ -179,6 +194,8 @@ impl_error!(electrum_client::Error, Electrum);
impl_error!(crate::blockchain::esplora::EsploraError, Esplora);
#[cfg(feature = "key-value-db")]
impl_error!(sled::Error, Sled);
#[cfg(feature = "rpc")]
impl_error!(bitcoincore_rpc::Error, Rpc);
#[cfg(feature = "compact_filters")]
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {

View File

@@ -192,7 +192,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// ```
/// use bdk::bitcoin::PublicKey;
///
/// use bdk::keys::{DescriptorKey, KeyError, ScriptContext, IntoDescriptorKey};
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
///
/// pub struct MyKeyType {
/// pubkey: PublicKey,
@@ -211,8 +211,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey;
///
/// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub, KeyError,
/// ScriptContext, IntoDescriptorKey,
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
/// IntoDescriptorKey, KeyError, ScriptContext,
/// };
///
/// pub struct MyKeyType {
@@ -237,7 +237,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// ```
/// use bdk::bitcoin::PublicKey;
///
/// use bdk::keys::{DescriptorKey, ExtScriptContext, KeyError, ScriptContext, IntoDescriptorKey};
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
///
/// pub struct MyKeyType {
/// is_legacy: bool,
@@ -266,7 +266,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey;
/// use std::str::FromStr;
///
/// use bdk::keys::{DescriptorKey, KeyError, IntoDescriptorKey};
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
///
/// pub struct MySegwitOnlyKeyType {
/// pubkey: PublicKey,

View File

@@ -14,9 +14,6 @@
// only enables the `doc_cfg` feature when
// the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
// only enables the nightly `external_doc` feature when
// `test-md-docs` is enabled
#![cfg_attr(feature = "test-md-docs", feature(external_doc))]
//! A modern, lightweight, descriptor-based wallet library written in Rust.
//!
@@ -43,36 +40,39 @@
//! interact with the bitcoin P2P network.
//!
//! ```toml
//! bdk = "0.6.0"
//! ```
//!
//! ## Sync the balance of a descriptor
//!
//! ### Example
//! ```ignore
//! use bdk::Wallet;
//! use bdk::database::MemoryDatabase;
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
//!
//! use bdk::electrum_client::Client;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let client = 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,
//! MemoryDatabase::default(),
//! ElectrumBlockchain::from(client)
//! )?;
//!
//! wallet.sync(noop_progress(), None)?;
//!
//! println!("Descriptor balance: {} SAT", wallet.get_balance()?);
//!
//! Ok(())
//! }
//! bdk = "0.8.0"
//! ```
#![cfg_attr(
feature = "electrum",
doc = r##"
## Sync the balance of a descriptor
### Example
```no_run
use bdk::Wallet;
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::electrum_client::Client;
fn main() -> Result<(), bdk::Error> {
let client = 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,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
Ok(())
}
```
"##
)]
//!
//! ## Generate a few addresses
//!
@@ -97,57 +97,63 @@
//! Ok(())
//! }
//! ```
//!
//! ## Create a transaction
//!
//! ### Example
//! ```ignore
//! use base64::decode;
//! use bdk::{FeeRate, Wallet};
//! use bdk::database::MemoryDatabase;
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
//!
//! use bdk::electrum_client::Client;
//!
//! use bitcoin::consensus::serialize;
//! use bdk::wallet::AddressIndex::New;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let client = 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,
//! MemoryDatabase::default(),
//! ElectrumBlockchain::from(client)
//! )?;
//!
//! wallet.sync(noop_progress(), None)?;
//!
//! let send_to = wallet.get_address(New)?;
//! let (psbt, details) = wallet.build_tx()
//! .add_recipient(send_to.script_pubkey(), 50_000)
//! .enable_rbf()
//! .do_not_spend_change()
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
//! .finish()?;
//!
//! println!("Transaction details: {:#?}", details);
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
//!
//! Ok(())
//! }
//! ```
#![cfg_attr(
feature = "electrum",
doc = r##"
## Create a transaction
### Example
```no_run
use bdk::{FeeRate, Wallet};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::electrum_client::Client;
use bitcoin::consensus::serialize;
use bdk::wallet::AddressIndex::New;
fn main() -> Result<(), bdk::Error> {
let client = 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,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
let mut builder = wallet.build_tx();
builder
.add_recipient(send_to.script_pubkey(), 50_000)
.enable_rbf()
.do_not_spend_change()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
builder.finish()?
};
println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", &psbt);
Ok(())
}
```
"##
)]
//!
//! ## Sign a transaction
//!
//! ### Example
//! ```ignore
//! use base64::decode;
//! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase;
//! ```no_run
//! use std::str::FromStr;
//!
//! use bitcoin::consensus::deserialize;
//! use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
//!
//! use bdk::{Wallet, SignOptions};
//! use bdk::database::MemoryDatabase;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline(
@@ -158,9 +164,9 @@
//! )?;
//!
//! let psbt = "...";
//! let psbt = deserialize(&base64::decode(psbt).unwrap())?;
//! let mut psbt = Psbt::from_str(psbt)?;
//!
//! let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
//! let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
//!
//! Ok(())
//! }
@@ -216,6 +222,9 @@ extern crate bdk_macros;
#[cfg(feature = "compact_filters")]
extern crate lazy_static;
#[cfg(feature = "rpc")]
pub extern crate bitcoincore_rpc;
#[cfg(feature = "electrum")]
pub extern crate electrum_client;
@@ -227,16 +236,10 @@ pub extern crate sled;
#[allow(unused_imports)]
#[cfg(test)]
#[macro_use]
extern crate testutils;
#[allow(unused_imports)]
#[cfg(test)]
#[macro_use]
extern crate testutils_macros;
#[allow(unused_imports)]
#[cfg(test)]
#[macro_use]
extern crate serial_test;
pub extern crate serial_test;
#[macro_use]
pub(crate) mod error;
@@ -256,6 +259,7 @@ 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;
pub use wallet::Wallet;
@@ -263,3 +267,10 @@ pub use wallet::Wallet;
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown")
}
// We should consider putting this under a feature flag but we need the macro in doctets so we need
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
//
// Stuff in here is too rough to document atm
#[doc(hidden)]
pub mod testutils;

View File

@@ -9,14 +9,15 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut;
pub trait PsbtUtils {
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
}
impl PsbtUtils for PSBT {
impl PsbtUtils for Psbt {
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
let tx = &self.global.unsigned_tx;
@@ -37,3 +38,85 @@ impl PsbtUtils for PSBT {
}
}
}
#[cfg(test)]
mod test {
use crate::bitcoin::TxIn;
use crate::psbt::Psbt;
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
use crate::wallet::AddressIndex;
use crate::SignOptions;
use std::str::FromStr;
// from bip 174
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_legacy() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[0].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_segwit() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[1].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_tx_input() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.global.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
fn test_psbt_sign_with_finalized() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
// add a finalized input
psbt.inputs.push(psbt_bip.inputs[0].clone());
psbt.global
.unsigned_tx
.input
.push(psbt_bip.global.unsigned_tx.input[0].clone());
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
}
}

View File

@@ -0,0 +1,879 @@
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};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr;
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use std::collections::HashMap;
use std::env;
use std::ops::Deref;
use std::path::PathBuf;
use std::time::Duration;
pub struct TestClient {
client: RpcClient,
electrum: ElectrumClient,
}
impl TestClient {
pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
let client = RpcClient::new(
format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
get_auth(),
)
.unwrap();
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
TestClient { client, electrum }
}
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
// wait for electrs to index the tx
exponential_backoff_poll(|| {
trace!("wait_for_tx {}", txid);
self.electrum
.script_get_history(monitor_script)
.unwrap()
.iter()
.position(|entry| entry.tx_hash == txid)
});
}
fn wait_for_block(&mut self, min_height: usize) {
self.electrum.block_headers_subscribe().unwrap();
loop {
let header = exponential_backoff_poll(|| {
self.electrum.ping().unwrap();
self.electrum.block_headers_pop().unwrap()
});
if header.height >= min_height {
break;
}
}
}
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
assert!(
!meta_tx.output.is_empty(),
"can't create a transaction with no outputs"
);
let mut map = HashMap::new();
let mut required_balance = 0;
for out in &meta_tx.output {
required_balance += out.value;
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
}
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
}
// FIXME: core can't create a tx with two outputs to the same address
let tx = self
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
.unwrap();
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
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;
}
}
let tx = self
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
.unwrap();
// broadcast through electrum so that it caches the tx immediately
let txid = self
.electrum
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
.unwrap();
if let Some(num) = meta_tx.min_confirmations {
self.generate(num, None);
}
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
.unwrap()
.script_pubkey();
self.wait_for_tx(txid, &monitor_script);
debug!("Sent tx: {}", txid);
txid
}
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
let tx = self.get_raw_transaction_info(txid, None).unwrap();
assert!(
tx.confirmations.is_none(),
"Can't bump tx {} because it's already confirmed",
txid
);
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
let monitor_script =
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
self.wait_for_tx(new_txid, &monitor_script);
debug!("Bumped {}, new txid {}", txid, new_txid);
new_txid
}
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
.unwrap();
trace!("getblocktemplate: {:#?}", block_template);
let header = BlockHeader {
version: block_template["version"].as_i64().unwrap() as i32,
prev_blockhash: BlockHash::from_hex(
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
};
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();
// burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
witness: vec![witness_reserved_value],
}],
output: vec![],
};
let mut txdata = vec![coinbase_tx.clone()];
txdata.extend_from_slice(&txs);
let mut block = Block { header, txdata };
let witness_root = block.witness_root();
let witness_commitment =
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
// now update and replace the coinbase tx
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
coinbase_tx.output.push(TxOut {
value: 0,
script_pubkey: coinbase_witness_commitment_script.into(),
});
block.txdata[0] = coinbase_tx;
// set merkle root
let merkle_root = block.merkle_root();
block.header.merkle_root = merkle_root;
assert!(block.check_merkle_root());
assert!(block.check_witness_commitment());
// now do PoW :)
let target = block.header.target();
while block.header.validate_pow(&target).is_err() {
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
}
let block_hex: String = serialize(&block).to_hex();
debug!("generated block hex: {}", block_hex);
self.electrum.block_headers_subscribe().unwrap();
let submit_result: serde_json::Value =
self.call("submitblock", &[block_hex.into()]).unwrap();
debug!("submitblock: {:?}", submit_result);
assert!(
submit_result.is_null(),
"submitblock error: {:?}",
submit_result.as_str()
);
self.wait_for_block(height as usize);
block.header.block_hash().to_hex()
}
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
let best_hash = hashes.last().unwrap();
let height = self.get_block_info(best_hash).unwrap().height;
self.wait_for_block(height);
debug!("Generated blocks to new height {}", height);
}
pub fn invalidate(&mut self, num_blocks: u64) {
self.electrum.block_headers_subscribe().unwrap();
let best_hash = self.get_best_block_hash().unwrap();
let initial_height = self.get_block_info(&best_hash).unwrap().height;
let mut to_invalidate = best_hash;
for i in 1..=num_blocks {
trace!(
"Invalidating block {}/{} ({})",
i,
num_blocks,
to_invalidate
);
self.invalidate_block(&to_invalidate).unwrap();
to_invalidate = self.get_best_block_hash().unwrap();
}
self.wait_for_block(initial_height - num_blocks as usize);
debug!(
"Invalidated {} blocks to new height of {}",
num_blocks,
initial_height - num_blocks as usize
);
}
pub fn reorg(&mut self, num_blocks: u64) {
self.invalidate(num_blocks);
self.generate(num_blocks, None);
}
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
Address::from_str(
&self
.get_new_address(None, address_type)
.unwrap()
.to_string(),
)
.unwrap()
}
}
pub fn get_electrum_url() -> String {
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
}
impl Deref for TestClient {
type Target = RpcClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl Default for TestClient {
fn default() -> Self {
let rpc_host_and_port =
env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
Self::new(rpc_host_and_port, wallet)
}
}
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
where
F: FnMut() -> Option<T>,
{
let mut delay = Duration::from_millis(64);
loop {
match poll() {
Some(data) => break data,
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
None => {}
}
std::thread::sleep(delay);
}
}
// TODO: we currently only support env vars, we could also parse a toml file
fn get_auth() -> Auth {
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
env::var("BDK_RPC_USER").unwrap(),
env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(PathBuf::from(
env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
}
}
/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
/// the setup required to run the tests yourself.
#[macro_export]
macro_rules! bdk_blockchain_tests {
(
fn test_instance() -> $blockchain:ty $block:block) => {
#[cfg(test)]
mod bdk_blockchain_tests {
use $crate::bitcoin::Network;
use $crate::testutils::blockchain_tests::TestClient;
use $crate::blockchain::noop_progress;
use $crate::database::MemoryDatabase;
use $crate::types::KeychainKind;
use $crate::{Wallet, FeeRate};
use $crate::testutils;
use $crate::serial_test::serial;
use super::*;
fn get_blockchain() -> $blockchain {
$block
}
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
}
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
let _ = env_logger::try_init();
let descriptors = testutils! {
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
};
let test_client = TestClient::default();
let wallet = get_wallet_from_descriptors(&descriptors);
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
#[cfg(feature = "test-rpc")]
wallet.sync(noop_progress(), None).unwrap();
(wallet, descriptors, test_client)
}
#[test]
#[serial]
fn test_sync_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
println!("{:?}", tx);
let txid = test_client.receive(tx);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time");
}
#[test]
#[serial]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
}
#[test]
#[serial]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
}
#[test]
#[serial]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 105_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
}
#[test]
#[serial]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
}
#[test]
#[serial]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
}
#[test]
#[serial]
fn test_sync_receive_rbf_replaced() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, new_txid, "incorrect txid after bump");
assert_eq!(list_tx_item.received, 50_000, "incorrect received after bump");
assert_eq!(list_tx_item.sent, 0, "incorrect sent after bump");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect height after bump");
}
// FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
// doesn't work for some reason.
#[cfg(not(feature = "esplora"))]
#[test]
#[serial]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert!(list_tx_item.confirmation_time.is_some(), "incorrect confirmation_time");
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time after invalidate");
}
#[test]
#[serial]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
}
#[test]
#[serial]
fn test_update_confirmation_time_after_generate() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let details = tx_map.get(&received_txid).unwrap();
assert!(details.confirmation_time.is_none());
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let details = tx_map.get(&received_txid).unwrap();
assert!(details.confirmation_time.is_some());
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000, "incorrect received from receiver");
assert_eq!(received.sent, 0, "incorrect sent from receiver");
let sent = tx_map.get(&sent_txid).unwrap();
assert_eq!(sent.received, details.received, "incorrect received from sender");
assert_eq!(sent.sent, details.sent, "incorrect sent from sender");
assert_eq!(sent.fee.unwrap_or(0), details.fee.unwrap_or(0), "incorrect fees from sender");
}
#[test]
#[serial]
fn test_sync_long_change_chain() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut total_sent = 0;
for _ in 0..5 {
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 5_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
total_sent += 5_000 + details.fee.unwrap_or(0);
}
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
}
#[test]
#[serial]
fn test_sync_bump_fee_basic() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
}
#[test]
#[serial]
fn test_sync_bump_fee_remove_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
assert_eq!(new_details.received, 0, "incorrect received after change removal");
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input_no_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
println!("{:#?}", new_details);
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
assert_eq!(new_details.received, 0, "incorrect received after add input");
}
#[test]
#[serial]
fn test_sync_receive_coinbase() {
let (wallet, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
test_client.generate(1, Some(wallet_addr));
#[cfg(feature = "rpc")]
{
// rpc consider coinbase only when mature (100 blocks)
let node_addr = test_client.get_node_address(None);
test_client.generate(100, Some(node_addr));
}
wallet.sync(noop_progress(), None).unwrap();
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
}
}
}
}

230
src/testutils/mod.rs Normal file
View File

@@ -0,0 +1,230 @@
// 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.
#![allow(missing_docs)]
#[cfg(feature = "test-blockchains")]
pub mod blockchain_tests;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, PublicKey};
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
#[derive(Clone, Debug)]
pub struct TestIncomingOutput {
pub value: u64,
pub to_address: String,
}
impl TestIncomingOutput {
pub fn new(value: u64, to_address: Address) -> Self {
Self {
value,
to_address: to_address.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct TestIncomingTx {
pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>,
pub locktime: Option<i64>,
pub replaceable: Option<bool>,
}
impl TestIncomingTx {
pub fn new(
output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>,
locktime: Option<i64>,
replaceable: Option<bool>,
) -> Self {
Self {
output,
min_confirmations,
locktime,
replaceable,
}
}
pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output);
}
}
#[doc(hidden)]
pub trait TranslateDescriptor {
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey>;
}
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey> {
let translate = |key: &DescriptorPublicKey| -> PublicKey {
match key {
DescriptorPublicKey::XPub(xpub) => {
xpub.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("hardened derivation steps")
.public_key
}
DescriptorPublicKey::SinglePub(key) => key.key,
}
};
self.derive(index)
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
});
( @internal $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
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.derive_translated(&secp, $child).address(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) });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
let locktime = None::<i64>$(.or(Some($locktime)))?;
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
});
( @literal $key:expr ) => ({
let key = $key.to_string();
(key, None::<String>, None::<String>)
});
( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({
use rand::Rng;
let mut seed = [0u8; 32];
rand::thread_rng().fill(&mut seed[..]);
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
bitcoin::Network::Testnet,
&seed,
);
let external_path = None::<String>$(.or(Some($external_path.to_string())))?;
let internal_path = None::<String>$(.or(Some($internal_path.to_string())))?;
(key.unwrap().to_string(), external_path, internal_path)
});
( @generate_wif ) => ({
use rand::Rng;
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
rand::thread_rng().fill(&mut key[..]);
(bitcoin::PrivateKey {
compressed: true,
network: bitcoin::Network::Testnet,
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
}.to_string(), None::<String>, None::<String>)
});
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
let mut map = std::collections::HashMap::new();
$(
let alias: &str = $alias;
map.insert(alias, testutils!( $($key_type)* ));
)+
map
});
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use miniscript::descriptor::Descriptor;
use miniscript::TranslatePk;
#[allow(unused_assignments, unused_mut)]
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = 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 external = external.to_string();
let internal = None::<String>$(.or({
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
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())
}))?;
(external, internal)
})
}

View File

@@ -80,7 +80,7 @@ impl std::default::Default for FeeRate {
/// An unspent output owned by a [`Wallet`].
///
/// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalUtxo {
/// Reference to a transaction output
pub outpoint: OutPoint,
@@ -139,7 +139,7 @@ impl Utxo {
}
if let Some(txout) = &psbt_input.witness_utxo {
return &txout;
return txout;
}
unreachable!("Foreign UTXOs will always have one of these set")
@@ -155,16 +155,35 @@ pub struct TransactionDetails {
pub transaction: Option<Transaction>,
/// Transaction id
pub txid: Txid,
/// Timestamp
pub timestamp: u64,
/// Received value (sats)
pub received: u64,
/// Sent value (sats)
pub sent: u64,
/// Fee value (sats)
pub fees: u64,
/// Confirmed in block height, `None` means unconfirmed
pub height: Option<u32>,
/// Fee value (sats) if available
pub fee: Option<u64>,
/// If the transaction is confirmed, contains height and timestamp of the block containing the
/// transaction, unconfirmed transaction contains `None`.
pub confirmation_time: Option<ConfirmationTime>,
}
/// Block height and timestamp of the block containing the confirmed transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct ConfirmationTime {
/// confirmation block height
pub height: u32,
/// confirmation block timestamp
pub timestamp: u64,
}
impl ConfirmationTime {
/// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some`
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
match (height, timestamp) {
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
_ => None,
}
}
}
#[cfg(test)]

View File

@@ -146,7 +146,7 @@ mod test {
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
wallet.add_address_validator(Arc::new(TestValidator));
let addr = testutils!(@external descriptors, 10);
let addr = crate::testutils!(@external descriptors, 10);
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
builder.finish().unwrap();

View File

@@ -46,17 +46,25 @@
//! let mut selected_amount = 0;
//! let mut additional_weight = 0;
//! let all_utxos_selected = required_utxos
//! .into_iter().chain(optional_utxos)
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| {
//! **selected_amount += weighted_utxo.utxo.txout().value;
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
//! Some(weighted_utxo.utxo)
//! })
//! .into_iter()
//! .chain(optional_utxos)
//! .scan(
//! (&mut selected_amount, &mut additional_weight),
//! |(selected_amount, additional_weight), weighted_utxo| {
//! **selected_amount += weighted_utxo.utxo.txout().value;
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
//! Some(weighted_utxo.utxo)
//! },
//! )
//! .collect::<Vec<_>>();
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
//! let amount_needed_with_fees = (fee_amount + additional_fees).ceil() as u64 + amount_needed;
//! if amount_needed_with_fees > selected_amount {
//! return Err(bdk::Error::InsufficientFunds{ needed: amount_needed_with_fees, available: selected_amount });
//! let amount_needed_with_fees =
//! (fee_amount + additional_fees).ceil() as u64 + amount_needed;
//! if amount_needed_with_fees > selected_amount {
//! return Err(bdk::Error::InsufficientFunds {
//! needed: amount_needed_with_fees,
//! available: selected_amount,
//! });
//! }
//!
//! Ok(CoinSelectionResult {
@@ -72,8 +80,7 @@
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
//! let (psbt, details) = {
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
//! builder
//! .add_recipient(to_address.script_pubkey(), 50_000);
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
//! builder.finish()?
//! };
//!
@@ -255,8 +262,8 @@ impl OutputGroup {
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
OutputGroup {
weighted_utxo,
effective_value,
fee,
effective_value,
}
}
}
@@ -654,7 +661,7 @@ mod test {
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0);
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
}
#[test]
@@ -675,7 +682,7 @@ mod test {
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0);
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
}
#[test]
@@ -696,7 +703,7 @@ mod test {
assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 118.0);
assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
}
#[test]
@@ -756,7 +763,7 @@ mod test {
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 254.0);
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
}
#[test]
@@ -777,7 +784,7 @@ mod test {
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0);
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
}
#[test]
@@ -798,7 +805,7 @@ mod test {
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300010);
assert_eq!(result.fee_amount, 254.0);
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
}
#[test]
@@ -968,7 +975,7 @@ mod test {
cost_of_change,
)
.unwrap();
assert_eq!(result.fee_amount, 186.0);
assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
assert_eq!(result.selected_amount(), 100_000);
}
@@ -1031,9 +1038,8 @@ mod test {
);
assert!(result.selected_amount() > target_amount);
assert_eq!(
result.fee_amount,
50.0 + result.selected.len() as f32 * 68.0
assert!(
(result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
);
}
}

View File

@@ -128,7 +128,7 @@ impl WalletExport {
Ok(txs) => {
let mut heights = txs
.into_iter()
.map(|tx| tx.height.unwrap_or(0))
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
.collect::<Vec<_>>();
heights.sort_unstable();
@@ -212,6 +212,7 @@ mod test {
use crate::database::{memory::MemoryDatabase, BatchOperations};
use crate::types::TransactionDetails;
use crate::wallet::Wallet;
use crate::ConfirmationTime;
fn get_test_db() -> MemoryDatabase {
let mut db = MemoryDatabase::new();
@@ -221,11 +222,14 @@ mod test {
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
)
.unwrap(),
timestamp: 12345678,
received: 100_000,
sent: 0,
fees: 500,
height: Some(5000),
fee: Some(500),
confirmation_time: Some(ConfirmationTime {
timestamp: 12345678,
height: 5000,
}),
})
.unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,12 @@ pub enum SignerError {
MissingWitnessScript,
/// The fingerprint and derivation path are missing from the psbt input
MissingHdKeypath,
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
/// explicitly allowed them
///
/// To enable signing transactions with non-standard sighashes set
/// [`SignOptions::allow_all_sighashes`] to `true`.
NonStandardSighash,
}
impl fmt::Display for SignerError {
@@ -206,11 +212,17 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
return Err(SignerError::InputIndexOutOfRange);
}
if psbt.inputs[input_index].final_script_sig.is_some()
|| psbt.inputs[input_index].final_script_witness.is_some()
{
return Ok(());
}
let (public_key, full_path) = match psbt.inputs[input_index]
.bip32_derivation
.iter()
.filter_map(|(pk, &(fingerprint, ref path))| {
if self.matches(&(fingerprint, path.clone()), &secp).is_some() {
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
Some((pk, path))
} else {
None
@@ -228,12 +240,12 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
&full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
[origin_path.len()..],
);
self.xkey.derive_priv(&secp, &deriv_path).unwrap()
self.xkey.derive_priv(secp, &deriv_path).unwrap()
}
None => self.xkey.derive_priv(&secp, &full_path).unwrap(),
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
};
if &derived_key.private_key.public_key(&secp) != public_key {
if &derived_key.private_key.public_key(secp) != public_key {
Err(SignerError::InvalidKey)
} else {
derived_key.private_key.sign(psbt, Some(input_index), secp)
@@ -245,7 +257,7 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
}
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(&secp))
SignerId::from(self.root_fingerprint(secp))
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
@@ -261,11 +273,17 @@ impl Signer for PrivateKey {
secp: &SecpCtx,
) -> Result<(), SignerError> {
let input_index = input_index.unwrap();
if input_index >= psbt.inputs.len() {
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let pubkey = self.public_key(&secp);
if psbt.inputs[input_index].final_script_sig.is_some()
|| psbt.inputs[input_index].final_script_witness.is_some()
{
return Ok(());
}
let pubkey = self.public_key(secp);
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(());
}
@@ -427,6 +445,50 @@ impl SignersContainer {
}
}
/// Options for a software signer
///
/// Adjust the behavior of our software signers and the way a transaction is finalized
#[derive(Debug, Clone)]
pub struct SignOptions {
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
/// provided
///
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into
/// paying a fee larger than expected.
///
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
/// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
/// should correctly produce a signature, at the expense of an increased trust in the creator
/// of the PSBT.
///
/// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
pub trust_witness_utxo: bool,
/// Whether the wallet should assume a specific height has been reached when trying to finalize
/// a transaction
///
/// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
/// timelock height has already been reached. This option allows overriding the "current height" to let the
/// wallet use timelocks in the future to spend a coin.
pub assume_height: Option<u32>,
/// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
/// what its value is
///
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
pub allow_all_sighashes: bool,
}
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
trust_witness_utxo: false,
assume_height: None,
allow_all_sighashes: false,
}
}
}
pub(crate) trait ComputeSighash {
fn sighash(
psbt: &psbt::PartiallySignedTransaction,
@@ -439,7 +501,7 @@ impl ComputeSighash for Legacy {
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() {
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
@@ -487,32 +549,49 @@ impl ComputeSighash for Segwitv0 {
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() {
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.global.unsigned_tx.input[input_index];
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
let witness_utxo = psbt_input
.witness_utxo
.as_ref()
.ok_or(SignerError::MissingNonWitnessUtxo)?;
let value = witness_utxo.value;
// Always try first with the non-witness utxo
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
// Check the provided prev-tx
if prev_tx.txid() != tx_input.previous_output.txid {
return Err(SignerError::InvalidNonWitnessUtxo);
}
// The output should be present, if it's missing the `non_witness_utxo` is invalid
prev_tx
.output
.get(tx_input.previous_output.vout as usize)
.ok_or(SignerError::InvalidNonWitnessUtxo)?
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
// before we get to this point
witness_utxo
} else {
// Nothing has been provided
return Err(SignerError::MissingNonWitnessUtxo);
};
let value = utxo.value;
let script = match psbt_input.witness_script {
Some(ref witness_script) => witness_script.clone(),
None => {
if witness_utxo.script_pubkey.is_v0_p2wpkh() {
p2wpkh_script_code(&witness_utxo.script_pubkey)
if utxo.script_pubkey.is_v0_p2wpkh() {
p2wpkh_script_code(&utxo.script_pubkey)
} else if psbt_input
.redeem_script
.as_ref()
.map(Script::is_v0_p2wpkh)
.unwrap_or(false)
{
p2wpkh_script_code(&psbt_input.redeem_script.as_ref().unwrap())
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
} else {
return Err(SignerError::MissingWitnessScript);
}

View File

@@ -41,7 +41,7 @@ use std::collections::HashSet;
use std::default::Default;
use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as PSBT};
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
use miniscript::descriptor::DescriptorTrait;
@@ -87,9 +87,9 @@ impl TxBuilderContext for BumpFee {}
/// let (psbt1, details) = {
/// let mut builder = wallet.build_tx();
/// builder
/// .ordering(TxOrdering::Untouched)
/// .add_recipient(addr1.script_pubkey(), 50_000)
/// .add_recipient(addr2.script_pubkey(), 50_000);
/// .ordering(TxOrdering::Untouched)
/// .add_recipient(addr1.script_pubkey(), 50_000)
/// .add_recipient(addr2.script_pubkey(), 50_000);
/// builder.finish()?
/// };
///
@@ -103,7 +103,10 @@ impl TxBuilderContext for BumpFee {}
/// builder.finish()?
/// };
///
/// assert_eq!(psbt1.global.unsigned_tx.output[..2], psbt2.global.unsigned_tx.output[..2]);
/// assert_eq!(
/// psbt1.global.unsigned_tx.output[..2],
/// psbt2.global.unsigned_tx.output[..2]
/// );
/// # Ok::<(), bdk::Error>(())
/// ```
///
@@ -119,9 +122,6 @@ impl TxBuilderContext for BumpFee {}
#[derive(Debug)]
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<B, D>,
// params and coin_selection are Options not becasue they are optionally set (they are always
// there) but because `.finish()` uses `Option::take` to get an owned value from a &mut self.
// They are only `None` after `.finish()` is called.
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
@@ -146,7 +146,7 @@ pub(crate) struct TxParams {
pub(crate) rbf: Option<RbfValue>,
pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) force_non_witness_utxo: bool,
pub(crate) only_witness_utxo: bool,
pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>,
@@ -249,7 +249,8 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// let mut path = BTreeMap::new();
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
///
/// let builder = wallet.build_tx()
/// let builder = wallet
/// .build_tx()
/// .add_recipient(to_address.script_pubkey(), 50_000)
/// .policy_path(path, KeychainKind::External);
///
@@ -336,10 +337,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
///
/// Note if you set [`force_non_witness_utxo`] any `psbt_input` you pass to this method must
/// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
///
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo
/// [`only_witness_utxo`]: Self::only_witness_utxo
/// [`finish`]: Self::finish
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
pub fn add_foreign_utxo(
@@ -464,12 +465,13 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
self
}
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit
/// descriptors.
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
/// SegWit descriptors.
///
/// This is useful for signers which always require it, like Trezor hardware wallets.
pub fn force_non_witness_utxo(&mut self) -> &mut Self {
self.params.force_non_witness_utxo = true;
/// This reduces the size of the PSBT, but some signers might reject them due to the lack of
/// the `non_witness_utxo`.
pub fn only_witness_utxo(&mut self) -> &mut Self {
self.params.only_witness_utxo = true;
self
}
@@ -520,7 +522,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<(PSBT, TransactionDetails), Error> {
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
self.wallet.create_tx(self.coin_selection, self.params)
}

View File

@@ -201,31 +201,31 @@ mod test {
#[test]
fn test_check_nsequence_rbf_msb_set() {
let result = check_nsequence_rbf(0x80000000, 5000);
assert_eq!(result, false);
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_lt_csv() {
let result = check_nsequence_rbf(4000, 5000);
assert_eq!(result, false);
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_different_unit() {
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
assert_eq!(result, false);
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_mask() {
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
assert_eq!(result, true);
assert!(result);
}
#[test]
fn test_check_nsequence_rbf_same_unit_blocks() {
let result = check_nsequence_rbf(10_000, 5000);
assert_eq!(result, true);
assert!(result);
}
#[test]
@@ -234,25 +234,25 @@ mod test {
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
);
assert_eq!(result, true);
assert!(result);
}
#[test]
fn test_check_nlocktime_lt_cltv() {
let result = check_nlocktime(4000, 5000);
assert_eq!(result, false);
assert!(!result);
}
#[test]
fn test_check_nlocktime_different_unit() {
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
assert_eq!(result, false);
assert!(!result);
}
#[test]
fn test_check_nlocktime_same_unit_blocks() {
let result = check_nlocktime(10_000, 5000);
assert_eq!(result, true);
assert!(result);
}
#[test]
@@ -261,6 +261,6 @@ mod test {
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
BLOCKS_TIMELOCK_THRESHOLD + 5000,
);
assert_eq!(result, true);
assert!(result);
}
}

View File

@@ -1,25 +0,0 @@
[package]
name = "bdk-testutils-macros"
version = "0.5.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils-macros"
description = "Supporting testing macros for `bdk`"
keywords = ["bdk"]
license = "MIT OR Apache-2.0"
[lib]
proc-macro = true
name = "testutils_macros"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
syn = { version = "1.0", features = ["parsing", "full"] }
proc-macro2 = "1.0"
quote = "1.0"
[features]
debug = ["syn/extra-traits"]

View File

@@ -1,553 +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.
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
use syn::spanned::Spanned;
use syn::{parse, parse2, Ident, ReturnType};
#[proc_macro_attribute]
pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream {
let root_ident = if !attr.is_empty() {
match parse::<syn::ExprPath>(attr) {
Ok(parsed) => parsed,
Err(e) => {
let error_string = e.to_string();
return (quote! {
compile_error!("Invalid crate path: {:?}", #error_string)
})
.into();
}
}
} else {
parse2::<syn::ExprPath>(quote! { bdk }).unwrap()
};
match parse::<syn::ItemFn>(item) {
Err(_) => (quote! {
compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s")
})
.into(),
Ok(parsed) => {
let parsed_sig_ident = parsed.sig.ident.clone();
let mod_name = Ident::new(
&format!("generated_tests_{}", parsed_sig_ident.to_string()),
parsed.span(),
);
let return_type = match parsed.sig.output {
ReturnType::Type(_, ref t) => t.clone(),
ReturnType::Default => {
return (quote! {
compile_error!("The tagged function must return a type that impl `Blockchain`")
}).into();
}
};
let output = quote! {
#parsed
mod #mod_name {
use bitcoin::Network;
use miniscript::Descriptor;
use testutils::{TestClient, serial};
use #root_ident::blockchain::{Blockchain, noop_progress};
use #root_ident::descriptor::ExtendedDescriptor;
use #root_ident::database::MemoryDatabase;
use #root_ident::types::KeychainKind;
use #root_ident::{Wallet, TxBuilder, FeeRate};
use #root_ident::wallet::AddressIndex::New;
use super::*;
fn get_blockchain() -> #return_type {
#parsed_sig_ident()
}
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<#return_type, MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
}
fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option<String>), TestClient) {
let descriptors = testutils! {
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
};
let test_client = TestClient::new();
let wallet = get_wallet_from_descriptors(&descriptors);
(wallet, descriptors, test_client)
}
#[test]
#[serial]
fn test_sync_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
println!("{:?}", tx);
let txid = test_client.receive(tx);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 105_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
}
#[test]
#[serial]
fn test_sync_receive_rbf_replaced() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, new_txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert!(list_tx_item.height.is_some());
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000);
assert_eq!(received.sent, 0);
let sent = tx_map.get(&sent_txid).unwrap();
assert_eq!(sent.received, details.received);
assert_eq!(sent.sent, details.sent);
assert_eq!(sent.fees, details.fees);
}
#[test]
#[serial]
fn test_sync_long_change_chain() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut total_sent = 0;
for _ in 0..5 {
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 5_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
total_sent += 5_000 + details.fees;
}
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
}
#[test]
#[serial]
fn test_sync_bump_fee() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_remove_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input_no_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
let (new_psbt, new_details) = builder.finish().unwrap();
println!("{:#?}", new_details);
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
}
#[test]
#[serial]
fn test_sync_receive_coinbase() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address(New).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.generate(1, Some(wallet_addr));
wallet.sync(noop_progress(), None).unwrap();
assert!(wallet.get_balance().unwrap() > 0);
}
}
};
output.into()
}
}
}

View File

@@ -1,2 +0,0 @@
target/
Cargo.lock

View File

@@ -1,26 +0,0 @@
[package]
name = "bdk-testutils"
version = "0.4.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils"
description = "Supporting testing utilities for `bdk`"
keywords = ["bdk"]
license = "MIT OR Apache-2.0"
[lib]
name = "testutils"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serial_test = "0.4"
bitcoin = "0.26"
bitcoincore-rpc = "0.13"
miniscript = "5.1"
electrum-client = "0.6.0"

View File

@@ -1,564 +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.
#[macro_use]
extern crate serde_json;
pub use serial_test::serial;
use std::collections::HashMap;
use std::env;
use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::sha256d;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
// TODO: we currently only support env vars, we could also parse a toml file
fn get_auth() -> Auth {
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
env::var("BDK_RPC_USER").unwrap(),
env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(PathBuf::from(
env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
}
}
pub fn get_electrum_url() -> String {
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
}
pub struct TestClient {
client: RpcClient,
electrum: ElectrumClient,
}
#[derive(Clone, Debug)]
pub struct TestIncomingOutput {
pub value: u64,
pub to_address: String,
}
impl TestIncomingOutput {
pub fn new(value: u64, to_address: Address) -> Self {
Self {
value,
to_address: to_address.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct TestIncomingTx {
pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>,
pub locktime: Option<i64>,
pub replaceable: Option<bool>,
}
impl TestIncomingTx {
pub fn new(
output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>,
locktime: Option<i64>,
replaceable: Option<bool>,
) -> Self {
Self {
output,
min_confirmations,
locktime,
replaceable,
}
}
pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output);
}
}
#[doc(hidden)]
pub trait TranslateDescriptor {
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey>;
}
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey> {
let translate = |key: &DescriptorPublicKey| -> PublicKey {
match key {
DescriptorPublicKey::XPub(xpub) => {
xpub.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("hardened derivation steps")
.public_key
}
DescriptorPublicKey::SinglePub(key) => key.key,
}
};
self.derive(index)
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
}
}
#[macro_export]
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::TranslateDescriptor;
let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
});
( @internal $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::TranslateDescriptor;
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.derive_translated(&secp, $child).address(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) });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
let mut outs = Vec::new();
$( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
let mut locktime = None::<i64>;
$( locktime = Some($locktime); )*
let mut min_confirmations = None::<u64>;
$( min_confirmations = Some($confirmations); )*
let mut replaceable = None::<bool>;
$( replaceable = Some($replaceable); )*
testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
});
( @literal $key:expr ) => ({
let key = $key.to_string();
(key, None::<String>, None::<String>)
});
( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({
use rand::Rng;
let mut seed = [0u8; 32];
rand::thread_rng().fill(&mut seed[..]);
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
bitcoin::Network::Testnet,
&seed,
);
let mut external_path = None::<String>;
$( external_path = Some($external_path.to_string()); )*
let mut internal_path = None::<String>;
$( internal_path = Some($internal_path.to_string()); )*
(key.unwrap().to_string(), external_path, internal_path)
});
( @generate_wif ) => ({
use rand::Rng;
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
rand::thread_rng().fill(&mut key[..]);
(bitcoin::PrivateKey {
compressed: true,
network: bitcoin::Network::Testnet,
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
}.to_string(), None::<String>, None::<String>)
});
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
let mut map = std::collections::HashMap::new();
$(
let alias: &str = $alias;
map.insert(alias, testutils!( $($key_type)* ));
)+
map
});
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use std::convert::TryInto;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use miniscript::TranslatePk;
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = 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 external = external.to_string();
let mut internal = None::<String>;
$(
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
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()
}
});
internal = Some(string_internal.to_string());
)*
(external, internal)
})
}
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
where
F: FnMut() -> Option<T>,
{
let mut delay = Duration::from_millis(64);
loop {
match poll() {
Some(data) => break data,
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
None => {}
}
std::thread::sleep(delay);
}
}
impl TestClient {
pub fn new() -> Self {
let url = env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
let client =
RpcClient::new(format!("http://{}/wallet/{}", url, wallet), get_auth()).unwrap();
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
TestClient { client, electrum }
}
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
// wait for electrs to index the tx
exponential_backoff_poll(|| {
trace!("wait_for_tx {}", txid);
self.electrum
.script_get_history(monitor_script)
.unwrap()
.iter()
.position(|entry| entry.tx_hash == txid)
});
}
fn wait_for_block(&mut self, min_height: usize) {
self.electrum.block_headers_subscribe().unwrap();
loop {
let header = exponential_backoff_poll(|| {
self.electrum.ping().unwrap();
self.electrum.block_headers_pop().unwrap()
});
if header.height >= min_height {
break;
}
}
}
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
assert!(
!meta_tx.output.is_empty(),
"can't create a transaction with no outputs"
);
let mut map = HashMap::new();
let mut required_balance = 0;
for out in &meta_tx.output {
required_balance += out.value;
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
}
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
}
// FIXME: core can't create a tx with two outputs to the same address
let tx = self
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
.unwrap();
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
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;
}
}
let tx = self
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
.unwrap();
// broadcast through electrum so that it caches the tx immediately
let txid = self
.electrum
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
.unwrap();
if let Some(num) = meta_tx.min_confirmations {
self.generate(num, None);
}
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
.unwrap()
.script_pubkey();
self.wait_for_tx(txid, &monitor_script);
debug!("Sent tx: {}", txid);
txid
}
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
let tx = self.get_raw_transaction_info(txid, None).unwrap();
assert!(
tx.confirmations.is_none(),
"Can't bump tx {} because it's already confirmed",
txid
);
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
let monitor_script =
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
self.wait_for_tx(new_txid, &monitor_script);
debug!("Bumped {}, new txid {}", txid, new_txid);
new_txid
}
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
.unwrap();
trace!("getblocktemplate: {:#?}", block_template);
let header = BlockHeader {
version: block_template["version"].as_i64().unwrap() as i32,
prev_blockhash: BlockHash::from_hex(
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
};
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();
// burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
witness: vec![witness_reserved_value],
}],
output: vec![],
};
let mut txdata = vec![coinbase_tx.clone()];
txdata.extend_from_slice(&txs);
let mut block = Block { header, txdata };
let witness_root = block.witness_root();
let witness_commitment =
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
// now update and replace the coinbase tx
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
coinbase_tx.output.push(TxOut {
value: 0,
script_pubkey: coinbase_witness_commitment_script.into(),
});
block.txdata[0] = coinbase_tx;
// set merkle root
let merkle_root = block.merkle_root();
block.header.merkle_root = merkle_root;
assert!(block.check_merkle_root());
assert!(block.check_witness_commitment());
// now do PoW :)
let target = block.header.target();
while block.header.validate_pow(&target).is_err() {
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
}
let block_hex: String = serialize(&block).to_hex();
debug!("generated block hex: {}", block_hex);
self.electrum.block_headers_subscribe().unwrap();
let submit_result: serde_json::Value =
self.call("submitblock", &[block_hex.into()]).unwrap();
debug!("submitblock: {:?}", submit_result);
assert!(
submit_result.is_null(),
"submitblock error: {:?}",
submit_result.as_str()
);
self.wait_for_block(height as usize);
block.header.block_hash().to_hex()
}
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
let best_hash = hashes.last().unwrap();
let height = self.get_block_info(best_hash).unwrap().height;
self.wait_for_block(height);
debug!("Generated blocks to new height {}", height);
}
pub fn invalidate(&mut self, num_blocks: u64) {
self.electrum.block_headers_subscribe().unwrap();
let best_hash = self.get_best_block_hash().unwrap();
let initial_height = self.get_block_info(&best_hash).unwrap().height;
let mut to_invalidate = best_hash;
for i in 1..=num_blocks {
trace!(
"Invalidating block {}/{} ({})",
i,
num_blocks,
to_invalidate
);
self.invalidate_block(&to_invalidate).unwrap();
to_invalidate = self.get_best_block_hash().unwrap();
}
self.wait_for_block(initial_height - num_blocks as usize);
debug!(
"Invalidated {} blocks to new height of {}",
num_blocks,
initial_height - num_blocks as usize
);
}
pub fn reorg(&mut self, num_blocks: u64) {
self.invalidate(num_blocks);
self.generate(num_blocks, None);
}
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
Address::from_str(
&self
.get_new_address(None, address_type)
.unwrap()
.to_string(),
)
.unwrap()
}
}
impl Deref for TestClient {
type Target = RpcClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}