Compare commits
189 Commits
release/0.
...
release/0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32db387104 | ||
|
|
4f7d567f47 | ||
|
|
5c7b2af0bc | ||
|
|
a7589c5baa | ||
|
|
0010ecd94a | ||
|
|
690411722e | ||
|
|
f2e12d0ccd | ||
|
|
7001b14b4c | ||
|
|
13cf72ffa7 | ||
|
|
d7163c3a97 | ||
|
|
cf13c80991 | ||
|
|
7c57965999 | ||
|
|
3d69f1c291 | ||
|
|
3451d1c12e | ||
|
|
4fbd8520e6 | ||
|
|
bfd7b2f65d | ||
|
|
061f15af00 | ||
|
|
2bff4e5e56 | ||
|
|
138acc3b7d | ||
|
|
d6e1dd1040 | ||
|
|
76034772cb | ||
|
|
12507c707f | ||
|
|
de358f8cdc | ||
|
|
08668ac462 | ||
|
|
0a3734ed2b | ||
|
|
a5d1a3d65c | ||
|
|
7bc2980905 | ||
|
|
34e792e193 | ||
|
|
7b1ad1b629 | ||
|
|
a8f9f6c43a | ||
|
|
c9b1b6d076 | ||
|
|
cd078903a7 | ||
|
|
588c17ff69 | ||
|
|
baf7eaace6 | ||
|
|
8026bd9476 | ||
|
|
e2bd96012a | ||
|
|
9be63e66ec | ||
|
|
9f9ffd0efd | ||
|
|
2db881519a | ||
|
|
d9adfbe047 | ||
|
|
74e2c477f1 | ||
|
|
c5952dd09a | ||
|
|
134b19a9cb | ||
|
|
2c01b6118f | ||
|
|
03d3c786f2 | ||
|
|
0f03831274 | ||
|
|
dc7adb7161 | ||
|
|
5eeba6cced | ||
|
|
5eb74af414 | ||
|
|
ac19c19f21 | ||
|
|
ef03da0a76 | ||
|
|
9d85c9667f | ||
|
|
85bd126c6c | ||
|
|
7fdacdbad4 | ||
|
|
9c0a769675 | ||
|
|
11865fddff | ||
|
|
e8df3d2d91 | ||
|
|
a63c51f35d | ||
|
|
1730e0150f | ||
|
|
5a415979af | ||
|
|
a713a5a062 | ||
|
|
419dc248b6 | ||
|
|
632dabaa07 | ||
|
|
2756411ef7 | ||
|
|
50af51da5a | ||
|
|
ae919061e2 | ||
|
|
7ac87b8f99 | ||
|
|
ac051d7ae9 | ||
|
|
00d426b885 | ||
|
|
42fde6d457 | ||
|
|
8e0d00a3ea | ||
|
|
235011feef | ||
|
|
a1477405d1 | ||
|
|
558e37afa7 | ||
|
|
6bae52e6f2 | ||
|
|
32ae95f463 | ||
|
|
3644a452c1 | ||
|
|
5c940c33cb | ||
|
|
277e18f5cb | ||
|
|
8d3b2a9581 | ||
|
|
45a4ae5828 | ||
|
|
6db5b4a094 | ||
|
|
9d2024434e | ||
|
|
9165faef95 | ||
|
|
46c344feb0 | ||
|
|
78d26f6eb3 | ||
|
|
844856d39e | ||
|
|
b5a120c649 | ||
|
|
92b9597f8b | ||
|
|
556105780b | ||
|
|
af6bde3997 | ||
|
|
4bd1fd2441 | ||
|
|
45db468c9b | ||
|
|
2c02a44586 | ||
|
|
01141bed5a | ||
|
|
87e8646743 | ||
|
|
dd51380520 | ||
|
|
73d4f6d3b1 | ||
|
|
2af678aa84 | ||
|
|
1c94108d7e | ||
|
|
5d00f82388 | ||
|
|
98748906f6 | ||
|
|
dd832cb57a | ||
|
|
e3a17f67d9 | ||
|
|
c2e4ba8cbd | ||
|
|
1d9fdd01fa | ||
|
|
db9d43ed2f | ||
|
|
ec22fa2ad0 | ||
|
|
0e92820af4 | ||
|
|
e85aa247cb | ||
|
|
612da165f8 | ||
|
|
1fd62a7afc | ||
|
|
8a5f89e129 | ||
|
|
063d51fd75 | ||
|
|
0e0d5a0e95 | ||
|
|
bb55923a7d | ||
|
|
f184557fa0 | ||
|
|
77c7d0aae9 | ||
|
|
5ff8320e3b | ||
|
|
e68d3b9e63 | ||
|
|
97bc9dc717 | ||
|
|
6a15036867 | ||
|
|
17d0ae0f71 | ||
|
|
d020dede37 | ||
|
|
5c566bb05e | ||
|
|
b289c4ec2d | ||
|
|
2283444f72 | ||
|
|
a0e5820c32 | ||
|
|
04dc28d2b4 | ||
|
|
fa4c73a4d1 | ||
|
|
2bf8121b18 | ||
|
|
688ff96c8e | ||
|
|
ed3ef94071 | ||
|
|
ed78d18f60 | ||
|
|
e1a1372bae | ||
|
|
3283a200bc | ||
|
|
3f9b4cdca9 | ||
|
|
a85ef62698 | ||
|
|
32699234b6 | ||
|
|
8fbe40a918 | ||
|
|
d9b9b3dc46 | ||
|
|
20d36c71d4 | ||
|
|
ef08fbd3c7 | ||
|
|
5320c8353e | ||
|
|
c1bfaf9b1e | ||
|
|
0643f76c1f | ||
|
|
89cb425e69 | ||
|
|
461397e590 | ||
|
|
c67116fb55 | ||
|
|
572c3ee70d | ||
|
|
ff1abc63e0 | ||
|
|
308708952b | ||
|
|
fe1877fb18 | ||
|
|
cdc7057813 | ||
|
|
c121dd0252 | ||
|
|
8553821133 | ||
|
|
8a5a87b075 | ||
|
|
1312184ed7 | ||
|
|
906598ad92 | ||
|
|
fbd98b4c5a | ||
|
|
87b07456bd | ||
|
|
82de8b50da | ||
|
|
35feb107ed | ||
|
|
2471908151 | ||
|
|
0b1a399f4e | ||
|
|
cea79872d7 | ||
|
|
4c1749a13a | ||
|
|
939a1156c6 | ||
|
|
e5486536ae | ||
|
|
00164588f2 | ||
|
|
a16c18255c | ||
|
|
7aa2746c51 | ||
|
|
616aa8259a | ||
|
|
8795da4839 | ||
|
|
9c405e9c70 | ||
|
|
2d83af4905 | ||
|
|
b4100a7189 | ||
|
|
cfb67fc25b | ||
|
|
7201e09db9 | ||
|
|
e7a56a9268 | ||
|
|
4628a10191 | ||
|
|
2f325328c5 | ||
|
|
cca69481eb | ||
|
|
6e8744d59d | ||
|
|
79f73df545 | ||
|
|
9db8d3a410 | ||
|
|
c752ccbdde | ||
|
|
6931d0bd1f | ||
|
|
545beec743 |
89
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
89
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: Minor Release
|
||||||
|
about: Create a new minor release [for release managers only]
|
||||||
|
title: 'Release MAJOR.MINOR+1.0'
|
||||||
|
labels: 'release'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a new minor release
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
<--release summary to be used in announcements-->
|
||||||
|
|
||||||
|
### Commit
|
||||||
|
|
||||||
|
<--latest commit ID to include in this release-->
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||||
|
branch **development** version is *MAJOR.MINOR.0*.
|
||||||
|
|
||||||
|
#### On the day of the feature freeze
|
||||||
|
|
||||||
|
Change the `master` branch to the next MINOR+1 version:
|
||||||
|
|
||||||
|
- [ ] Switch to the `master` branch.
|
||||||
|
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR+1`, eg. `bump_dev_0_22`.
|
||||||
|
- [ ] Bump the `bump_dev_MAJOR_MINOR+1` branch to the next development MINOR+1 version.
|
||||||
|
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
|
||||||
|
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
|
||||||
|
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR+1` branch to `master`.
|
||||||
|
- Title PR "Bump version to MAJOR.MINOR+1.0".
|
||||||
|
|
||||||
|
Create a new release branch and release candidate tag:
|
||||||
|
|
||||||
|
- [ ] Double check that your local `master` is up-to-date with the upstream repo.
|
||||||
|
- [ ] Create a new branch called `release/MAJOR.MINOR+1` from `master`.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0-RC.1`
|
||||||
|
- Use message "Release MAJOR.MINOR+1.0 RC.1".
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Push the `release/MAJOR.MINOR` branch and new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-RC.1` tag.
|
||||||
|
|
||||||
|
If any issues need to be fixed before the *MAJOR.MINOR+1.0* version is released:
|
||||||
|
|
||||||
|
- [ ] Merge fix PRs to the `master` branch.
|
||||||
|
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- [ ] Verify fixes in `release/MAJOR.MINOR+1` branch.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0-RC.x+1`, where x is the current release candidate number.
|
||||||
|
- Use tag message "Release MAJOR.MINOR+1.0 RC.x+1".
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-RC.x+1` tag.
|
||||||
|
|
||||||
|
#### On the day of the release
|
||||||
|
|
||||||
|
Tag and publish new release:
|
||||||
|
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0`
|
||||||
|
- The first line of the tag message should be "Release MAJOR.MINOR+1.0".
|
||||||
|
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Wait for the CI to finish one last time.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- [ ] Publish **all** the updated crates to crates.io.
|
||||||
|
- [ ] Create the release on GitHub.
|
||||||
|
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||||
|
- Set the title to `Release MAJOR.MINOR+1.0`.
|
||||||
|
- In the release notes body put the **Summary** and **Changelog**.
|
||||||
|
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||||
|
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||||
|
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||||
|
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||||
|
- [ ] Celebrate 🎉
|
||||||
|
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
|
[crates.io]: https://crates.io/crates/bdk
|
||||||
|
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||||
|
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||||
67
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
67
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: Patch Release
|
||||||
|
about: Create a new patch release [for release managers only]
|
||||||
|
title: 'Release MAJOR.MINOR.PATCH+1'
|
||||||
|
labels: 'release'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a new patch release
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
<--release summary to be used in announcements-->
|
||||||
|
|
||||||
|
### Commit
|
||||||
|
|
||||||
|
<--latest commit ID to include in this release-->
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||||
|
branch **development** version is *MAJOR.MINOR.PATCH*.
|
||||||
|
|
||||||
|
### On the day of the patch release
|
||||||
|
|
||||||
|
Change the `master` branch to the new PATCH+1 version:
|
||||||
|
|
||||||
|
- [ ] Switch to the `master` branch.
|
||||||
|
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR_PATCH+1`, eg. `bump_dev_0_22_1`.
|
||||||
|
- [ ] Bump the `bump_dev_MAJOR_MINOR` branch to the next development PATCH+1 version.
|
||||||
|
- Change the `Cargo.toml` version value to `MAJOR.MINOR.PATCH+1`.
|
||||||
|
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
|
||||||
|
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR_PATCH+1` branch to `master`.
|
||||||
|
- Title PR "Bump version to MAJOR.MINOR.PATCH+1".
|
||||||
|
|
||||||
|
Cherry-pick, tag and publish new PATCH+1 release:
|
||||||
|
|
||||||
|
- [ ] Merge fix PRs to the `master` branch.
|
||||||
|
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR` branch to be patched.
|
||||||
|
- [ ] Verify fixes in `release/MAJOR.MINOR` branch.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR.PATCH+1`
|
||||||
|
- The first line of the tag message should be "Release MAJOR.MINOR.PATCH+1".
|
||||||
|
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Wait for the CI to finish one last time.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- [ ] Publish **all** the updated crates to crates.io.
|
||||||
|
- [ ] Create the release on GitHub.
|
||||||
|
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||||
|
- Set the title to `Release MAJOR.MINOR.PATCH+1`.
|
||||||
|
- In the release notes body put the **Summary** and **Changelog**.
|
||||||
|
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||||
|
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||||
|
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||||
|
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||||
|
- [ ] Celebrate 🎉
|
||||||
|
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
|
[crates.io]: https://crates.io/crates/bdk
|
||||||
|
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||||
|
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -9,6 +9,11 @@
|
|||||||
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
|
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
|
||||||
of the PR were done in a specific way -->
|
of the PR were done in a specific way -->
|
||||||
|
|
||||||
|
### Changelog notice
|
||||||
|
|
||||||
|
<!-- Notice the release manager should include in the release tag message changelog -->
|
||||||
|
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
|
||||||
|
|
||||||
### Checklists
|
### Checklists
|
||||||
|
|
||||||
#### All Submissions:
|
#### All Submissions:
|
||||||
@@ -21,7 +26,6 @@ of the PR were done in a specific way -->
|
|||||||
|
|
||||||
* [ ] I've added tests for the new feature
|
* [ ] I've added tests for the new feature
|
||||||
* [ ] I've added docs for the new feature
|
* [ ] I've added docs for the new feature
|
||||||
* [ ] I've updated `CHANGELOG.md`
|
|
||||||
|
|
||||||
#### Bugfixes:
|
#### Bugfixes:
|
||||||
|
|
||||||
|
|||||||
48
.github/workflows/code_coverage.yml
vendored
48
.github/workflows/code_coverage.yml
vendored
@@ -3,35 +3,53 @@ on: [push]
|
|||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
Codecov:
|
Codecov:
|
||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: '0'
|
RUSTFLAGS: "-Cinstrument-coverage"
|
||||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
RUSTDOCFLAGS: "-Cinstrument-coverage"
|
||||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Install lcov tools
|
||||||
|
run: sudo apt-get install lcov -y
|
||||||
- name: Install rustup
|
- name: Install rustup
|
||||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default nightly
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
|
- name: Add llvm tools
|
||||||
|
run: rustup component add llvm-tools-preview
|
||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Test
|
- name: Cache cargo
|
||||||
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
uses: actions/cache@v3
|
||||||
|
|
||||||
- id: coverage
|
|
||||||
name: Generate coverage
|
|
||||||
uses: actions-rs/grcov@v0.1.5
|
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ steps.coverage.outputs.report }}
|
path: |
|
||||||
directory: ./coverage/reports/
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Install grcov
|
||||||
|
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features default,minimal,all-keys,compact_filters,key-value-db,compiler,sqlite,sqlite-bundled,test-electrum,verify,test-rpc
|
||||||
|
- name: Run grcov
|
||||||
|
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
|
||||||
|
- name: Generate HTML coverage report
|
||||||
|
run: genhtml -o coverage-report.html ./coverage/lcov.info
|
||||||
|
|
||||||
|
- name: Coveralls upload
|
||||||
|
uses: coverallsapp/github-action@master
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage-report.html
|
||||||
|
|||||||
44
.github/workflows/cont_integration.yml
vendored
44
.github/workflows/cont_integration.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust:
|
rust:
|
||||||
- version: 1.56.0 # STABLE
|
- version: 1.60.0 # STABLE
|
||||||
clippy: true
|
clippy: true
|
||||||
- version: 1.46.0 # MSRV
|
- version: 1.56.1 # MSRV
|
||||||
features:
|
features:
|
||||||
- default
|
- default
|
||||||
- minimal
|
- minimal
|
||||||
@@ -90,12 +90,19 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
blockchain:
|
blockchain:
|
||||||
- name: electrum
|
- name: electrum
|
||||||
|
testprefix: blockchain::electrum::test
|
||||||
features: test-electrum,verify
|
features: test-electrum,verify
|
||||||
- name: rpc
|
- name: rpc
|
||||||
|
testprefix: blockchain::rpc::test
|
||||||
features: test-rpc
|
features: test-rpc
|
||||||
|
- name: rpc-legacy
|
||||||
|
testprefix: blockchain::rpc::test
|
||||||
|
features: test-rpc-legacy
|
||||||
- name: esplora
|
- name: esplora
|
||||||
|
testprefix: esplora
|
||||||
features: test-esplora,use-esplora-reqwest,verify
|
features: test-esplora,use-esplora-reqwest,verify
|
||||||
- name: esplora
|
- name: esplora
|
||||||
|
testprefix: esplora
|
||||||
features: test-esplora,use-esplora-ureq,verify
|
features: test-esplora,use-esplora-ureq,verify
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -114,7 +121,7 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.testprefix }}::bdk_blockchain_tests
|
||||||
|
|
||||||
check-wasm:
|
check-wasm:
|
||||||
name: Check WASM
|
name: Check WASM
|
||||||
@@ -139,7 +146,7 @@ jobs:
|
|||||||
- run: sudo apt-get update || exit 1
|
- run: sudo apt-get update || exit 1
|
||||||
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default 1.56.0 # STABLE
|
run: rustup default 1.56.1 # STABLE
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
- name: Add target wasm32
|
- name: Add target wasm32
|
||||||
@@ -165,3 +172,32 @@ jobs:
|
|||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||||
|
|
||||||
|
test_harware_wallet:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- version: 1.60.0 # STABLE
|
||||||
|
- version: 1.56.1 # MSRV
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Build simulator image
|
||||||
|
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
|
||||||
|
- name: Run simulator image
|
||||||
|
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
||||||
|
- name: Install Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: Install python dependencies
|
||||||
|
run: pip install hwi==2.1.1 protobuf==3.20.1
|
||||||
|
- name: Set default toolchain
|
||||||
|
run: rustup default ${{ matrix.rust.version }}
|
||||||
|
- name: Set profile
|
||||||
|
run: rustup set profile minimal
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features test-hardware-signer
|
||||||
|
|||||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,22 +1,61 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project prior to release **0.22.0** are documented in this file. Future
|
||||||
|
changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
|
||||||
|
Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk/releases)
|
||||||
|
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [v0.21.0] - [v0.20.0]
|
||||||
|
|
||||||
|
- Add `descriptor::checksum::get_checksum_bytes` method.
|
||||||
|
- Add `Excess` enum to handle remaining amount after coin selection.
|
||||||
|
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
|
||||||
|
- Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef<Path>
|
||||||
|
- Add the ability to specify which leaves to sign in a taproot transaction through `TapLeavesOptions` in `SignOptions`
|
||||||
|
- Add the ability to specify whether a taproot transaction should be signed using the internal key or not, using `sign_with_tap_internal_key` in `SignOptions`
|
||||||
|
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
|
||||||
|
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
|
||||||
|
- New `RpcBlockchain` implementation with various fixes.
|
||||||
|
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
|
||||||
|
|
||||||
|
## [v0.20.0] - [v0.19.0]
|
||||||
|
|
||||||
|
- New MSRV set to `1.56.1`
|
||||||
|
- Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced)
|
||||||
|
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
|
||||||
|
- Set coin type in BIP44, BIP49, and BIP84 templates
|
||||||
|
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
|
||||||
|
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
|
||||||
|
- Deprecate `AddressValidator`
|
||||||
|
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
|
||||||
|
|
||||||
|
## [v0.19.0] - [v0.18.0]
|
||||||
|
|
||||||
|
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
|
||||||
|
- New MSRV set to `1.56`
|
||||||
|
- Unpinned tokio to `1`
|
||||||
|
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
|
||||||
|
- Upgrade to rust-bitcoin `0.28`
|
||||||
|
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
|
||||||
|
- Update `PkOrF` in the policy module to become an enum
|
||||||
|
- Add experimental support for Taproot, including:
|
||||||
|
- Support for `tr()` descriptors with complex tapscript trees
|
||||||
|
- Creation of Taproot PSBTs (BIP-371)
|
||||||
|
- Signing Taproot PSBTs (key spend and script spend)
|
||||||
|
- Support for `tr()` descriptors in the `descriptor!()` macro
|
||||||
|
- Add support for Bitcoin Core 23.0 when using the `rpc` blockchain
|
||||||
|
|
||||||
## [v0.18.0] - [v0.17.0]
|
## [v0.18.0] - [v0.17.0]
|
||||||
|
|
||||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
|
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, i.e. for mobile platforms.
|
||||||
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
||||||
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
||||||
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
||||||
- Improve key generation traits
|
- Improve key generation traits
|
||||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
||||||
- Bump `miniscript` dependency version to `^6.1`.
|
- Bump `miniscript` dependency version to `^6.1`.
|
||||||
|
|
||||||
## [v0.17.0] - [v0.16.1]
|
## [v0.17.0] - [v0.16.1]
|
||||||
|
|
||||||
@@ -37,6 +76,7 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
|||||||
- Stop making a request for the block height when calling `Wallet:new`.
|
- Stop making a request for the block height when calling `Wallet:new`.
|
||||||
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
||||||
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
||||||
|
- remove `flush` method from the `Database` trait.
|
||||||
|
|
||||||
## [v0.16.1] - [v0.16.0]
|
## [v0.16.1] - [v0.16.0]
|
||||||
|
|
||||||
@@ -450,4 +490,6 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
|
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
|
||||||
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
|
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
|
||||||
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
||||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...HEAD
|
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
|
||||||
|
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
|
||||||
|
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
|
||||||
|
|||||||
34
Cargo.toml
34
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.19.0-dev"
|
version = "0.22.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -14,17 +14,17 @@ license = "MIT OR Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bdk-macros = "^0.6"
|
bdk-macros = "^0.6"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
miniscript = { version = "^6.1", features = ["use-serde"] }
|
miniscript = { version = "7.0", features = ["use-serde"] }
|
||||||
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
|
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
rand = "^0.7"
|
rand = "^0.7"
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.8", optional = true }
|
electrum-client = { version = "0.11", optional = true }
|
||||||
rusqlite = { version = "0.25.3", optional = true }
|
rusqlite = { version = "0.27.0", optional = true }
|
||||||
ahash = { version = "=0.7.4", optional = true }
|
ahash = { version = "0.7.6", optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
||||||
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
@@ -33,16 +33,17 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
|
|||||||
cc = { version = ">=1.0.64", optional = true }
|
cc = { version = ">=1.0.64", optional = true }
|
||||||
socks = { version = "0.3", optional = true }
|
socks = { version = "0.3", optional = true }
|
||||||
lazy_static = { version = "1.4", optional = true }
|
lazy_static = { version = "1.4", optional = true }
|
||||||
|
hwi = { version = "0.2.2", optional = true }
|
||||||
|
|
||||||
bip39 = { version = "1.0.1", optional = true }
|
bip39 = { version = "1.0.1", optional = true }
|
||||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||||
|
|
||||||
# Needed by bdk_blockchain_tests macro
|
# Needed by bdk_blockchain_tests macro and the `rpc` feature
|
||||||
bitcoincore-rpc = { version = "0.14", optional = true }
|
bitcoincore-rpc = { version = "0.15", optional = true }
|
||||||
|
|
||||||
# Platform-specific dependencies
|
# Platform-specific dependencies
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tokio = { version = "~1.14", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -61,6 +62,7 @@ key-value-db = ["sled"]
|
|||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
keys-bip39 = ["bip39"]
|
keys-bip39 = ["bip39"]
|
||||||
rpc = ["bitcoincore-rpc"]
|
rpc = ["bitcoincore-rpc"]
|
||||||
|
hardware-signer = ["hwi"]
|
||||||
|
|
||||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||||
# blocking except for the `EsploraBlockchain` which can be either async or
|
# blocking except for the `EsploraBlockchain` which can be either async or
|
||||||
@@ -88,16 +90,18 @@ reqwest-default-tls = ["reqwest/default-tls"]
|
|||||||
|
|
||||||
# Debug/Test features
|
# Debug/Test features
|
||||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||||
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
|
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||||
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
|
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||||
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
|
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
|
||||||
|
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
|
test-hardware-signer = ["hardware-signer"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
electrsd = { version= "0.15", features = ["trigger", "bitcoind_22_0"] }
|
electrsd = "0.20"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
@@ -113,11 +117,11 @@ required-features = ["compiler"]
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "rpcwallet"
|
name = "rpcwallet"
|
||||||
path = "examples/rpcwallet.rs"
|
path = "examples/rpcwallet.rs"
|
||||||
required-features = ["keys-bip39", "key-value-db", "rpc"]
|
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
|
features = ["compiler", "electrum", "esplora", "use-esplora-ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
||||||
# defines the configuration attribute `docsrs`
|
# defines the configuration attribute `docsrs`
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|||||||
@@ -1,46 +1,16 @@
|
|||||||
# Development Cycle
|
# Development Cycle
|
||||||
|
|
||||||
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
This project follows a regular releasing schedule similar to the one [used by the Rust language]. In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
||||||
|
|
||||||
|
This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API changes in the changelog info attached to each release tag.
|
||||||
|
|
||||||
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
||||||
|
|
||||||
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
||||||
|
|
||||||
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
||||||
|
|
||||||
```
|
To create a new release a release manager will create a new issue using the `Release` template and follow the template instructions.
|
||||||
master: - - - - * - - - * - - - - - - * - - - * ...
|
|
||||||
| / | |
|
|
||||||
release/0.x.0: * - - # | |
|
|
||||||
| /
|
|
||||||
release/0.y.0: * - - #
|
|
||||||
```
|
|
||||||
|
|
||||||
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
|
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
## Making the Release
|
|
||||||
|
|
||||||
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
|
|
||||||
|
|
||||||
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
|
|
||||||
|
|
||||||
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
|
|
||||||
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
|
|
||||||
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
|
|
||||||
4. During the one week of feature freeze run additional tests on the release branch.
|
|
||||||
5. If a bug is found:
|
|
||||||
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
|
|
||||||
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
|
|
||||||
6. Update the changelog with the new release version.
|
|
||||||
7. Update `src/lib.rs` with the new version (line ~43)
|
|
||||||
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
|
|
||||||
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
|
||||||
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
|
|
||||||
11. Publish **all** the updated crates to crates.io.
|
|
||||||
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
|
||||||
13. Merge the release branch back into `master`.
|
|
||||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (ie. `bdk-macros = { path = "./macros"}`)
|
|
||||||
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
|
||||||
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
|
||||||
17. Announce the release on Twitter, Discord and Telegram.
|
|
||||||
18. Celebrate :tada:
|
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -11,9 +11,9 @@
|
|||||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||||
<a href="https://blog.rust-lang.org/2020/08/27/Rust-1.46.0.html"><img alt="Rustc Version 1.46+" src="https://img.shields.io/badge/rustc-1.46%2B-lightgrey.svg"/></a>
|
<a href="https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
|
||||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -43,15 +43,15 @@ use bdk::Wallet;
|
|||||||
use bdk::database::MemoryDatabase;
|
use bdk::database::MemoryDatabase;
|
||||||
use bdk::blockchain::ElectrumBlockchain;
|
use bdk::blockchain::ElectrumBlockchain;
|
||||||
use bdk::SyncOptions;
|
use bdk::SyncOptions;
|
||||||
|
|
||||||
use bdk::electrum_client::Client;
|
use bdk::electrum_client::Client;
|
||||||
|
use bdk::bitcoin::Network;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||||
bitcoin::Network::Testnet,
|
Network::Testnet,
|
||||||
MemoryDatabase::default(),
|
MemoryDatabase::default(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
|
|||||||
use bdk::electrum_client::Client;
|
use bdk::electrum_client::Client;
|
||||||
use bdk::wallet::AddressIndex::New;
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
|
use bitcoin::base64;
|
||||||
use bitcoin::consensus::serialize;
|
use bitcoin::consensus::serialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@@ -131,6 +132,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
```rust,no_run
|
```rust,no_run
|
||||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||||
|
|
||||||
|
use bitcoin::base64;
|
||||||
use bitcoin::consensus::deserialize;
|
use bitcoin::consensus::deserialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@@ -154,7 +156,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
|
|
||||||
### Unit testing
|
### Unit testing
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -162,11 +164,11 @@ cargo test
|
|||||||
|
|
||||||
Integration testing require testing features, for example:
|
Integration testing require testing features, for example:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cargo test --features test-electrum
|
cargo test --features test-electrum
|
||||||
```
|
```
|
||||||
|
|
||||||
The other options are `test-esplora` or `test-rpc`.
|
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
|
||||||
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
9
ci/Dockerfile.ledger
Normal file
9
ci/Dockerfile.ledger
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Taken from bitcoindevkit/rust-hwi
|
||||||
|
FROM ghcr.io/ledgerhq/speculos
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install wget -y
|
||||||
|
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf
|
||||||
|
ADD automation.json /speculos/automation.json
|
||||||
|
|
||||||
|
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
|
||||||
30
ci/automation.json
Normal file
30
ci/automation.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount",
|
||||||
|
"actions": [
|
||||||
|
[ "button", 2, true ],
|
||||||
|
[ "button", 2, false ]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Sign",
|
||||||
|
"conditions": [
|
||||||
|
[ "seen", false ]
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
[ "button", 2, true ],
|
||||||
|
[ "button", 2, false ],
|
||||||
|
[ "setbool", "seen", true ]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regexp": "Approve|Sign|Accept",
|
||||||
|
"actions": [
|
||||||
|
[ "button", 3, true ],
|
||||||
|
[ "button", 3, false ]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
codecov.yaml
13
codecov.yaml
@@ -1,13 +0,0 @@
|
|||||||
coverage:
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
threshold: 1%
|
|
||||||
base: auto
|
|
||||||
informational: false
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
threshold: 100%
|
|
||||||
base: auto
|
|
||||||
@@ -14,6 +14,7 @@ use std::sync::Arc;
|
|||||||
use bdk::bitcoin;
|
use bdk::bitcoin;
|
||||||
use bdk::database::MemoryDatabase;
|
use bdk::database::MemoryDatabase;
|
||||||
use bdk::descriptor::HdKeyPaths;
|
use bdk::descriptor::HdKeyPaths;
|
||||||
|
#[allow(deprecated)]
|
||||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
||||||
use bdk::KeychainKind;
|
use bdk::KeychainKind;
|
||||||
use bdk::Wallet;
|
use bdk::Wallet;
|
||||||
@@ -25,6 +26,7 @@ use bitcoin::{Network, Script};
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct DummyValidator;
|
struct DummyValidator;
|
||||||
|
#[allow(deprecated)]
|
||||||
impl AddressValidator for DummyValidator {
|
impl AddressValidator for DummyValidator {
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
@@ -50,6 +52,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
||||||
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
wallet.add_address_validator(Arc::new(DummyValidator));
|
||||||
|
|
||||||
wallet.get_address(New)?;
|
wallet.get_address(New)?;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let network = matches
|
let network = matches
|
||||||
.value_of("network")
|
.value_of("network")
|
||||||
.map(|n| Network::from_str(n))
|
.map(Network::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap_or(Network::Testnet);
|
.unwrap_or(Network::Testnet);
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
auth: bitcoind_auth,
|
auth: bitcoind_auth,
|
||||||
network: Network::Regtest,
|
network: Network::Regtest,
|
||||||
wallet_name,
|
wallet_name,
|
||||||
skip_blocks: None,
|
sync_params: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the above configuration to create a RPC blockchain backend
|
// Use the above configuration to create a RPC blockchain backend
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
macro_rules! impl_from {
|
macro_rules! impl_from {
|
||||||
|
( boxed $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => {
|
||||||
|
$( $cfg )*
|
||||||
|
impl From<$from> for $to {
|
||||||
|
fn from(inner: $from) -> Self {
|
||||||
|
<$to>::$variant(Box::new(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
( $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => {
|
( $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => {
|
||||||
$( $cfg )*
|
$( $cfg )*
|
||||||
impl From<$from> for $to {
|
impl From<$from> for $to {
|
||||||
@@ -68,19 +76,19 @@ pub enum AnyBlockchain {
|
|||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
||||||
/// Electrum client
|
/// Electrum client
|
||||||
Electrum(electrum::ElectrumBlockchain),
|
Electrum(Box<electrum::ElectrumBlockchain>),
|
||||||
#[cfg(feature = "esplora")]
|
#[cfg(feature = "esplora")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
||||||
/// Esplora client
|
/// Esplora client
|
||||||
Esplora(esplora::EsploraBlockchain),
|
Esplora(Box<esplora::EsploraBlockchain>),
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||||
/// Compact filters client
|
/// Compact filters client
|
||||||
CompactFilters(compact_filters::CompactFiltersBlockchain),
|
CompactFilters(Box<compact_filters::CompactFiltersBlockchain>),
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||||
/// RPC client
|
/// RPC client
|
||||||
Rpc(rpc::RpcBlockchain),
|
Rpc(Box<rpc::RpcBlockchain>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
@@ -112,6 +120,13 @@ impl GetTx for AnyBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[maybe_async]
|
||||||
|
impl GetBlockHash for AnyBlockchain {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
maybe_await!(impl_inner_method!(self, get_block_hash, height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl WalletSync for AnyBlockchain {
|
impl WalletSync for AnyBlockchain {
|
||||||
fn wallet_sync<D: BatchDatabase>(
|
fn wallet_sync<D: BatchDatabase>(
|
||||||
@@ -141,10 +156,10 @@ impl WalletSync for AnyBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
impl_from!(boxed electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
||||||
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
impl_from!(boxed esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
||||||
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
impl_from!(boxed compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||||
impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||||
|
|
||||||
/// Type that can contain any of the blockchain configurations defined by the library
|
/// Type that can contain any of the blockchain configurations defined by the library
|
||||||
///
|
///
|
||||||
@@ -207,19 +222,19 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
|||||||
Ok(match config {
|
Ok(match config {
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
AnyBlockchainConfig::Electrum(inner) => {
|
AnyBlockchainConfig::Electrum(inner) => {
|
||||||
AnyBlockchain::Electrum(electrum::ElectrumBlockchain::from_config(inner)?)
|
AnyBlockchain::Electrum(Box::new(electrum::ElectrumBlockchain::from_config(inner)?))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "esplora")]
|
#[cfg(feature = "esplora")]
|
||||||
AnyBlockchainConfig::Esplora(inner) => {
|
AnyBlockchainConfig::Esplora(inner) => {
|
||||||
AnyBlockchain::Esplora(esplora::EsploraBlockchain::from_config(inner)?)
|
AnyBlockchain::Esplora(Box::new(esplora::EsploraBlockchain::from_config(inner)?))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
|
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(Box::new(
|
||||||
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
||||||
),
|
)),
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
AnyBlockchainConfig::Rpc(inner) => {
|
AnyBlockchainConfig::Rpc(inner) => {
|
||||||
AnyBlockchain::Rpc(rpc::RpcBlockchain::from_config(inner)?)
|
AnyBlockchain::Rpc(Box::new(rpc::RpcBlockchain::from_config(inner)?))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ impl GetTx for CompactFiltersBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GetBlockHash for CompactFiltersBlockchain {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
self.headers
|
||||||
|
.get_block_hash(height as usize)?
|
||||||
|
.ok_or(Error::CompactFilters(
|
||||||
|
CompactFiltersError::BlockHashNotFound,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WalletSync for CompactFiltersBlockchain {
|
impl WalletSync for CompactFiltersBlockchain {
|
||||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
@@ -536,6 +546,8 @@ pub enum CompactFiltersError {
|
|||||||
InvalidFilter,
|
InvalidFilter,
|
||||||
/// The peer is missing a block in the valid chain
|
/// The peer is missing a block in the valid chain
|
||||||
MissingBlock,
|
MissingBlock,
|
||||||
|
/// Block hash at specified height not found
|
||||||
|
BlockHashNotFound,
|
||||||
/// The data stored in the block filters storage are corrupted
|
/// The data stored in the block filters storage are corrupted
|
||||||
DataCorruption,
|
DataCorruption,
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::io::BufReader;
|
||||||
use std::net::{TcpStream, ToSocketAddrs};
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -19,14 +20,13 @@ use socks::{Socks5Stream, ToTargetAddr};
|
|||||||
|
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
use bitcoin::consensus::Encodable;
|
use bitcoin::consensus::{Decodable, Encodable};
|
||||||
use bitcoin::hash_types::BlockHash;
|
use bitcoin::hash_types::BlockHash;
|
||||||
use bitcoin::network::constants::ServiceFlags;
|
use bitcoin::network::constants::ServiceFlags;
|
||||||
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
|
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
|
||||||
use bitcoin::network::message_blockdata::*;
|
use bitcoin::network::message_blockdata::*;
|
||||||
use bitcoin::network::message_filter::*;
|
use bitcoin::network::message_filter::*;
|
||||||
use bitcoin::network::message_network::VersionMessage;
|
use bitcoin::network::message_network::VersionMessage;
|
||||||
use bitcoin::network::stream_reader::StreamReader;
|
|
||||||
use bitcoin::network::Address;
|
use bitcoin::network::Address;
|
||||||
use bitcoin::{Block, Network, Transaction, Txid, Wtxid};
|
use bitcoin::{Block, Network, Transaction, Txid, Wtxid};
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ impl Mempool {
|
|||||||
TxIdentifier::Wtxid(wtxid) => self.0.read().unwrap().wtxids.get(&wtxid).cloned(),
|
TxIdentifier::Wtxid(wtxid) => self.0.read().unwrap().wtxids.get(&wtxid).cloned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
txid.map(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
|
txid.and_then(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
|
||||||
.flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether or not the mempool contains a transaction with a given txid
|
/// Return whether or not the mempool contains a transaction with a given txid
|
||||||
@@ -111,6 +110,7 @@ impl Mempool {
|
|||||||
|
|
||||||
/// A Bitcoin peer
|
/// A Bitcoin peer
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct Peer {
|
pub struct Peer {
|
||||||
writer: Arc<Mutex<TcpStream>>,
|
writer: Arc<Mutex<TcpStream>>,
|
||||||
responses: Arc<RwLock<ResponsesMap>>,
|
responses: Arc<RwLock<ResponsesMap>>,
|
||||||
@@ -327,9 +327,10 @@ impl Peer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut reader = StreamReader::new(connection, None);
|
let mut reader = BufReader::new(connection);
|
||||||
loop {
|
loop {
|
||||||
let raw_message: RawNetworkMessage = check_disconnect!(reader.read_next());
|
let raw_message: RawNetworkMessage =
|
||||||
|
check_disconnect!(Decodable::consensus_decode(&mut reader));
|
||||||
|
|
||||||
let in_message = if raw_message.magic != network.magic() {
|
let in_message = if raw_message.magic != network.magic() {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
@@ -79,6 +80,16 @@ impl Blockchain for ElectrumBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for ElectrumBlockchain {
|
||||||
|
type Target = Client;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for ElectrumBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for ElectrumBlockchain {
|
impl GetHeight for ElectrumBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
||||||
@@ -96,6 +107,13 @@ impl GetTx for ElectrumBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GetBlockHash for ElectrumBlockchain {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
let block_header = self.client.block_header(height as usize)?;
|
||||||
|
Ok(block_header.block_hash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WalletSync for ElectrumBlockchain {
|
impl WalletSync for ElectrumBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
&self,
|
&self,
|
||||||
@@ -106,7 +124,10 @@ impl WalletSync for ElectrumBlockchain {
|
|||||||
let mut block_times = HashMap::<u32, u32>::new();
|
let mut block_times = HashMap::<u32, u32>::new();
|
||||||
let mut txid_to_height = HashMap::<Txid, u32>::new();
|
let mut txid_to_height = HashMap::<Txid, u32>::new();
|
||||||
let mut tx_cache = TxCache::new(database, &self.client);
|
let mut tx_cache = TxCache::new(database, &self.client);
|
||||||
let chunk_size = self.stop_gap;
|
|
||||||
|
// Set chunk_size to the smallest value capable of finding a gap greater than stop_gap.
|
||||||
|
let chunk_size = self.stop_gap + 1;
|
||||||
|
|
||||||
// The electrum server has been inconsistent somehow in its responses during sync. For
|
// The electrum server has been inconsistent somehow in its responses during sync. For
|
||||||
// example, we do a batch request of transactions and the response contains less
|
// example, we do a batch request of transactions and the response contains less
|
||||||
// tranascations than in the request. This should never happen but we don't want to panic.
|
// tranascations than in the request. This should never happen but we don't want to panic.
|
||||||
@@ -142,21 +163,12 @@ impl WalletSync for ElectrumBlockchain {
|
|||||||
|
|
||||||
Request::Conftime(conftime_req) => {
|
Request::Conftime(conftime_req) => {
|
||||||
// collect up to chunk_size heights to fetch from electrum
|
// collect up to chunk_size heights to fetch from electrum
|
||||||
let needs_block_height = {
|
let needs_block_height = conftime_req
|
||||||
let mut needs_block_height_iter = conftime_req
|
.request()
|
||||||
.request()
|
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
||||||
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
.filter(|height| block_times.get(height).is_none())
|
||||||
.filter(|height| block_times.get(height).is_none());
|
.take(chunk_size)
|
||||||
let mut needs_block_height = HashSet::new();
|
.collect::<HashSet<u32>>();
|
||||||
|
|
||||||
while needs_block_height.len() < chunk_size {
|
|
||||||
match needs_block_height_iter.next() {
|
|
||||||
Some(height) => needs_block_height.insert(height),
|
|
||||||
None => break,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
needs_block_height
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_block_headers = self
|
let new_block_headers = self
|
||||||
.client
|
.client
|
||||||
@@ -320,8 +332,93 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "test-electrum")]
|
#[cfg(feature = "test-electrum")]
|
||||||
crate::bdk_blockchain_tests! {
|
mod test {
|
||||||
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
use std::sync::Arc;
|
||||||
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
|
||||||
|
use super::*;
|
||||||
|
use crate::database::MemoryDatabase;
|
||||||
|
use crate::testutils::blockchain_tests::TestClient;
|
||||||
|
use crate::testutils::configurable_blockchain_tests::ConfigurableBlockchainTester;
|
||||||
|
use crate::wallet::{AddressIndex, Wallet};
|
||||||
|
|
||||||
|
crate::bdk_blockchain_tests! {
|
||||||
|
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
||||||
|
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_factory() -> (TestClient, Arc<ElectrumBlockchain>) {
|
||||||
|
let test_client = TestClient::default();
|
||||||
|
|
||||||
|
let factory = Arc::new(ElectrumBlockchain::from(
|
||||||
|
Client::new(&test_client.electrsd.electrum_url).unwrap(),
|
||||||
|
));
|
||||||
|
|
||||||
|
(test_client, factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_electrum_blockchain_factory() {
|
||||||
|
let (_test_client, factory) = get_factory();
|
||||||
|
|
||||||
|
let a = factory.build("aaaaaa", None).unwrap();
|
||||||
|
let b = factory.build("bbbbbb", None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
a.client.block_headers_subscribe().unwrap().height,
|
||||||
|
b.client.block_headers_subscribe().unwrap().height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_electrum_blockchain_factory_sync_wallet() {
|
||||||
|
let (mut test_client, factory) = get_factory();
|
||||||
|
|
||||||
|
let db = MemoryDatabase::new();
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
|
||||||
|
None,
|
||||||
|
bitcoin::Network::Regtest,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let address = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
|
||||||
|
let tx = testutils! {
|
||||||
|
@tx ( (@addr address.address) => 50_000 )
|
||||||
|
};
|
||||||
|
test_client.receive(tx);
|
||||||
|
|
||||||
|
factory
|
||||||
|
.sync_wallet(&wallet, None, Default::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_electrum_with_variable_configs() {
|
||||||
|
struct ElectrumTester;
|
||||||
|
|
||||||
|
impl ConfigurableBlockchainTester<ElectrumBlockchain> for ElectrumTester {
|
||||||
|
const BLOCKCHAIN_NAME: &'static str = "Electrum";
|
||||||
|
|
||||||
|
fn config_with_stop_gap(
|
||||||
|
&self,
|
||||||
|
test_client: &mut TestClient,
|
||||||
|
stop_gap: usize,
|
||||||
|
) -> Option<ElectrumBlockchainConfig> {
|
||||||
|
Some(ElectrumBlockchainConfig {
|
||||||
|
url: test_client.electrsd.electrum_url.clone(),
|
||||||
|
socks5: None,
|
||||||
|
retry: 0,
|
||||||
|
timeout: None,
|
||||||
|
stop_gap: stop_gap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectrumTester.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
|
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
|
||||||
use crate::BlockTime;
|
use crate::BlockTime;
|
||||||
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid};
|
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness};
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone, Debug)]
|
#[derive(serde::Deserialize, Clone, Debug)]
|
||||||
pub struct PrevOut {
|
pub struct PrevOut {
|
||||||
@@ -63,7 +63,7 @@ impl Tx {
|
|||||||
},
|
},
|
||||||
script_sig: vin.scriptsig,
|
script_sig: vin.scriptsig,
|
||||||
sequence: vin.sequence,
|
sequence: vin.sequence,
|
||||||
witness: vin.witness,
|
witness: Witness::from_vec(vin.witness),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
output: self
|
output: self
|
||||||
|
|||||||
@@ -209,4 +209,38 @@ mod test {
|
|||||||
"should inherit from value for 25"
|
"should inherit from value for 25"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "test-esplora")]
|
||||||
|
fn test_esplora_with_variable_configs() {
|
||||||
|
use crate::testutils::{
|
||||||
|
blockchain_tests::TestClient,
|
||||||
|
configurable_blockchain_tests::ConfigurableBlockchainTester,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EsploraTester;
|
||||||
|
|
||||||
|
impl ConfigurableBlockchainTester<EsploraBlockchain> for EsploraTester {
|
||||||
|
const BLOCKCHAIN_NAME: &'static str = "Esplora";
|
||||||
|
|
||||||
|
fn config_with_stop_gap(
|
||||||
|
&self,
|
||||||
|
test_client: &mut TestClient,
|
||||||
|
stop_gap: usize,
|
||||||
|
) -> Option<EsploraBlockchainConfig> {
|
||||||
|
Some(EsploraBlockchainConfig {
|
||||||
|
base_url: format!(
|
||||||
|
"http://{}",
|
||||||
|
test_client.electrsd.esplora_url.as_ref().unwrap()
|
||||||
|
),
|
||||||
|
proxy: None,
|
||||||
|
concurrency: None,
|
||||||
|
stop_gap: stop_gap,
|
||||||
|
timeout: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EsploraTester.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
//! Esplora by way of `reqwest` HTTP client.
|
//! Esplora by way of `reqwest` HTTP client.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::consensus::{deserialize, serialize};
|
use bitcoin::consensus::{deserialize, serialize};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
@@ -31,8 +32,9 @@ use crate::database::BatchDatabase;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
/// Structure encapsulates Esplora client
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UrlClient {
|
pub struct UrlClient {
|
||||||
url: String,
|
url: String,
|
||||||
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
||||||
// when the target platform is wasm32.
|
// when the target platform is wasm32.
|
||||||
@@ -101,6 +103,16 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for EsploraBlockchain {
|
||||||
|
type Target = UrlClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.url_client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
@@ -115,6 +127,14 @@ impl GetTx for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[maybe_async]
|
||||||
|
impl GetBlockHash for EsploraBlockchain {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
let block_header = await_or_block!(self.url_client._get_header(height as u32))?;
|
||||||
|
Ok(block_header.block_hash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl WalletSync for EsploraBlockchain {
|
impl WalletSync for EsploraBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
@@ -203,7 +223,6 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
};
|
};
|
||||||
|
|
||||||
database.commit_batch(batch_update)?;
|
database.commit_batch(batch_update)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -33,8 +34,9 @@ use crate::database::BatchDatabase;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
/// Structure encapsulates ureq Esplora client
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct UrlClient {
|
pub struct UrlClient {
|
||||||
url: String,
|
url: String,
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,7 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
let _txid = self.url_client._broadcast(tx)?;
|
self.url_client._broadcast(tx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,16 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for EsploraBlockchain {
|
||||||
|
type Target = UrlClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.url_client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(self.url_client._get_height()?)
|
Ok(self.url_client._get_height()?)
|
||||||
@@ -110,6 +122,13 @@ impl GetTx for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GetBlockHash for EsploraBlockchain {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
let block_header = self.url_client._get_header(height as u32)?;
|
||||||
|
Ok(block_header.block_hash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WalletSync for EsploraBlockchain {
|
impl WalletSync for EsploraBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ use std::ops::Deref;
|
|||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bitcoin::{Transaction, Txid};
|
use bitcoin::{BlockHash, Transaction, Txid};
|
||||||
|
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::wallet::{wallet_name_from_descriptor, Wallet};
|
||||||
|
use crate::{FeeRate, KeychainKind};
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "electrum",
|
feature = "electrum",
|
||||||
@@ -86,7 +87,7 @@ pub enum Capability {
|
|||||||
|
|
||||||
/// Trait that defines the actions that must be supported by a blockchain backend
|
/// Trait that defines the actions that must be supported by a blockchain backend
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
pub trait Blockchain: WalletSync + GetHeight + GetTx {
|
pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash {
|
||||||
/// Return the set of [`Capability`] supported by this backend
|
/// Return the set of [`Capability`] supported by this backend
|
||||||
fn get_capabilities(&self) -> HashSet<Capability>;
|
fn get_capabilities(&self) -> HashSet<Capability>;
|
||||||
/// Broadcast a transaction
|
/// Broadcast a transaction
|
||||||
@@ -109,6 +110,13 @@ pub trait GetTx {
|
|||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[maybe_async]
|
||||||
|
/// Trait for getting block hash by block height
|
||||||
|
pub trait GetBlockHash {
|
||||||
|
/// fetch block hash given its height
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait for blockchains that can sync by updating the database directly.
|
/// Trait for blockchains that can sync by updating the database directly.
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
pub trait WalletSync {
|
pub trait WalletSync {
|
||||||
@@ -164,6 +172,106 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
|
|||||||
fn from_config(config: &Self::Config) -> Result<Self, Error>;
|
fn from_config(config: &Self::Config) -> Result<Self, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for blockchains that don't contain any state
|
||||||
|
///
|
||||||
|
/// Statless blockchains can be used to sync multiple wallets with different descriptors.
|
||||||
|
///
|
||||||
|
/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
|
||||||
|
/// blockchain.
|
||||||
|
pub trait StatelessBlockchain: Blockchain {}
|
||||||
|
|
||||||
|
/// Trait for a factory of blockchains that share the underlying connection or configuration
|
||||||
|
#[cfg_attr(
|
||||||
|
not(feature = "async-interface"),
|
||||||
|
doc = r##"
|
||||||
|
## Example
|
||||||
|
|
||||||
|
This example shows how to sync multiple walles and return the sum of their balances
|
||||||
|
|
||||||
|
```no_run
|
||||||
|
# use bdk::Error;
|
||||||
|
# use bdk::blockchain::*;
|
||||||
|
# use bdk::database::*;
|
||||||
|
# use bdk::wallet::*;
|
||||||
|
# use bdk::*;
|
||||||
|
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
|
||||||
|
Ok(wallets
|
||||||
|
.iter()
|
||||||
|
.map(|w| -> Result<_, Error> {
|
||||||
|
blockchain_factory.sync_wallet(&w, None, SyncOptions::default())?;
|
||||||
|
w.get_balance()
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"##
|
||||||
|
)]
|
||||||
|
pub trait BlockchainFactory {
|
||||||
|
/// The type returned when building a blockchain from this factory
|
||||||
|
type Inner: Blockchain;
|
||||||
|
|
||||||
|
/// Build a new blockchain for the given descriptor wallet_name
|
||||||
|
///
|
||||||
|
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
|
||||||
|
/// from the factory. Since it's not possible to override the value to `None`, set it to
|
||||||
|
/// `Some(0)` to rescan from the genesis.
|
||||||
|
fn build(
|
||||||
|
&self,
|
||||||
|
wallet_name: &str,
|
||||||
|
override_skip_blocks: Option<u32>,
|
||||||
|
) -> Result<Self::Inner, Error>;
|
||||||
|
|
||||||
|
/// Build a new blockchain for a given wallet
|
||||||
|
///
|
||||||
|
/// Internally uses [`wallet_name_from_descriptor`] to derive the name, and then calls
|
||||||
|
/// [`BlockchainFactory::build`] to create the blockchain instance.
|
||||||
|
fn build_for_wallet<D: BatchDatabase>(
|
||||||
|
&self,
|
||||||
|
wallet: &Wallet<D>,
|
||||||
|
override_skip_blocks: Option<u32>,
|
||||||
|
) -> Result<Self::Inner, Error> {
|
||||||
|
let wallet_name = wallet_name_from_descriptor(
|
||||||
|
wallet.public_descriptor(KeychainKind::External)?.unwrap(),
|
||||||
|
wallet.public_descriptor(KeychainKind::Internal)?,
|
||||||
|
wallet.network(),
|
||||||
|
wallet.secp_ctx(),
|
||||||
|
)?;
|
||||||
|
self.build(&wallet_name, override_skip_blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use [`BlockchainFactory::build_for_wallet`] to get a blockchain, then sync the wallet
|
||||||
|
///
|
||||||
|
/// This can be used when a new blockchain would only be used to sync a wallet and then
|
||||||
|
/// immediately dropped. Keep in mind that specific blockchain factories may perform slow
|
||||||
|
/// operations to build a blockchain for a given wallet, so if a wallet needs to be synced
|
||||||
|
/// often it's recommended to use [`BlockchainFactory::build_for_wallet`] to reuse the same
|
||||||
|
/// blockchain multiple times.
|
||||||
|
#[cfg(not(any(target_arch = "wasm32", feature = "async-interface")))]
|
||||||
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(not(any(target_arch = "wasm32", feature = "async-interface"))))
|
||||||
|
)]
|
||||||
|
fn sync_wallet<D: BatchDatabase>(
|
||||||
|
&self,
|
||||||
|
wallet: &Wallet<D>,
|
||||||
|
override_skip_blocks: Option<u32>,
|
||||||
|
sync_options: crate::wallet::SyncOptions,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?;
|
||||||
|
wallet.sync(&blockchain, sync_options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
|
||||||
|
type Inner = Self;
|
||||||
|
|
||||||
|
fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
|
||||||
|
Ok(Arc::clone(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Data sent with a progress update over a [`channel`]
|
/// Data sent with a progress update over a [`channel`]
|
||||||
pub type ProgressData = (f32, Option<String>);
|
pub type ProgressData = (f32, Option<String>);
|
||||||
|
|
||||||
@@ -258,6 +366,13 @@ impl<T: GetHeight> GetHeight for Arc<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[maybe_async]
|
||||||
|
impl<T: GetBlockHash> GetBlockHash for Arc<T> {
|
||||||
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||||
|
maybe_await!(self.deref().get_block_hash(height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl<T: WalletSync> WalletSync for Arc<T> {
|
impl<T: WalletSync> WalletSync for Arc<T> {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
||||||
|
error::MissingCachedScripts,
|
||||||
wallet::time::Instant,
|
wallet::time::Instant,
|
||||||
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
||||||
};
|
};
|
||||||
@@ -34,11 +35,12 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
|||||||
let scripts_needed = db
|
let scripts_needed = db
|
||||||
.iter_script_pubkeys(Some(keychain))?
|
.iter_script_pubkeys(Some(keychain))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect::<VecDeque<_>>();
|
||||||
let state = State::new(db);
|
let state = State::new(db);
|
||||||
|
|
||||||
Ok(Request::Script(ScriptReq {
|
Ok(Request::Script(ScriptReq {
|
||||||
state,
|
state,
|
||||||
|
initial_scripts_needed: scripts_needed.len(),
|
||||||
scripts_needed,
|
scripts_needed,
|
||||||
script_index: 0,
|
script_index: 0,
|
||||||
stop_gap,
|
stop_gap,
|
||||||
@@ -50,6 +52,7 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
|||||||
pub struct ScriptReq<'a, D: BatchDatabase> {
|
pub struct ScriptReq<'a, D: BatchDatabase> {
|
||||||
state: State<'a, D>,
|
state: State<'a, D>,
|
||||||
script_index: usize,
|
script_index: usize,
|
||||||
|
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
|
||||||
scripts_needed: VecDeque<Script>,
|
scripts_needed: VecDeque<Script>,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
|
|||||||
self.script_index += 1;
|
self.script_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in txids {
|
self.scripts_needed.drain(..txids.len());
|
||||||
self.scripts_needed.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_active_index = self
|
// last active index: 0 => No last active
|
||||||
|
let last = self
|
||||||
.state
|
.state
|
||||||
.last_active_index
|
.last_active_index
|
||||||
.get(&self.keychain)
|
.get(&self.keychain)
|
||||||
.map(|x| x + 1)
|
.map(|&l| l + 1)
|
||||||
.unwrap_or(0); // so no addresses active maps to 0
|
.unwrap_or(0);
|
||||||
|
// remaining scripts left to check
|
||||||
|
let remaining = self.scripts_needed.len();
|
||||||
|
// difference between current index and last active index
|
||||||
|
let current_gap = self.script_index - last;
|
||||||
|
|
||||||
Ok(
|
// this is a hack to check whether the scripts are coming from a derivable descriptor
|
||||||
if self.script_index > last_active_index + self.stop_gap
|
// we assume for non-derivable descriptors, the initial script count is always 1
|
||||||
|| self.scripts_needed.is_empty()
|
let is_derivable = self.initial_scripts_needed > 1;
|
||||||
{
|
|
||||||
debug!(
|
debug!(
|
||||||
"finished scanning for transactions for keychain {:?} at index {}",
|
"sync: last={}, remaining={}, diff={}, stop_gap={}",
|
||||||
self.keychain, last_active_index
|
last, remaining, current_gap, self.stop_gap
|
||||||
);
|
);
|
||||||
// we're done here -- check if we need to do the next keychain
|
|
||||||
if let Some(keychain) = self.next_keychains.pop() {
|
if is_derivable {
|
||||||
self.keychain = keychain;
|
if remaining > 0 {
|
||||||
self.script_index = 0;
|
// we still have scriptPubKeys to do requests for
|
||||||
self.scripts_needed = self
|
return Ok(Request::Script(self));
|
||||||
.state
|
}
|
||||||
.db
|
|
||||||
.iter_script_pubkeys(Some(keychain))?
|
if last > 0 && current_gap < self.stop_gap {
|
||||||
.into_iter()
|
// current gap is not large enough to stop, but we are unable to keep checking since
|
||||||
.collect();
|
// we have exhausted cached scriptPubKeys, so return error
|
||||||
Request::Script(self)
|
let err = MissingCachedScripts {
|
||||||
} else {
|
last_count: self.script_index,
|
||||||
Request::Tx(TxReq { state: self.state })
|
missing_count: self.stop_gap - current_gap,
|
||||||
}
|
};
|
||||||
} else {
|
return Err(Error::MissingCachedScripts(err));
|
||||||
Request::Script(self)
|
}
|
||||||
},
|
|
||||||
)
|
// we have exhausted cached scriptPubKeys and found no txs, continue
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"finished scanning for txs of keychain {:?} at index {:?}",
|
||||||
|
self.keychain, last
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(keychain) = self.next_keychains.pop() {
|
||||||
|
// we still have another keychain to request txs with
|
||||||
|
let scripts_needed = self
|
||||||
|
.state
|
||||||
|
.db
|
||||||
|
.iter_script_pubkeys(Some(keychain))?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<VecDeque<_>>();
|
||||||
|
|
||||||
|
self.keychain = keychain;
|
||||||
|
self.script_index = 0;
|
||||||
|
self.initial_scripts_needed = scripts_needed.len();
|
||||||
|
self.scripts_needed = scripts_needed;
|
||||||
|
return Ok(Request::Script(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have finished requesting txids, let's get the actual txs.
|
||||||
|
Ok(Request::Tx(TxReq { state: self.state }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +325,8 @@ struct State<'a, D> {
|
|||||||
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
||||||
/// The start of the sync
|
/// The start of the sync
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
|
/// Missing number of scripts to cache per keychain
|
||||||
|
missing_script_counts: HashMap<KeychainKind, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, D: BatchDatabase> State<'a, D> {
|
impl<'a, D: BatchDatabase> State<'a, D> {
|
||||||
@@ -305,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
tx_needed: BTreeSet::default(),
|
tx_needed: BTreeSet::default(),
|
||||||
tx_missing_conftime: BTreeMap::default(),
|
tx_missing_conftime: BTreeMap::default(),
|
||||||
start_time: Instant::new(),
|
start_time: Instant::new(),
|
||||||
|
missing_script_counts: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn into_db_update(self) -> Result<D::Batch, Error> {
|
fn into_db_update(self) -> Result<D::Batch, Error> {
|
||||||
@@ -314,6 +348,22 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
let finished_txs = make_txs_consistent(&self.finished_txs);
|
let finished_txs = make_txs_consistent(&self.finished_txs);
|
||||||
let observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
|
let observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
|
||||||
let txids_to_delete = existing_txids.difference(&observed_txids);
|
let txids_to_delete = existing_txids.difference(&observed_txids);
|
||||||
|
|
||||||
|
// Ensure `last_active_index` does not decrement database's current state.
|
||||||
|
let index_updates = self
|
||||||
|
.last_active_index
|
||||||
|
.iter()
|
||||||
|
.map(|(keychain, sync_index)| {
|
||||||
|
let sync_index = *sync_index as u32;
|
||||||
|
let index_res = match self.db.get_last_index(*keychain) {
|
||||||
|
Ok(Some(db_index)) => Ok(std::cmp::max(db_index, sync_index)),
|
||||||
|
Ok(None) => Ok(sync_index),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
};
|
||||||
|
index_res.map(|index| (*keychain, index))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<(KeychainKind, u32)>, _>>()?;
|
||||||
|
|
||||||
let mut batch = self.db.begin_batch();
|
let mut batch = self.db.begin_batch();
|
||||||
|
|
||||||
// Delete old txs that no longer exist
|
// Delete old txs that no longer exist
|
||||||
@@ -377,8 +427,10 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
batch.set_tx(finished_tx)?;
|
batch.set_tx(finished_tx)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (keychain, last_active_index) in self.last_active_index {
|
// apply index updates
|
||||||
batch.set_last_index(keychain, last_active_index as u32)?;
|
for (keychain, new_index) in index_updates {
|
||||||
|
debug!("updating index ({}, {})", keychain.as_byte(), new_index);
|
||||||
|
batch.set_last_index(keychain, new_index)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
|
|||||||
@@ -255,10 +255,6 @@ impl Database for AnyDatabase {
|
|||||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
|
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<(), Error> {
|
|
||||||
impl_inner_method!(AnyDatabase, self, flush)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BatchOperations for AnyBatch {
|
impl BatchOperations for AnyBatch {
|
||||||
|
|||||||
@@ -166,16 +166,9 @@ macro_rules! impl_batch_operations {
|
|||||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||||
let res = self.remove(key);
|
let res = self.remove(key);
|
||||||
let res = $process_delete!(res);
|
$process_delete!(res)
|
||||||
|
.map(ivec_to_u32)
|
||||||
match res {
|
.transpose()
|
||||||
None => Ok(None),
|
|
||||||
Some(b) => {
|
|
||||||
let array: [u8; 4] = b.as_ref().try_into().map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
|
||||||
let val = u32::from_be_bytes(array);
|
|
||||||
Ok(Some(val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||||
@@ -357,16 +350,7 @@ impl Database for Tree {
|
|||||||
|
|
||||||
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||||
self.get(key)?
|
self.get(key)?.map(ivec_to_u32).transpose()
|
||||||
.map(|b| -> Result<_, Error> {
|
|
||||||
let array: [u8; 4] = b
|
|
||||||
.as_ref()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
|
||||||
let val = u32::from_be_bytes(array);
|
|
||||||
Ok(val)
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||||
@@ -393,19 +377,17 @@ impl Database for Tree {
|
|||||||
|
|
||||||
Some(new.to_be_bytes().to_vec())
|
Some(new.to_be_bytes().to_vec())
|
||||||
})?
|
})?
|
||||||
.map_or(Ok(0), |b| -> Result<_, Error> {
|
.map_or(Ok(0), ivec_to_u32)
|
||||||
let array: [u8; 4] = b
|
|
||||||
.as_ref()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
|
||||||
let val = u32::from_be_bytes(array);
|
|
||||||
Ok(val)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<(), Error> {
|
fn ivec_to_u32(b: sled::IVec) -> Result<u32, Error> {
|
||||||
Ok(Tree::flush(self).map(|_| ())?)
|
let array: [u8; 4] = b
|
||||||
}
|
.as_ref()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
||||||
|
let val = u32::from_be_bytes(array);
|
||||||
|
Ok(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BatchDatabase for Tree {
|
impl BatchDatabase for Tree {
|
||||||
|
|||||||
@@ -449,10 +449,6 @@ impl Database for MemoryDatabase {
|
|||||||
|
|
||||||
Ok(*value)
|
Ok(*value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BatchDatabase for MemoryDatabase {
|
impl BatchDatabase for MemoryDatabase {
|
||||||
@@ -486,15 +482,23 @@ impl ConfigurableDatabase for MemoryDatabase {
|
|||||||
/// don't have `test` set.
|
/// don't have `test` set.
|
||||||
macro_rules! populate_test_db {
|
macro_rules! populate_test_db {
|
||||||
($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{
|
($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{
|
||||||
|
$crate::populate_test_db!($db, $tx_meta, $current_height, (@coinbase false))
|
||||||
|
}};
|
||||||
|
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use $crate::database::BatchOperations;
|
use $crate::database::SyncTime;
|
||||||
|
use $crate::database::{BatchOperations, Database};
|
||||||
let mut db = $db;
|
let mut db = $db;
|
||||||
let tx_meta = $tx_meta;
|
let tx_meta = $tx_meta;
|
||||||
let current_height: Option<u32> = $current_height;
|
let current_height: Option<u32> = $current_height;
|
||||||
|
let mut input = vec![$crate::bitcoin::TxIn::default()];
|
||||||
|
if !$is_coinbase {
|
||||||
|
input[0].previous_output.vout = 0;
|
||||||
|
}
|
||||||
let tx = $crate::bitcoin::Transaction {
|
let tx = $crate::bitcoin::Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: 0,
|
||||||
input: vec![],
|
input,
|
||||||
output: tx_meta
|
output: tx_meta
|
||||||
.output
|
.output
|
||||||
.iter()
|
.iter()
|
||||||
@@ -508,10 +512,31 @@ macro_rules! populate_test_db {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime {
|
// Set Confirmation time only if current height is provided.
|
||||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
// panics if `tx_meta.min_confirmation` is Some, and current_height is None.
|
||||||
timestamp: 0,
|
let confirmation_time = tx_meta
|
||||||
});
|
.min_confirmations
|
||||||
|
.and_then(|v| if v == 0 { None } else { Some(v) })
|
||||||
|
.map(|conf| $crate::BlockTime {
|
||||||
|
height: current_height.expect("Current height is needed for testing transaction with min-confirmation values").checked_sub(conf as u32).unwrap() + 1,
|
||||||
|
timestamp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the database sync_time.
|
||||||
|
// Check if the current_height is less than already known sync height, apply the max
|
||||||
|
// If any of them is None, the other will be applied instead.
|
||||||
|
// If both are None, this will not be set.
|
||||||
|
if let Some(height) = db.get_sync_time().unwrap()
|
||||||
|
.map(|sync_time| sync_time.block_time.height)
|
||||||
|
.max(current_height) {
|
||||||
|
let sync_time = SyncTime {
|
||||||
|
block_time: BlockTime {
|
||||||
|
height,
|
||||||
|
timestamp: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
db.set_sync_time(sync_time).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let tx_details = $crate::TransactionDetails {
|
let tx_details = $crate::TransactionDetails {
|
||||||
transaction: Some(tx.clone()),
|
transaction: Some(tx.clone()),
|
||||||
|
|||||||
@@ -158,13 +158,6 @@ pub trait Database: BatchOperations {
|
|||||||
///
|
///
|
||||||
/// It should insert and return `0` if not present in the database
|
/// It should insert and return `0` if not present in the database
|
||||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
||||||
|
|
||||||
#[deprecated(
|
|
||||||
since = "0.18.0",
|
|
||||||
note = "The flush function is only needed for the sled database on mobile, instead for mobile use the sqlite database."
|
|
||||||
)]
|
|
||||||
/// Force changes to be written to disk
|
|
||||||
fn flush(&mut self) -> Result<(), Error>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for a database that supports batch operations
|
/// Trait for a database that supports batch operations
|
||||||
@@ -200,8 +193,7 @@ pub(crate) trait DatabaseUtils: Database {
|
|||||||
D: FnOnce() -> Result<Option<Transaction>, Error>,
|
D: FnOnce() -> Result<Option<Transaction>, Error>,
|
||||||
{
|
{
|
||||||
self.get_tx(txid, true)?
|
self.get_tx(txid, true)?
|
||||||
.map(|t| t.transaction)
|
.and_then(|t| t.transaction)
|
||||||
.flatten()
|
|
||||||
.map_or_else(default, |t| Ok(Some(t)))
|
.map_or_else(default, |t| Ok(Some(t)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +316,8 @@ pub mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tree.set_utxo(&utxo).unwrap();
|
tree.set_utxo(&utxo).unwrap();
|
||||||
|
tree.set_utxo(&utxo).unwrap();
|
||||||
|
assert_eq!(tree.iter_utxos().unwrap().len(), 1);
|
||||||
assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo));
|
assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +369,34 @@ pub mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn test_list_transaction<D: Database>(mut tree: D) {
|
||||||
|
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
|
||||||
|
let tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||||
|
let txid = tx.txid();
|
||||||
|
let mut tx_details = TransactionDetails {
|
||||||
|
transaction: Some(tx),
|
||||||
|
txid,
|
||||||
|
received: 1337,
|
||||||
|
sent: 420420,
|
||||||
|
fee: Some(140),
|
||||||
|
confirmation_time: Some(BlockTime {
|
||||||
|
timestamp: 123456,
|
||||||
|
height: 1000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
tree.set_tx(&tx_details).unwrap();
|
||||||
|
|
||||||
|
// get raw tx
|
||||||
|
assert_eq!(tree.iter_txs(true).unwrap(), vec![tx_details.clone()]);
|
||||||
|
|
||||||
|
// now get without raw tx
|
||||||
|
tx_details.transaction = None;
|
||||||
|
|
||||||
|
// get not raw tx
|
||||||
|
assert_eq!(tree.iter_txs(false).unwrap(), vec![tx_details.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn test_last_index<D: Database>(mut tree: D) {
|
pub fn test_last_index<D: Database>(mut tree: D) {
|
||||||
tree.set_last_index(KeychainKind::External, 1337).unwrap();
|
tree.set_last_index(KeychainKind::External, 1337).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
// <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
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
use bitcoin::hash_types::Txid;
|
use bitcoin::hash_types::Txid;
|
||||||
@@ -41,6 +43,16 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
|
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
|
||||||
"DROP TABLE transaction_details_old;",
|
"DROP TABLE transaction_details_old;",
|
||||||
"ALTER TABLE utxos ADD COLUMN is_spent;",
|
"ALTER TABLE utxos ADD COLUMN is_spent;",
|
||||||
|
// drop all data due to possible inconsistencies with duplicate utxos, re-sync required
|
||||||
|
"DELETE FROM checksums;",
|
||||||
|
"DELETE FROM last_derivation_indices;",
|
||||||
|
"DELETE FROM script_pubkeys;",
|
||||||
|
"DELETE FROM sync_time;",
|
||||||
|
"DELETE FROM transaction_details;",
|
||||||
|
"DELETE FROM transactions;",
|
||||||
|
"DELETE FROM utxos;",
|
||||||
|
"DROP INDEX idx_txid_vout;",
|
||||||
|
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sqlite database stored on filesystem
|
/// Sqlite database stored on filesystem
|
||||||
@@ -50,7 +62,7 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SqliteDatabase {
|
pub struct SqliteDatabase {
|
||||||
/// Path on the local filesystem to store the sqlite file
|
/// Path on the local filesystem to store the sqlite file
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
/// A rusqlite connection object to the sqlite database
|
/// A rusqlite connection object to the sqlite database
|
||||||
pub connection: Connection,
|
pub connection: Connection,
|
||||||
}
|
}
|
||||||
@@ -58,9 +70,12 @@ pub struct SqliteDatabase {
|
|||||||
impl SqliteDatabase {
|
impl SqliteDatabase {
|
||||||
/// Instantiate a new SqliteDatabase instance by creating a connection
|
/// Instantiate a new SqliteDatabase instance by creating a connection
|
||||||
/// to the database stored at path
|
/// to the database stored at path
|
||||||
pub fn new(path: String) -> Self {
|
pub fn new<T: AsRef<Path>>(path: T) -> Self {
|
||||||
let connection = get_connection(&path).unwrap();
|
let connection = get_connection(&path).unwrap();
|
||||||
SqliteDatabase { path, connection }
|
SqliteDatabase {
|
||||||
|
path: PathBuf::from(path.as_ref()),
|
||||||
|
connection,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn insert_script_pubkey(
|
fn insert_script_pubkey(
|
||||||
&self,
|
&self,
|
||||||
@@ -86,7 +101,7 @@ impl SqliteDatabase {
|
|||||||
script: &[u8],
|
script: &[u8],
|
||||||
is_spent: bool,
|
is_spent: bool,
|
||||||
) -> Result<i64, Error> {
|
) -> Result<i64, Error> {
|
||||||
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script, is_spent) VALUES (:value, :keychain, :vout, :txid, :script, :is_spent)")?;
|
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script, is_spent) VALUES (:value, :keychain, :vout, :txid, :script, :is_spent) ON CONFLICT(txid, vout) DO UPDATE SET value=:value, keychain=:keychain, script=:script, is_spent=:is_spent")?;
|
||||||
statement.execute(named_params! {
|
statement.execute(named_params! {
|
||||||
":value": value,
|
":value": value,
|
||||||
":keychain": keychain,
|
":keychain": keychain,
|
||||||
@@ -390,7 +405,7 @@ impl SqliteDatabase {
|
|||||||
let sent: u64 = row.get(3)?;
|
let sent: u64 = row.get(3)?;
|
||||||
let fee: Option<u64> = row.get(4)?;
|
let fee: Option<u64> = row.get(4)?;
|
||||||
let height: Option<u32> = row.get(5)?;
|
let height: Option<u32> = row.get(5)?;
|
||||||
let raw_tx: Option<Vec<u8>> = row.get(7)?;
|
let raw_tx: Option<Vec<u8>> = row.get(6)?;
|
||||||
let tx: Option<Transaction> = match raw_tx {
|
let tx: Option<Transaction> = match raw_tx {
|
||||||
Some(raw_tx) => {
|
Some(raw_tx) => {
|
||||||
let tx: Transaction = deserialize(&raw_tx)?;
|
let tx: Transaction = deserialize(&raw_tx)?;
|
||||||
@@ -881,10 +896,6 @@ impl Database for SqliteDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BatchDatabase for SqliteDatabase {
|
impl BatchDatabase for SqliteDatabase {
|
||||||
@@ -902,7 +913,7 @@ impl BatchDatabase for SqliteDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connection(path: &str) -> Result<Connection, Error> {
|
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
|
||||||
let connection = Connection::open(path)?;
|
let connection = Connection::open(path)?;
|
||||||
migrate(&connection)?;
|
migrate(&connection)?;
|
||||||
Ok(connection)
|
Ok(connection)
|
||||||
@@ -1020,4 +1031,9 @@ pub mod test {
|
|||||||
fn test_sync_time() {
|
fn test_sync_time() {
|
||||||
crate::database::test::test_sync_time(get_database());
|
crate::database::test::test_sync_time(get_database());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_txs() {
|
||||||
|
crate::database::test::test_list_transaction(get_database());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,10 @@
|
|||||||
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
||||||
//! checksum of a descriptor
|
//! checksum of a descriptor
|
||||||
|
|
||||||
use std::iter::FromIterator;
|
|
||||||
|
|
||||||
use crate::descriptor::DescriptorError;
|
use crate::descriptor::DescriptorError;
|
||||||
|
|
||||||
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||||
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||||
let c0 = c >> 35;
|
let c0 = c >> 35;
|
||||||
@@ -43,15 +41,17 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
|||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the checksum of a descriptor
|
/// Computes the checksum bytes of a descriptor
|
||||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||||
let mut c = 1;
|
let mut c = 1;
|
||||||
let mut cls = 0;
|
let mut cls = 0;
|
||||||
let mut clscount = 0;
|
let mut clscount = 0;
|
||||||
for ch in desc.chars() {
|
|
||||||
|
for ch in desc.as_bytes() {
|
||||||
let pos = INPUT_CHARSET
|
let pos = INPUT_CHARSET
|
||||||
.find(ch)
|
.iter()
|
||||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
|
.position(|b| b == ch)
|
||||||
|
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
|
||||||
c = poly_mod(c, pos & 31);
|
c = poly_mod(c, pos & 31);
|
||||||
cls = cls * 3 + (pos >> 5);
|
cls = cls * 3 + (pos >> 5);
|
||||||
clscount += 1;
|
clscount += 1;
|
||||||
@@ -67,17 +67,18 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
|||||||
(0..8).for_each(|_| c = poly_mod(c, 0));
|
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||||
c ^= 1;
|
c ^= 1;
|
||||||
|
|
||||||
let mut chars = Vec::with_capacity(8);
|
let mut checksum = [0_u8; 8];
|
||||||
for j in 0..8 {
|
for j in 0..8 {
|
||||||
chars.push(
|
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||||
CHECKSUM_CHARSET
|
|
||||||
.chars()
|
|
||||||
.nth(((c >> (5 * (7 - j))) & 31) as usize)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_iter(chars))
|
Ok(checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the checksum of a descriptor
|
||||||
|
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||||
|
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||||
|
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -97,17 +98,12 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_checksum_invalid_character() {
|
fn test_get_checksum_invalid_character() {
|
||||||
let sparkle_heart = vec![240, 159, 146, 150];
|
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||||
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
|
|
||||||
.unwrap()
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.unwrap();
|
|
||||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_checksum(&invalid_desc).err(),
|
get_checksum(&invalid_desc).err(),
|
||||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
|
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::hashes::hash160;
|
use bitcoin::hashes::hash160;
|
||||||
use bitcoin::PublicKey;
|
use bitcoin::{PublicKey, XOnlyPublicKey};
|
||||||
|
|
||||||
use miniscript::{descriptor::Wildcard, Descriptor, DescriptorPublicKey};
|
use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard};
|
||||||
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
|
use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk};
|
||||||
|
|
||||||
use crate::wallet::utils::SecpCtx;
|
use crate::wallet::utils::SecpCtx;
|
||||||
|
|
||||||
@@ -128,21 +128,44 @@ impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
|
|||||||
fn is_uncompressed(&self) -> bool {
|
fn is_uncompressed(&self) -> bool {
|
||||||
self.0.is_uncompressed()
|
self.0.is_uncompressed()
|
||||||
}
|
}
|
||||||
fn serialized_len(&self) -> usize {
|
|
||||||
self.0.serialized_len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
||||||
fn to_public_key(&self) -> PublicKey {
|
fn to_public_key(&self) -> PublicKey {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
DescriptorPublicKey::SinglePub(ref spub) => spub.key.to_public_key(),
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
DescriptorPublicKey::XPub(ref xpub) => {
|
key: SinglePubKey::XOnly(_),
|
||||||
|
..
|
||||||
|
}) => panic!("Found x-only public key in non-tr descriptor"),
|
||||||
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::FullKey(ref pk),
|
||||||
|
..
|
||||||
|
}) => *pk,
|
||||||
|
DescriptorPublicKey::XPub(ref xpub) => PublicKey::new(
|
||||||
xpub.xkey
|
xpub.xkey
|
||||||
.derive_pub(self.1, &xpub.derivation_path)
|
.derive_pub(self.1, &xpub.derivation_path)
|
||||||
.expect("Shouldn't fail, only normal derivations")
|
.expect("Shouldn't fail, only normal derivations")
|
||||||
.public_key
|
.public_key,
|
||||||
}
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_x_only_pubkey(&self) -> XOnlyPublicKey {
|
||||||
|
match &self.0 {
|
||||||
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::XOnly(ref pk),
|
||||||
|
..
|
||||||
|
}) => *pk,
|
||||||
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::FullKey(ref pk),
|
||||||
|
..
|
||||||
|
}) => XOnlyPublicKey::from(pk.inner),
|
||||||
|
DescriptorPublicKey::XPub(ref xpub) => XOnlyPublicKey::from(
|
||||||
|
xpub.xkey
|
||||||
|
.derive_pub(self.1, &xpub.derivation_path)
|
||||||
|
.expect("Shouldn't fail, only normal derivations")
|
||||||
|
.public_key,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,48 @@ macro_rules! impl_top_level_pk {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_top_level_tr {
|
||||||
|
( $internal_key:expr, $tap_tree:expr ) => {{
|
||||||
|
use $crate::miniscript::descriptor::{
|
||||||
|
Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr,
|
||||||
|
};
|
||||||
|
use $crate::miniscript::Tap;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
||||||
|
|
||||||
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
|
$internal_key
|
||||||
|
.into_descriptor_key()
|
||||||
|
.and_then(|key: DescriptorKey<Tap>| key.extract(&secp))
|
||||||
|
.map_err($crate::descriptor::DescriptorError::Key)
|
||||||
|
.and_then(|(pk, mut key_map, mut valid_networks)| {
|
||||||
|
let tap_tree = $tap_tree.map(
|
||||||
|
|(tap_tree, tree_keymap, tree_networks): (
|
||||||
|
TapTree<DescriptorPublicKey>,
|
||||||
|
KeyMap,
|
||||||
|
ValidNetworks,
|
||||||
|
)| {
|
||||||
|
key_map.extend(tree_keymap.into_iter());
|
||||||
|
valid_networks =
|
||||||
|
$crate::keys::merge_networks(&valid_networks, &tree_networks);
|
||||||
|
|
||||||
|
tap_tree
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Descriptor::<DescriptorPublicKey>::Tr(Tr::new(pk, tap_tree)?),
|
||||||
|
key_map,
|
||||||
|
valid_networks,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_leaf_opcode {
|
macro_rules! impl_leaf_opcode {
|
||||||
@@ -228,6 +270,62 @@ macro_rules! impl_sortedmulti {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! parse_tap_tree {
|
||||||
|
( @merge $tree_a:expr, $tree_b:expr) => {{
|
||||||
|
use std::sync::Arc;
|
||||||
|
use $crate::miniscript::descriptor::TapTree;
|
||||||
|
|
||||||
|
$tree_a
|
||||||
|
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
|
||||||
|
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
|
||||||
|
a_keymap.extend(b_keymap.into_iter());
|
||||||
|
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||||
|
})
|
||||||
|
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Two sub-trees
|
||||||
|
( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{
|
||||||
|
let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
|
||||||
|
let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
|
||||||
|
|
||||||
|
$crate::parse_tap_tree!(@merge tree_a, tree_b)
|
||||||
|
}};
|
||||||
|
|
||||||
|
// One leaf and a sub-tree
|
||||||
|
( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{
|
||||||
|
let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
|
||||||
|
let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
|
||||||
|
|
||||||
|
$crate::parse_tap_tree!(@merge tree_a, tree_b)
|
||||||
|
}};
|
||||||
|
( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
|
||||||
|
let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
|
||||||
|
let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
|
||||||
|
|
||||||
|
$crate::parse_tap_tree!(@merge tree_a, tree_b)
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Two leaves
|
||||||
|
( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
|
||||||
|
let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
|
||||||
|
let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
|
||||||
|
|
||||||
|
$crate::parse_tap_tree!(@merge tree_a, tree_b)
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Single leaf
|
||||||
|
( $op:ident ( $( $minisc:tt )* ) ) => {{
|
||||||
|
use std::sync::Arc;
|
||||||
|
use $crate::miniscript::descriptor::TapTree;
|
||||||
|
|
||||||
|
$crate::fragment!( $op ( $( $minisc )* ) )
|
||||||
|
.map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! apply_modifier {
|
macro_rules! apply_modifier {
|
||||||
@@ -441,6 +539,15 @@ macro_rules! descriptor {
|
|||||||
( wsh ( $( $minisc:tt )* ) ) => ({
|
( wsh ( $( $minisc:tt )* ) ) => ({
|
||||||
$crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
|
$crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
( tr ( $internal_key:expr ) ) => ({
|
||||||
|
$crate::impl_top_level_tr!($internal_key, None)
|
||||||
|
});
|
||||||
|
( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({
|
||||||
|
let tap_tree = $crate::parse_tap_tree!( $( $taptree )* );
|
||||||
|
tap_tree
|
||||||
|
.and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree)))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -480,6 +587,23 @@ impl<A, B, C> From<(A, (B, (C, ())))> for TupleThree<A, B, C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! group_multi_keys {
|
||||||
|
( $( $key:expr ),+ ) => {{
|
||||||
|
use $crate::keys::IntoDescriptorKey;
|
||||||
|
|
||||||
|
let keys = vec![
|
||||||
|
$(
|
||||||
|
$key.into_descriptor_key(),
|
||||||
|
)*
|
||||||
|
];
|
||||||
|
|
||||||
|
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err($crate::descriptor::DescriptorError::Key)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fragment_internal {
|
macro_rules! fragment_internal {
|
||||||
@@ -640,21 +764,22 @@ macro_rules! fragment {
|
|||||||
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
|
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
|
||||||
});
|
});
|
||||||
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
|
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||||
$crate::keys::make_multi($thresh, $keys)
|
|
||||||
});
|
|
||||||
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
|
||||||
use $crate::keys::IntoDescriptorKey;
|
|
||||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
let keys = vec![
|
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
|
||||||
$(
|
});
|
||||||
$key.into_descriptor_key(),
|
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||||
)*
|
$crate::group_multi_keys!( $( $key ),* )
|
||||||
];
|
.and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) ))
|
||||||
|
});
|
||||||
|
( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||||
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
|
||||||
.map_err($crate::descriptor::DescriptorError::Key)
|
});
|
||||||
.and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp))
|
( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||||
|
$crate::group_multi_keys!( $( $key ),* )
|
||||||
|
.and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) ))
|
||||||
});
|
});
|
||||||
|
|
||||||
// `sortedmulti()` is handled separately
|
// `sortedmulti()` is handled separately
|
||||||
@@ -714,7 +839,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
// - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
||||||
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
||||||
|
|
||||||
// expected script for pk and bare manually created
|
// expected script for pk and bare manually created
|
||||||
@@ -1054,7 +1179,9 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
|
#[should_panic(
|
||||||
|
expected = "Miniscript(ContextError(CompressedOnly(\"04b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a87378ec38ff91d43e8c2092ebda601780485263da089465619e0358a5c1be7ac91f4\")))"
|
||||||
|
)]
|
||||||
fn test_dsl_miniscript_checks() {
|
fn test_dsl_miniscript_checks() {
|
||||||
let mut uncompressed_pk =
|
let mut uncompressed_pk =
|
||||||
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
||||||
@@ -1062,4 +1189,35 @@ mod test {
|
|||||||
|
|
||||||
descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
|
descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dsl_tr_only_key() {
|
||||||
|
let private_key =
|
||||||
|
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
||||||
|
let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
descriptor.to_string(),
|
||||||
|
"tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dsl_tr_simple_tree() {
|
||||||
|
let private_key =
|
||||||
|
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
||||||
|
let (descriptor, _, _) =
|
||||||
|
descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dsl_tr_single_leaf() {
|
||||||
|
let private_key =
|
||||||
|
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
||||||
|
let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ pub enum Error {
|
|||||||
/// Error while extracting and manipulating policies
|
/// Error while extracting and manipulating policies
|
||||||
Policy(crate::descriptor::policy::PolicyError),
|
Policy(crate::descriptor::policy::PolicyError),
|
||||||
|
|
||||||
/// Invalid character found in the descriptor checksum
|
/// Invalid byte found in the descriptor checksum
|
||||||
InvalidDescriptorCharacter(char),
|
InvalidDescriptorCharacter(u8),
|
||||||
|
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
Bip32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
|
|||||||
@@ -14,14 +14,15 @@
|
|||||||
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
||||||
//! from [`miniscript`].
|
//! from [`miniscript`].
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||||
use bitcoin::util::psbt;
|
use bitcoin::util::{psbt, taproot};
|
||||||
use bitcoin::{Network, PublicKey, Script, TxOut};
|
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||||
|
use bitcoin::{Network, Script, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorType, InnerXKey};
|
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
|
||||||
pub use miniscript::{
|
pub use miniscript::{
|
||||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||||
@@ -58,7 +59,14 @@ pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
|||||||
///
|
///
|
||||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||||
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||||
pub type HdKeyPaths = BTreeMap<PublicKey, KeySource>;
|
pub type HdKeyPaths = BTreeMap<secp256k1::PublicKey, KeySource>;
|
||||||
|
|
||||||
|
/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or
|
||||||
|
/// [`psbt::Output`]
|
||||||
|
///
|
||||||
|
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||||
|
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||||
|
pub type TapKeyOrigins = BTreeMap<bitcoin::XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
|
||||||
|
|
||||||
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
||||||
pub trait IntoWalletDescriptor {
|
pub trait IntoWalletDescriptor {
|
||||||
@@ -126,13 +134,13 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
|||||||
|
|
||||||
let check_key = |pk: &DescriptorPublicKey| {
|
let check_key = |pk: &DescriptorPublicKey| {
|
||||||
let (pk, _, networks) = if self.0.is_witness() {
|
let (pk, _, networks) = if self.0.is_witness() {
|
||||||
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||||
pk.clone().into_descriptor_key()?;
|
pk.clone().into_descriptor_key()?;
|
||||||
desciptor_key.extract(secp)?
|
descriptor_key.extract(secp)?
|
||||||
} else {
|
} else {
|
||||||
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||||
pk.clone().into_descriptor_key()?;
|
pk.clone().into_descriptor_key()?;
|
||||||
desciptor_key.extract(secp)?
|
descriptor_key.extract(secp)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if networks.contains(&network) {
|
if networks.contains(&network) {
|
||||||
@@ -301,17 +309,29 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DerivedDescriptorMeta {
|
pub(crate) trait DerivedDescriptorMeta {
|
||||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError>;
|
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths;
|
||||||
|
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DescriptorMeta {
|
pub(crate) trait DescriptorMeta {
|
||||||
fn is_witness(&self) -> bool;
|
fn is_witness(&self) -> bool;
|
||||||
|
fn is_taproot(&self) -> bool;
|
||||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
||||||
fn derive_from_hd_keypaths<'s>(
|
fn derive_from_hd_keypaths<'s>(
|
||||||
&self,
|
&self,
|
||||||
hd_keypaths: &HdKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>>;
|
) -> Option<DerivedDescriptor<'s>>;
|
||||||
|
fn derive_from_tap_key_origins<'s>(
|
||||||
|
&self,
|
||||||
|
tap_key_origins: &TapKeyOrigins,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Option<DerivedDescriptor<'s>>;
|
||||||
|
fn derive_from_psbt_key_origins<'s>(
|
||||||
|
&self,
|
||||||
|
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Option<DerivedDescriptor<'s>>;
|
||||||
fn derive_from_psbt_input<'s>(
|
fn derive_from_psbt_input<'s>(
|
||||||
&self,
|
&self,
|
||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
@@ -328,23 +348,34 @@ pub(crate) trait DescriptorScripts {
|
|||||||
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
||||||
fn psbt_redeem_script(&self) -> Option<Script> {
|
fn psbt_redeem_script(&self) -> Option<Script> {
|
||||||
match self.desc_type() {
|
match self.desc_type() {
|
||||||
DescriptorType::ShWpkh => Some(self.explicit_script()),
|
DescriptorType::ShWpkh => Some(self.explicit_script().unwrap()),
|
||||||
DescriptorType::ShWsh => Some(self.explicit_script().to_v0_p2wsh()),
|
DescriptorType::ShWsh => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
||||||
DescriptorType::Sh => Some(self.explicit_script()),
|
DescriptorType::Sh => Some(self.explicit_script().unwrap()),
|
||||||
DescriptorType::Bare => Some(self.explicit_script()),
|
DescriptorType::Bare => Some(self.explicit_script().unwrap()),
|
||||||
DescriptorType::ShSortedMulti => Some(self.explicit_script()),
|
DescriptorType::ShSortedMulti => Some(self.explicit_script().unwrap()),
|
||||||
_ => None,
|
DescriptorType::ShWshSortedMulti => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
||||||
|
DescriptorType::Pkh
|
||||||
|
| DescriptorType::Wpkh
|
||||||
|
| DescriptorType::Tr
|
||||||
|
| DescriptorType::Wsh
|
||||||
|
| DescriptorType::WshSortedMulti => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn psbt_witness_script(&self) -> Option<Script> {
|
fn psbt_witness_script(&self) -> Option<Script> {
|
||||||
match self.desc_type() {
|
match self.desc_type() {
|
||||||
DescriptorType::Wsh => Some(self.explicit_script()),
|
DescriptorType::Wsh => Some(self.explicit_script().unwrap()),
|
||||||
DescriptorType::ShWsh => Some(self.explicit_script()),
|
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
|
||||||
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
||||||
Some(self.explicit_script())
|
Some(self.explicit_script().unwrap())
|
||||||
}
|
}
|
||||||
_ => None,
|
DescriptorType::Bare
|
||||||
|
| DescriptorType::Sh
|
||||||
|
| DescriptorType::Pkh
|
||||||
|
| DescriptorType::Wpkh
|
||||||
|
| DescriptorType::ShSortedMulti
|
||||||
|
| DescriptorType::Tr
|
||||||
|
| DescriptorType::ShWpkh => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,6 +393,10 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_taproot(&self) -> bool {
|
||||||
|
self.desc_type() == DescriptorType::Tr
|
||||||
|
}
|
||||||
|
|
||||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
||||||
let mut answer = Vec::new();
|
let mut answer = Vec::new();
|
||||||
|
|
||||||
@@ -376,61 +411,124 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
Ok(answer)
|
Ok(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_from_hd_keypaths<'s>(
|
fn derive_from_psbt_key_origins<'s>(
|
||||||
&self,
|
&self,
|
||||||
hd_keypaths: &HdKeyPaths,
|
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||||
secp: &'s SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<DerivedDescriptor<'s>> {
|
) -> Option<DerivedDescriptor<'s>> {
|
||||||
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
|
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||||
|
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||||
|
path: &DerivationPath,
|
||||||
|
expected: &SinglePubKey| {
|
||||||
|
let derived = xpub
|
||||||
|
.xkey
|
||||||
|
.derive_pub(secp, path)
|
||||||
|
.expect("The path should never contain hardened derivation steps")
|
||||||
|
.public_key;
|
||||||
|
|
||||||
|
match expected {
|
||||||
|
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||||
|
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut path_found = None;
|
let mut path_found = None;
|
||||||
self.for_each_key(|key| {
|
|
||||||
if path_found.is_some() {
|
|
||||||
// already found a matching path, we are done
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// using `for_any_key` should make this stop as soon as we return `true`
|
||||||
|
self.for_any_key(|key| {
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||||
// Check if the key matches one entry in our `index`. If it does, `matches()` will
|
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
|
||||||
// return the "prefix" that matched, so we remove that prefix from the full path
|
// return the "prefix" that matched, so we remove that prefix from the full path
|
||||||
// found in `index` and save it in `derive_path`. We expect this to be a derivation
|
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
|
||||||
// path of length 1 if the key is `wildcard` and an empty path otherwise.
|
// path of length 1 if the key is `wildcard` and an empty path otherwise.
|
||||||
let root_fingerprint = xpub.root_fingerprint(secp);
|
let root_fingerprint = xpub.root_fingerprint(secp);
|
||||||
let derivation_path: Option<Vec<ChildNumber>> = index
|
let derive_path = key_origins
|
||||||
.get_key_value(&root_fingerprint)
|
.get_key_value(&root_fingerprint)
|
||||||
.and_then(|(fingerprint, path)| {
|
.and_then(|(fingerprint, (path, expected))| {
|
||||||
xpub.matches(&(**fingerprint, (*path).clone()), secp)
|
xpub.matches(&(*fingerprint, (*path).clone()), secp)
|
||||||
|
.zip(Some((path, expected)))
|
||||||
})
|
})
|
||||||
.map(|prefix| {
|
.and_then(|(prefix, (full_path, expected))| {
|
||||||
index
|
let derive_path = full_path
|
||||||
.get(&xpub.root_fingerprint(secp))
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.skip(prefix.into_iter().count())
|
.skip(prefix.into_iter().count())
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect::<DerivationPath>();
|
||||||
|
|
||||||
|
// `derive_path` only contains the replacement index for the wildcard, if present, or
|
||||||
|
// an empty path for fixed descriptors. To verify the key we also need the normal steps
|
||||||
|
// that come before the wildcard, so we take them directly from `xpub` and then append
|
||||||
|
// the final index
|
||||||
|
if verify_key(
|
||||||
|
xpub,
|
||||||
|
&xpub.derivation_path.extend(derive_path.clone()),
|
||||||
|
expected,
|
||||||
|
) {
|
||||||
|
Some(derive_path)
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Key `{}` derived with {} yields an unexpected key",
|
||||||
|
root_fingerprint,
|
||||||
|
derive_path
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
match derivation_path {
|
match derive_path {
|
||||||
Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
|
Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
|
||||||
// Ignore hardened wildcards
|
// Ignore hardened wildcards
|
||||||
if let ChildNumber::Normal { index } = path[0] {
|
if let ChildNumber::Normal { index } = path[0] {
|
||||||
path_found = Some(index)
|
path_found = Some(index);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
|
Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
|
||||||
path_found = Some(0)
|
path_found = Some(0);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
false
|
||||||
});
|
});
|
||||||
|
|
||||||
path_found.map(|path| self.as_derived(path, secp))
|
path_found.map(|path| self.as_derived(path, secp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn derive_from_hd_keypaths<'s>(
|
||||||
|
&self,
|
||||||
|
hd_keypaths: &HdKeyPaths,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Option<DerivedDescriptor<'s>> {
|
||||||
|
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
||||||
|
let key_origins = hd_keypaths
|
||||||
|
.iter()
|
||||||
|
.map(|(pk, (fingerprint, path))| {
|
||||||
|
(
|
||||||
|
*fingerprint,
|
||||||
|
(path, SinglePubKey::FullKey(PublicKey::new(*pk))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.derive_from_psbt_key_origins(key_origins, secp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_from_tap_key_origins<'s>(
|
||||||
|
&self,
|
||||||
|
tap_key_origins: &TapKeyOrigins,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Option<DerivedDescriptor<'s>> {
|
||||||
|
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
|
||||||
|
let key_origins = tap_key_origins
|
||||||
|
.iter()
|
||||||
|
.map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk))))
|
||||||
|
.collect();
|
||||||
|
self.derive_from_psbt_key_origins(key_origins, secp)
|
||||||
|
}
|
||||||
|
|
||||||
fn derive_from_psbt_input<'s>(
|
fn derive_from_psbt_input<'s>(
|
||||||
&self,
|
&self,
|
||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
@@ -440,6 +538,9 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||||
return Some(derived);
|
return Some(derived);
|
||||||
}
|
}
|
||||||
|
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
||||||
|
return Some(derived);
|
||||||
|
}
|
||||||
if self.is_deriveable() {
|
if self.is_deriveable() {
|
||||||
// We can't try to bruteforce the derivation index, exit here
|
// We can't try to bruteforce the derivation index, exit here
|
||||||
return None;
|
return None;
|
||||||
@@ -448,7 +549,10 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
let descriptor = self.as_derived_fixed(secp);
|
let descriptor = self.as_derived_fixed(secp);
|
||||||
match descriptor.desc_type() {
|
match descriptor.desc_type() {
|
||||||
// TODO: add pk() here
|
// TODO: add pk() here
|
||||||
DescriptorType::Pkh | DescriptorType::Wpkh | DescriptorType::ShWpkh
|
DescriptorType::Pkh
|
||||||
|
| DescriptorType::Wpkh
|
||||||
|
| DescriptorType::ShWpkh
|
||||||
|
| DescriptorType::Tr
|
||||||
if utxo.is_some()
|
if utxo.is_some()
|
||||||
&& descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
|
&& descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
|
||||||
{
|
{
|
||||||
@@ -456,7 +560,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
}
|
}
|
||||||
DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
|
DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
|
||||||
if psbt_input.redeem_script.is_some()
|
if psbt_input.redeem_script.is_some()
|
||||||
&& &descriptor.explicit_script()
|
&& &descriptor.explicit_script().unwrap()
|
||||||
== psbt_input.redeem_script.as_ref().unwrap() =>
|
== psbt_input.redeem_script.as_ref().unwrap() =>
|
||||||
{
|
{
|
||||||
Some(descriptor)
|
Some(descriptor)
|
||||||
@@ -466,7 +570,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
| DescriptorType::ShWshSortedMulti
|
| DescriptorType::ShWshSortedMulti
|
||||||
| DescriptorType::WshSortedMulti
|
| DescriptorType::WshSortedMulti
|
||||||
if psbt_input.witness_script.is_some()
|
if psbt_input.witness_script.is_some()
|
||||||
&& &descriptor.explicit_script()
|
&& &descriptor.explicit_script().unwrap()
|
||||||
== psbt_input.witness_script.as_ref().unwrap() =>
|
== psbt_input.witness_script.as_ref().unwrap() =>
|
||||||
{
|
{
|
||||||
Some(descriptor)
|
Some(descriptor)
|
||||||
@@ -477,7 +581,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
||||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError> {
|
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths {
|
||||||
let mut answer = BTreeMap::new();
|
let mut answer = BTreeMap::new();
|
||||||
self.for_each_key(|key| {
|
self.for_each_key(|key| {
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||||
@@ -495,7 +599,64 @@ impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
|||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(answer)
|
answer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins {
|
||||||
|
use miniscript::ToPublicKey;
|
||||||
|
|
||||||
|
let mut answer = BTreeMap::new();
|
||||||
|
let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| {
|
||||||
|
let key_origin = match pk.deref() {
|
||||||
|
DescriptorPublicKey::XPub(xpub) => {
|
||||||
|
Some((xpub.root_fingerprint(secp), xpub.full_path(&[])))
|
||||||
|
}
|
||||||
|
DescriptorPublicKey::SinglePub(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is the internal key, we only insert the key origin if it's not None.
|
||||||
|
// For keys found in the tap tree we always insert a key origin (because the signer
|
||||||
|
// looks for it to know which leaves to sign for), even though it may be None
|
||||||
|
match (lh, key_origin) {
|
||||||
|
(None, Some(ko)) => {
|
||||||
|
answer
|
||||||
|
.entry(pk.to_x_only_pubkey())
|
||||||
|
.or_insert_with(|| (vec![], ko));
|
||||||
|
}
|
||||||
|
(Some(lh), origin) => {
|
||||||
|
answer
|
||||||
|
.entry(pk.to_x_only_pubkey())
|
||||||
|
.or_insert_with(|| (vec![], origin.unwrap_or_default()))
|
||||||
|
.0
|
||||||
|
.push(lh);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Descriptor::Tr(tr) = &self {
|
||||||
|
// Internal key first, then iterate the scripts
|
||||||
|
insert_path(tr.internal_key(), None);
|
||||||
|
|
||||||
|
for (_, ms) in tr.iter_scripts() {
|
||||||
|
// Assume always the same leaf version
|
||||||
|
let leaf_hash = taproot::TapLeafHash::from_script(
|
||||||
|
&ms.encode(),
|
||||||
|
taproot::LeafVersion::TapScript,
|
||||||
|
);
|
||||||
|
|
||||||
|
for key in ms.iter_pk_pkh() {
|
||||||
|
let key = match key {
|
||||||
|
miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk,
|
||||||
|
miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk,
|
||||||
|
};
|
||||||
|
|
||||||
|
insert_path(&key, Some(leaf_hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +668,7 @@ mod test {
|
|||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::util::{bip32, psbt};
|
use bitcoin::util::{bip32, psbt};
|
||||||
|
use bitcoin::Script;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::psbt::PsbtUtils;
|
use crate::psbt::PsbtUtils;
|
||||||
@@ -770,4 +932,22 @@ mod test {
|
|||||||
DescriptorError::DuplicatedKeys
|
DescriptorError::DuplicatedKeys
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sh_wsh_sortedmulti_redeemscript() {
|
||||||
|
use super::{AsDerived, DescriptorScripts};
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
|
||||||
|
let (descriptor, _) =
|
||||||
|
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
||||||
|
|
||||||
|
let descriptor = descriptor.as_derived(0, &secp);
|
||||||
|
|
||||||
|
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
|
||||||
|
assert_eq!(descriptor.psbt_witness_script(), Some(script));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//! # use std::sync::Arc;
|
//! # use std::sync::Arc;
|
||||||
//! # use bdk::descriptor::*;
|
//! # use bdk::descriptor::*;
|
||||||
|
//! # use bdk::wallet::signer::*;
|
||||||
//! # use bdk::bitcoin::secp256k1::Secp256k1;
|
//! # use bdk::bitcoin::secp256k1::Secp256k1;
|
||||||
//! use bdk::descriptor::policy::BuildSatisfaction;
|
//! use bdk::descriptor::policy::BuildSatisfaction;
|
||||||
//! let secp = Secp256k1::new();
|
//! let secp = Secp256k1::new();
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?;
|
//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?;
|
||||||
//! println!("{:?}", extended_desc);
|
//! println!("{:?}", extended_desc);
|
||||||
//!
|
//!
|
||||||
//! let signers = Arc::new(key_map.into());
|
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
|
||||||
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
|
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
|
||||||
//! println!("policy: {}", serde_json::to_string(&policy)?);
|
//! println!("policy: {}", serde_json::to_string(&policy)?);
|
||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
@@ -44,59 +45,64 @@ use serde::{Serialize, Serializer};
|
|||||||
|
|
||||||
use bitcoin::hashes::*;
|
use bitcoin::hashes::*;
|
||||||
use bitcoin::util::bip32::Fingerprint;
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
use bitcoin::PublicKey;
|
use bitcoin::{PublicKey, XOnlyPublicKey};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner};
|
use miniscript::descriptor::{
|
||||||
|
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner,
|
||||||
|
};
|
||||||
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
|
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
use crate::descriptor::ExtractPolicy;
|
use crate::descriptor::ExtractPolicy;
|
||||||
|
use crate::keys::ExtScriptContext;
|
||||||
use crate::wallet::signer::{SignerId, SignersContainer};
|
use crate::wallet::signer::{SignerId, SignersContainer};
|
||||||
use crate::wallet::utils::{self, After, Older, SecpCtx};
|
use crate::wallet::utils::{self, After, Older, SecpCtx};
|
||||||
|
|
||||||
use super::checksum::get_checksum;
|
use super::checksum::get_checksum;
|
||||||
use super::error::Error;
|
use super::error::Error;
|
||||||
use super::XKeyUtils;
|
use super::XKeyUtils;
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
|
||||||
use miniscript::psbt::PsbtInputSatisfier;
|
use miniscript::psbt::PsbtInputSatisfier;
|
||||||
|
|
||||||
/// Raw public key or extended key fingerprint
|
/// A unique identifier for a key
|
||||||
#[derive(Debug, Clone, Default, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
pub struct PkOrF {
|
#[serde(rename_all = "snake_case")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub enum PkOrF {
|
||||||
pubkey: Option<PublicKey>,
|
/// A legacy public key
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
Pubkey(PublicKey),
|
||||||
pubkey_hash: Option<hash160::Hash>,
|
/// A x-only public key
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
XOnlyPubkey(XOnlyPublicKey),
|
||||||
fingerprint: Option<Fingerprint>,
|
/// An extended key fingerprint
|
||||||
|
Fingerprint(Fingerprint),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PkOrF {
|
impl PkOrF {
|
||||||
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
||||||
match k {
|
match k {
|
||||||
DescriptorPublicKey::SinglePub(pubkey) => PkOrF {
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
pubkey: Some(pubkey.key),
|
key: SinglePubKey::FullKey(pk),
|
||||||
..Default::default()
|
..
|
||||||
},
|
}) => PkOrF::Pubkey(*pk),
|
||||||
DescriptorPublicKey::XPub(xpub) => PkOrF {
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
fingerprint: Some(xpub.root_fingerprint(secp)),
|
key: SinglePubKey::XOnly(pk),
|
||||||
..Default::default()
|
..
|
||||||
},
|
}) => PkOrF::XOnlyPubkey(*pk),
|
||||||
|
DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An item that needs to be satisfied
|
/// An item that needs to be satisfied
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
||||||
pub enum SatisfiableItem {
|
pub enum SatisfiableItem {
|
||||||
// Leaves
|
// Leaves
|
||||||
/// Signature for a raw public key
|
/// ECDSA Signature for a raw public key
|
||||||
Signature(PkOrF),
|
EcdsaSignature(PkOrF),
|
||||||
/// Signature for an extended key fingerprint
|
/// Schnorr Signature for a raw public key
|
||||||
SignatureKey(PkOrF),
|
SchnorrSignature(PkOrF),
|
||||||
/// SHA256 preimage hash
|
/// SHA256 preimage hash
|
||||||
Sha256Preimage {
|
Sha256Preimage {
|
||||||
/// The digest value
|
/// The digest value
|
||||||
@@ -243,7 +249,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Represent if and how much a policy item is satisfied by the wallet's descriptor
|
/// Represent if and how much a policy item is satisfied by the wallet's descriptor
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
||||||
pub enum Satisfaction {
|
pub enum Satisfaction {
|
||||||
/// Only a partial satisfaction of some kind of threshold policy
|
/// Only a partial satisfaction of some kind of threshold policy
|
||||||
@@ -357,7 +363,8 @@ impl Satisfaction {
|
|||||||
// we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one
|
// we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one
|
||||||
// condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we
|
// condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we
|
||||||
// consider every possible options and check whether or not they are compatible.
|
// consider every possible options and check whether or not they are compatible.
|
||||||
.map(|i_vec| {
|
// since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out
|
||||||
|
.flat_map(|i_vec| {
|
||||||
mix(i_vec
|
mix(i_vec
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
@@ -371,9 +378,6 @@ impl Satisfaction {
|
|||||||
.map(|x| (i_vec.clone(), x))
|
.map(|x| (i_vec.clone(), x))
|
||||||
.collect::<Vec<(Vec<usize>, Vec<Condition>)>>()
|
.collect::<Vec<(Vec<usize>, Vec<Condition>)>>()
|
||||||
})
|
})
|
||||||
// .inspect(|x: &Vec<(Vec<usize>, Vec<Condition>)>| println!("fetch {:?}", x))
|
|
||||||
// since the previous step can turn one item of the iterator into multiple ones, we call flatten to expand them out
|
|
||||||
.flatten()
|
|
||||||
// .inspect(|x| println!("flat {:?}", x))
|
// .inspect(|x| println!("flat {:?}", x))
|
||||||
// try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err
|
// try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err
|
||||||
.map(|(key, val)| {
|
.map(|(key, val)| {
|
||||||
@@ -419,7 +423,7 @@ impl From<bool> for Satisfaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Descriptor spending policy
|
/// Descriptor spending policy
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct Policy {
|
pub struct Policy {
|
||||||
/// Identifier for this policy node
|
/// Identifier for this policy node
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -567,7 +571,7 @@ impl Policy {
|
|||||||
Ok(Some(policy))
|
Ok(Some(policy))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_multisig(
|
fn make_multisig<Ctx: ScriptContext + 'static>(
|
||||||
keys: &[DescriptorPublicKey],
|
keys: &[DescriptorPublicKey],
|
||||||
signers: &SignersContainer,
|
signers: &SignersContainer,
|
||||||
build_sat: BuildSatisfaction,
|
build_sat: BuildSatisfaction,
|
||||||
@@ -601,7 +605,7 @@ impl Policy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(psbt) = build_sat.psbt() {
|
if let Some(psbt) = build_sat.psbt() {
|
||||||
if signature_in_psbt(psbt, key, secp) {
|
if Ctx::find_signature(psbt, key, secp) {
|
||||||
satisfaction.add(
|
satisfaction.add(
|
||||||
&Satisfaction::Complete {
|
&Satisfaction::Complete {
|
||||||
condition: Default::default(),
|
condition: Default::default(),
|
||||||
@@ -717,18 +721,27 @@ impl From<SatisfiableItem> for Policy {
|
|||||||
|
|
||||||
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
||||||
match key {
|
match key {
|
||||||
DescriptorPublicKey::SinglePub(pubkey) => pubkey.key.to_pubkeyhash().into(),
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::FullKey(pk),
|
||||||
|
..
|
||||||
|
}) => pk.to_pubkeyhash().into(),
|
||||||
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::XOnly(pk),
|
||||||
|
..
|
||||||
|
}) => pk.to_pubkeyhash().into(),
|
||||||
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature(
|
fn make_generic_signature<M: Fn() -> SatisfiableItem, F: Fn(&Psbt) -> bool>(
|
||||||
key: &DescriptorPublicKey,
|
key: &DescriptorPublicKey,
|
||||||
signers: &SignersContainer,
|
signers: &SignersContainer,
|
||||||
build_sat: BuildSatisfaction,
|
build_sat: BuildSatisfaction,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
|
make_policy: M,
|
||||||
|
find_sig: F,
|
||||||
) -> Policy {
|
) -> Policy {
|
||||||
let mut policy: Policy = SatisfiableItem::Signature(PkOrF::from_key(key, secp)).into();
|
let mut policy: Policy = make_policy().into();
|
||||||
|
|
||||||
policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
|
policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
|
||||||
Satisfaction::Complete {
|
Satisfaction::Complete {
|
||||||
@@ -739,7 +752,7 @@ fn signature(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(psbt) = build_sat.psbt() {
|
if let Some(psbt) = build_sat.psbt() {
|
||||||
policy.satisfaction = if signature_in_psbt(psbt, key, secp) {
|
policy.satisfaction = if find_sig(psbt) {
|
||||||
Satisfaction::Complete {
|
Satisfaction::Complete {
|
||||||
condition: Default::default(),
|
condition: Default::default(),
|
||||||
}
|
}
|
||||||
@@ -751,26 +764,119 @@ fn signature(
|
|||||||
policy
|
policy
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
|
fn generic_sig_in_psbt<
|
||||||
|
// C is for "check", it's a closure we use to *check* if a psbt input contains the signature
|
||||||
|
// for a specific key
|
||||||
|
C: Fn(&PsbtInput, &SinglePubKey) -> bool,
|
||||||
|
// E is for "extract", it extracts a key from the bip32 derivations found in the psbt input
|
||||||
|
E: Fn(&PsbtInput, Fingerprint) -> Option<SinglePubKey>,
|
||||||
|
>(
|
||||||
|
psbt: &Psbt,
|
||||||
|
key: &DescriptorPublicKey,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
check: C,
|
||||||
|
extract: E,
|
||||||
|
) -> bool {
|
||||||
//TODO check signature validity
|
//TODO check signature validity
|
||||||
psbt.inputs.iter().all(|input| match key {
|
psbt.inputs.iter().all(|input| match key {
|
||||||
DescriptorPublicKey::SinglePub(key) => input.partial_sigs.contains_key(&key.key),
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
|
||||||
DescriptorPublicKey::XPub(xpub) => {
|
DescriptorPublicKey::XPub(xpub) => {
|
||||||
let pubkey = input
|
|
||||||
.bip32_derivation
|
|
||||||
.iter()
|
|
||||||
.find(|(_, (f, _))| *f == xpub.root_fingerprint(secp))
|
|
||||||
.map(|(p, _)| p);
|
|
||||||
//TODO check actual derivation matches
|
//TODO check actual derivation matches
|
||||||
match pubkey {
|
match extract(input, xpub.root_fingerprint(secp)) {
|
||||||
Some(pubkey) => input.partial_sigs.contains_key(pubkey),
|
Some(pubkey) => check(input, &pubkey),
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
|
trait SigExt: ScriptContext {
|
||||||
|
fn make_signature(
|
||||||
|
key: &DescriptorPublicKey,
|
||||||
|
signers: &SignersContainer,
|
||||||
|
build_sat: BuildSatisfaction,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Policy;
|
||||||
|
|
||||||
|
fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ScriptContext + 'static> SigExt for T {
|
||||||
|
fn make_signature(
|
||||||
|
key: &DescriptorPublicKey,
|
||||||
|
signers: &SignersContainer,
|
||||||
|
build_sat: BuildSatisfaction,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Policy {
|
||||||
|
if T::as_enum().is_taproot() {
|
||||||
|
make_generic_signature(
|
||||||
|
key,
|
||||||
|
signers,
|
||||||
|
build_sat,
|
||||||
|
secp,
|
||||||
|
|| SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)),
|
||||||
|
|psbt| Self::find_signature(psbt, key, secp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
make_generic_signature(
|
||||||
|
key,
|
||||||
|
signers,
|
||||||
|
build_sat,
|
||||||
|
secp,
|
||||||
|
|| SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)),
|
||||||
|
|psbt| Self::find_signature(psbt, key, secp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
|
||||||
|
if T::as_enum().is_taproot() {
|
||||||
|
generic_sig_in_psbt(
|
||||||
|
psbt,
|
||||||
|
key,
|
||||||
|
secp,
|
||||||
|
|input, pk| {
|
||||||
|
let pk = match pk {
|
||||||
|
SinglePubKey::XOnly(pk) => pk,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
input.tap_script_sigs.keys().any(|(sk, _)| sk == pk)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|input, fing| {
|
||||||
|
input
|
||||||
|
.tap_key_origins
|
||||||
|
.iter()
|
||||||
|
.find(|(_, (_, (f, _)))| f == &fing)
|
||||||
|
.map(|(pk, _)| SinglePubKey::XOnly(*pk))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
generic_sig_in_psbt(
|
||||||
|
psbt,
|
||||||
|
key,
|
||||||
|
secp,
|
||||||
|
|input, pk| match pk {
|
||||||
|
SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|input, fing| {
|
||||||
|
input
|
||||||
|
.bip32_derivation
|
||||||
|
.iter()
|
||||||
|
.find(|(_, (f, _))| f == &fing)
|
||||||
|
.map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
|
||||||
fn extract_policy(
|
fn extract_policy(
|
||||||
&self,
|
&self,
|
||||||
signers: &SignersContainer,
|
signers: &SignersContainer,
|
||||||
@@ -780,8 +886,10 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
|||||||
Ok(match &self.node {
|
Ok(match &self.node {
|
||||||
// Leaves
|
// Leaves
|
||||||
Terminal::True | Terminal::False => None,
|
Terminal::True | Terminal::False => None,
|
||||||
Terminal::PkK(pubkey) => Some(signature(pubkey, signers, build_sat, secp)),
|
Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)),
|
||||||
Terminal::PkH(pubkey_hash) => Some(signature(pubkey_hash, signers, build_sat, secp)),
|
Terminal::PkH(pubkey_hash) => {
|
||||||
|
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
|
||||||
|
}
|
||||||
Terminal::After(value) => {
|
Terminal::After(value) => {
|
||||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
||||||
policy.contribution = Satisfaction::Complete {
|
policy.contribution = Satisfaction::Complete {
|
||||||
@@ -842,8 +950,8 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
|||||||
Terminal::Hash160(hash) => {
|
Terminal::Hash160(hash) => {
|
||||||
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
|
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
|
||||||
}
|
}
|
||||||
Terminal::Multi(k, pks) => {
|
Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
|
||||||
Policy::make_multisig(pks, signers, build_sat, *k, false, secp)?
|
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
|
||||||
}
|
}
|
||||||
// Identities
|
// Identities
|
||||||
Terminal::Alt(inner)
|
Terminal::Alt(inner)
|
||||||
@@ -934,13 +1042,13 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
|||||||
build_sat: BuildSatisfaction,
|
build_sat: BuildSatisfaction,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<Option<Policy>, Error> {
|
) -> Result<Option<Policy>, Error> {
|
||||||
fn make_sortedmulti<Ctx: ScriptContext>(
|
fn make_sortedmulti<Ctx: ScriptContext + 'static>(
|
||||||
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
|
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
|
||||||
signers: &SignersContainer,
|
signers: &SignersContainer,
|
||||||
build_sat: BuildSatisfaction,
|
build_sat: BuildSatisfaction,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<Option<Policy>, Error> {
|
) -> Result<Option<Policy>, Error> {
|
||||||
Ok(Policy::make_multisig(
|
Ok(Policy::make_multisig::<Ctx>(
|
||||||
keys.pks.as_ref(),
|
keys.pks.as_ref(),
|
||||||
signers,
|
signers,
|
||||||
build_sat,
|
build_sat,
|
||||||
@@ -951,10 +1059,25 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
|
Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature(
|
||||||
Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
|
pk.as_inner(),
|
||||||
|
signers,
|
||||||
|
build_sat,
|
||||||
|
secp,
|
||||||
|
))),
|
||||||
|
Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
|
||||||
|
pk.as_inner(),
|
||||||
|
signers,
|
||||||
|
build_sat,
|
||||||
|
secp,
|
||||||
|
))),
|
||||||
Descriptor::Sh(sh) => match sh.as_inner() {
|
Descriptor::Sh(sh) => match sh.as_inner() {
|
||||||
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
|
ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
|
||||||
|
pk.as_inner(),
|
||||||
|
signers,
|
||||||
|
build_sat,
|
||||||
|
secp,
|
||||||
|
))),
|
||||||
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
|
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
|
||||||
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
|
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
|
||||||
ShInner::Wsh(wsh) => match wsh.as_inner() {
|
ShInner::Wsh(wsh) => match wsh.as_inner() {
|
||||||
@@ -969,6 +1092,28 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
|||||||
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
|
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
|
||||||
},
|
},
|
||||||
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
|
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
|
||||||
|
Descriptor::Tr(tr) => {
|
||||||
|
// If there's no tap tree, treat this as a single sig, otherwise build a `Thresh`
|
||||||
|
// node with threshold = 1 and the key spend signature plus all the tree leaves
|
||||||
|
let key_spend_sig =
|
||||||
|
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
|
||||||
|
|
||||||
|
if tr.taptree().is_none() {
|
||||||
|
Ok(Some(key_spend_sig))
|
||||||
|
} else {
|
||||||
|
let mut items = vec![key_spend_sig];
|
||||||
|
items.append(
|
||||||
|
&mut tr
|
||||||
|
.iter_scripts()
|
||||||
|
.filter_map(|(_, ms)| {
|
||||||
|
ms.extract_policy(signers, build_sat, secp).transpose()
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Policy::make_thresh(items, 1)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -980,7 +1125,7 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::descriptor::derived::AsDerived;
|
use crate::descriptor::derived::AsDerived;
|
||||||
use crate::descriptor::policy::SatisfiableItem::{Multisig, Signature, Thresh};
|
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
|
||||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||||
use crate::wallet::signer::SignersContainer;
|
use crate::wallet::signer::SignersContainer;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
@@ -1002,7 +1147,7 @@ mod test {
|
|||||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||||
let path = bip32::DerivationPath::from_str(path).unwrap();
|
let path = bip32::DerivationPath::from_str(path).unwrap();
|
||||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||||
let tpub = bip32::ExtendedPubKey::from_private(secp, &tprv);
|
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
|
||||||
let fingerprint = tprv.fingerprint(secp);
|
let fingerprint = tprv.fingerprint(secp);
|
||||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||||
@@ -1021,30 +1166,26 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||||
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
|
|
||||||
);
|
|
||||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||||
|
|
||||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||||
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||||
);
|
);
|
||||||
@@ -1060,7 +1201,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1068,8 +1209,8 @@ mod test {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
// TODO should this be "Satisfaction::None" since we have no prv keys?
|
// TODO should this be "Satisfaction::None" since we have no prv keys?
|
||||||
// TODO should items and conditions not be empty?
|
// TODO should items and conditions not be empty?
|
||||||
@@ -1092,15 +1233,15 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1124,7 +1265,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1132,8 +1273,8 @@ mod test {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||||
@@ -1156,7 +1297,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1164,8 +1305,8 @@ mod test {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
||||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1189,15 +1330,13 @@ mod test {
|
|||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
let single_key = wallet_desc.derive(0);
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = single_key
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||||
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
|
|
||||||
);
|
|
||||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||||
|
|
||||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||||
@@ -1205,15 +1344,13 @@ mod test {
|
|||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
let single_key = wallet_desc.derive(0);
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = single_key
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||||
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||||
);
|
);
|
||||||
@@ -1232,7 +1369,7 @@ mod test {
|
|||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let single_key = wallet_desc.derive(0);
|
let single_key = wallet_desc.derive(0);
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = single_key
|
let policy = single_key
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1240,8 +1377,8 @@ mod test {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||||
@@ -1275,7 +1412,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1314,7 +1451,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1339,7 +1476,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1357,7 +1494,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1379,10 +1516,10 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers = keymap.into();
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
let policy = wallet_desc
|
let policy = wallet_desc
|
||||||
.extract_policy(&signers, BuildSatisfaction::None, &secp)
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -1445,7 +1582,7 @@ mod test {
|
|||||||
addr.to_string()
|
addr.to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
|
let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
|
||||||
|
|
||||||
@@ -1506,7 +1643,7 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
let addr = wallet_desc
|
let addr = wallet_desc
|
||||||
.as_derived(0, &secp)
|
.as_derived(0, &secp)
|
||||||
@@ -1594,9 +1731,185 @@ mod test {
|
|||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp);
|
let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp);
|
||||||
assert!(policy.is_ok());
|
assert!(policy.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_tr_key_spend() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
|
||||||
|
let desc = descriptor!(tr(prvkey)).unwrap();
|
||||||
|
let (wallet_desc, keymap) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
|
let policy = wallet_desc
|
||||||
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
policy,
|
||||||
|
Some(Policy {
|
||||||
|
id: "48u0tz0n".to_string(),
|
||||||
|
item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)),
|
||||||
|
satisfaction: Satisfaction::None,
|
||||||
|
contribution: Satisfaction::Complete {
|
||||||
|
condition: Condition::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_tr_script_spend() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
|
||||||
|
let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap();
|
||||||
|
let (wallet_desc, keymap) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||||
|
|
||||||
|
let policy = wallet_desc
|
||||||
|
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1])
|
||||||
|
);
|
||||||
|
|
||||||
|
let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
|
||||||
|
let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
|
||||||
|
|
||||||
|
let thresh_items = match policy.item {
|
||||||
|
SatisfiableItem::Thresh { items, .. } => items,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(thresh_items[0].item, bob_sig);
|
||||||
|
assert_eq!(thresh_items[1].item, alice_sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_tr_satisfaction_key_spend() {
|
||||||
|
const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA==";
|
||||||
|
const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA=";
|
||||||
|
|
||||||
|
let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
|
||||||
|
let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
|
||||||
|
let desc = descriptor!(tr(pubkey)).unwrap();
|
||||||
|
let (wallet_desc, _) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let policy_unsigned = wallet_desc
|
||||||
|
.extract_policy(
|
||||||
|
&SignersContainer::default(),
|
||||||
|
BuildSatisfaction::Psbt(&unsigned_psbt),
|
||||||
|
&secp,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let policy_signed = wallet_desc
|
||||||
|
.extract_policy(
|
||||||
|
&SignersContainer::default(),
|
||||||
|
BuildSatisfaction::Psbt(&signed_psbt),
|
||||||
|
&secp,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(policy_unsigned.satisfaction, Satisfaction::None);
|
||||||
|
assert_eq!(
|
||||||
|
policy_signed.satisfaction,
|
||||||
|
Satisfaction::Complete {
|
||||||
|
condition: Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_tr_satisfaction_script_spend() {
|
||||||
|
const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
|
||||||
|
const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
|
||||||
|
|
||||||
|
let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
|
||||||
|
let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
|
||||||
|
let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap();
|
||||||
|
let (wallet_desc, _) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let policy_unsigned = wallet_desc
|
||||||
|
.extract_policy(
|
||||||
|
&SignersContainer::default(),
|
||||||
|
BuildSatisfaction::Psbt(&unsigned_psbt),
|
||||||
|
&secp,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let policy_signed = wallet_desc
|
||||||
|
.extract_policy(
|
||||||
|
&SignersContainer::default(),
|
||||||
|
BuildSatisfaction::Psbt(&signed_psbt),
|
||||||
|
&secp,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1])
|
||||||
|
);
|
||||||
|
|
||||||
|
let satisfied_items = match policy_signed.item {
|
||||||
|
SatisfiableItem::Thresh { items, .. } => items,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
satisfied_items[0].satisfaction,
|
||||||
|
Satisfaction::Complete {
|
||||||
|
condition: Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
satisfied_items[1].satisfaction,
|
||||||
|
Satisfaction::Complete {
|
||||||
|
condition: Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,18 +40,19 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
|||||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||||
/// use bdk::miniscript::Legacy;
|
/// use bdk::miniscript::Legacy;
|
||||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||||
|
/// use bitcoin::Network;
|
||||||
///
|
///
|
||||||
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
||||||
///
|
///
|
||||||
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
||||||
/// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait DescriptorTemplate {
|
pub trait DescriptorTemplate {
|
||||||
/// Build the complete descriptor
|
/// Build the complete descriptor
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError>;
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
|
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
|
||||||
@@ -62,7 +63,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
|||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
self.build()?.into_wallet_descriptor(secp, network)
|
self.build(network)?.into_wallet_descriptor(secp, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
|||||||
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||||
|
|
||||||
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
descriptor!(pkh(self.0))
|
descriptor!(pkh(self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
|||||||
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||||
|
|
||||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
descriptor!(sh(wpkh(self.0)))
|
descriptor!(sh(wpkh(self.0)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,12 +165,12 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
|||||||
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||||
|
|
||||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
descriptor!(wpkh(self.0))
|
descriptor!(wpkh(self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BIP44 template. Expands to `pkh(key/44'/0'/0'/{0,1}/*)`
|
/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
@@ -193,21 +194,21 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
|||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
|
/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
|
||||||
///
|
///
|
||||||
/// This assumes that the key used has already been derived with `m/44'/0'/0'`.
|
/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet.
|
||||||
///
|
///
|
||||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||||
///
|
///
|
||||||
@@ -240,12 +241,12 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
|||||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BIP49 template. Expands to `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))`
|
||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
@@ -269,15 +270,15 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
|||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,18 +311,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
|||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BIP84 template. Expands to `wpkh(key/84'/0'/0'/{0,1}/*)`
|
/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)`
|
||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
@@ -345,15 +346,15 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
|||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,8 +393,8 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
|||||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,10 +407,19 @@ macro_rules! expand_make_bipxx {
|
|||||||
bip: u32,
|
bip: u32,
|
||||||
key: K,
|
key: K,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
|
network: Network,
|
||||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||||
let mut derivation_path = Vec::with_capacity(4);
|
let mut derivation_path = Vec::with_capacity(4);
|
||||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
||||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
|
||||||
|
match network {
|
||||||
|
Network::Bitcoin => {
|
||||||
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||||
|
|
||||||
match keychain {
|
match keychain {
|
||||||
@@ -466,6 +476,40 @@ mod test {
|
|||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
|
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||||
|
#[test]
|
||||||
|
fn test_bip44_template_cointype() {
|
||||||
|
use bitcoin::util::bip32::ChildNumber::{self, Hardened};
|
||||||
|
|
||||||
|
let xprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||||
|
assert_eq!(Network::Bitcoin, xprvkey.network);
|
||||||
|
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
||||||
|
.build(Network::Bitcoin)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||||
|
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||||
|
let purpose = path.get(0).unwrap();
|
||||||
|
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||||
|
let coin_type = path.get(1).unwrap();
|
||||||
|
assert!(matches!(coin_type, Hardened { index: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
|
assert_eq!(Network::Testnet, tprvkey.network);
|
||||||
|
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
||||||
|
.build(Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||||
|
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||||
|
let purpose = path.get(0).unwrap();
|
||||||
|
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||||
|
let coin_type = path.get(1).unwrap();
|
||||||
|
assert!(matches!(coin_type, Hardened { index: 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// verify template descriptor generates expected address(es)
|
// verify template descriptor generates expected address(es)
|
||||||
fn check(
|
fn check(
|
||||||
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
|
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
|
||||||
@@ -497,7 +541,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Pkh(prvkey).build(),
|
P2Pkh(prvkey).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||||
@@ -508,7 +552,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Pkh(pubkey).build(),
|
P2Pkh(pubkey).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||||
@@ -522,7 +566,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Wpkh_P2Sh(prvkey).build(),
|
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||||
@@ -533,7 +577,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Wpkh_P2Sh(pubkey).build(),
|
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||||
@@ -547,7 +591,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Wpkh(prvkey).build(),
|
P2Wpkh(prvkey).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||||
@@ -558,7 +602,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2Wpkh(pubkey).build(),
|
P2Wpkh(pubkey).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||||
@@ -570,7 +614,7 @@ mod test {
|
|||||||
fn test_bip44_template() {
|
fn test_bip44_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip44(prvkey, KeychainKind::External).build(),
|
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -580,7 +624,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip44(prvkey, KeychainKind::Internal).build(),
|
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -597,7 +641,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -607,7 +651,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -623,7 +667,7 @@ mod test {
|
|||||||
fn test_bip49_template() {
|
fn test_bip49_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip49(prvkey, KeychainKind::External).build(),
|
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -633,7 +677,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip49(prvkey, KeychainKind::Internal).build(),
|
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -650,7 +694,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -660,7 +704,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -676,7 +720,7 @@ mod test {
|
|||||||
fn test_bip84_template() {
|
fn test_bip84_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip84(prvkey, KeychainKind::External).build(),
|
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -686,7 +730,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip84(prvkey, KeychainKind::Internal).build(),
|
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -703,7 +747,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -713,7 +757,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
#[doc(include = "../README.md")]
|
#[doc = include_str!("../README.md")]
|
||||||
#[cfg(doctest)]
|
#[cfg(doctest)]
|
||||||
pub struct ReadmeDoctests;
|
pub struct ReadmeDoctests;
|
||||||
|
|||||||
16
src/error.rs
16
src/error.rs
@@ -13,7 +13,7 @@ use std::fmt;
|
|||||||
|
|
||||||
use crate::bitcoin::Network;
|
use crate::bitcoin::Network;
|
||||||
use crate::{descriptor, wallet, wallet::address_validator};
|
use crate::{descriptor, wallet, wallet::address_validator};
|
||||||
use bitcoin::OutPoint;
|
use bitcoin::{OutPoint, Txid};
|
||||||
|
|
||||||
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -125,6 +125,10 @@ pub enum Error {
|
|||||||
//DifferentDescriptorStructure,
|
//DifferentDescriptorStructure,
|
||||||
//Uncapable(crate::blockchain::Capability),
|
//Uncapable(crate::blockchain::Capability),
|
||||||
//MissingCachedAddresses,
|
//MissingCachedAddresses,
|
||||||
|
/// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which
|
||||||
|
/// are needed to satisfy `stop_gap`.
|
||||||
|
MissingCachedScripts(MissingCachedScripts),
|
||||||
|
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
/// Electrum client error
|
/// Electrum client error
|
||||||
Electrum(electrum_client::Error),
|
Electrum(electrum_client::Error),
|
||||||
@@ -145,6 +149,16 @@ pub enum Error {
|
|||||||
Rusqlite(rusqlite::Error),
|
Rusqlite(rusqlite::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
||||||
|
/// on cached `scriptPubKey`s.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MissingCachedScripts {
|
||||||
|
/// Number of scripts in which txs were requested during last request.
|
||||||
|
pub last_count: usize,
|
||||||
|
/// Minimum number of scripts to cache more of in order to satisfy `stop_gap`.
|
||||||
|
pub missing_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{:?}", self)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use bitcoin::Network;
|
|||||||
|
|
||||||
use miniscript::ScriptContext;
|
use miniscript::ScriptContext;
|
||||||
|
|
||||||
pub use bip39::{Language, Mnemonic};
|
pub use bip39::{Error, Language, Mnemonic};
|
||||||
|
|
||||||
type Seed = [u8; 64];
|
type Seed = [u8; 64];
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ use std::str::FromStr;
|
|||||||
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
||||||
|
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::{Network, PrivateKey, PublicKey};
|
use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
|
||||||
|
|
||||||
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||||
pub use miniscript::descriptor::{
|
pub use miniscript::descriptor::{
|
||||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
||||||
SortedMultiVec,
|
SinglePubKey, SortedMultiVec,
|
||||||
};
|
};
|
||||||
pub use miniscript::ScriptContext;
|
pub use miniscript::ScriptContext;
|
||||||
use miniscript::{Miniscript, Terminal};
|
use miniscript::{Miniscript, Terminal};
|
||||||
@@ -127,6 +127,8 @@ pub enum ScriptContextEnum {
|
|||||||
Legacy,
|
Legacy,
|
||||||
/// Segwitv0 scripts
|
/// Segwitv0 scripts
|
||||||
Segwitv0,
|
Segwitv0,
|
||||||
|
/// Taproot scripts
|
||||||
|
Tap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptContextEnum {
|
impl ScriptContextEnum {
|
||||||
@@ -139,6 +141,11 @@ impl ScriptContextEnum {
|
|||||||
pub fn is_segwit_v0(&self) -> bool {
|
pub fn is_segwit_v0(&self) -> bool {
|
||||||
self == &ScriptContextEnum::Segwitv0
|
self == &ScriptContextEnum::Segwitv0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the script context is [`ScriptContextEnum::Tap`]
|
||||||
|
pub fn is_taproot(&self) -> bool {
|
||||||
|
self == &ScriptContextEnum::Tap
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait that adds extra useful methods to [`ScriptContext`]s
|
/// Trait that adds extra useful methods to [`ScriptContext`]s
|
||||||
@@ -155,6 +162,11 @@ pub trait ExtScriptContext: ScriptContext {
|
|||||||
fn is_segwit_v0() -> bool {
|
fn is_segwit_v0() -> bool {
|
||||||
Self::as_enum().is_segwit_v0()
|
Self::as_enum().is_segwit_v0()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1
|
||||||
|
fn is_taproot() -> bool {
|
||||||
|
Self::as_enum().is_taproot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||||
@@ -162,6 +174,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
match TypeId::of::<Ctx>() {
|
match TypeId::of::<Ctx>() {
|
||||||
t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
|
t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
|
||||||
t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
|
t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
|
||||||
|
t if t == TypeId::of::<miniscript::Tap>() => ScriptContextEnum::Tap,
|
||||||
_ => unimplemented!("Unknown ScriptContext type"),
|
_ => unimplemented!("Unknown ScriptContext type"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +225,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
///
|
///
|
||||||
/// use bdk::keys::{
|
/// use bdk::keys::{
|
||||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||||
/// IntoDescriptorKey, KeyError, ScriptContext,
|
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// pub struct MyKeyType {
|
/// pub struct MyKeyType {
|
||||||
@@ -224,7 +237,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// Ok(DescriptorKey::from_public(
|
/// Ok(DescriptorKey::from_public(
|
||||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
/// origin: None,
|
/// origin: None,
|
||||||
/// key: self.pubkey,
|
/// key: SinglePubKey::FullKey(self.pubkey),
|
||||||
/// }),
|
/// }),
|
||||||
/// mainnet_network(),
|
/// mainnet_network(),
|
||||||
/// ))
|
/// ))
|
||||||
@@ -319,7 +332,6 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
|||||||
match self {
|
match self {
|
||||||
ExtendedKey::Private((mut xprv, _)) => {
|
ExtendedKey::Private((mut xprv, _)) => {
|
||||||
xprv.network = network;
|
xprv.network = network;
|
||||||
xprv.private_key.network = network;
|
|
||||||
Some(xprv)
|
Some(xprv)
|
||||||
}
|
}
|
||||||
ExtendedKey::Public(_) => None,
|
ExtendedKey::Public(_) => None,
|
||||||
@@ -334,7 +346,7 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
|||||||
secp: &Secp256k1<C>,
|
secp: &Secp256k1<C>,
|
||||||
) -> bip32::ExtendedPubKey {
|
) -> bip32::ExtendedPubKey {
|
||||||
let mut xpub = match self {
|
let mut xpub = match self {
|
||||||
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_private(secp, &xprv),
|
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
|
||||||
ExtendedKey::Public((xpub, _)) => xpub,
|
ExtendedKey::Public((xpub, _)) => xpub,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -388,7 +400,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
/// network: self.network,
|
/// network: self.network,
|
||||||
/// depth: 0,
|
/// depth: 0,
|
||||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
/// private_key: self.key_data,
|
/// private_key: self.key_data.inner,
|
||||||
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||||
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||||
/// };
|
/// };
|
||||||
@@ -420,7 +432,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
|||||||
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
||||||
/// depth: 0,
|
/// depth: 0,
|
||||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
/// private_key: self.key_data,
|
/// private_key: self.key_data.inner,
|
||||||
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||||
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||||
/// };
|
/// };
|
||||||
@@ -698,11 +710,11 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
|
|||||||
entropy: Self::Entropy,
|
entropy: Self::Entropy,
|
||||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||||
// pick a arbitrary network here, but say that we support all of them
|
// pick a arbitrary network here, but say that we support all of them
|
||||||
let key = secp256k1::SecretKey::from_slice(&entropy)?;
|
let inner = secp256k1::SecretKey::from_slice(&entropy)?;
|
||||||
let private_key = PrivateKey {
|
let private_key = PrivateKey {
|
||||||
compressed: options.compressed,
|
compressed: options.compressed,
|
||||||
network: Network::Bitcoin,
|
network: Network::Bitcoin,
|
||||||
key,
|
inner,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(GeneratedKey::new(private_key, any_network()))
|
Ok(GeneratedKey::new(private_key, any_network()))
|
||||||
@@ -780,13 +792,18 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
|||||||
|
|
||||||
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn make_multi<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
pub fn make_multi<
|
||||||
|
Pk: IntoDescriptorKey<Ctx>,
|
||||||
|
Ctx: ScriptContext,
|
||||||
|
V: Fn(usize, Vec<DescriptorPublicKey>) -> Terminal<DescriptorPublicKey, Ctx>,
|
||||||
|
>(
|
||||||
thresh: usize,
|
thresh: usize,
|
||||||
|
variant: V,
|
||||||
pks: Vec<Pk>,
|
pks: Vec<Pk>,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
|
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
|
||||||
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
|
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
|
||||||
let minisc = Miniscript::from_ast(Terminal::Multi(thresh, pks))?;
|
let minisc = Miniscript::from_ast(variant(thresh, pks))?;
|
||||||
|
|
||||||
minisc.check_miniscript()?;
|
minisc.check_miniscript()?;
|
||||||
|
|
||||||
@@ -841,7 +858,17 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
|||||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
key: self,
|
key: SinglePubKey::FullKey(self),
|
||||||
|
origin: None,
|
||||||
|
})
|
||||||
|
.into_descriptor_key()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||||
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
|
key: SinglePubKey::XOnly(self),
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
.into_descriptor_key()
|
.into_descriptor_key()
|
||||||
@@ -943,29 +970,6 @@ pub mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keys_wif_network() {
|
|
||||||
// test mainnet wif
|
|
||||||
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
|
||||||
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
|
||||||
let xkey = generated_xprv.into_extended_key().unwrap();
|
|
||||||
|
|
||||||
let network = Network::Bitcoin;
|
|
||||||
let xprv = xkey.into_xprv(network).unwrap();
|
|
||||||
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
|
|
||||||
assert_eq!(wif.network, network);
|
|
||||||
|
|
||||||
// test testnet wif
|
|
||||||
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
|
||||||
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
|
||||||
let xkey = generated_xprv.into_extended_key().unwrap();
|
|
||||||
|
|
||||||
let network = Network::Testnet;
|
|
||||||
let xprv = xkey.into_xprv(network).unwrap();
|
|
||||||
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
|
|
||||||
assert_eq!(wif.network, network);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "keys-bip39")]
|
#[cfg(feature = "keys-bip39")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keys_wif_network_bip39() {
|
fn test_keys_wif_network_bip39() {
|
||||||
@@ -977,8 +981,7 @@ pub mod test {
|
|||||||
.into_extended_key()
|
.into_extended_key()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
|
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
|
||||||
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wif.network, Network::Testnet);
|
assert_eq!(xprv.network, Network::Testnet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/lib.rs
20
src/lib.rs
@@ -43,10 +43,6 @@
|
|||||||
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
|
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
|
||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
|
||||||
//! bdk = "0.18.0"
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! # Examples
|
//! # Examples
|
||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
feature = "electrum",
|
feature = "electrum",
|
||||||
@@ -81,6 +77,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
//!
|
//!
|
||||||
//! ## Generate a few addresses
|
//! ## Generate a few addresses
|
||||||
//!
|
//!
|
||||||
|
//! ### Example
|
||||||
//! ```
|
//! ```
|
||||||
//! use bdk::{Wallet};
|
//! use bdk::{Wallet};
|
||||||
//! use bdk::database::MemoryDatabase;
|
//! use bdk::database::MemoryDatabase;
|
||||||
@@ -249,6 +246,14 @@ pub extern crate sled;
|
|||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
pub extern crate rusqlite;
|
pub extern crate rusqlite;
|
||||||
|
|
||||||
|
// We should consider putting this under a feature flag but we need the macro in doctests 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)]
|
||||||
|
#[macro_use]
|
||||||
|
pub mod testutils;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
@@ -277,10 +282,3 @@ pub use wallet::Wallet;
|
|||||||
pub fn version() -> &'static str {
|
pub fn version() -> &'static str {
|
||||||
env!("CARGO_PKG_VERSION", "unknown")
|
env!("CARGO_PKG_VERSION", "unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should consider putting this under a feature flag but we need the macro in doctests 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;
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub trait PsbtUtils {
|
|||||||
impl PsbtUtils for Psbt {
|
impl PsbtUtils for Psbt {
|
||||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||||
let tx = &self.global.unsigned_tx;
|
let tx = &self.unsigned_tx;
|
||||||
|
|
||||||
if input_index >= tx.input.len() {
|
if input_index >= tx.input.len() {
|
||||||
return None;
|
return None;
|
||||||
@@ -93,7 +93,7 @@ mod test {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
psbt.global.unsigned_tx.input.push(TxIn::default());
|
psbt.unsigned_tx.input.push(TxIn::default());
|
||||||
let options = SignOptions {
|
let options = SignOptions {
|
||||||
trust_witness_utxo: true,
|
trust_witness_utxo: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -112,10 +112,9 @@ mod test {
|
|||||||
|
|
||||||
// add a finalized input
|
// add a finalized input
|
||||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||||
psbt.global
|
psbt.unsigned_tx
|
||||||
.unsigned_tx
|
|
||||||
.input
|
.input
|
||||||
.push(psbt_bip.global.unsigned_tx.input[0].clone());
|
.push(psbt_bip.unsigned_tx.input[0].clone());
|
||||||
|
|
||||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::testutils::TestIncomingTx;
|
|||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
use bitcoin::hashes::sha256d;
|
use bitcoin::hashes::sha256d;
|
||||||
use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
use bitcoin::{Address, Amount, Script, Transaction, Txid, Witness};
|
||||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
@@ -193,7 +193,7 @@ impl TestClient {
|
|||||||
previous_output: OutPoint::null(),
|
previous_output: OutPoint::null(),
|
||||||
script_sig: Builder::new().push_int(height).into_script(),
|
script_sig: Builder::new().push_int(height).into_script(),
|
||||||
sequence: 0xFFFFFFFF,
|
sequence: 0xFFFFFFFF,
|
||||||
witness: vec![witness_reserved_value],
|
witness: Witness::from_vec(vec![witness_reserved_value]),
|
||||||
}],
|
}],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
};
|
};
|
||||||
@@ -203,23 +203,31 @@ impl TestClient {
|
|||||||
|
|
||||||
let mut block = Block { header, txdata };
|
let mut block = Block { header, txdata };
|
||||||
|
|
||||||
let witness_root = block.witness_root();
|
if let Some(witness_root) = block.witness_root() {
|
||||||
let witness_commitment =
|
let witness_commitment = Block::compute_witness_commitment(
|
||||||
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
|
&witness_root,
|
||||||
|
&coinbase_tx.input[0]
|
||||||
|
.witness
|
||||||
|
.last()
|
||||||
|
.expect("Should contain the witness reserved value"),
|
||||||
|
);
|
||||||
|
|
||||||
// now update and replace the coinbase tx
|
// now update and replace the coinbase tx
|
||||||
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
||||||
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
||||||
|
|
||||||
|
coinbase_tx.output.push(TxOut {
|
||||||
|
value: 0,
|
||||||
|
script_pubkey: coinbase_witness_commitment_script.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
coinbase_tx.output.push(TxOut {
|
|
||||||
value: 0,
|
|
||||||
script_pubkey: coinbase_witness_commitment_script.into(),
|
|
||||||
});
|
|
||||||
block.txdata[0] = coinbase_tx;
|
block.txdata[0] = coinbase_tx;
|
||||||
|
|
||||||
// set merkle root
|
// set merkle root
|
||||||
let merkle_root = block.merkle_root();
|
if let Some(merkle_root) = block.compute_merkle_root() {
|
||||||
block.header.merkle_root = merkle_root;
|
block.header.merkle_root = merkle_root;
|
||||||
|
}
|
||||||
|
|
||||||
assert!(block.check_merkle_root());
|
assert!(block.check_merkle_root());
|
||||||
assert!(block.check_witness_commitment());
|
assert!(block.check_witness_commitment());
|
||||||
@@ -364,6 +372,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
use $crate::blockchain::Blockchain;
|
use $crate::blockchain::Blockchain;
|
||||||
use $crate::database::MemoryDatabase;
|
use $crate::database::MemoryDatabase;
|
||||||
use $crate::types::KeychainKind;
|
use $crate::types::KeychainKind;
|
||||||
|
use $crate::wallet::AddressIndex;
|
||||||
use $crate::{Wallet, FeeRate, SyncOptions};
|
use $crate::{Wallet, FeeRate, SyncOptions};
|
||||||
use $crate::testutils;
|
use $crate::testutils;
|
||||||
|
|
||||||
@@ -379,11 +388,34 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap()
|
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_single_sig() -> (Wallet<MemoryDatabase>, $blockchain, (String, Option<String>), TestClient) {
|
#[allow(dead_code)]
|
||||||
|
enum WalletType {
|
||||||
|
WpkhSingleSig,
|
||||||
|
TaprootKeySpend,
|
||||||
|
TaprootScriptSpend,
|
||||||
|
TaprootScriptSpend2,
|
||||||
|
TaprootScriptSpend3,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_wallet(ty: WalletType) -> (Wallet<MemoryDatabase>, $blockchain, (String, Option<String>), TestClient) {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
let descriptors = testutils! {
|
let descriptors = match ty {
|
||||||
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
WalletType::WpkhSingleSig => testutils! {
|
||||||
|
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
||||||
|
},
|
||||||
|
WalletType::TaprootKeySpend => testutils! {
|
||||||
|
@descriptors ( "tr(Alice)" ) ( "tr(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
||||||
|
},
|
||||||
|
WalletType::TaprootScriptSpend => testutils! {
|
||||||
|
@descriptors ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( @keys ( "Key" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Script" => (@generate_xprv "/0/*", "/1/*") ) )
|
||||||
|
},
|
||||||
|
WalletType::TaprootScriptSpend2 => testutils! {
|
||||||
|
@descriptors ( "tr(Alice,pk(Bob))" ) ( "tr(Alice,pk(Bob))" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*") ) )
|
||||||
|
},
|
||||||
|
WalletType::TaprootScriptSpend3 => testutils! {
|
||||||
|
@descriptors ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*"), "Carol" => (@generate_xprv "/0/*", "/1/*") ) )
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let test_client = TestClient::default();
|
let test_client = TestClient::default();
|
||||||
@@ -391,12 +423,16 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
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
|
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
|
||||||
#[cfg(feature = "test-rpc")]
|
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
(wallet, blockchain, descriptors, test_client)
|
(wallet, blockchain, descriptors, test_client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_single_sig() -> (Wallet<MemoryDatabase>, $blockchain, (String, Option<String>), TestClient) {
|
||||||
|
init_wallet(WalletType::WpkhSingleSig)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_simple() {
|
fn test_sync_simple() {
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
@@ -412,13 +448,13 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
// the RPC blockchain needs to call `sync()` during initialization to import the
|
// the RPC blockchain needs to call `sync()` during initialization to import the
|
||||||
// addresses (see `init_single_sig()`), so we skip this assertion
|
// addresses (see `init_single_sig()`), so we skip this assertion
|
||||||
#[cfg(not(feature = "test-rpc"))]
|
#[cfg(not(any(feature = "test-rpc", feature = "test-rpc-legacy")))]
|
||||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
|
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
@@ -441,7 +477,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 100_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +486,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
assert_eq!(wallet.get_balance().unwrap().get_total(), 0);
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
@@ -458,8 +494,16 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().confirmed, 100_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -472,7 +516,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 105_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
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");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
||||||
|
|
||||||
@@ -496,7 +540,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
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");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
||||||
}
|
}
|
||||||
@@ -510,14 +554,14 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 25_000 )
|
@tx ( (@external descriptors, 0) => 25_000 )
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -530,7 +574,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
||||||
|
|
||||||
@@ -544,7 +588,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 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_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
||||||
|
|
||||||
@@ -567,8 +611,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
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_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
|
|
||||||
@@ -581,7 +624,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after invalidate");
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
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.txid, txid, "incorrect txid after invalidate");
|
||||||
@@ -599,7 +642,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
@@ -610,12 +653,71 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().trusted_pending, details.received, "incorrect balance after send");
|
||||||
|
|
||||||
|
test_client.generate(1, Some(node_addr));
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().confirmed, details.received, "incorrect balance after send");
|
||||||
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
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");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Syncing wallet should not result in wallet address index to decrement.
|
||||||
|
// This is critical as we should always ensure to not reuse addresses.
|
||||||
|
#[test]
|
||||||
|
fn test_sync_address_index_should_not_decrement() {
|
||||||
|
let (wallet, blockchain, _descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
const ADDRS_TO_FUND: u32 = 7;
|
||||||
|
const ADDRS_TO_IGNORE: u32 = 11;
|
||||||
|
|
||||||
|
let mut first_addr_index: u32 = 0;
|
||||||
|
|
||||||
|
(0..ADDRS_TO_FUND + ADDRS_TO_IGNORE).for_each(|i| {
|
||||||
|
let new_addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
first_addr_index = new_addr.index;
|
||||||
|
}
|
||||||
|
assert_eq!(new_addr.index, i+first_addr_index, "unexpected new address index (before sync)");
|
||||||
|
|
||||||
|
if i < ADDRS_TO_FUND {
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ((@addr new_addr.address) => 50_000)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
let new_addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
assert_eq!(new_addr.index, ADDRS_TO_FUND+ADDRS_TO_IGNORE+first_addr_index, "unexpected new address index (after sync)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if user does not explicitly grab new addresses, the address index should
|
||||||
|
// increment after sync (if wallet has a balance).
|
||||||
|
#[test]
|
||||||
|
fn test_sync_address_index_should_increment() {
|
||||||
|
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
const START_FUND: u32 = 4;
|
||||||
|
const END_FUND: u32 = 20;
|
||||||
|
|
||||||
|
// "secretly" fund wallet via given range
|
||||||
|
(START_FUND..END_FUND).for_each(|addr_index| {
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ((@external descriptors, addr_index) => 50_000)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
let address = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
assert_eq!(address.index, END_FUND, "unexpected new address index (after sync)");
|
||||||
|
}
|
||||||
|
|
||||||
/// Send two conflicting transactions to the same address twice in a row.
|
/// Send two conflicting transactions to the same address twice in a row.
|
||||||
/// The coins should only be received once!
|
/// The coins should only be received once!
|
||||||
#[test]
|
#[test]
|
||||||
@@ -630,7 +732,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
let tx1 = {
|
let tx1 = {
|
||||||
@@ -653,9 +755,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
blockchain.broadcast(&tx1).expect("broadcasting first");
|
||||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
||||||
|
|
||||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
||||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
assert_eq!(receiver_wallet.get_balance().expect("balance").untrusted_pending, 49_000, "should have received coins once and only once");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -681,7 +782,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
let balance = wallet.get_balance().unwrap();
|
||||||
|
assert_eq!(balance.untrusted_pending + balance.get_spendable(), 100_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -695,7 +797,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
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();
|
let details = tx_map.get(&received_txid).unwrap();
|
||||||
@@ -719,7 +821,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
@@ -731,7 +833,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&sent_tx).unwrap();
|
blockchain.broadcast(&sent_tx).unwrap();
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance after receive");
|
||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||||
@@ -762,7 +864,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut total_sent = 0;
|
let mut total_sent = 0;
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
@@ -779,7 +881,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance after chain");
|
||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
|
|
||||||
@@ -789,7 +891,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +905,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||||
@@ -812,8 +914,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).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().get_spendable(), 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");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance from received");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||||
@@ -822,8 +924,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).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().get_spendable(), 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_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance from received after bump");
|
||||||
|
|
||||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
}
|
}
|
||||||
@@ -838,7 +940,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -847,8 +949,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect received after send");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
|
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
|
||||||
@@ -857,7 +959,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after change removal");
|
||||||
assert_eq!(new_details.received, 0, "incorrect received 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");
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
@@ -873,7 +975,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -882,7 +984,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 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");
|
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();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
@@ -893,20 +995,20 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance after add input");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_bump_fee_add_input_no_change() {
|
fn test_sync_bump_fee_add_input_no_change() {
|
||||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -915,7 +1017,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 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");
|
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();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
@@ -928,7 +1030,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after add input");
|
||||||
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -942,7 +1044,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
let data = [42u8;80];
|
let data = [42u8;80];
|
||||||
@@ -957,7 +1059,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
|
|
||||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
let _ = tx_map.get(&tx.txid()).unwrap();
|
let _ = tx_map.get(&tx.txid()).unwrap();
|
||||||
@@ -968,25 +1070,28 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
println!("wallet addr: {}", wallet_addr);
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().immature, 0, "incorrect balance");
|
||||||
|
|
||||||
test_client.generate(1, Some(wallet_addr));
|
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(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
|
||||||
|
assert!(wallet.get_balance().unwrap().immature > 0, "incorrect balance after receiving coinbase");
|
||||||
|
|
||||||
|
// make coinbase mature (100 blocks)
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
test_client.generate(100, Some(node_addr));
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert!(wallet.get_balance().unwrap().confirmed > 0, "incorrect balance after maturing coinbase");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
fn test_send_to_bech32m_addr() {
|
fn test_send_to_bech32m_addr() {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use serde;
|
use serde;
|
||||||
@@ -1059,7 +1164,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "wallet has incorrect balance");
|
||||||
|
|
||||||
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
||||||
|
|
||||||
@@ -1071,7 +1176,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx();
|
||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "wallet has incorrect balance after send");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
||||||
test_client.generate(1, None);
|
test_client.generate(1, None);
|
||||||
@@ -1173,16 +1278,16 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let blockchain = get_blockchain(&test_client);
|
let blockchain = get_blockchain(&test_client);
|
||||||
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||||
#[cfg(feature = "test-rpc")]
|
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
let _ = test_client.receive(testutils! {
|
let _ = test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
let tx = {
|
let tx = {
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
@@ -1195,6 +1300,165 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_taproot_key_spend() {
|
||||||
|
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootKeySpend);
|
||||||
|
|
||||||
|
let _ = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
|
let tx = {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000);
|
||||||
|
let (mut psbt, _details) = builder.finish().unwrap();
|
||||||
|
wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
psbt.extract_tx()
|
||||||
|
};
|
||||||
|
blockchain.broadcast(&tx).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_taproot_script_spend() {
|
||||||
|
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootScriptSpend);
|
||||||
|
|
||||||
|
let _ = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
|
||||||
|
});
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000);
|
||||||
|
|
||||||
|
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
||||||
|
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();
|
||||||
|
|
||||||
|
let ext_path = vec![(ext_policy.id.clone(), vec![1])].into_iter().collect();
|
||||||
|
let int_path = vec![(int_policy.id.clone(), vec![1])].into_iter().collect();
|
||||||
|
|
||||||
|
let tx = {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000)
|
||||||
|
.policy_path(ext_path, KeychainKind::External)
|
||||||
|
.policy_path(int_path, KeychainKind::Internal);
|
||||||
|
let (mut psbt, _details) = builder.finish().unwrap();
|
||||||
|
wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
psbt.extract_tx()
|
||||||
|
};
|
||||||
|
blockchain.broadcast(&tx).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_sign_taproot_core_keyspend_psbt() {
|
||||||
|
test_sign_taproot_core_psbt(WalletType::TaprootKeySpend);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_sign_taproot_core_scriptspend2_psbt() {
|
||||||
|
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_sign_taproot_core_scriptspend3_psbt() {
|
||||||
|
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "test-rpc-legacy"))]
|
||||||
|
fn test_sign_taproot_core_psbt(wallet_type: WalletType) {
|
||||||
|
use std::str::FromStr;
|
||||||
|
use serde_json;
|
||||||
|
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||||
|
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||||
|
|
||||||
|
let (wallet, _blockchain, _descriptors, test_client) = init_wallet(wallet_type);
|
||||||
|
|
||||||
|
// TODO replace once rust-bitcoincore-rpc with PR 174 released
|
||||||
|
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/174
|
||||||
|
let _createwallet_result: Value = test_client.bitcoind.client.call("createwallet", &["taproot_wallet".into(), true.into(), true.into(), serde_json::to_value("").unwrap(), false.into(), true.into(), true.into(), false.into()]).expect("created wallet");
|
||||||
|
|
||||||
|
let external_descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External);
|
||||||
|
|
||||||
|
// TODO replace once bitcoind released with support for rust-bitcoincore-rpc PR 174
|
||||||
|
let taproot_wallet_client = Client::new(&test_client.bitcoind.rpc_url_with_wallet("taproot_wallet"), Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone())).unwrap();
|
||||||
|
|
||||||
|
let descriptor_info = taproot_wallet_client.get_descriptor_info(external_descriptor.to_string().as_str()).expect("descriptor info");
|
||||||
|
|
||||||
|
let import_descriptor_args = json!([{
|
||||||
|
"desc": descriptor_info.descriptor,
|
||||||
|
"active": true,
|
||||||
|
"timestamp": "now",
|
||||||
|
"label":"taproot key spend",
|
||||||
|
}]);
|
||||||
|
let _importdescriptors_result: Value = taproot_wallet_client.call("importdescriptors", &[import_descriptor_args]).expect("import wallet");
|
||||||
|
let generate_to_address: bitcoin::Address = taproot_wallet_client.call("getnewaddress", &["test address".into(), "bech32m".into()]).expect("new address");
|
||||||
|
let _generatetoaddress_result = taproot_wallet_client.generate_to_address(101, &generate_to_address).expect("generated to address");
|
||||||
|
let send_to_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string();
|
||||||
|
let change_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string();
|
||||||
|
let send_addr_amounts = json!([{
|
||||||
|
send_to_address: "0.4321"
|
||||||
|
}]);
|
||||||
|
let send_options = json!({
|
||||||
|
"change_address": change_address,
|
||||||
|
"psbt": true,
|
||||||
|
});
|
||||||
|
let send_result: Value = taproot_wallet_client.call("send", &[send_addr_amounts, Value::Null, "unset".into(), Value::Null, send_options]).expect("send psbt");
|
||||||
|
let core_psbt = send_result["psbt"].as_str().expect("core psbt str");
|
||||||
|
|
||||||
|
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
|
|
||||||
|
// Test parsing core created PSBT
|
||||||
|
let mut psbt = PartiallySignedTransaction::from_str(&core_psbt).expect("core taproot psbt");
|
||||||
|
|
||||||
|
// Test signing core created PSBT
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert_eq!(finalized, true);
|
||||||
|
|
||||||
|
// Test with updated psbt
|
||||||
|
let update_result: Value = taproot_wallet_client.call("utxoupdatepsbt", &[core_psbt.into()]).expect("update psbt utxos");
|
||||||
|
let core_updated_psbt = update_result.as_str().expect("core updated psbt");
|
||||||
|
|
||||||
|
// Test parsing core created and updated PSBT
|
||||||
|
let mut psbt = PartiallySignedTransaction::from_str(&core_updated_psbt).expect("core taproot psbt");
|
||||||
|
|
||||||
|
// Test signing core created and updated PSBT
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert_eq!(finalized, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_block_hash() {
|
||||||
|
use bitcoincore_rpc::{ RpcApi };
|
||||||
|
use crate::blockchain::GetBlockHash;
|
||||||
|
|
||||||
|
// create wallet with init_wallet
|
||||||
|
let (_, blockchain, _descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64;
|
||||||
|
let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap();
|
||||||
|
|
||||||
|
// use get_block_hash to get best block hash and compare with best_hash above
|
||||||
|
let block_hash = blockchain.get_block_hash(height).unwrap();
|
||||||
|
assert_eq!(best_hash, block_hash);
|
||||||
|
|
||||||
|
// generate blocks to address
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
test_client.generate(10, Some(node_addr));
|
||||||
|
|
||||||
|
let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64;
|
||||||
|
let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap();
|
||||||
|
|
||||||
|
let block_hash = blockchain.get_block_hash(height).unwrap();
|
||||||
|
assert_eq!(best_hash, block_hash);
|
||||||
|
|
||||||
|
// try to get hash for block that has not yet been created.
|
||||||
|
assert!(blockchain.get_block_hash(height + 1).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
257
src/testutils/configurable_blockchain_tests.rs
Normal file
257
src/testutils/configurable_blockchain_tests.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
use bitcoin::Network;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blockchain::ConfigurableBlockchain, database::MemoryDatabase, testutils, wallet::AddressIndex,
|
||||||
|
Wallet,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::blockchain_tests::TestClient;
|
||||||
|
|
||||||
|
/// Trait for testing [`ConfigurableBlockchain`] implementations.
|
||||||
|
pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
||||||
|
/// Blockchain name for logging.
|
||||||
|
const BLOCKCHAIN_NAME: &'static str;
|
||||||
|
|
||||||
|
/// Generates a blockchain config with a given stop_gap.
|
||||||
|
///
|
||||||
|
/// If this returns [`Option::None`], then the associated tests will not run.
|
||||||
|
fn config_with_stop_gap(
|
||||||
|
&self,
|
||||||
|
_test_client: &mut TestClient,
|
||||||
|
_stop_gap: usize,
|
||||||
|
) -> Option<B::Config> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs all avaliable tests.
|
||||||
|
fn run(&self) {
|
||||||
|
let test_client = &mut TestClient::default();
|
||||||
|
|
||||||
|
if self.config_with_stop_gap(test_client, 0).is_some() {
|
||||||
|
test_wallet_sync_with_stop_gaps(test_client, self);
|
||||||
|
test_wallet_sync_fulfills_missing_script_cache(test_client, self);
|
||||||
|
test_wallet_sync_self_transfer_tx(test_client, self);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{}: Skipped tests requiring config_with_stop_gap.",
|
||||||
|
Self::BLOCKCHAIN_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test whether blockchain implementation syncs with expected behaviour given different `stop_gap`
|
||||||
|
/// parameters.
|
||||||
|
///
|
||||||
|
/// For each test vector:
|
||||||
|
/// * Fill wallet's derived addresses with balances (as specified by test vector).
|
||||||
|
/// * [0..addrs_before] => 1000sats for each address
|
||||||
|
/// * [addrs_before..actual_gap] => empty addresses
|
||||||
|
/// * [actual_gap..addrs_after] => 1000sats for each address
|
||||||
|
/// * Then, perform wallet sync and obtain wallet balance
|
||||||
|
/// * Check balance is within expected range (we can compare `stop_gap` and `actual_gap` to
|
||||||
|
/// determine this).
|
||||||
|
fn test_wallet_sync_with_stop_gaps<T, B>(test_client: &mut TestClient, tester: &T)
|
||||||
|
where
|
||||||
|
T: ConfigurableBlockchainTester<B>,
|
||||||
|
B: ConfigurableBlockchain,
|
||||||
|
{
|
||||||
|
// Generates wallet descriptor
|
||||||
|
let descriptor_of_account = |account_index: usize| -> String {
|
||||||
|
format!("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/{account_index}/*)")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Amount (in satoshis) provided to a single address (which expects to have a balance)
|
||||||
|
const AMOUNT_PER_TX: u64 = 1000;
|
||||||
|
|
||||||
|
// [stop_gap, actual_gap, addrs_before, addrs_after]
|
||||||
|
//
|
||||||
|
// [0] stop_gap: Passed to [`ElectrumBlockchainConfig`]
|
||||||
|
// [1] actual_gap: Range size of address indexes without a balance
|
||||||
|
// [2] addrs_before: Range size of address indexes (before gap) which contains a balance
|
||||||
|
// [3] addrs_after: Range size of address indexes (after gap) which contains a balance
|
||||||
|
let test_vectors: Vec<[u64; 4]> = vec![
|
||||||
|
[0, 0, 0, 5],
|
||||||
|
[0, 0, 5, 5],
|
||||||
|
[0, 1, 5, 5],
|
||||||
|
[0, 2, 5, 5],
|
||||||
|
[1, 0, 5, 5],
|
||||||
|
[1, 1, 5, 5],
|
||||||
|
[1, 2, 5, 5],
|
||||||
|
[2, 1, 5, 5],
|
||||||
|
[2, 2, 5, 5],
|
||||||
|
[2, 3, 5, 5],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (account_index, vector) in test_vectors.into_iter().enumerate() {
|
||||||
|
let [stop_gap, actual_gap, addrs_before, addrs_after] = vector;
|
||||||
|
let descriptor = descriptor_of_account(account_index);
|
||||||
|
|
||||||
|
let blockchain = B::from_config(
|
||||||
|
&tester
|
||||||
|
.config_with_stop_gap(test_client, stop_gap as _)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let wallet =
|
||||||
|
Wallet::new(&descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||||
|
|
||||||
|
// fill server-side with txs to specified address indexes
|
||||||
|
// return the max balance of the wallet (also the actual balance)
|
||||||
|
let max_balance = (0..addrs_before)
|
||||||
|
.chain(addrs_before + actual_gap..addrs_before + actual_gap + addrs_after)
|
||||||
|
.fold(0_u64, |sum, i| {
|
||||||
|
let address = wallet.get_address(AddressIndex::Peek(i as _)).unwrap();
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@addr address.address) => AMOUNT_PER_TX )
|
||||||
|
});
|
||||||
|
sum + AMOUNT_PER_TX
|
||||||
|
});
|
||||||
|
|
||||||
|
// minimum allowed balance of wallet (based on stop gap)
|
||||||
|
let min_balance = if actual_gap > stop_gap {
|
||||||
|
addrs_before * AMOUNT_PER_TX
|
||||||
|
} else {
|
||||||
|
max_balance
|
||||||
|
};
|
||||||
|
let details = format!(
|
||||||
|
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
|
||||||
|
stop_gap, actual_gap, addrs_before, addrs_after,
|
||||||
|
);
|
||||||
|
println!("{}", details);
|
||||||
|
|
||||||
|
// perform wallet sync
|
||||||
|
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||||
|
|
||||||
|
let wallet_balance = wallet.get_balance().unwrap().get_total();
|
||||||
|
println!(
|
||||||
|
"max: {}, min: {}, actual: {}",
|
||||||
|
max_balance, min_balance, wallet_balance
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
wallet_balance <= max_balance,
|
||||||
|
"wallet balance is greater than received amount: {}",
|
||||||
|
details
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
wallet_balance >= min_balance,
|
||||||
|
"wallet balance is smaller than expected: {}",
|
||||||
|
details
|
||||||
|
);
|
||||||
|
|
||||||
|
// generate block to confirm new transactions
|
||||||
|
test_client.generate(1, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With a `stop_gap` of x and every x addresses having a balance of 1000 (for y addresses),
|
||||||
|
/// we expect `Wallet::sync` to correctly self-cache addresses, so that the resulting balance,
|
||||||
|
/// after sync, should be y * 1000.
|
||||||
|
fn test_wallet_sync_fulfills_missing_script_cache<T, B>(test_client: &mut TestClient, tester: &T)
|
||||||
|
where
|
||||||
|
T: ConfigurableBlockchainTester<B>,
|
||||||
|
B: ConfigurableBlockchain,
|
||||||
|
{
|
||||||
|
// wallet descriptor
|
||||||
|
let descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/200/*)";
|
||||||
|
|
||||||
|
// amount in sats per tx
|
||||||
|
const AMOUNT_PER_TX: u64 = 1000;
|
||||||
|
|
||||||
|
// addr constants
|
||||||
|
const ADDR_COUNT: usize = 6;
|
||||||
|
const ADDR_GAP: usize = 60;
|
||||||
|
|
||||||
|
let blockchain =
|
||||||
|
B::from_config(&tester.config_with_stop_gap(test_client, ADDR_GAP).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||||
|
|
||||||
|
let expected_balance = (0..ADDR_COUNT).fold(0_u64, |sum, i| {
|
||||||
|
let addr_i = i * ADDR_GAP;
|
||||||
|
let address = wallet.get_address(AddressIndex::Peek(addr_i as _)).unwrap();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"tx: {} sats => [{}] {}",
|
||||||
|
AMOUNT_PER_TX,
|
||||||
|
addr_i,
|
||||||
|
address.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@addr address.address) => AMOUNT_PER_TX )
|
||||||
|
});
|
||||||
|
test_client.generate(1, None);
|
||||||
|
|
||||||
|
sum + AMOUNT_PER_TX
|
||||||
|
});
|
||||||
|
println!("expected balance: {}, syncing...", expected_balance);
|
||||||
|
|
||||||
|
// perform sync
|
||||||
|
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||||
|
println!("sync done!");
|
||||||
|
|
||||||
|
let balance = wallet.get_balance().unwrap().get_total();
|
||||||
|
assert_eq!(balance, expected_balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a `stop_gap`, a wallet with a 2 transactions, one sending to `scriptPubKey` at derivation
|
||||||
|
/// index of `stop_gap`, and the other spending from the same `scriptPubKey` into another
|
||||||
|
/// `scriptPubKey` at derivation index of `stop_gap * 2`, we expect `Wallet::sync` to perform
|
||||||
|
/// correctly, so that we detect the total balance.
|
||||||
|
fn test_wallet_sync_self_transfer_tx<T, B>(test_client: &mut TestClient, tester: &T)
|
||||||
|
where
|
||||||
|
T: ConfigurableBlockchainTester<B>,
|
||||||
|
B: ConfigurableBlockchain,
|
||||||
|
{
|
||||||
|
const TRANSFER_AMOUNT: u64 = 10_000;
|
||||||
|
const STOP_GAP: usize = 75;
|
||||||
|
|
||||||
|
let descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/*)";
|
||||||
|
|
||||||
|
let blockchain =
|
||||||
|
B::from_config(&tester.config_with_stop_gap(test_client, STOP_GAP).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||||
|
|
||||||
|
let address1 = wallet
|
||||||
|
.get_address(AddressIndex::Peek(STOP_GAP as _))
|
||||||
|
.unwrap();
|
||||||
|
let address2 = wallet
|
||||||
|
.get_address(AddressIndex::Peek((STOP_GAP * 2) as _))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@addr address1.address) => TRANSFER_AMOUNT )
|
||||||
|
});
|
||||||
|
test_client.generate(1, None);
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(address2.script_pubkey(), TRANSFER_AMOUNT / 2);
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
assert!(wallet.sign(&mut psbt, Default::default()).unwrap());
|
||||||
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
|
|
||||||
|
test_client.generate(1, None);
|
||||||
|
|
||||||
|
// obtain what is expected
|
||||||
|
let fee = details.fee.unwrap();
|
||||||
|
let expected_balance = TRANSFER_AMOUNT - fee;
|
||||||
|
println!("fee={}, expected_balance={}", fee, expected_balance);
|
||||||
|
|
||||||
|
// actually test the wallet
|
||||||
|
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||||
|
let balance = wallet.get_balance().unwrap().get_total();
|
||||||
|
assert_eq!(balance, expected_balance);
|
||||||
|
|
||||||
|
// now try with a fresh wallet
|
||||||
|
let fresh_wallet =
|
||||||
|
Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||||
|
fresh_wallet.sync(&blockchain, Default::default()).unwrap();
|
||||||
|
let fresh_balance = fresh_wallet.get_balance().unwrap().get_total();
|
||||||
|
assert_eq!(fresh_balance, expected_balance);
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
#[cfg(feature = "test-blockchains")]
|
#[cfg(feature = "test-blockchains")]
|
||||||
pub mod blockchain_tests;
|
pub mod blockchain_tests;
|
||||||
|
|
||||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
#[cfg(test)]
|
||||||
use bitcoin::{Address, PublicKey, Txid};
|
#[cfg(feature = "test-blockchains")]
|
||||||
|
pub mod configurable_blockchain_tests;
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorPublicKey;
|
use bitcoin::{Address, Txid};
|
||||||
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TestIncomingInput {
|
pub struct TestIncomingInput {
|
||||||
@@ -96,39 +96,6 @@ impl TestIncomingTx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! testutils {
|
macro_rules! testutils {
|
||||||
@@ -136,23 +103,23 @@ macro_rules! testutils {
|
|||||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||||
|
|
||||||
use $crate::testutils::TranslateDescriptor;
|
use $crate::descriptor::AsDerived;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
parsed.as_derived($child, &secp).address(bitcoin::Network::Regtest).expect("No address form")
|
||||||
});
|
});
|
||||||
( @internal $descriptors:expr, $child:expr ) => ({
|
( @internal $descriptors:expr, $child:expr ) => ({
|
||||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||||
|
|
||||||
use $crate::testutils::TranslateDescriptor;
|
use $crate::descriptor::AsDerived;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||||
parsed.derive_translated(&secp, $child).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
parsed.as_derived($child, &secp).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
||||||
});
|
});
|
||||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||||
|
|||||||
163
src/types.rs
163
src/types.rs
@@ -51,14 +51,44 @@ impl AsRef<[u8]> for KeychainKind {
|
|||||||
pub struct FeeRate(f32);
|
pub struct FeeRate(f32);
|
||||||
|
|
||||||
impl FeeRate {
|
impl FeeRate {
|
||||||
|
/// Create a new instance checking the value provided
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
|
fn new_checked(value: f32) -> Self {
|
||||||
|
assert!(value.is_normal() || value == 0.0);
|
||||||
|
assert!(value.is_sign_positive());
|
||||||
|
|
||||||
|
FeeRate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
||||||
|
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
||||||
|
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
||||||
FeeRate(btc_per_kvb * 1e5)
|
FeeRate::new_checked(btc_per_kvb * 1e5)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||||
pub const fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
///
|
||||||
FeeRate(sat_per_vb)
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
|
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_vb)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||||
@@ -78,7 +108,7 @@ impl FeeRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value as satoshi/vbyte
|
/// Return the value as satoshi/vbyte
|
||||||
pub fn as_sat_vb(&self) -> f32 {
|
pub fn as_sat_per_vb(&self) -> f32 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +119,7 @@ impl FeeRate {
|
|||||||
|
|
||||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||||
(self.as_sat_vb() * vbytes as f32).ceil() as u64
|
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +232,12 @@ pub struct TransactionDetails {
|
|||||||
pub txid: Txid,
|
pub txid: Txid,
|
||||||
|
|
||||||
/// Received value (sats)
|
/// Received value (sats)
|
||||||
|
/// Sum of owned outputs of this transaction.
|
||||||
pub received: u64,
|
pub received: u64,
|
||||||
/// Sent value (sats)
|
/// Sent value (sats)
|
||||||
|
/// Sum of owned inputs of this transaction.
|
||||||
pub sent: u64,
|
pub sent: u64,
|
||||||
/// Fee value (sats) if available.
|
/// Fee value (sats) if confirmed.
|
||||||
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||||
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||||
/// funds while offline.
|
/// funds while offline.
|
||||||
@@ -240,13 +272,130 @@ impl BlockTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Balance differentiated in various categories
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
|
pub struct Balance {
|
||||||
|
/// All coinbase outputs not yet matured
|
||||||
|
pub immature: u64,
|
||||||
|
/// Unconfirmed UTXOs generated by a wallet tx
|
||||||
|
pub trusted_pending: u64,
|
||||||
|
/// Unconfirmed UTXOs received from an external wallet
|
||||||
|
pub untrusted_pending: u64,
|
||||||
|
/// Confirmed and immediately spendable balance
|
||||||
|
pub confirmed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Balance {
|
||||||
|
/// Get sum of trusted_pending and confirmed coins
|
||||||
|
pub fn get_spendable(&self) -> u64 {
|
||||||
|
self.confirmed + self.trusted_pending
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the whole balance visible to the wallet
|
||||||
|
pub fn get_total(&self) -> u64 {
|
||||||
|
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Balance {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
|
||||||
|
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Balance {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
immature: self.immature + other.immature,
|
||||||
|
trusted_pending: self.trusted_pending + other.trusted_pending,
|
||||||
|
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
|
||||||
|
confirmed: self.confirmed + other.confirmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::iter::Sum for Balance {
|
||||||
|
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
||||||
|
iter.fold(
|
||||||
|
Balance {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|a, b| a + b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_store_feerate_in_const() {
|
fn can_store_feerate_in_const() {
|
||||||
const _MY_RATE: FeeRate = FeeRate::from_sat_per_vb(10.0);
|
|
||||||
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_value() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_nan() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_inf() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_feerate_pos_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_btc_per_kvb() {
|
||||||
|
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_vbyte() {
|
||||||
|
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_default_min_relay_fee() {
|
||||||
|
let fee = FeeRate::default_min_relay_fee();
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kvb() {
|
||||||
|
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kwu() {
|
||||||
|
let fee = FeeRate::from_sat_per_kwu(250.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ impl std::error::Error for AddressValidatorError {}
|
|||||||
/// validator will be propagated up to the original caller that triggered the address generation.
|
/// validator will be propagated up to the original caller that triggered the address generation.
|
||||||
///
|
///
|
||||||
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
||||||
|
#[deprecated = "AddressValidator was rarely used. Address validation can occur outside of BDK"]
|
||||||
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
||||||
/// Validate or inspect an address
|
/// Validate or inspect an address
|
||||||
fn validate(
|
fn validate(
|
||||||
@@ -120,6 +121,7 @@ mod test {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct TestValidator;
|
struct TestValidator;
|
||||||
|
#[allow(deprecated)]
|
||||||
impl AddressValidator for TestValidator {
|
impl AddressValidator for TestValidator {
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
@@ -135,6 +137,7 @@ mod test {
|
|||||||
#[should_panic(expected = "InvalidScript")]
|
#[should_panic(expected = "InvalidScript")]
|
||||||
fn test_address_validator_external() {
|
fn test_address_validator_external() {
|
||||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
#[allow(deprecated)]
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
wallet.add_address_validator(Arc::new(TestValidator));
|
||||||
|
|
||||||
wallet.get_address(New).unwrap();
|
wallet.get_address(New).unwrap();
|
||||||
@@ -144,6 +147,7 @@ mod test {
|
|||||||
#[should_panic(expected = "InvalidScript")]
|
#[should_panic(expected = "InvalidScript")]
|
||||||
fn test_address_validator_internal() {
|
fn test_address_validator_internal() {
|
||||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
#[allow(deprecated)]
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
wallet.add_address_validator(Arc::new(TestValidator));
|
||||||
|
|
||||||
let addr = crate::testutils!(@external descriptors, 10);
|
let addr = crate::testutils!(@external descriptors, 10);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@ impl FromStr for FullyNodedExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_checksum(s: String) -> String {
|
fn remove_checksum(s: String) -> String {
|
||||||
s.splitn(2, '#').next().map(String::from).unwrap()
|
s.split_once('#').map(|(a, _)| String::from(a)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FullyNodedExport {
|
impl FullyNodedExport {
|
||||||
|
|||||||
64
src/wallet/hardwaresigner.rs
Normal file
64
src/wallet/hardwaresigner.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
//! HWI Signer
|
||||||
|
//!
|
||||||
|
//! This module contains a simple implementation of a Custom signer for rust-hwi
|
||||||
|
|
||||||
|
use bitcoin::psbt::PartiallySignedTransaction;
|
||||||
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
|
|
||||||
|
use hwi::error::Error;
|
||||||
|
use hwi::types::{HWIChain, HWIDevice};
|
||||||
|
use hwi::HWIClient;
|
||||||
|
|
||||||
|
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Custom signer for Hardware Wallets
|
||||||
|
///
|
||||||
|
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
|
||||||
|
pub struct HWISigner {
|
||||||
|
fingerprint: Fingerprint,
|
||||||
|
client: HWIClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HWISigner {
|
||||||
|
/// Create a instance from the specified device and chain
|
||||||
|
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
|
||||||
|
let client = HWIClient::get_client(device, false, chain)?;
|
||||||
|
Ok(HWISigner {
|
||||||
|
fingerprint: device.fingerprint,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerCommon for HWISigner {
|
||||||
|
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||||
|
SignerId::Fingerprint(self.fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This implementation ignores `sign_options`
|
||||||
|
impl TransactionSigner for HWISigner {
|
||||||
|
fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
psbt: &mut PartiallySignedTransaction,
|
||||||
|
_sign_options: &crate::SignOptions,
|
||||||
|
_secp: &crate::wallet::utils::SecpCtx,
|
||||||
|
) -> Result<(), SignerError> {
|
||||||
|
psbt.combine(self.client.sign_tx(psbt)?.psbt)
|
||||||
|
.expect("Failed to combine HW signed psbt with passed PSBT");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
1987
src/wallet/mod.rs
1987
src/wallet/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
|||||||
//! # #[derive(Debug)]
|
//! # #[derive(Debug)]
|
||||||
//! # struct CustomHSM;
|
//! # struct CustomHSM;
|
||||||
//! # impl CustomHSM {
|
//! # impl CustomHSM {
|
||||||
//! # fn sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! # fn connect() -> Self {
|
//! # fn connect() -> Self {
|
||||||
@@ -47,25 +47,23 @@
|
|||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! impl Signer for CustomSigner {
|
//! impl SignerCommon for CustomSigner {
|
||||||
//! fn sign(
|
|
||||||
//! &self,
|
|
||||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
|
||||||
//! input_index: Option<usize>,
|
|
||||||
//! _secp: &Secp256k1<All>,
|
|
||||||
//! ) -> Result<(), SignerError> {
|
|
||||||
//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?;
|
|
||||||
//! self.device.sign_input(psbt, input_index)?;
|
|
||||||
//!
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||||
//! self.device.get_id()
|
//! self.device.get_id()
|
||||||
//! }
|
//! }
|
||||||
|
//! }
|
||||||
//!
|
//!
|
||||||
//! fn sign_whole_tx(&self) -> bool {
|
//! impl InputSigner for CustomSigner {
|
||||||
//! false
|
//! fn sign_input(
|
||||||
|
//! &self,
|
||||||
|
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
//! input_index: usize,
|
||||||
|
//! _sign_options: &SignOptions,
|
||||||
|
//! _secp: &Secp256k1<All>,
|
||||||
|
//! ) -> Result<(), SignerError> {
|
||||||
|
//! self.device.hsm_sign_input(psbt, input_index)?;
|
||||||
|
//!
|
||||||
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
@@ -85,22 +83,26 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Bound::Included;
|
use std::ops::{Bound::Included, Deref};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bitcoin::blockdata::opcodes;
|
use bitcoin::blockdata::opcodes;
|
||||||
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
||||||
use bitcoin::hashes::{hash160, Hash};
|
use bitcoin::hashes::{hash160, Hash};
|
||||||
use bitcoin::secp256k1::{Message, Secp256k1};
|
use bitcoin::secp256k1::Message;
|
||||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||||
use bitcoin::util::{bip143, psbt};
|
use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot};
|
||||||
use bitcoin::{PrivateKey, Script, SigHash, SigHashType};
|
use bitcoin::{secp256k1, XOnlyPublicKey};
|
||||||
|
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, KeyMap};
|
use miniscript::descriptor::{
|
||||||
use miniscript::{Legacy, MiniscriptKey, Segwitv0};
|
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
|
||||||
|
KeyMap, SinglePubKey,
|
||||||
|
};
|
||||||
|
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
|
||||||
|
|
||||||
use super::utils::SecpCtx;
|
use super::utils::SecpCtx;
|
||||||
use crate::descriptor::XKeyUtils;
|
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||||
|
|
||||||
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
|
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
|
||||||
/// multiple of them
|
/// multiple of them
|
||||||
@@ -153,6 +155,26 @@ pub enum SignerError {
|
|||||||
/// To enable signing transactions with non-standard sighashes set
|
/// To enable signing transactions with non-standard sighashes set
|
||||||
/// [`SignOptions::allow_all_sighashes`] to `true`.
|
/// [`SignOptions::allow_all_sighashes`] to `true`.
|
||||||
NonStandardSighash,
|
NonStandardSighash,
|
||||||
|
/// Invalid SIGHASH for the signing context in use
|
||||||
|
InvalidSighash,
|
||||||
|
/// Error while computing the hash to sign
|
||||||
|
SighashError(sighash::Error),
|
||||||
|
/// Error while signing using hardware wallets
|
||||||
|
#[cfg(feature = "hardware-signer")]
|
||||||
|
HWIError(hwi::error::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hardware-signer")]
|
||||||
|
impl From<hwi::error::Error> for SignerError {
|
||||||
|
fn from(e: hwi::error::Error) -> Self {
|
||||||
|
SignerError::HWIError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sighash::Error> for SignerError {
|
||||||
|
fn from(e: sighash::Error) -> Self {
|
||||||
|
SignerError::SighashError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SignerError {
|
impl fmt::Display for SignerError {
|
||||||
@@ -163,27 +185,46 @@ impl fmt::Display for SignerError {
|
|||||||
|
|
||||||
impl std::error::Error for SignerError {}
|
impl std::error::Error for SignerError {}
|
||||||
|
|
||||||
/// Trait for signers
|
/// Signing context
|
||||||
///
|
///
|
||||||
/// This trait can be implemented to provide customized signers to the wallet. For an example see
|
/// Used by our software signers to determine the type of signatures to make
|
||||||
/// [`this module`](crate::wallet::signer)'s documentation.
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub trait Signer: fmt::Debug + Send + Sync {
|
pub enum SignerContext {
|
||||||
/// Sign a PSBT
|
/// Legacy context
|
||||||
///
|
Legacy,
|
||||||
/// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole
|
/// Segwit v0 context (BIP 143)
|
||||||
/// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and
|
Segwitv0,
|
||||||
/// can be ignored.
|
/// Taproot context (BIP 340)
|
||||||
fn sign(
|
Tap {
|
||||||
&self,
|
/// Whether the signer can sign for the internal key or not
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
is_internal_key: bool,
|
||||||
input_index: Option<usize>,
|
},
|
||||||
secp: &SecpCtx,
|
}
|
||||||
) -> Result<(), SignerError>;
|
|
||||||
|
|
||||||
/// Return whether or not the signer signs the whole transaction in one go instead of every
|
/// Wrapper structure to pair a signer with its context
|
||||||
/// input individually
|
#[derive(Debug, Clone)]
|
||||||
fn sign_whole_tx(&self) -> bool;
|
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
|
||||||
|
signer: S,
|
||||||
|
ctx: SignerContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Sized + fmt::Debug + Clone> SignerWrapper<S> {
|
||||||
|
/// Create a wrapped signer from a signer and a context
|
||||||
|
pub fn new(signer: S, ctx: SignerContext) -> Self {
|
||||||
|
SignerWrapper { signer, ctx }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Sized + fmt::Debug + Clone> Deref for SignerWrapper<S> {
|
||||||
|
type Target = S;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.signer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common signer methods
|
||||||
|
pub trait SignerCommon: fmt::Debug + Send + Sync {
|
||||||
/// Return the [`SignerId`] for this signer
|
/// Return the [`SignerId`] for this signer
|
||||||
///
|
///
|
||||||
/// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
|
/// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
|
||||||
@@ -200,14 +241,69 @@ pub trait Signer: fmt::Debug + Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
/// PSBT Input signer
|
||||||
fn sign(
|
///
|
||||||
|
/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing
|
||||||
|
/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation
|
||||||
|
/// for [`TransactionSigner`].
|
||||||
|
pub trait InputSigner: SignerCommon {
|
||||||
|
/// Sign a single psbt input
|
||||||
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
input_index: Option<usize>,
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Result<(), SignerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PSBT signer
|
||||||
|
///
|
||||||
|
/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction
|
||||||
|
/// at once.
|
||||||
|
pub trait TransactionSigner: SignerCommon {
|
||||||
|
/// Sign all the inputs of the psbt
|
||||||
|
fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
sign_options: &SignOptions,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Result<(), SignerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InputSigner> TransactionSigner for T {
|
||||||
|
fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
sign_options: &SignOptions,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Result<(), SignerError> {
|
||||||
|
for input_index in 0..psbt.inputs.len() {
|
||||||
|
self.sign_input(psbt, input_index, sign_options, secp)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||||
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
|
SignerId::from(self.root_fingerprint(secp))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||||
|
Some(DescriptorSecretKey::XPrv(self.signer.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||||
|
fn sign_input(
|
||||||
|
&self,
|
||||||
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
let input_index = input_index.unwrap();
|
|
||||||
if input_index >= psbt.inputs.len() {
|
if input_index >= psbt.inputs.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
@@ -218,19 +314,23 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tap_key_origins = psbt.inputs[input_index]
|
||||||
|
.tap_key_origins
|
||||||
|
.iter()
|
||||||
|
.map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource));
|
||||||
let (public_key, full_path) = match psbt.inputs[input_index]
|
let (public_key, full_path) = match psbt.inputs[input_index]
|
||||||
.bip32_derivation
|
.bip32_derivation
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(pk, &(fingerprint, ref path))| {
|
.map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
|
||||||
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
|
.chain(tap_key_origins)
|
||||||
Some((pk, path))
|
.find_map(|(pk, keysource)| {
|
||||||
|
if self.matches(keysource, secp).is_some() {
|
||||||
|
Some((pk, keysource.1.clone()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
}) {
|
||||||
.next()
|
Some((pk, full_path)) => (pk, full_path),
|
||||||
{
|
|
||||||
Some((pk, full_path)) => (pk, full_path.clone()),
|
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,35 +345,49 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
|||||||
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 {
|
let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key);
|
||||||
|
let valid_key = match public_key {
|
||||||
|
SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true,
|
||||||
|
SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !valid_key {
|
||||||
Err(SignerError::InvalidKey)
|
Err(SignerError::InvalidKey)
|
||||||
} else {
|
} else {
|
||||||
derived_key.private_key.sign(psbt, Some(input_index), secp)
|
// HD wallets imply compressed keys
|
||||||
|
let priv_key = PrivateKey {
|
||||||
|
compressed: true,
|
||||||
|
network: self.xkey.network,
|
||||||
|
inner: derived_key.private_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_whole_tx(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
|
||||||
SignerId::from(self.root_fingerprint(secp))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
|
||||||
Some(DescriptorSecretKey::XPrv(self.clone()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Signer for PrivateKey {
|
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||||
fn sign(
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
|
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||||
|
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||||
|
key: self.signer,
|
||||||
|
origin: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputSigner for SignerWrapper<PrivateKey> {
|
||||||
|
fn sign_input(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
input_index: Option<usize>,
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
let input_index = input_index.unwrap();
|
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,49 +397,136 @@ impl Signer for PrivateKey {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let pubkey = self.public_key(secp);
|
let pubkey = PublicKey::from_private_key(secp, self);
|
||||||
|
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||||
|
|
||||||
|
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
||||||
|
if is_internal_key
|
||||||
|
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||||
|
&& sign_options.sign_with_tap_internal_key
|
||||||
|
{
|
||||||
|
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||||
|
sign_psbt_schnorr(
|
||||||
|
&self.inner,
|
||||||
|
x_only_pubkey,
|
||||||
|
None,
|
||||||
|
&mut psbt.inputs[input_index],
|
||||||
|
hash,
|
||||||
|
hash_ty,
|
||||||
|
secp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((leaf_hashes, _)) =
|
||||||
|
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
|
||||||
|
{
|
||||||
|
let leaf_hashes = leaf_hashes
|
||||||
|
.iter()
|
||||||
|
.filter(|lh| {
|
||||||
|
// Removing the leaves we shouldn't sign for
|
||||||
|
let should_sign = match &sign_options.tap_leaves_options {
|
||||||
|
TapLeavesOptions::All => true,
|
||||||
|
TapLeavesOptions::Include(v) => v.contains(lh),
|
||||||
|
TapLeavesOptions::Exclude(v) => !v.contains(lh),
|
||||||
|
TapLeavesOptions::None => false,
|
||||||
|
};
|
||||||
|
// Filtering out the leaves without our key
|
||||||
|
should_sign
|
||||||
|
&& !psbt.inputs[input_index]
|
||||||
|
.tap_script_sigs
|
||||||
|
.contains_key(&(x_only_pubkey, **lh))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for lh in leaf_hashes {
|
||||||
|
let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
|
||||||
|
sign_psbt_schnorr(
|
||||||
|
&self.inner,
|
||||||
|
x_only_pubkey,
|
||||||
|
Some(lh),
|
||||||
|
&mut psbt.inputs[input_index],
|
||||||
|
hash,
|
||||||
|
hash_ty,
|
||||||
|
secp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: use the presence of `witness_utxo` as an indication that we should make a bip143
|
let (hash, hash_ty) = match self.ctx {
|
||||||
// sig. Does this make sense? Should we add an extra argument to explicitly switch between
|
SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?,
|
||||||
// these? The original idea was to declare sign() as sign<Ctx: ScriptContex>() and use Ctx,
|
SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?,
|
||||||
// but that violates the rules for trait-objects, so we can't do it.
|
_ => return Ok(()), // handled above
|
||||||
let (hash, sighash) = match psbt.inputs[input_index].witness_utxo {
|
|
||||||
Some(_) => Segwitv0::sighash(psbt, input_index)?,
|
|
||||||
None => Legacy::sighash(psbt, input_index)?,
|
|
||||||
};
|
};
|
||||||
|
sign_psbt_ecdsa(
|
||||||
let signature = secp.sign(
|
&self.inner,
|
||||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
pubkey,
|
||||||
&self.key,
|
&mut psbt.inputs[input_index],
|
||||||
|
hash,
|
||||||
|
hash_ty,
|
||||||
|
secp,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut final_signature = Vec::with_capacity(75);
|
|
||||||
final_signature.extend_from_slice(&signature.serialize_der());
|
|
||||||
final_signature.push(sighash.as_u32() as u8);
|
|
||||||
|
|
||||||
psbt.inputs[input_index]
|
|
||||||
.partial_sigs
|
|
||||||
.insert(pubkey, final_signature);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sign_whole_tx(&self) -> bool {
|
fn sign_psbt_ecdsa(
|
||||||
false
|
secret_key: &secp256k1::SecretKey,
|
||||||
}
|
pubkey: PublicKey,
|
||||||
|
psbt_input: &mut psbt::Input,
|
||||||
|
hash: bitcoin::Sighash,
|
||||||
|
hash_ty: EcdsaSighashType,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) {
|
||||||
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
|
let sig = secp.sign_ecdsa(msg, secret_key);
|
||||||
|
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||||
|
.expect("invalid or corrupted ecdsa signature");
|
||||||
|
|
||||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
|
||||||
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
// Calling this with `leaf_hash` = `None` will sign for key-spend
|
||||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
fn sign_psbt_schnorr(
|
||||||
key: *self,
|
secret_key: &secp256k1::SecretKey,
|
||||||
origin: None,
|
pubkey: XOnlyPublicKey,
|
||||||
}))
|
leaf_hash: Option<taproot::TapLeafHash>,
|
||||||
|
psbt_input: &mut psbt::Input,
|
||||||
|
hash: taproot::TapSighashHash,
|
||||||
|
hash_ty: SchnorrSighashType,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) {
|
||||||
|
use schnorr::TapTweak;
|
||||||
|
|
||||||
|
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
||||||
|
let keypair = match leaf_hash {
|
||||||
|
None => keypair
|
||||||
|
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||||
|
.into_inner(),
|
||||||
|
Some(_) => keypair, // no tweak for script spend
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
|
let sig = secp.sign_schnorr(msg, &keypair);
|
||||||
|
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair))
|
||||||
|
.expect("invalid or corrupted schnorr signature");
|
||||||
|
|
||||||
|
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
||||||
|
|
||||||
|
if let Some(lh) = leaf_hash {
|
||||||
|
psbt_input
|
||||||
|
.tap_script_sigs
|
||||||
|
.insert((pubkey, lh), final_signature);
|
||||||
|
} else {
|
||||||
|
psbt_input.tap_key_sig = Some(final_signature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +561,7 @@ impl From<(SignerId, SignerOrdering)> for SignersContainerKey {
|
|||||||
|
|
||||||
/// Container for multiple signers
|
/// Container for multiple signers
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn Signer>>);
|
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn TransactionSigner>>);
|
||||||
|
|
||||||
impl SignersContainer {
|
impl SignersContainer {
|
||||||
/// Create a map of public keys to secret keys
|
/// Create a map of public keys to secret keys
|
||||||
@@ -371,24 +572,37 @@ impl SignersContainer {
|
|||||||
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
|
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<KeyMap> for SignersContainer {
|
/// Build a new signer container from a [`KeyMap`]
|
||||||
fn from(keymap: KeyMap) -> SignersContainer {
|
///
|
||||||
let secp = Secp256k1::new();
|
/// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to
|
||||||
|
/// the signers
|
||||||
|
pub fn build(
|
||||||
|
keymap: KeyMap,
|
||||||
|
descriptor: &Descriptor<DescriptorPublicKey>,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> SignersContainer {
|
||||||
let mut container = SignersContainer::new();
|
let mut container = SignersContainer::new();
|
||||||
|
|
||||||
for (_, secret) in keymap {
|
for (pubkey, secret) in keymap {
|
||||||
|
let ctx = match descriptor {
|
||||||
|
Descriptor::Tr(tr) => SignerContext::Tap {
|
||||||
|
is_internal_key: tr.internal_key() == &pubkey,
|
||||||
|
},
|
||||||
|
_ if descriptor.is_witness() => SignerContext::Segwitv0,
|
||||||
|
_ => SignerContext::Legacy,
|
||||||
|
};
|
||||||
|
|
||||||
match secret {
|
match secret {
|
||||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
||||||
SignerId::from(private_key.key.public_key(&secp).to_pubkeyhash()),
|
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
|
||||||
SignerOrdering::default(),
|
SignerOrdering::default(),
|
||||||
Arc::new(private_key.key),
|
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
||||||
),
|
),
|
||||||
DescriptorSecretKey::XPrv(xprv) => container.add_external(
|
DescriptorSecretKey::XPrv(xprv) => container.add_external(
|
||||||
SignerId::from(xprv.root_fingerprint(&secp)),
|
SignerId::from(xprv.root_fingerprint(secp)),
|
||||||
SignerOrdering::default(),
|
SignerOrdering::default(),
|
||||||
Arc::new(xprv),
|
Arc::new(SignerWrapper::new(xprv, ctx)),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -409,13 +623,17 @@ impl SignersContainer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
id: SignerId,
|
id: SignerId,
|
||||||
ordering: SignerOrdering,
|
ordering: SignerOrdering,
|
||||||
signer: Arc<dyn Signer>,
|
signer: Arc<dyn TransactionSigner>,
|
||||||
) -> Option<Arc<dyn Signer>> {
|
) -> Option<Arc<dyn TransactionSigner>> {
|
||||||
self.0.insert((id, ordering).into(), signer)
|
self.0.insert((id, ordering).into(), signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a signer from the container and returns it
|
/// Removes a signer from the container and returns it
|
||||||
pub fn remove(&mut self, id: SignerId, ordering: SignerOrdering) -> Option<Arc<dyn Signer>> {
|
pub fn remove(
|
||||||
|
&mut self,
|
||||||
|
id: SignerId,
|
||||||
|
ordering: SignerOrdering,
|
||||||
|
) -> Option<Arc<dyn TransactionSigner>> {
|
||||||
self.0.remove(&(id, ordering).into())
|
self.0.remove(&(id, ordering).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,12 +646,12 @@ impl SignersContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of signers in the container, sorted by lowest to highest `ordering`
|
/// Returns the list of signers in the container, sorted by lowest to highest `ordering`
|
||||||
pub fn signers(&self) -> Vec<&Arc<dyn Signer>> {
|
pub fn signers(&self) -> Vec<&Arc<dyn TransactionSigner>> {
|
||||||
self.0.values().collect()
|
self.0.values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the signer with lowest ordering for a given id in the container.
|
/// Finds the signer with lowest ordering for a given id in the container.
|
||||||
pub fn find(&self, id: SignerId) -> Option<&Arc<dyn Signer>> {
|
pub fn find(&self, id: SignerId) -> Option<&Arc<dyn TransactionSigner>> {
|
||||||
self.0
|
self.0
|
||||||
.range((
|
.range((
|
||||||
Included(&(id.clone(), SignerOrdering(0)).into()),
|
Included(&(id.clone(), SignerOrdering(0)).into()),
|
||||||
@@ -477,38 +695,99 @@ pub struct SignOptions {
|
|||||||
///
|
///
|
||||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||||
pub allow_all_sighashes: bool,
|
pub allow_all_sighashes: bool,
|
||||||
|
|
||||||
|
/// Whether to remove partial_sigs from psbt inputs while finalizing psbt.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` which will remove partial_sigs after finalizing.
|
||||||
|
pub remove_partial_sigs: bool,
|
||||||
|
|
||||||
|
/// Whether to try finalizing psbt input after the inputs are signed.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
||||||
|
pub try_finalize: bool,
|
||||||
|
|
||||||
|
/// Specifies which Taproot script-spend leaves we should sign for. This option is
|
||||||
|
/// ignored if we're signing a non-taproot PSBT.
|
||||||
|
///
|
||||||
|
/// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
|
||||||
|
pub tap_leaves_options: TapLeavesOptions,
|
||||||
|
|
||||||
|
/// Whether we should try to sign a taproot transaction with the taproot internal key
|
||||||
|
/// or not. This option is ignored if we're signing a non-taproot PSBT.
|
||||||
|
///
|
||||||
|
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||||
|
pub sign_with_tap_internal_key: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Customize which taproot script-path leaves the signer should sign.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum TapLeavesOptions {
|
||||||
|
/// The signer will sign all the leaves it has a key for.
|
||||||
|
All,
|
||||||
|
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
||||||
|
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||||
|
Include(Vec<taproot::TapLeafHash>),
|
||||||
|
/// The signer won't sign the specified leaves.
|
||||||
|
Exclude(Vec<taproot::TapLeafHash>),
|
||||||
|
/// The signer won't sign any leaf.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TapLeavesOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
TapLeavesOptions::All
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::derivable_impls)]
|
||||||
impl Default for SignOptions {
|
impl Default for SignOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
SignOptions {
|
SignOptions {
|
||||||
trust_witness_utxo: false,
|
trust_witness_utxo: false,
|
||||||
assume_height: None,
|
assume_height: None,
|
||||||
allow_all_sighashes: false,
|
allow_all_sighashes: false,
|
||||||
|
remove_partial_sigs: true,
|
||||||
|
try_finalize: true,
|
||||||
|
tap_leaves_options: TapLeavesOptions::default(),
|
||||||
|
sign_with_tap_internal_key: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ComputeSighash {
|
pub(crate) trait ComputeSighash {
|
||||||
|
type Extra;
|
||||||
|
type Sighash;
|
||||||
|
type SighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
) -> Result<(SigHash, SigHashType), SignerError>;
|
extra: Self::Extra,
|
||||||
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComputeSighash for Legacy {
|
impl ComputeSighash for Legacy {
|
||||||
|
type Extra = ();
|
||||||
|
type Sighash = bitcoin::Sighash;
|
||||||
|
type SighashType = EcdsaSighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
_extra: (),
|
||||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||||
|
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
let psbt_input = &psbt.inputs[input_index];
|
let psbt_input = &psbt.inputs[input_index];
|
||||||
let tx_input = &psbt.global.unsigned_tx.input[input_index];
|
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||||
|
|
||||||
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
|
let sighash = psbt_input
|
||||||
|
.sighash_type
|
||||||
|
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||||
|
.ecdsa_hash_ty()
|
||||||
|
.map_err(|_| SignerError::InvalidSighash)?;
|
||||||
let script = match psbt_input.redeem_script {
|
let script = match psbt_input.redeem_script {
|
||||||
Some(ref redeem_script) => redeem_script.clone(),
|
Some(ref redeem_script) => redeem_script.clone(),
|
||||||
None => {
|
None => {
|
||||||
@@ -526,9 +805,11 @@ impl ComputeSighash for Legacy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
psbt.global
|
sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
|
||||||
.unsigned_tx
|
input_index,
|
||||||
.signature_hash(input_index, &script, sighash.as_u32()),
|
&script,
|
||||||
|
sighash.to_u32(),
|
||||||
|
)?,
|
||||||
sighash,
|
sighash,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -545,18 +826,27 @@ fn p2wpkh_script_code(script: &Script) -> Script {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ComputeSighash for Segwitv0 {
|
impl ComputeSighash for Segwitv0 {
|
||||||
|
type Extra = ();
|
||||||
|
type Sighash = bitcoin::Sighash;
|
||||||
|
type SighashType = EcdsaSighashType;
|
||||||
|
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
_extra: (),
|
||||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||||
|
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
let psbt_input = &psbt.inputs[input_index];
|
let psbt_input = &psbt.inputs[input_index];
|
||||||
let tx_input = &psbt.global.unsigned_tx.input[input_index];
|
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||||
|
|
||||||
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
|
let sighash = psbt_input
|
||||||
|
.sighash_type
|
||||||
|
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||||
|
.ecdsa_hash_ty()
|
||||||
|
.map_err(|_| SignerError::InvalidSighash)?;
|
||||||
|
|
||||||
// Always try first with the non-witness utxo
|
// Always try first with the non-witness utxo
|
||||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||||
@@ -599,17 +889,72 @@ impl ComputeSighash for Segwitv0 {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
bip143::SigHashCache::new(&psbt.global.unsigned_tx).signature_hash(
|
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
|
||||||
input_index,
|
input_index,
|
||||||
&script,
|
&script,
|
||||||
value,
|
value,
|
||||||
sighash,
|
sighash,
|
||||||
),
|
)?,
|
||||||
sighash,
|
sighash,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ComputeSighash for Tap {
|
||||||
|
type Extra = Option<taproot::TapLeafHash>;
|
||||||
|
type Sighash = taproot::TapSighashHash;
|
||||||
|
type SighashType = SchnorrSighashType;
|
||||||
|
|
||||||
|
fn sighash(
|
||||||
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
|
input_index: usize,
|
||||||
|
extra: Self::Extra,
|
||||||
|
) -> Result<(Self::Sighash, SchnorrSighashType), SignerError> {
|
||||||
|
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||||
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
let psbt_input = &psbt.inputs[input_index];
|
||||||
|
|
||||||
|
let sighash_type = psbt_input
|
||||||
|
.sighash_type
|
||||||
|
.unwrap_or_else(|| SchnorrSighashType::Default.into())
|
||||||
|
.schnorr_hash_ty()
|
||||||
|
.map_err(|_| SignerError::InvalidSighash)?;
|
||||||
|
let witness_utxos = psbt
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|i| i.witness_utxo)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut all_witness_utxos = vec![];
|
||||||
|
|
||||||
|
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||||
|
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
|
||||||
|
let prevouts = if is_anyone_can_pay {
|
||||||
|
sighash::Prevouts::One(
|
||||||
|
input_index,
|
||||||
|
witness_utxos[input_index]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(SignerError::MissingWitnessUtxo)?,
|
||||||
|
)
|
||||||
|
} else if witness_utxos.iter().all(Option::is_some) {
|
||||||
|
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
|
||||||
|
sighash::Prevouts::All(&all_witness_utxos)
|
||||||
|
} else {
|
||||||
|
return Err(SignerError::MissingWitnessUtxo);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assume no OP_CODESEPARATOR
|
||||||
|
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
|
||||||
|
sighash_type,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialOrd for SignersContainerKey {
|
impl PartialOrd for SignersContainerKey {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
@@ -640,12 +985,11 @@ mod signers_container_tests {
|
|||||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
|
||||||
use bitcoin::Network;
|
use bitcoin::Network;
|
||||||
use miniscript::ScriptContext;
|
use miniscript::ScriptContext;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
fn is_equal(this: &Arc<dyn Signer>, that: &Arc<DummySigner>) -> bool {
|
fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
this.id(&secp) == that.id(&secp)
|
this.id(&secp) == that.id(&secp)
|
||||||
}
|
}
|
||||||
@@ -660,11 +1004,11 @@ mod signers_container_tests {
|
|||||||
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
|
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
|
||||||
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
|
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
|
||||||
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
|
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
|
||||||
let (_, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let signers = SignersContainer::from(keymap);
|
let signers = SignersContainer::build(keymap, &wallet_desc, &secp);
|
||||||
assert_eq!(signers.ids().len(), 2);
|
assert_eq!(signers.ids().len(), 2);
|
||||||
|
|
||||||
let signers = signers.signers();
|
let signers = signers.signers();
|
||||||
@@ -726,22 +1070,20 @@ mod signers_container_tests {
|
|||||||
number: u64,
|
number: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Signer for DummySigner {
|
impl SignerCommon for DummySigner {
|
||||||
fn sign(
|
|
||||||
&self,
|
|
||||||
_psbt: &mut PartiallySignedTransaction,
|
|
||||||
_input_index: Option<usize>,
|
|
||||||
_secp: &SecpCtx,
|
|
||||||
) -> Result<(), SignerError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self, _secp: &SecpCtx) -> SignerId {
|
fn id(&self, _secp: &SecpCtx) -> SignerId {
|
||||||
SignerId::Dummy(self.number)
|
SignerId::Dummy(self.number)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sign_whole_tx(&self) -> bool {
|
impl TransactionSigner for DummySigner {
|
||||||
true
|
fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
_sign_options: &SignOptions,
|
||||||
|
_secp: &SecpCtx,
|
||||||
|
) -> Result<(), SignerError> {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,7 +1098,7 @@ mod signers_container_tests {
|
|||||||
let secp: Secp256k1<All> = Secp256k1::new();
|
let secp: Secp256k1<All> = Secp256k1::new();
|
||||||
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
||||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||||
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
|
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
|
||||||
let fingerprint = tprv.fingerprint(&secp);
|
let fingerprint = tprv.fingerprint(&secp);
|
||||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ use std::default::Default;
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
use bitcoin::{OutPoint, Script, Transaction};
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorTrait;
|
use miniscript::descriptor::DescriptorTrait;
|
||||||
|
|
||||||
@@ -103,10 +103,7 @@ impl TxBuilderContext for BumpFee {}
|
|||||||
/// builder.finish()?
|
/// builder.finish()?
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// assert_eq!(
|
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
|
||||||
/// psbt1.global.unsigned_tx.output[..2],
|
|
||||||
/// psbt2.global.unsigned_tx.output[..2]
|
|
||||||
/// );
|
|
||||||
/// # Ok::<(), bdk::Error>(())
|
/// # Ok::<(), bdk::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
@@ -140,7 +137,7 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) utxos: Vec<WeightedUtxo>,
|
pub(crate) utxos: Vec<WeightedUtxo>,
|
||||||
pub(crate) unspendable: HashSet<OutPoint>,
|
pub(crate) unspendable: HashSet<OutPoint>,
|
||||||
pub(crate) manually_selected_only: bool,
|
pub(crate) manually_selected_only: bool,
|
||||||
pub(crate) sighash: Option<SigHashType>,
|
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
||||||
pub(crate) ordering: TxOrdering,
|
pub(crate) ordering: TxOrdering,
|
||||||
pub(crate) locktime: Option<u32>,
|
pub(crate) locktime: Option<u32>,
|
||||||
pub(crate) rbf: Option<RbfValue>,
|
pub(crate) rbf: Option<RbfValue>,
|
||||||
@@ -150,6 +147,8 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) add_global_xpubs: bool,
|
pub(crate) add_global_xpubs: bool,
|
||||||
pub(crate) include_output_redeem_witness_script: bool,
|
pub(crate) include_output_redeem_witness_script: bool,
|
||||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||||
|
pub(crate) current_height: Option<u32>,
|
||||||
|
pub(crate) allow_dust: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -337,8 +336,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
|
/// 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`.
|
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
|
||||||
///
|
///
|
||||||
/// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
|
/// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
|
||||||
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
|
/// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
|
||||||
|
/// is called.
|
||||||
///
|
///
|
||||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||||
/// [`finish`]: Self::finish
|
/// [`finish`]: Self::finish
|
||||||
@@ -412,7 +412,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
/// Sign with a specific sig hash
|
/// Sign with a specific sig hash
|
||||||
///
|
///
|
||||||
/// **Use this option very carefully**
|
/// **Use this option very carefully**
|
||||||
pub fn sighash(&mut self, sighash: SigHashType) -> &mut Self {
|
pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self {
|
||||||
self.params.sighash = Some(sighash);
|
self.params.sighash = Some(sighash);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -517,7 +517,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the building the transaction.
|
/// Finish building the transaction.
|
||||||
///
|
///
|
||||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||||
///
|
///
|
||||||
@@ -545,6 +545,30 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the current blockchain height.
|
||||||
|
///
|
||||||
|
/// This will be used to:
|
||||||
|
/// 1. Set the nLockTime for preventing fee sniping.
|
||||||
|
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
|
||||||
|
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
|
||||||
|
/// mature at `current_height`, we ignore them in the coin selection.
|
||||||
|
/// If you want to create a transaction that spends immature coinbase inputs, manually
|
||||||
|
/// add them using [`TxBuilder::add_utxos`].
|
||||||
|
///
|
||||||
|
/// In both cases, if you don't provide a current height, we use the last sync height.
|
||||||
|
pub fn current_height(&mut self, height: u32) -> &mut Self {
|
||||||
|
self.params.current_height = Some(height);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether or not the dust limit is checked.
|
||||||
|
///
|
||||||
|
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
|
||||||
|
pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
|
||||||
|
self.params.allow_dust = allow_dust;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
||||||
@@ -576,6 +600,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
|||||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||||
///
|
///
|
||||||
|
/// If you choose not to set any recipients, you should either provide the utxos that the
|
||||||
|
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
|
||||||
|
///
|
||||||
/// When bumping the fees of a transaction made with this option, you probably want to
|
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||||
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||||
///
|
///
|
||||||
@@ -606,6 +633,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
|||||||
///
|
///
|
||||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||||
/// [`add_recipient`]: Self::add_recipient
|
/// [`add_recipient`]: Self::add_recipient
|
||||||
|
/// [`add_utxos`]: Self::add_utxos
|
||||||
/// [`drain_wallet`]: Self::drain_wallet
|
/// [`drain_wallet`]: Self::drain_wallet
|
||||||
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
||||||
self.params.drain_to = Some(script_pubkey);
|
self.params.drain_to = Some(script_pubkey);
|
||||||
@@ -615,7 +643,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
|||||||
|
|
||||||
// methods supported only by bump_fee
|
// methods supported only by bump_fee
|
||||||
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||||
/// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
|
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
|
||||||
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||||
/// will attempt to find a change output to shrink instead.
|
/// will attempt to find a change output to shrink instead.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ mod test {
|
|||||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
||||||
};
|
};
|
||||||
use crate::bitcoin::Address;
|
use crate::bitcoin::Address;
|
||||||
use crate::types::FeeRate;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -164,24 +163,6 @@ mod test {
|
|||||||
assert!(!294.is_dust(&script_p2wpkh));
|
assert!(!294.is_dust(&script_p2wpkh));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_btc_per_kb() {
|
|
||||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_sats_vbyte() {
|
|
||||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_default_min_relay_fee() {
|
|
||||||
let fee = FeeRate::default_min_relay_fee();
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_msb_set() {
|
fn test_check_nsequence_rbf_msb_set() {
|
||||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user