Compare commits
329 Commits
get_change
...
v0.25.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d288cbbbbc | ||
|
|
005447c81e | ||
|
|
5168d41a93 | ||
|
|
4c5ceaff14 | ||
|
|
b5fcddcf1a | ||
|
|
d570ff2c65 | ||
|
|
21c96c9c81 | ||
|
|
c51d544932 | ||
|
|
5e56c3b3c1 | ||
|
|
235961a934 | ||
|
|
df905a8d5e | ||
|
|
8b68cf9546 | ||
|
|
150f4d6f41 | ||
|
|
1c95ca33a8 | ||
|
|
108edc3a6b | ||
|
|
f99a6b9f43 | ||
|
|
aedbc8c97d | ||
|
|
5c42102c79 | ||
|
|
5d5b2fb88c | ||
|
|
9cb6f70fc0 | ||
|
|
5720e38033 | ||
|
|
e9bbb8724f | ||
|
|
648282e602 | ||
|
|
c7a43d941f | ||
|
|
1ffd59d469 | ||
|
|
ae4f4e5416 | ||
|
|
9854fd34ea | ||
|
|
60057a7bf7 | ||
|
|
ea47d7a35b | ||
|
|
f2181f5467 | ||
|
|
34987d58ec | ||
|
|
1c76084db8 | ||
|
|
68dd6d2031 | ||
|
|
1437e1ecfe | ||
|
|
1a71eb1f47 | ||
|
|
0695e9fb3e | ||
|
|
a4a43ea860 | ||
|
|
b627455b8f | ||
|
|
1331193800 | ||
|
|
7de8be46c0 | ||
|
|
55145f57a1 | ||
|
|
8e8fd49e04 | ||
|
|
d7bfe68e2d | ||
|
|
b11c86d074 | ||
|
|
e2a4a5884b | ||
|
|
fd34956c29 | ||
|
|
b5b92248c7 | ||
|
|
cf2bc388f2 | ||
|
|
5baf46f84d | ||
|
|
a8cf34e809 | ||
|
|
af0b3698c6 | ||
|
|
92ad4876c4 | ||
|
|
b14e4ee3a0 | ||
|
|
e6f2d029fa | ||
|
|
bbf524b3f9 | ||
|
|
dbf6bf5fdf | ||
|
|
aff41d6e1c | ||
|
|
e2bf9734b1 | ||
|
|
c3faf05be9 | ||
|
|
aad5461ee1 | ||
|
|
0a7a1f4ef2 | ||
|
|
5e9965fca7 | ||
|
|
54d768412a | ||
|
|
97b6fb06aa | ||
|
|
da7670801b | ||
|
|
e1fa0b6695 | ||
|
|
dfeb08fa00 | ||
|
|
8963e8c9f4 | ||
|
|
562cb81cad | ||
|
|
b12dec3620 | ||
|
|
e65edbf53c | ||
|
|
88307045b0 | ||
|
|
e06c3f945c | ||
|
|
ab41679368 | ||
|
|
7b12f35698 | ||
|
|
aa0ea6aeff | ||
|
|
c3a7bbb3ff | ||
|
|
1c4d47825b | ||
|
|
fa998de4b1 | ||
|
|
06310f1dd0 | ||
|
|
8dd02094df | ||
|
|
0010ecd94a | ||
|
|
690411722e | ||
|
|
7001b14b4c | ||
|
|
13cf72ffa7 | ||
|
|
d7163c3a97 | ||
|
|
cf13c80991 | ||
|
|
7c57965999 | ||
|
|
3d69f1c291 | ||
|
|
3451d1c12e | ||
|
|
4fbd8520e6 | ||
|
|
bfd7b2f65d | ||
|
|
061f15af00 | ||
|
|
369e17b801 | ||
|
|
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 | ||
|
|
b5c8ce924b | ||
|
|
e3ce50059f | ||
|
|
8a2a6bbcee | ||
|
|
122e6e7140 | ||
|
|
1018bb2b17 | ||
|
|
9ed36875f1 | ||
|
|
502882d27c | ||
|
|
a328607d27 | ||
|
|
f90e3f978e | ||
|
|
68e1b32d81 | ||
|
|
44758f9483 | ||
|
|
c350064dae | ||
|
|
92746440db | ||
|
|
e4eb95fb9c | ||
|
|
c307bacb9c | ||
|
|
a111d25476 | ||
|
|
c752ccbdde | ||
|
|
0621ca89d5 | ||
|
|
adef166b22 | ||
|
|
213f18f7b7 | ||
|
|
8cd055090d | ||
|
|
1b9014846c | ||
|
|
9c0141b5e3 | ||
|
|
2698fc0219 | ||
|
|
6931d0bd1f | ||
|
|
545beec743 | ||
|
|
bac15bb207 | ||
|
|
06b80fdb15 | ||
|
|
ff6db18726 | ||
|
|
86abd8698f | ||
|
|
0d9c2f76e0 | ||
|
|
63d5bcee93 | ||
|
|
8a98e69e78 | ||
|
|
c6eeb7b989 | ||
|
|
3334c8da07 | ||
|
|
ce09203431 | ||
|
|
cac312d34f | ||
|
|
4b1be68965 | ||
|
|
559cfc4373 | ||
|
|
1e9a684b54 | ||
|
|
52bc63e48f | ||
|
|
9a6db15d26 | ||
|
|
52bcd105eb | ||
|
|
1803f5ea8a | ||
|
|
f2f0efc0b3 | ||
|
|
3e4678d8e3 | ||
|
|
0cc4700bd6 | ||
|
|
660faab1e2 | ||
|
|
45767fcaf7 | ||
|
|
d03aa85108 | ||
|
|
adf7d0c126 | ||
|
|
4291f84d79 | ||
|
|
f0188f49a8 | ||
|
|
edf2f0ce06 | ||
|
|
364ad95e85 | ||
|
|
fbb50ad1c8 | ||
|
|
035307ef54 | ||
|
|
c0e75fc1a8 | ||
|
|
dcd90f8b61 | ||
|
|
410a51355b | ||
|
|
326bfe82a8 | ||
|
|
b23a0747b5 | ||
|
|
552765bb58 | ||
|
|
f3e479fa7f | ||
|
|
5698c683c6 | ||
|
|
a83aa0461c |
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Request a new feature or change to an existing feature
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the enhancement**
|
||||
<!-- A clear and concise description of what you would like added or changed. -->
|
||||
|
||||
**Use case**
|
||||
<!-- Tell us how you or others will use this new feature or change to an existing feature. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the enhancement here. -->
|
||||
98
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
98
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
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`.
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.1".
|
||||
- [ ] 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.
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.x+1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.x+1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.x+1".
|
||||
- [ ] 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:
|
||||
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
|
||||
- [ ] 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/
|
||||
70
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
70
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
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.
|
||||
- [ ] Bump the `release/MAJOR.MINOR.PATCH+1` branch to `MAJOR.MINOR.PATCH+1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR.MINOR.PATCH+1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
|
||||
- [ ] 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
|
||||
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
|
||||
|
||||
#### 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 docs for the new feature
|
||||
* [ ] I've updated `CHANGELOG.md`
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
|
||||
56
.github/workflows/code_coverage.yml
vendored
56
.github/workflows/code_coverage.yml
vendored
@@ -1,37 +1,61 @@
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Code Coverage
|
||||
|
||||
jobs:
|
||||
|
||||
Codecov:
|
||||
name: Code Coverage
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
RUSTDOCFLAGS: "-Cinstrument-coverage"
|
||||
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install lcov tools
|
||||
run: sudo apt-get install lcov -y
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add llvm tools
|
||||
run: rustup component add llvm-tools-preview
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
||||
|
||||
- id: coverage
|
||||
name: Generate coverage
|
||||
uses: actions-rs/grcov@v0.1.5
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
directory: ./coverage/reports/
|
||||
path: |
|
||||
~/.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
|
||||
# WARNING: this is not testing the following features: test-esplora, test-hardware-signer, async-interface
|
||||
# This is because some of our features are mutually exclusive, and generating various reports and
|
||||
# merging them doesn't seem to be working very well.
|
||||
# For more info, see:
|
||||
# - https://github.com/bitcoindevkit/bdk/issues/696
|
||||
# - https://github.com/bitcoindevkit/bdk/pull/748#issuecomment-1242721040
|
||||
run: cargo test --features all-keys,compact_filters,compiler,key-value-db,sqlite,sqlite-bundled,test-electrum,test-rpc,verify
|
||||
- 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
|
||||
|
||||
59
.github/workflows/cont_integration.yml
vendored
59
.github/workflows/cont_integration.yml
vendored
@@ -10,24 +10,25 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- version: 1.56.0 # STABLE
|
||||
- version: 1.60.0 # STABLE
|
||||
clippy: true
|
||||
- version: 1.46.0 # MSRV
|
||||
- version: 1.56.1 # MSRV
|
||||
features:
|
||||
- default
|
||||
- minimal
|
||||
- all-keys
|
||||
- minimal,use-esplora-ureq
|
||||
- minimal,use-esplora-blocking
|
||||
- key-value-db
|
||||
- electrum
|
||||
- compact_filters
|
||||
- esplora,ureq,key-value-db,electrum
|
||||
- use-esplora-blocking,key-value-db,electrum
|
||||
- compiler
|
||||
- rpc
|
||||
- verify
|
||||
- async-interface
|
||||
- use-esplora-reqwest
|
||||
- use-esplora-async
|
||||
- sqlite
|
||||
- sqlite-bundled
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -89,13 +90,20 @@ jobs:
|
||||
matrix:
|
||||
blockchain:
|
||||
- name: electrum
|
||||
testprefix: blockchain::electrum::test
|
||||
features: test-electrum,verify
|
||||
- name: rpc
|
||||
testprefix: blockchain::rpc::test
|
||||
features: test-rpc
|
||||
- name: rpc-legacy
|
||||
testprefix: blockchain::rpc::test
|
||||
features: test-rpc-legacy
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-reqwest,verify
|
||||
testprefix: esplora
|
||||
features: test-esplora,use-esplora-async,verify
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-ureq,verify
|
||||
testprefix: esplora
|
||||
features: test-esplora,use-esplora-blocking,verify
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -113,8 +121,8 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- 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:
|
||||
name: Check WASM
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -138,7 +146,7 @@ jobs:
|
||||
- run: sudo apt-get update || exit 1
|
||||
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||
- name: Set default toolchain
|
||||
run: rustup default 1.56.0 # STABLE
|
||||
run: rustup default 1.56.1 # STABLE
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add target wasm32
|
||||
@@ -146,7 +154,7 @@ jobs:
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Check
|
||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
|
||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
@@ -164,3 +172,32 @@ jobs:
|
||||
run: rustup update
|
||||
- name: Check fmt
|
||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||
|
||||
test_hardware_wallet:
|
||||
runs-on: ubuntu-20.04
|
||||
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
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Build docs
|
||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
|
||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,use-esplora-blocking,compact_filters,rpc,key-value-db,sqlite,all-keys,verify,hardware-signer -- --cfg docsrs -Dwarnings
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/.vscode
|
||||
|
||||
*.swp
|
||||
.idea
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -1,15 +1,86 @@
|
||||
# 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/),
|
||||
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]
|
||||
|
||||
- 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.
|
||||
- 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`.
|
||||
- Improve key generation traits
|
||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
||||
- Bump `miniscript` dependency version to `^6.1`.
|
||||
|
||||
## [v0.17.0] - [v0.16.1]
|
||||
|
||||
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
|
||||
- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
|
||||
- `verify` flag removed from `TransactionDetails`.
|
||||
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
|
||||
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
|
||||
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
|
||||
|
||||
### Sync API change
|
||||
|
||||
To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||
|
||||
- Removed `Blockchain` from Wallet.
|
||||
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
|
||||
- Deprecated `Wallet::new_offline` (all wallets are offline now)
|
||||
- Changed `Wallet::sync` to take a `Blockchain`.
|
||||
- Stop making a request for the block height when calling `Wallet:new`.
|
||||
- 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`.
|
||||
- remove `flush` method from the `Database` trait.
|
||||
|
||||
## [v0.16.1] - [v0.16.0]
|
||||
|
||||
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
|
||||
|
||||
## [v0.16.0] - [v0.15.0]
|
||||
|
||||
@@ -399,7 +470,6 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Use `MemoryDatabase` in the compiler example
|
||||
- Make the REPL return JSON
|
||||
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...HEAD
|
||||
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
||||
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
|
||||
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
||||
@@ -416,4 +486,10 @@ final transaction is created by calling `finish` on the builder.
|
||||
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
|
||||
[v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0
|
||||
[v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0
|
||||
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
|
||||
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
|
||||
[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.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
||||
[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
|
||||
|
||||
107
Cargo.toml
107
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.16.1-dev"
|
||||
version = "0.25.0"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -14,40 +14,39 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
bdk-macros = "^0.6"
|
||||
log = "^0.4"
|
||||
miniscript = { version = "^6.0", features = ["use-serde"] }
|
||||
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
|
||||
miniscript = { version = "8.0", features = ["serde"] }
|
||||
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.7"
|
||||
rand = "^0.8"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.8", optional = true }
|
||||
rusqlite = { version = "0.25.3", optional = true }
|
||||
ahash = { version = "=0.7.4", optional = true }
|
||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
||||
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
||||
electrum-client = { version = "0.12", optional = true }
|
||||
esplora-client = { version = "0.2", default-features = false, optional = true }
|
||||
rusqlite = { version = "0.27.0", optional = true }
|
||||
ahash = { version = "0.7.6", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
||||
cc = { version = ">=1.0.64", optional = true }
|
||||
socks = { version = "0.3", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
hwi = { version = "0.3.0", optional = true }
|
||||
|
||||
bip39 = { version = "1.0.1", optional = true }
|
||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||
|
||||
# Needed by bdk_blockchain_tests macro
|
||||
bitcoincore-rpc = { version = "0.14", optional = true }
|
||||
# Needed by bdk_blockchain_tests macro and the `rpc` feature
|
||||
bitcoincore-rpc = { version = "0.16", optional = true }
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "~1.14", features = ["rt"] }
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
async-trait = "0.1"
|
||||
js-sys = "0.3"
|
||||
rand = { version = "^0.7", features = ["wasm-bindgen"] }
|
||||
|
||||
[features]
|
||||
minimal = []
|
||||
@@ -55,11 +54,13 @@ compiler = ["miniscript/compiler"]
|
||||
verify = ["bitcoinconsensus"]
|
||||
default = ["key-value-db", "electrum"]
|
||||
sqlite = ["rusqlite", "ahash"]
|
||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
|
||||
compact_filters = ["rocksdb", "socks", "cc"]
|
||||
key-value-db = ["sled"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
rpc = ["bitcoincore-rpc"]
|
||||
hardware-signer = ["hwi"]
|
||||
|
||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||
# blocking except for the `EsploraBlockchain` which can be either async or
|
||||
@@ -67,39 +68,48 @@ rpc = ["bitcoincore-rpc"]
|
||||
#
|
||||
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
|
||||
# access to the asynchronous method implementations. Then, if Esplora is wanted,
|
||||
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
|
||||
# enable the `use-esplora-async` feature.
|
||||
# - Users wanting blocking HTTP calls can use any of the other blockchain
|
||||
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
|
||||
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
|
||||
# use Esplora should enable the `use-esplora-blocking` feature.
|
||||
#
|
||||
# WARNING: Please take care with the features below, various combinations will
|
||||
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
||||
async-interface = ["async-trait"]
|
||||
electrum = ["electrum-client"]
|
||||
# MUST ALSO USE `--no-default-features`.
|
||||
use-esplora-reqwest = ["esplora", "reqwest", "reqwest/socks", "futures"]
|
||||
use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
|
||||
use-esplora-async = ["esplora", "esplora-client/async", "futures"]
|
||||
use-esplora-blocking = ["esplora", "esplora-client/blocking"]
|
||||
# Deprecated aliases
|
||||
use-esplora-reqwest = ["use-esplora-async"]
|
||||
use-esplora-ureq = ["use-esplora-blocking"]
|
||||
# Typical configurations will not need to use `esplora` feature directly.
|
||||
esplora = []
|
||||
|
||||
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
|
||||
reqwest-default-tls = ["reqwest/default-tls"]
|
||||
# Use below feature with `use-esplora-async` to enable reqwest default TLS support
|
||||
reqwest-default-tls = ["esplora-client/async-https"]
|
||||
|
||||
# Debug/Test features
|
||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
|
||||
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "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-hardware-signer = ["hardware-signer"]
|
||||
|
||||
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
|
||||
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
|
||||
dev-getrandom-wasm = ["getrandom/js"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
clap = "2.33"
|
||||
electrsd = { version= "0.13", features = ["trigger", "bitcoind_22_0"] }
|
||||
electrsd = "0.21"
|
||||
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
|
||||
base64 = "^0.13"
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
[[example]]
|
||||
name = "compact_filters_balance"
|
||||
required-features = ["compact_filters"]
|
||||
@@ -109,9 +119,48 @@ name = "miniscriptc"
|
||||
path = "examples/compiler.rs"
|
||||
required-features = ["compiler"]
|
||||
|
||||
[[example]]
|
||||
name = "policy"
|
||||
path = "examples/policy.rs"
|
||||
|
||||
[[example]]
|
||||
name = "rpcwallet"
|
||||
path = "examples/rpcwallet.rs"
|
||||
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||
|
||||
[[example]]
|
||||
name = "psbt_signer"
|
||||
path = "examples/psbt_signer.rs"
|
||||
required-features = ["electrum"]
|
||||
|
||||
[[example]]
|
||||
name = "hardware_signer"
|
||||
path = "examples/hardware_signer.rs"
|
||||
required-features = ["electrum", "hardware-signer"]
|
||||
|
||||
[[example]]
|
||||
name = "electrum_backend"
|
||||
path = "examples/electrum_backend.rs"
|
||||
required-features = ["electrum"]
|
||||
|
||||
[[example]]
|
||||
name = "esplora_backend_synchronous"
|
||||
path = "examples/esplora_backend_synchronous.rs"
|
||||
required-features = ["use-esplora-ureq"]
|
||||
|
||||
[[example]]
|
||||
name = "esplora_backend_asynchronous"
|
||||
path = "examples/esplora_backend_asynchronous.rs"
|
||||
required-features = ["use-esplora-reqwest", "reqwest-default-tls", "async-interface"]
|
||||
|
||||
[[example]]
|
||||
name = "mnemonic_to_descriptors"
|
||||
path = "examples/mnemonic_to_descriptors.rs"
|
||||
required-features = ["all-keys"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
[package.metadata.docs.rs]
|
||||
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
|
||||
features = ["compiler", "electrum", "esplora", "use-esplora-blocking", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
||||
# defines the configuration attribute `docsrs`
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
# 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**.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
master: - - - - * - - - * - - - - - - * - - - * ...
|
||||
| / | |
|
||||
release/0.x.0: * - - # | |
|
||||
| /
|
||||
release/0.y.0: * - - #
|
||||
```
|
||||
To create a new release a release manager will create a new issue using the `Release` template and follow the template instructions.
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
|
||||
[Semantic Versioning]: https://semver.org/
|
||||
|
||||
48
README.md
48
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://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://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://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>
|
||||
</p>
|
||||
|
||||
@@ -41,21 +41,21 @@ The `bdk` library aims to be the core building block for Bitcoin wallets of any
|
||||
```rust,no_run
|
||||
use bdk::Wallet;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::SyncOptions;
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::bitcoin::Network;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||
|
||||
@@ -70,7 +70,7 @@ use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
@@ -88,26 +88,26 @@ fn main() -> Result<(), bdk::Error> {
|
||||
### Create a transaction
|
||||
|
||||
```rust,no_run
|
||||
use bdk::{FeeRate, Wallet};
|
||||
use bdk::{FeeRate, Wallet, SyncOptions};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
use base64;
|
||||
use bitcoin::consensus::serialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
let send_to = wallet.get_address(New)?;
|
||||
let (psbt, details) = {
|
||||
@@ -132,10 +132,11 @@ fn main() -> Result<(), bdk::Error> {
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use base64;
|
||||
use bitcoin::consensus::deserialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
@@ -155,7 +156,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
|
||||
### Unit testing
|
||||
|
||||
```
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
@@ -163,13 +164,24 @@ cargo test
|
||||
|
||||
Integration testing require testing features, for example:
|
||||
|
||||
```
|
||||
```bash
|
||||
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.
|
||||
|
||||
## Running under WASM
|
||||
|
||||
If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
```
|
||||
|
||||
This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
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
|
||||
@@ -1,61 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use bdk::bitcoin;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::descriptor::HdKeyPaths;
|
||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
||||
use bdk::KeychainKind;
|
||||
use bdk::Wallet;
|
||||
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::{Network, Script};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DummyValidator;
|
||||
impl AddressValidator for DummyValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
keychain: KeychainKind,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
script: &Script,
|
||||
) -> Result<(), AddressValidatorError> {
|
||||
let (_, path) = hd_keypaths
|
||||
.values()
|
||||
.find(|(fing, _)| fing == &Fingerprint::from_hex("bc123c3e").unwrap())
|
||||
.ok_or(AddressValidatorError::InvalidScript)?;
|
||||
|
||||
println!(
|
||||
"Validating `{:?}` {} address, script: {}",
|
||||
keychain, path, script
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
||||
let mut wallet =
|
||||
Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
||||
|
||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
||||
|
||||
wallet.get_address(New)?;
|
||||
wallet.get_address(New)?;
|
||||
wallet.get_address(New)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
// licenses.
|
||||
|
||||
use bdk::blockchain::compact_filters::*;
|
||||
use bdk::blockchain::noop_progress;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::*;
|
||||
use bitcoin::*;
|
||||
@@ -35,9 +34,8 @@ fn main() -> Result<(), CompactFiltersError> {
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
|
||||
|
||||
let database = MemoryDatabase::default();
|
||||
let wallet =
|
||||
Arc::new(Wallet::new(descriptor, None, Network::Testnet, database, blockchain).unwrap());
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
let wallet = Arc::new(Wallet::new(descriptor, None, Network::Testnet, database).unwrap());
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
info!("balance: {}", wallet.get_balance()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bitcoin;
|
||||
extern crate clap;
|
||||
extern crate log;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
@@ -21,8 +20,6 @@ use std::str::FromStr;
|
||||
|
||||
use log::info;
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
@@ -31,75 +28,49 @@ use bdk::database::memory::MemoryDatabase;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
|
||||
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
|
||||
/// can be derived from the policy.
|
||||
///
|
||||
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
||||
let matches = App::new("Miniscript Compiler")
|
||||
.arg(
|
||||
Arg::with_name("POLICY")
|
||||
.help("Sets the spending policy to compile")
|
||||
.required(true)
|
||||
.index(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("TYPE")
|
||||
.help("Sets the script type used to embed the compiled policy")
|
||||
.required(true)
|
||||
.index(2)
|
||||
.possible_values(&["sh", "wsh", "sh-wsh"]),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("parsed_policy")
|
||||
.long("parsed_policy")
|
||||
.short("p")
|
||||
.help("Also return the parsed spending policy in JSON format"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("network")
|
||||
.short("n")
|
||||
.long("network")
|
||||
.help("Sets the network")
|
||||
.takes_value(true)
|
||||
.default_value("testnet")
|
||||
.possible_values(&["testnet", "regtest", "bitcoin", "signet"]),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let policy_str = matches.value_of("POLICY").unwrap();
|
||||
info!("Compiling policy: {}", policy_str);
|
||||
// We start with a generic miniscript policy string
|
||||
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
|
||||
info!("Compiling policy: \n{}", policy_str);
|
||||
|
||||
// Parse the string as a [`Concrete`] type miniscript policy.
|
||||
let policy = Concrete::<String>::from_str(policy_str)?;
|
||||
|
||||
let descriptor = match matches.value_of("TYPE").unwrap() {
|
||||
"sh" => Descriptor::new_sh(policy.compile()?)?,
|
||||
"wsh" => Descriptor::new_wsh(policy.compile()?)?,
|
||||
"sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?)?,
|
||||
_ => panic!("Invalid type"),
|
||||
};
|
||||
// Create a `wsh` type descriptor from the policy.
|
||||
// `policy.compile()` returns the resulting miniscript from the policy.
|
||||
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
|
||||
|
||||
info!("... Descriptor: {}", descriptor);
|
||||
info!("Compiled into following Descriptor: \n{}", descriptor);
|
||||
|
||||
let database = MemoryDatabase::new();
|
||||
|
||||
let network = matches
|
||||
.value_of("network")
|
||||
.map(|n| Network::from_str(n))
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap_or(Network::Testnet);
|
||||
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database)?;
|
||||
// Create a new wallet from this descriptor
|
||||
let wallet = Wallet::new(&format!("{}", descriptor), None, Network::Regtest, database)?;
|
||||
|
||||
info!("... First address: {}", wallet.get_address(New)?);
|
||||
info!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.get_address(New)?
|
||||
);
|
||||
|
||||
if matches.is_present("parsed_policy") {
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
info!(
|
||||
"... Spending policy:\n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
}
|
||||
// BDK also has it's own `Policy` structure to represent the spending condition in a more
|
||||
// human readable json format.
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
info!(
|
||||
"The BDK spending policy: \n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
87
examples/electrum_backend.rs
Normal file
87
examples/electrum_backend.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::template::Bip84;
|
||||
use bdk::wallet::export::FullyNodedExport;
|
||||
use bdk::{KeychainKind, SyncOptions, Wallet};
|
||||
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bitcoin::util::bip32;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Electrum server.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --example electrum_backend` in the root folder.
|
||||
fn main() {
|
||||
let network = Network::Testnet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let electrum_url = "ssl://electrum.blockstream.info:60002";
|
||||
|
||||
run(&network, electrum_url, xpriv);
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn run(network: &Network, electrum_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
// Apparently it works only with Electrs (not EletrumX)
|
||||
let blockchain = ElectrumBlockchain::from(Client::new(electrum_url).unwrap());
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 6500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 5359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
93
examples/esplora_backend_asynchronous.rs
Normal file
93
examples/esplora_backend_asynchronous.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::blockchain::Blockchain;
|
||||
use bdk::{
|
||||
blockchain::esplora::EsploraBlockchain,
|
||||
database::MemoryDatabase,
|
||||
template::Bip84,
|
||||
wallet::{export::FullyNodedExport, AddressIndex},
|
||||
KeychainKind, SyncOptions, Wallet,
|
||||
};
|
||||
use bitcoin::{
|
||||
util::bip32::{self, ExtendedPrivKey},
|
||||
Network,
|
||||
};
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||
/// using non blocking asynchronous calls with `reqwest`.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --no-default-features --features="use-esplora-reqwest, reqwest-default-tls, async-interface" --example esplora_backend_asynchronous`
|
||||
/// in the root folder.
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let network = Network::Signet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||
|
||||
run(&network, esplora_url, xpriv).await;
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet
|
||||
.sync(&blockchain, SyncOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 10500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 9359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
let _ = blockchain.broadcast(&tx);
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
89
examples/esplora_backend_synchronous.rs
Normal file
89
examples/esplora_backend_synchronous.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::blockchain::Blockchain;
|
||||
use bdk::{
|
||||
blockchain::esplora::EsploraBlockchain,
|
||||
database::MemoryDatabase,
|
||||
template::Bip84,
|
||||
wallet::{export::FullyNodedExport, AddressIndex},
|
||||
KeychainKind, SyncOptions, Wallet,
|
||||
};
|
||||
use bitcoin::{
|
||||
util::bip32::{self, ExtendedPrivKey},
|
||||
Network,
|
||||
};
|
||||
|
||||
pub mod utils;
|
||||
|
||||
use crate::utils::tx::build_signed_tx;
|
||||
|
||||
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||
/// using blocking calls with `ureq`.
|
||||
/// If enough amount is available, this will send a transaction to an address.
|
||||
/// Otherwise, this will display a wallet address to receive funds.
|
||||
///
|
||||
/// This can be run with `cargo run --features=use-esplora-ureq --example esplora_backend_synchronous`
|
||||
/// in the root folder.
|
||||
fn main() {
|
||||
let network = Network::Signet;
|
||||
|
||||
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||
|
||||
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||
|
||||
run(&network, esplora_url, xpriv);
|
||||
}
|
||||
|
||||
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||
Wallet::new(
|
||||
Bip84(*xpriv, KeychainKind::External),
|
||||
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||
*network,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||
|
||||
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||
|
||||
let wallet = create_wallet(network, &xpriv);
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
|
||||
println!("address: {}", address);
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
|
||||
println!("Available coins in BDK wallet : {} sats", balance);
|
||||
|
||||
if balance.confirmed > 10500 {
|
||||
// the wallet sends the amount to itself.
|
||||
let recipient_address = wallet
|
||||
.get_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address
|
||||
.to_string();
|
||||
|
||||
let amount = 9359;
|
||||
|
||||
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
|
||||
println!("tx id: {}", tx.txid());
|
||||
} else {
|
||||
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||
}
|
||||
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
.map_err(ToString::to_string)
|
||||
.map_err(bdk::Error::Generic)
|
||||
.unwrap();
|
||||
|
||||
println!("------\nWallet Backup: {}", export.to_string());
|
||||
}
|
||||
103
examples/hardware_signer.rs
Normal file
103
examples/hardware_signer.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use bdk::bitcoin::{Address, Network};
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::hwi::{types::HWIChain, HWIClient};
|
||||
use bdk::signer::SignerOrdering;
|
||||
use bdk::wallet::{hardwaresigner::HWISigner, AddressIndex};
|
||||
use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
|
||||
use electrum_client::Client;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
// This example shows how to sync a wallet, create a transaction, sign it
|
||||
// and broadcast it using an external hardware wallet.
|
||||
// The hardware wallet must be connected to the computer and unlocked before
|
||||
// running the example. Also, the `hwi` python package should be installed
|
||||
// and available in the environment.
|
||||
//
|
||||
// To avoid loss of funds, consider using an hardware wallet simulator:
|
||||
// * Coldcard: https://github.com/Coldcard/firmware
|
||||
// * Ledger: https://github.com/LedgerHQ/speculos
|
||||
// * Trezor: https://docs.trezor.io/trezor-firmware/core/emulator/index.html
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Hold tight, I'm connecting to your hardware wallet...");
|
||||
|
||||
// Listing all the available hardware wallet devices...
|
||||
let devices = HWIClient::enumerate()?;
|
||||
let first_device = devices
|
||||
.first()
|
||||
.expect("No devices found. Either plug in a hardware wallet, or start a simulator.");
|
||||
// ...and creating a client out of the first one
|
||||
let client = HWIClient::get_client(first_device, true, HWIChain::Test)?;
|
||||
println!("Look what I found, a {}!", first_device.model);
|
||||
|
||||
// Getting the HW's public descriptors
|
||||
let descriptors = client.get_descriptors(None)?;
|
||||
println!(
|
||||
"The hardware wallet's descriptor is: {}",
|
||||
descriptors.receive[0]
|
||||
);
|
||||
|
||||
// Creating a custom signer from the device
|
||||
let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
|
||||
let mut wallet = Wallet::new(
|
||||
&descriptors.receive[0],
|
||||
Some(&descriptors.internal[0]),
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
// Adding the hardware signer to the BDK wallet
|
||||
wallet.add_signer(
|
||||
KeychainKind::External,
|
||||
SignerOrdering(200),
|
||||
Arc::new(custom_signer),
|
||||
);
|
||||
|
||||
// create client for Blockstream's testnet electrum server
|
||||
let blockchain =
|
||||
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
|
||||
println!("Syncing the wallet...");
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
// get deposit address
|
||||
let deposit_address = wallet.get_address(AddressIndex::New)?;
|
||||
|
||||
let balance = wallet.get_balance()?;
|
||||
println!("Wallet balances in SATs: {}", balance);
|
||||
|
||||
if balance.get_total() < 10000 {
|
||||
println!(
|
||||
"Send some sats from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
|
||||
addr = deposit_address.address
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
|
||||
let (mut psbt, _details) = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_wallet()
|
||||
.drain_to(return_address.script_pubkey())
|
||||
.enable_rbf()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
builder.finish()?
|
||||
};
|
||||
|
||||
// `sign` will call the hardware wallet asking for a signature
|
||||
assert!(
|
||||
wallet.sign(&mut psbt, SignOptions::default())?,
|
||||
"The hardware wallet couldn't finalize the transaction :("
|
||||
);
|
||||
|
||||
println!("Let's broadcast your tx...");
|
||||
let raw_transaction = psbt.extract_tx();
|
||||
let txid = raw_transaction.txid();
|
||||
|
||||
blockchain.broadcast(&raw_transaction)?;
|
||||
println!("Transaction broadcasted! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
60
examples/mnemonic_to_descriptors.rs
Normal file
60
examples/mnemonic_to_descriptors.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::util::bip32::DerivationPath;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor;
|
||||
use bdk::descriptor::IntoWalletDescriptor;
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::Tap;
|
||||
use bdk::Error as BDK_Error;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates how to generate a mnemonic phrase
|
||||
/// using BDK and use that to generate a descriptor string.
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
// In this example we are generating a 12 words mnemonic phrase
|
||||
// but it is also possible generate 15, 18, 21 and 24 words
|
||||
// using their respective `WordCount` variant.
|
||||
let mnemonic: GeneratedKey<_, Tap> =
|
||||
Mnemonic::generate((WordCount::Words12, Language::English))
|
||||
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
|
||||
|
||||
println!("Mnemonic phrase: {}", *mnemonic);
|
||||
let mnemonic_with_passphrase = (mnemonic, None);
|
||||
|
||||
// define external and internal derivation key path
|
||||
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
|
||||
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
|
||||
|
||||
// generate external and internal descriptor from mnemonic
|
||||
let (external_descriptor, ext_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
let (internal_descriptor, int_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
println!("tpub external descriptor: {}", external_descriptor);
|
||||
println!("tpub internal descriptor: {}", internal_descriptor);
|
||||
println!(
|
||||
"tprv external descriptor: {}",
|
||||
external_descriptor.to_string_with_secret(&ext_keymap)
|
||||
);
|
||||
println!(
|
||||
"tprv internal descriptor: {}",
|
||||
internal_descriptor.to_string_with_secret(&int_keymap)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
66
examples/policy.rs
Normal file
66
examples/policy.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
use std::error::Error;
|
||||
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk::wallet::signer::SignersContainer;
|
||||
|
||||
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
|
||||
///
|
||||
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
|
||||
/// This is useful to express complex miniscript spending conditions into more human readable form.
|
||||
/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
|
||||
/// to spend from.
|
||||
///
|
||||
/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
|
||||
/// one of the Extend Private key.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
||||
let secp = bitcoin::secp256k1::Secp256k1::new();
|
||||
|
||||
// The descriptor used in the example
|
||||
// The form is "wsh(multi(2, <privkey>, <pubkey>))"
|
||||
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
|
||||
// Use the descriptor string to derive the full descriptor and a keymap.
|
||||
// The wallet descriptor can be used to create a new bdk::wallet.
|
||||
// While the `keymap` can be used to create a `SignerContainer`.
|
||||
//
|
||||
// The `SignerContainer` can sign for `PSBT`s.
|
||||
// a bdk::wallet internally uses these to handle transaction signing.
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
log::info!("Example Descriptor for policy analysis : {}", wallet_desc);
|
||||
|
||||
// Create the signer with the keymap and descriptor.
|
||||
let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
|
||||
|
||||
// Extract the Policy from the given descriptor and signer.
|
||||
// Note that Policy is a wallet specific structure. It depends on the the descriptor, and
|
||||
// what the concerned wallet with a given signer can sign for.
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
|
||||
.expect("We expect a policy");
|
||||
|
||||
log::info!("Derived Policy for the descriptor {:#?}", policy);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
120
examples/psbt_signer.rs
Normal file
120
examples/psbt_signer.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::{descriptor, SyncOptions};
|
||||
use bdk::{FeeRate, SignOptions, Wallet};
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::{Address, Network};
|
||||
use electrum_client::Client;
|
||||
use miniscript::descriptor::DescriptorSecretKey;
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example shows how to sign and broadcast the transaction for a PSBT (Partially Signed
|
||||
/// Bitcoin Transaction) for a single key, witness public key hash (WPKH) based descriptor wallet.
|
||||
/// The electrum protocol is used to sync blockchain data from the testnet bitcoin network and
|
||||
/// wallet data is stored in an ephemeral in-memory database. The process steps are:
|
||||
/// 1. Create a "signing" wallet and a "watch-only" wallet based on the same private keys.
|
||||
/// 2. Deposit testnet funds into the watch only wallet.
|
||||
/// 3. Sync the watch only wallet and create a spending transaction to return all funds to the testnet faucet.
|
||||
/// 4. Sync the signing wallet and sign and finalize the PSBT created by the watch only wallet.
|
||||
/// 5. Broadcast the transactions from the finalized PSBT.
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
|
||||
let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
|
||||
let internal_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*").unwrap();
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let external_public_xkey = external_secret_xkey.to_public(&secp).unwrap();
|
||||
let internal_public_xkey = internal_secret_xkey.to_public(&secp).unwrap();
|
||||
|
||||
let signing_external_descriptor = descriptor!(wpkh(external_secret_xkey)).unwrap();
|
||||
let signing_internal_descriptor = descriptor!(wpkh(internal_secret_xkey)).unwrap();
|
||||
|
||||
let watch_only_external_descriptor = descriptor!(wpkh(external_public_xkey)).unwrap();
|
||||
let watch_only_internal_descriptor = descriptor!(wpkh(internal_public_xkey)).unwrap();
|
||||
|
||||
// create client for Blockstream's testnet electrum server
|
||||
let blockchain =
|
||||
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
|
||||
// create watch only wallet
|
||||
let watch_only_wallet: Wallet<MemoryDatabase> = Wallet::new(
|
||||
watch_only_external_descriptor,
|
||||
Some(watch_only_internal_descriptor),
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
// create signing wallet
|
||||
let signing_wallet: Wallet<MemoryDatabase> = Wallet::new(
|
||||
signing_external_descriptor,
|
||||
Some(signing_internal_descriptor),
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
println!("Syncing watch only wallet.");
|
||||
watch_only_wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
// get deposit address
|
||||
let deposit_address = watch_only_wallet.get_address(AddressIndex::New)?;
|
||||
|
||||
let balance = watch_only_wallet.get_balance()?;
|
||||
println!("Watch only wallet balances in SATs: {}", balance);
|
||||
|
||||
if balance.get_total() < 10000 {
|
||||
println!(
|
||||
"Send at least 10000 SATs (0.0001 BTC) from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
|
||||
addr = deposit_address.address
|
||||
);
|
||||
} else if balance.get_spendable() < 10000 {
|
||||
println!(
|
||||
"Wait for at least 10000 SATs of your wallet transactions to be confirmed...\nBe patient, this could take 10 mins or longer depending on how testnet is behaving."
|
||||
);
|
||||
for tx_details in watch_only_wallet
|
||||
.list_transactions(false)?
|
||||
.iter()
|
||||
.filter(|txd| txd.received > 0 && txd.confirmation_time.is_none())
|
||||
{
|
||||
println!(
|
||||
"See unconfirmed tx for {} SATs: https://mempool.space/testnet/tx/{}",
|
||||
tx_details.received, tx_details.txid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!("Creating a PSBT sending 9800 SATs plus fee to the u01.net testnet faucet return address 'tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt'.");
|
||||
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
|
||||
let mut builder = watch_only_wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(return_address.script_pubkey(), 9_800)
|
||||
.enable_rbf()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(1.0));
|
||||
|
||||
let (mut psbt, details) = builder.finish()?;
|
||||
println!("Transaction details: {:#?}", details);
|
||||
println!("Unsigned PSBT: {}", psbt);
|
||||
|
||||
// Sign and finalize the PSBT with the signing wallet
|
||||
let finalized = signing_wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
assert!(finalized, "The PSBT was not finalized!");
|
||||
println!("The PSBT has been signed and finalized.");
|
||||
|
||||
// Broadcast the transaction
|
||||
let raw_transaction = psbt.extract_tx();
|
||||
let txid = raw_transaction.txid();
|
||||
|
||||
blockchain.broadcast(&raw_transaction)?;
|
||||
println!("Transaction broadcast! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
229
examples/rpcwallet.rs
Normal file
229
examples/rpcwallet.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::Amount;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::bitcoincore_rpc::RpcApi;
|
||||
|
||||
use bdk::blockchain::rpc::{Auth, RpcBlockchain, RpcConfig};
|
||||
use bdk::blockchain::ConfigurableBlockchain;
|
||||
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{DerivableKey, GeneratableKey, GeneratedKey};
|
||||
|
||||
use bdk::miniscript::miniscript::Segwitv0;
|
||||
|
||||
use bdk::sled;
|
||||
use bdk::template::Bip84;
|
||||
use bdk::wallet::{signer::SignOptions, wallet_name_from_descriptor, AddressIndex, SyncOptions};
|
||||
use bdk::KeychainKind;
|
||||
use bdk::Wallet;
|
||||
|
||||
use bdk::blockchain::Blockchain;
|
||||
|
||||
use electrsd;
|
||||
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates a typical way to create a wallet and work with bdk.
|
||||
///
|
||||
/// This example bdk wallet is connected to a bitcoin core rpc regtest node,
|
||||
/// and will attempt to receive, create and broadcast transactions.
|
||||
///
|
||||
/// To start a bitcoind regtest node programmatically, this example uses
|
||||
/// `electrsd` library, which is also a bdk dev-dependency.
|
||||
///
|
||||
/// But you can start your own bitcoind backend, and the rest of the example should work fine.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// -- Setting up background bitcoind process
|
||||
|
||||
println!(">> Setting up bitcoind");
|
||||
|
||||
// Start the bitcoind process
|
||||
let bitcoind_conf = electrsd::bitcoind::Conf::default();
|
||||
|
||||
// electrsd will automatically download the bitcoin core binaries
|
||||
let bitcoind_exe =
|
||||
electrsd::bitcoind::downloaded_exe_path().expect("We should always have downloaded path");
|
||||
|
||||
// Launch bitcoind and gather authentication access
|
||||
let bitcoind = electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap();
|
||||
let bitcoind_auth = Auth::Cookie {
|
||||
file: bitcoind.params.cookie_file.clone(),
|
||||
};
|
||||
|
||||
// Get a new core address
|
||||
let core_address = bitcoind.client.get_new_address(None, None)?;
|
||||
|
||||
// Generate 101 blocks and use the above address as coinbase
|
||||
bitcoind.client.generate_to_address(101, &core_address)?;
|
||||
|
||||
println!(">> bitcoind setup complete");
|
||||
println!(
|
||||
"Available coins in Core wallet : {}",
|
||||
bitcoind.client.get_balance(None, None)?
|
||||
);
|
||||
|
||||
// -- Setting up the Wallet
|
||||
|
||||
println!("\n>> Setting up BDK wallet");
|
||||
|
||||
// Get a random private key
|
||||
let xprv = generate_random_ext_privkey()?;
|
||||
|
||||
// Use the derived descriptors from the privatekey to
|
||||
// create unique wallet name.
|
||||
// This is a special utility function exposed via `bdk::wallet_name_from_descriptor()`
|
||||
let wallet_name = wallet_name_from_descriptor(
|
||||
Bip84(xprv.clone(), KeychainKind::External),
|
||||
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
|
||||
Network::Regtest,
|
||||
&Secp256k1::new(),
|
||||
)?;
|
||||
|
||||
// Create a database (using default sled type) to store wallet data
|
||||
let mut datadir = PathBuf::from_str("/tmp/")?;
|
||||
datadir.push(".bdk-example");
|
||||
let database = sled::open(datadir)?;
|
||||
let database = database.open_tree(wallet_name.clone())?;
|
||||
|
||||
// Create a RPC configuration of the running bitcoind backend we created in last step
|
||||
// Note: If you are using custom regtest node, use the appropriate url and auth
|
||||
let rpc_config = RpcConfig {
|
||||
url: bitcoind.params.rpc_socket.to_string(),
|
||||
auth: bitcoind_auth,
|
||||
network: Network::Regtest,
|
||||
wallet_name,
|
||||
sync_params: None,
|
||||
};
|
||||
|
||||
// Use the above configuration to create a RPC blockchain backend
|
||||
let blockchain = RpcBlockchain::from_config(&rpc_config)?;
|
||||
|
||||
// Combine Database + Descriptor to create the final wallet
|
||||
let wallet = Wallet::new(
|
||||
Bip84(xprv.clone(), KeychainKind::External),
|
||||
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
|
||||
Network::Regtest,
|
||||
database,
|
||||
)?;
|
||||
|
||||
// The `wallet` and the `blockchain` are independent structs.
|
||||
// The wallet will be used to do all wallet level actions
|
||||
// The blockchain can be used to do all blockchain level actions.
|
||||
// For certain actions (like sync) the wallet will ask for a blockchain.
|
||||
|
||||
// Sync the wallet
|
||||
// The first sync is important as this will instantiate the
|
||||
// wallet files.
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
println!(">> BDK wallet setup complete.");
|
||||
println!(
|
||||
"Available initial coins in BDK wallet : {} sats",
|
||||
wallet.get_balance()?
|
||||
);
|
||||
|
||||
// -- Wallet transaction demonstration
|
||||
|
||||
println!("\n>> Sending coins: Core --> BDK, 10 BTC");
|
||||
// Get a new address to receive coins
|
||||
let bdk_new_addr = wallet.get_address(AddressIndex::New)?.address;
|
||||
|
||||
// Send 10 BTC from core wallet to bdk wallet
|
||||
bitcoind.client.send_to_address(
|
||||
&bdk_new_addr,
|
||||
Amount::from_btc(10.0)?,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Confirm transaction by generating 1 block
|
||||
bitcoind.client.generate_to_address(1, &core_address)?;
|
||||
|
||||
// Sync the BDK wallet
|
||||
// This time the sync will fetch the new transaction and update it in
|
||||
// wallet database
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
println!(">> Received coins in BDK wallet");
|
||||
println!(
|
||||
"Available balance in BDK wallet: {} sats",
|
||||
wallet.get_balance()?
|
||||
);
|
||||
|
||||
println!("\n>> Sending coins: BDK --> Core, 5 BTC");
|
||||
// Attempt to send back 5.0 BTC to core address by creating a transaction
|
||||
//
|
||||
// Transactions are created using a `TxBuilder`.
|
||||
// This helps us to systematically build a transaction with all
|
||||
// required customization.
|
||||
// A full list of APIs offered by `TxBuilder` can be found at
|
||||
// https://docs.rs/bdk/latest/bdk/wallet/tx_builder/struct.TxBuilder.html
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
|
||||
// For a regular transaction, just set the recipient and amount
|
||||
tx_builder.set_recipients(vec![(core_address.script_pubkey(), 500000000)]);
|
||||
|
||||
// Finalize the transaction and extract the PSBT
|
||||
let (mut psbt, _) = tx_builder.finish()?;
|
||||
|
||||
// Set signing option
|
||||
let signopt = SignOptions {
|
||||
assume_height: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Sign the psbt
|
||||
wallet.sign(&mut psbt, signopt)?;
|
||||
|
||||
// Extract the signed transaction
|
||||
let tx = psbt.extract_tx();
|
||||
|
||||
// Broadcast the transaction
|
||||
blockchain.broadcast(&tx)?;
|
||||
|
||||
// Confirm transaction by generating some blocks
|
||||
bitcoind.client.generate_to_address(1, &core_address)?;
|
||||
|
||||
// Sync the BDK wallet
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
println!(">> Coins sent to Core wallet");
|
||||
println!(
|
||||
"Remaining BDK wallet balance: {} sats",
|
||||
wallet.get_balance()?
|
||||
);
|
||||
println!("\nCongrats!! you made your first test transaction with bdk and bitcoin core.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function demonstrating privatekey extraction using bip39 mnemonic
|
||||
// The mnemonic can be shown to user to safekeeping and the same wallet
|
||||
// private descriptors can be recreated from it.
|
||||
fn generate_random_ext_privkey() -> Result<impl DerivableKey<Segwitv0> + Clone, Box<dyn Error>> {
|
||||
// a Bip39 passphrase can be set optionally
|
||||
let password = Some("random password".to_string());
|
||||
|
||||
// Generate a random mnemonic, and use that to create a "DerivableKey"
|
||||
let mnemonic: GeneratedKey<_, _> = Mnemonic::generate((WordCount::Words12, Language::English))
|
||||
.map_err(|e| e.expect("Unknown Error"))?;
|
||||
|
||||
// `Ok(mnemonic)` would also work if there's no passphrase and it would
|
||||
// yield the same result as this construct with `password` = `None`.
|
||||
Ok((mnemonic, password))
|
||||
}
|
||||
30
examples/utils/mod.rs
Normal file
30
examples/utils/mod.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
pub(crate) mod tx {
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk::{database::BatchDatabase, SignOptions, Wallet};
|
||||
use bitcoin::{Address, Transaction};
|
||||
|
||||
pub fn build_signed_tx<D: BatchDatabase>(
|
||||
wallet: &Wallet<D>,
|
||||
recipient_address: &str,
|
||||
amount: u64,
|
||||
) -> Transaction {
|
||||
// Create a transaction builder
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
|
||||
let to_address = Address::from_str(recipient_address).unwrap();
|
||||
|
||||
// Set recipient of the transaction
|
||||
tx_builder.set_recipients(vec![(to_address.script_pubkey(), amount)]);
|
||||
|
||||
// Finalise the transaction and extract PSBT
|
||||
let (mut psbt, _) = tx_builder.finish().unwrap();
|
||||
|
||||
// Sign the above psbt with signing option
|
||||
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
|
||||
// Extract the final transaction
|
||||
psbt.extract_tx()
|
||||
}
|
||||
}
|
||||
@@ -16,61 +16,17 @@
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! In this example both `wallet_electrum` and `wallet_esplora` have the same type of
|
||||
//! `Wallet<AnyBlockchain, MemoryDatabase>`. This means that they could both, for instance, be
|
||||
//! assigned to a struct member.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use bitcoin::Network;
|
||||
//! # use bdk::blockchain::*;
|
||||
//! # use bdk::database::MemoryDatabase;
|
||||
//! # use bdk::Wallet;
|
||||
//! # #[cfg(feature = "electrum")]
|
||||
//! # {
|
||||
//! let electrum_blockchain = ElectrumBlockchain::from(electrum_client::Client::new("...")?);
|
||||
//! let wallet_electrum: Wallet<AnyBlockchain, _> = Wallet::new(
|
||||
//! "...",
|
||||
//! None,
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! electrum_blockchain.into(),
|
||||
//! )?;
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
|
||||
//! # {
|
||||
//! let esplora_blockchain = EsploraBlockchain::new("...", 20);
|
||||
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
|
||||
//! "...",
|
||||
//! None,
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! esplora_blockchain.into(),
|
||||
//! )?;
|
||||
//! # }
|
||||
//!
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
//!
|
||||
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating wallets with any
|
||||
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating any
|
||||
//! blockchain type supported using a single line of code:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use bitcoin::Network;
|
||||
//! # use bdk::blockchain::*;
|
||||
//! # use bdk::database::MemoryDatabase;
|
||||
//! # use bdk::Wallet;
|
||||
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
|
||||
//! # {
|
||||
//! let config = serde_json::from_str("...")?;
|
||||
//! let blockchain = AnyBlockchain::from_config(&config)?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! "...",
|
||||
//! None,
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! blockchain,
|
||||
//! )?;
|
||||
//! let height = blockchain.get_height();
|
||||
//! # }
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
@@ -78,6 +34,14 @@
|
||||
use super::*;
|
||||
|
||||
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 )* ) => {
|
||||
$( $cfg )*
|
||||
impl From<$from> for $to {
|
||||
@@ -112,19 +76,19 @@ pub enum AnyBlockchain {
|
||||
#[cfg(feature = "electrum")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
||||
/// Electrum client
|
||||
Electrum(electrum::ElectrumBlockchain),
|
||||
Electrum(Box<electrum::ElectrumBlockchain>),
|
||||
#[cfg(feature = "esplora")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
||||
/// Esplora client
|
||||
Esplora(esplora::EsploraBlockchain),
|
||||
Esplora(Box<esplora::EsploraBlockchain>),
|
||||
#[cfg(feature = "compact_filters")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||
/// Compact filters client
|
||||
CompactFilters(compact_filters::CompactFiltersBlockchain),
|
||||
CompactFilters(Box<compact_filters::CompactFiltersBlockchain>),
|
||||
#[cfg(feature = "rpc")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||
/// RPC client
|
||||
Rpc(rpc::RpcBlockchain),
|
||||
Rpc(Box<rpc::RpcBlockchain>),
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
@@ -133,40 +97,69 @@ impl Blockchain for AnyBlockchain {
|
||||
maybe_await!(impl_inner_method!(self, get_capabilities))
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(impl_inner_method!(self, setup, database, progress_update))
|
||||
}
|
||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(impl_inner_method!(self, sync, database, progress_update))
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
maybe_await!(impl_inner_method!(self, get_tx, txid))
|
||||
}
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
maybe_await!(impl_inner_method!(self, broadcast, tx))
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
maybe_await!(impl_inner_method!(self, get_height))
|
||||
}
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
maybe_await!(impl_inner_method!(self, estimate_fee, target))
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
||||
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
||||
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||
impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||
#[maybe_async]
|
||||
impl GetHeight for AnyBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
maybe_await!(impl_inner_method!(self, get_height))
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetTx for AnyBlockchain {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
maybe_await!(impl_inner_method!(self, get_tx, txid))
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
impl WalletSync for AnyBlockchain {
|
||||
fn wallet_sync<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(impl_inner_method!(
|
||||
self,
|
||||
wallet_sync,
|
||||
database,
|
||||
progress_update
|
||||
))
|
||||
}
|
||||
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(impl_inner_method!(
|
||||
self,
|
||||
wallet_setup,
|
||||
database,
|
||||
progress_update
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(boxed electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
||||
impl_from!(boxed esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
||||
impl_from!(boxed compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||
impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||
|
||||
/// Type that can contain any of the blockchain configurations defined by the library
|
||||
///
|
||||
@@ -201,7 +194,7 @@ impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AnyBlockchainConfig {
|
||||
#[cfg(feature = "electrum")]
|
||||
@@ -229,19 +222,19 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
||||
Ok(match config {
|
||||
#[cfg(feature = "electrum")]
|
||||
AnyBlockchainConfig::Electrum(inner) => {
|
||||
AnyBlockchain::Electrum(electrum::ElectrumBlockchain::from_config(inner)?)
|
||||
AnyBlockchain::Electrum(Box::new(electrum::ElectrumBlockchain::from_config(inner)?))
|
||||
}
|
||||
#[cfg(feature = "esplora")]
|
||||
AnyBlockchainConfig::Esplora(inner) => {
|
||||
AnyBlockchain::Esplora(esplora::EsploraBlockchain::from_config(inner)?)
|
||||
AnyBlockchain::Esplora(Box::new(esplora::EsploraBlockchain::from_config(inner)?))
|
||||
}
|
||||
#[cfg(feature = "compact_filters")]
|
||||
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
|
||||
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(Box::new(
|
||||
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
||||
),
|
||||
)),
|
||||
#[cfg(feature = "rpc")]
|
||||
AnyBlockchainConfig::Rpc(inner) => {
|
||||
AnyBlockchain::Rpc(rpc::RpcBlockchain::from_config(inner)?)
|
||||
AnyBlockchain::Rpc(Box::new(rpc::RpcBlockchain::from_config(inner)?))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ mod peer;
|
||||
mod store;
|
||||
mod sync;
|
||||
|
||||
use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||
use crate::blockchain::*;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||
@@ -163,11 +163,19 @@ impl CompactFiltersBlockchain {
|
||||
if let Some(previous_output) = database.get_previous_output(&input.previous_output)? {
|
||||
inputs_sum += previous_output.value;
|
||||
|
||||
if database.is_mine(&previous_output.script_pubkey)? {
|
||||
// this output is ours, we have a path to derive it
|
||||
if let Some((keychain, _)) =
|
||||
database.get_path_from_script_pubkey(&previous_output.script_pubkey)?
|
||||
{
|
||||
outgoing += previous_output.value;
|
||||
|
||||
debug!("{} input #{} is mine, removing from utxo", tx.txid(), i);
|
||||
updates.del_utxo(&input.previous_output)?;
|
||||
debug!("{} input #{} is mine, setting utxo as spent", tx.txid(), i);
|
||||
updates.set_utxo(&LocalUtxo {
|
||||
outpoint: input.previous_output,
|
||||
txout: previous_output.clone(),
|
||||
keychain,
|
||||
is_spent: true,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +193,7 @@ impl CompactFiltersBlockchain {
|
||||
outpoint: OutPoint::new(tx.txid(), i as u32),
|
||||
txout: output.clone(),
|
||||
keychain,
|
||||
is_spent: false,
|
||||
})?;
|
||||
incoming += output.value;
|
||||
|
||||
@@ -225,11 +234,48 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
vec![Capability::FullHistory].into_iter().collect()
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
self.peers[0].broadcast_tx(tx.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
|
||||
// TODO
|
||||
Ok(FeeRate::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl GetHeight for CompactFiltersBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(self.headers.get_height()? as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetTx for CompactFiltersBlockchain {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.peers[0]
|
||||
.get_mempool()
|
||||
.get_tx(&Inventory::Transaction(*txid)))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
let first_peer = &self.peers[0];
|
||||
|
||||
@@ -430,31 +476,10 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.peers[0]
|
||||
.get_mempool()
|
||||
.get_tx(&Inventory::Transaction(*txid)))
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
self.peers[0].broadcast_tx(tx.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(self.headers.get_height()? as u32)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
|
||||
// TODO
|
||||
Ok(FeeRate::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data to connect to a Bitcoin P2P peer
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct BitcoinPeerConfig {
|
||||
/// Peer address such as 127.0.0.1:18333
|
||||
pub address: String,
|
||||
@@ -465,7 +490,7 @@ pub struct BitcoinPeerConfig {
|
||||
}
|
||||
|
||||
/// Configuration for a [`CompactFiltersBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CompactFiltersBlockchainConfig {
|
||||
/// List of peers to try to connect to for asking headers and filters
|
||||
pub peers: Vec<BitcoinPeerConfig>,
|
||||
@@ -521,6 +546,8 @@ pub enum CompactFiltersError {
|
||||
InvalidFilter,
|
||||
/// The peer is missing a block in the valid chain
|
||||
MissingBlock,
|
||||
/// Block hash at specified height not found
|
||||
BlockHashNotFound,
|
||||
/// The data stored in the block filters storage are corrupted
|
||||
DataCorruption,
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
// licenses.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||
use std::thread;
|
||||
@@ -19,14 +20,13 @@ use socks::{Socks5Stream, ToTargetAddr};
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use bitcoin::consensus::Encodable;
|
||||
use bitcoin::consensus::{Decodable, Encodable};
|
||||
use bitcoin::hash_types::BlockHash;
|
||||
use bitcoin::network::constants::ServiceFlags;
|
||||
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
|
||||
use bitcoin::network::message_blockdata::*;
|
||||
use bitcoin::network::message_filter::*;
|
||||
use bitcoin::network::message_network::VersionMessage;
|
||||
use bitcoin::network::stream_reader::StreamReader;
|
||||
use bitcoin::network::Address;
|
||||
use bitcoin::{Block, Network, Transaction, Txid, Wtxid};
|
||||
|
||||
@@ -75,7 +75,10 @@ impl Mempool {
|
||||
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
||||
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
||||
let identifer = match inventory {
|
||||
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None,
|
||||
Inventory::Error
|
||||
| Inventory::Block(_)
|
||||
| Inventory::WitnessBlock(_)
|
||||
| Inventory::CompactBlock(_) => return None,
|
||||
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
|
||||
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
|
||||
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),
|
||||
@@ -94,8 +97,7 @@ impl Mempool {
|
||||
TxIdentifier::Wtxid(wtxid) => self.0.read().unwrap().wtxids.get(&wtxid).cloned(),
|
||||
};
|
||||
|
||||
txid.map(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
|
||||
.flatten()
|
||||
txid.and_then(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
|
||||
}
|
||||
|
||||
/// Return whether or not the mempool contains a transaction with a given txid
|
||||
@@ -111,6 +113,7 @@ impl Mempool {
|
||||
|
||||
/// A Bitcoin peer
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Peer {
|
||||
writer: Arc<Mutex<TcpStream>>,
|
||||
responses: Arc<RwLock<ResponsesMap>>,
|
||||
@@ -327,9 +330,10 @@ impl Peer {
|
||||
};
|
||||
}
|
||||
|
||||
let mut reader = StreamReader::new(connection, None);
|
||||
let mut reader = BufReader::new(connection);
|
||||
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() {
|
||||
continue;
|
||||
|
||||
@@ -13,7 +13,6 @@ use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
@@ -22,9 +21,9 @@ use rand::{thread_rng, Rng};
|
||||
|
||||
use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch, DB};
|
||||
|
||||
use bitcoin::blockdata::constants::genesis_block;
|
||||
use bitcoin::consensus::{deserialize, encode::VarInt, serialize, Decodable, Encodable};
|
||||
use bitcoin::hash_types::{FilterHash, FilterHeader};
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::util::bip158::BlockFilter;
|
||||
use bitcoin::util::uint::Uint256;
|
||||
@@ -33,17 +32,8 @@ use bitcoin::BlockHash;
|
||||
use bitcoin::BlockHeader;
|
||||
use bitcoin::Network;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::CompactFiltersError;
|
||||
|
||||
lazy_static! {
|
||||
static ref MAINNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A29AB5F49FFFF001D1DAC2B7C0101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||
static ref TESTNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF001D1AA4AE180101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||
static ref REGTEST_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF7F20020000000101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||
static ref SIGNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A008F4D5FAE77031E8AD222030101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||
}
|
||||
|
||||
pub trait StoreType: Default + fmt::Debug {}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -113,42 +103,42 @@ where
|
||||
}
|
||||
|
||||
impl Encodable for BundleStatus {
|
||||
fn consensus_encode<W: Write>(&self, mut e: W) -> Result<usize, std::io::Error> {
|
||||
fn consensus_encode<W: Write + ?Sized>(&self, e: &mut W) -> Result<usize, std::io::Error> {
|
||||
let mut written = 0;
|
||||
|
||||
match self {
|
||||
BundleStatus::Init => {
|
||||
written += 0x00u8.consensus_encode(&mut e)?;
|
||||
written += 0x00u8.consensus_encode(e)?;
|
||||
}
|
||||
BundleStatus::CfHeaders { cf_headers } => {
|
||||
written += 0x01u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x01u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
|
||||
for header in cf_headers {
|
||||
written += header.consensus_encode(&mut e)?;
|
||||
written += header.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::CFilters { cf_filters } => {
|
||||
written += 0x02u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x02u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
for filter in cf_filters {
|
||||
written += filter.consensus_encode(&mut e)?;
|
||||
written += filter.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::Processed { cf_filters } => {
|
||||
written += 0x03u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x03u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
for filter in cf_filters {
|
||||
written += filter.consensus_encode(&mut e)?;
|
||||
written += filter.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
BundleStatus::Pruned => {
|
||||
written += 0x04u8.consensus_encode(&mut e)?;
|
||||
written += 0x04u8.consensus_encode(e)?;
|
||||
}
|
||||
BundleStatus::Tip { cf_filters } => {
|
||||
written += 0x05u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
|
||||
written += 0x05u8.consensus_encode(e)?;
|
||||
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
|
||||
for filter in cf_filters {
|
||||
written += filter.consensus_encode(&mut e)?;
|
||||
written += filter.consensus_encode(e)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,51 +148,53 @@ impl Encodable for BundleStatus {
|
||||
}
|
||||
|
||||
impl Decodable for BundleStatus {
|
||||
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
|
||||
let byte_type = u8::consensus_decode(&mut d)?;
|
||||
fn consensus_decode<D: Read + ?Sized>(
|
||||
d: &mut D,
|
||||
) -> Result<Self, bitcoin::consensus::encode::Error> {
|
||||
let byte_type = u8::consensus_decode(d)?;
|
||||
match byte_type {
|
||||
0x00 => Ok(BundleStatus::Init),
|
||||
0x01 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_headers = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
|
||||
cf_headers.push(FilterHeader::consensus_decode(d)?);
|
||||
}
|
||||
|
||||
Ok(BundleStatus::CfHeaders { cf_headers })
|
||||
}
|
||||
0x02 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||
}
|
||||
|
||||
Ok(BundleStatus::CFilters { cf_filters })
|
||||
}
|
||||
0x03 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||
}
|
||||
|
||||
Ok(BundleStatus::Processed { cf_filters })
|
||||
}
|
||||
0x04 => Ok(BundleStatus::Pruned),
|
||||
0x05 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
let num = VarInt::consensus_decode(d)?;
|
||||
let num = num.0 as usize;
|
||||
|
||||
let mut cf_filters = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
|
||||
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
|
||||
}
|
||||
|
||||
Ok(BundleStatus::Tip { cf_filters })
|
||||
@@ -224,12 +216,7 @@ pub struct ChainStore<T: StoreType> {
|
||||
|
||||
impl ChainStore<Full> {
|
||||
pub fn new(store: DB, network: Network) -> Result<Self, CompactFiltersError> {
|
||||
let genesis = match network {
|
||||
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
||||
Network::Testnet => TESTNET_GENESIS.deref(),
|
||||
Network::Regtest => REGTEST_GENESIS.deref(),
|
||||
Network::Signet => SIGNET_GENESIS.deref(),
|
||||
};
|
||||
let genesis = genesis_block(network);
|
||||
|
||||
let cf_name = "default".to_string();
|
||||
let cf_handle = store.cf_handle(&cf_name).unwrap();
|
||||
@@ -291,7 +278,11 @@ impl ChainStore<Full> {
|
||||
}
|
||||
|
||||
pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> {
|
||||
let new_cf_name: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect();
|
||||
let new_cf_name: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.map(|byte| byte as char)
|
||||
.take(16)
|
||||
.collect();
|
||||
let new_cf_name = format!("_headers:{}", new_cf_name);
|
||||
|
||||
let mut write_store = self.store.write().unwrap();
|
||||
@@ -647,14 +638,9 @@ impl CfStore {
|
||||
filter_type,
|
||||
};
|
||||
|
||||
let genesis = match headers_store.network {
|
||||
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
||||
Network::Testnet => TESTNET_GENESIS.deref(),
|
||||
Network::Regtest => REGTEST_GENESIS.deref(),
|
||||
Network::Signet => SIGNET_GENESIS.deref(),
|
||||
};
|
||||
let genesis = genesis_block(headers_store.network);
|
||||
|
||||
let filter = BlockFilter::new_script_filter(genesis, |utxo| {
|
||||
let filter = BlockFilter::new_script_filter(&genesis, |utxo| {
|
||||
Err(bitcoin::util::bip158::Error::UtxoMissing(*utxo))
|
||||
})?;
|
||||
let first_key = StoreEntry::CFilterTable((filter_type, Some(0))).get_key();
|
||||
@@ -667,7 +653,7 @@ impl CfStore {
|
||||
&first_key,
|
||||
(
|
||||
BundleStatus::Init,
|
||||
filter.filter_header(&FilterHeader::from_hash(Default::default())),
|
||||
filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
|
||||
)
|
||||
.serialize(),
|
||||
)?;
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use bitcoin::hash_types::{BlockHash, FilterHeader};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::network::message::NetworkMessage;
|
||||
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
||||
use bitcoin::util::bip158::BlockFilter;
|
||||
@@ -254,7 +255,7 @@ where
|
||||
|
||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||
locators_vec,
|
||||
Default::default(),
|
||||
Hash::all_zeros(),
|
||||
)))?;
|
||||
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
|
||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||
@@ -276,7 +277,7 @@ where
|
||||
while sync_height < peer.get_version().start_height as usize {
|
||||
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
|
||||
vec![last_hash],
|
||||
Default::default(),
|
||||
Hash::all_zeros(),
|
||||
)))?;
|
||||
if let NetworkMessage::Headers(headers) = peer
|
||||
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
//! ```
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
@@ -68,16 +69,65 @@ impl Blockchain for ElectrumBlockchain {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: Progress>(
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
Ok(FeeRate::from_btc_per_kvb(
|
||||
self.client.estimate_fee(target)? as f32
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ElectrumBlockchain {
|
||||
type Target = Client;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl StatelessBlockchain for ElectrumBlockchain {}
|
||||
|
||||
impl GetHeight for ElectrumBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
||||
|
||||
Ok(self
|
||||
.client
|
||||
.block_headers_subscribe()
|
||||
.map(|data| data.height as u32)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetTx for ElectrumBlockchain {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.client.transaction_get(txid).map(Option::Some)?)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
_progress_update: P,
|
||||
_progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
let mut block_times = HashMap::<u32, u32>::new();
|
||||
let mut txid_to_height = HashMap::<Txid, u32>::new();
|
||||
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
|
||||
// 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.
|
||||
@@ -113,21 +163,12 @@ impl Blockchain for ElectrumBlockchain {
|
||||
|
||||
Request::Conftime(conftime_req) => {
|
||||
// collect up to chunk_size heights to fetch from electrum
|
||||
let needs_block_height = {
|
||||
let mut needs_block_height_iter = conftime_req
|
||||
.request()
|
||||
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
||||
.filter(|height| block_times.get(height).is_none());
|
||||
let mut needs_block_height = HashSet::new();
|
||||
|
||||
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 needs_block_height = conftime_req
|
||||
.request()
|
||||
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
||||
.filter(|height| block_times.get(height).is_none())
|
||||
.take(chunk_size)
|
||||
.collect::<HashSet<u32>>();
|
||||
|
||||
let new_block_headers = self
|
||||
.client
|
||||
@@ -207,29 +248,6 @@ impl Blockchain for ElectrumBlockchain {
|
||||
database.commit_batch(batch_update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.client.transaction_get(txid).map(Option::Some)?)
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
||||
|
||||
Ok(self
|
||||
.client
|
||||
.block_headers_subscribe()
|
||||
.map(|data| data.height as u32)?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
Ok(FeeRate::from_btc_per_kvb(
|
||||
self.client.estimate_fee(target)? as f32
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct TxCache<'a, 'b, D> {
|
||||
@@ -278,7 +296,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
|
||||
}
|
||||
|
||||
/// Configuration for an [`ElectrumBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct ElectrumBlockchainConfig {
|
||||
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
||||
///
|
||||
@@ -314,8 +332,93 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-electrum")]
|
||||
crate::bdk_blockchain_tests! {
|
||||
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
||||
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
//! structs from the esplora API
|
||||
//!
|
||||
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
|
||||
use crate::BlockTime;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid};
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct PrevOut {
|
||||
pub value: u64,
|
||||
pub scriptpubkey: Script,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Vin {
|
||||
pub txid: Txid,
|
||||
pub vout: u32,
|
||||
// None if coinbase
|
||||
pub prevout: Option<PrevOut>,
|
||||
pub scriptsig: Script,
|
||||
#[serde(deserialize_with = "deserialize_witness")]
|
||||
pub witness: Vec<Vec<u8>>,
|
||||
pub sequence: u32,
|
||||
pub is_coinbase: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Vout {
|
||||
pub value: u64,
|
||||
pub scriptpubkey: Script,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct TxStatus {
|
||||
pub confirmed: bool,
|
||||
pub block_height: Option<u32>,
|
||||
pub block_time: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Tx {
|
||||
pub txid: Txid,
|
||||
pub version: i32,
|
||||
pub locktime: u32,
|
||||
pub vin: Vec<Vin>,
|
||||
pub vout: Vec<Vout>,
|
||||
pub status: TxStatus,
|
||||
pub fee: u64,
|
||||
}
|
||||
|
||||
impl Tx {
|
||||
pub fn to_tx(&self) -> Transaction {
|
||||
Transaction {
|
||||
version: self.version,
|
||||
lock_time: self.locktime,
|
||||
input: self
|
||||
.vin
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vin| TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
script_sig: vin.scriptsig,
|
||||
sequence: vin.sequence,
|
||||
witness: vin.witness,
|
||||
})
|
||||
.collect(),
|
||||
output: self
|
||||
.vout
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vout| TxOut {
|
||||
value: vout.value,
|
||||
script_pubkey: vout.scriptpubkey,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirmation_time(&self) -> Option<BlockTime> {
|
||||
match self.status {
|
||||
TxStatus {
|
||||
confirmed: true,
|
||||
block_height: Some(height),
|
||||
block_time: Some(timestamp),
|
||||
} => Some(BlockTime { timestamp, height }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
|
||||
self.vin
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|vin| {
|
||||
vin.prevout.map(|po| TxOut {
|
||||
script_pubkey: po.scriptpubkey,
|
||||
value: po.value,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_witness<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use crate::serde::Deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
let list = Vec::<String>::deserialize(d)?;
|
||||
list.into_iter()
|
||||
.map(|hex_str| Vec::<u8>::from_hex(&hex_str))
|
||||
.collect::<Result<Vec<Vec<u8>>, _>>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
@@ -12,49 +12,38 @@
|
||||
//! Esplora by way of `reqwest` HTTP client.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
|
||||
use bitcoin::consensus::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
use bitcoin::{Transaction, Txid};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use ::reqwest::{Client, StatusCode};
|
||||
use esplora_client::{convert_fee_rate, AsyncClient, Builder, Tx};
|
||||
use futures::stream::{FuturesOrdered, TryStreamExt};
|
||||
|
||||
use super::api::Tx;
|
||||
use crate::blockchain::esplora::EsploraError;
|
||||
use crate::blockchain::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UrlClient {
|
||||
url: String,
|
||||
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
||||
// when the target platform is wasm32.
|
||||
client: Client,
|
||||
concurrency: u8,
|
||||
}
|
||||
|
||||
/// Structure that implements the logic to sync with Esplora
|
||||
///
|
||||
/// ## Example
|
||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||
#[derive(Debug)]
|
||||
pub struct EsploraBlockchain {
|
||||
url_client: UrlClient,
|
||||
url_client: AsyncClient,
|
||||
stop_gap: usize,
|
||||
concurrency: u8,
|
||||
}
|
||||
|
||||
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||
fn from(url_client: UrlClient) -> Self {
|
||||
impl std::convert::From<AsyncClient> for EsploraBlockchain {
|
||||
fn from(url_client: AsyncClient) -> Self {
|
||||
EsploraBlockchain {
|
||||
url_client,
|
||||
stop_gap: 20,
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,19 +51,25 @@ impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||
impl EsploraBlockchain {
|
||||
/// Create a new instance of the client from a base URL and `stop_gap`.
|
||||
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||
let url_client = Builder::new(base_url)
|
||||
.build_async()
|
||||
.expect("Should never fail with no proxy and timeout");
|
||||
|
||||
Self::from_client(url_client, stop_gap)
|
||||
}
|
||||
|
||||
/// Build a new instance given a client
|
||||
pub fn from_client(url_client: AsyncClient, stop_gap: usize) -> Self {
|
||||
EsploraBlockchain {
|
||||
url_client: UrlClient {
|
||||
url: base_url.to_string(),
|
||||
client: Client::new(),
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
},
|
||||
url_client,
|
||||
stop_gap,
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the concurrency to use when doing batch queries against the Esplora instance.
|
||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||
self.url_client.concurrency = concurrency;
|
||||
self.concurrency = concurrency;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -91,10 +86,57 @@ impl Blockchain for EsploraBlockchain {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: Progress>(
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(await_or_block!(self.url_client.broadcast(tx))?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
let estimates = await_or_block!(self.url_client.get_fee_estimates())?;
|
||||
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
|
||||
target, estimates,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EsploraBlockchain {
|
||||
type Target = AsyncClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.url_client
|
||||
}
|
||||
}
|
||||
|
||||
impl StatelessBlockchain for EsploraBlockchain {}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetHeight for EsploraBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(await_or_block!(self.url_client.get_height())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetTx for EsploraBlockchain {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(await_or_block!(self.url_client.get_tx(txid))?)
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
Ok(await_or_block!(self
|
||||
.url_client
|
||||
.get_block_hash(height as u32))?)
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl WalletSync for EsploraBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
_progress_update: P,
|
||||
_progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
@@ -105,10 +147,10 @@ impl Blockchain for EsploraBlockchain {
|
||||
Request::Script(script_req) => {
|
||||
let futures: FuturesOrdered<_> = script_req
|
||||
.request()
|
||||
.take(self.url_client.concurrency as usize)
|
||||
.take(self.concurrency as usize)
|
||||
.map(|script| async move {
|
||||
let mut related_txs: Vec<Tx> =
|
||||
self.url_client._scripthash_txs(script, None).await?;
|
||||
self.url_client.scripthash_txs(script, None).await?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
@@ -118,7 +160,7 @@ impl Blockchain for EsploraBlockchain {
|
||||
loop {
|
||||
let new_related_txs: Vec<Tx> = self
|
||||
.url_client
|
||||
._scripthash_txs(
|
||||
.scripthash_txs(
|
||||
script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)
|
||||
@@ -158,6 +200,7 @@ impl Blockchain for EsploraBlockchain {
|
||||
.get(txid)
|
||||
.expect("must be in index")
|
||||
.confirmation_time()
|
||||
.map(Into::into)
|
||||
})
|
||||
.collect();
|
||||
conftime_req.satisfy(conftimes)?
|
||||
@@ -177,154 +220,30 @@ impl Blockchain for EsploraBlockchain {
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(await_or_block!(self.url_client._get_tx(txid))?)
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(await_or_block!(self.url_client._broadcast(tx))?)
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(await_or_block!(self.url_client._get_height())?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
|
||||
super::into_fee_rate(target, estimates)
|
||||
}
|
||||
}
|
||||
|
||||
impl UrlClient {
|
||||
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(&format!("{}/tx/{}/raw", self.url, txid))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if let StatusCode::NOT_FOUND = resp.status() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
|
||||
}
|
||||
|
||||
async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
|
||||
match self._get_tx(txid).await {
|
||||
Ok(Some(tx)) => Ok(tx),
|
||||
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
|
||||
let resp = self
|
||||
.client
|
||||
.get(&format!("{}/block-height/{}", self.url, block_height))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if let StatusCode::NOT_FOUND = resp.status() {
|
||||
return Err(EsploraError::HeaderHeightNotFound(block_height));
|
||||
}
|
||||
let bytes = resp.bytes().await?;
|
||||
let hash = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get(&format!("{}/block/{}/header", self.url, hash))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
||||
self.client
|
||||
.post(&format!("{}/tx", self.url))
|
||||
.body(serialize(transaction).to_hex())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _get_height(&self) -> Result<u32, EsploraError> {
|
||||
let req = self
|
||||
.client
|
||||
.get(&format!("{}/blocks/tip/height", self.url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(req.error_for_status()?.text().await?.parse()?)
|
||||
}
|
||||
|
||||
async fn _scripthash_txs(
|
||||
&self,
|
||||
script: &Script,
|
||||
last_seen: Option<Txid>,
|
||||
) -> Result<Vec<Tx>, EsploraError> {
|
||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
||||
let url = match last_seen {
|
||||
Some(last_seen) => format!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, script_hash, last_seen
|
||||
),
|
||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
||||
};
|
||||
Ok(self
|
||||
.client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Vec<Tx>>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
||||
Ok(self
|
||||
.client
|
||||
.get(&format!("{}/fee-estimates", self.url,))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<HashMap<String, f64>>()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
type Config = super::EsploraBlockchainConfig;
|
||||
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let map_e = |e: reqwest::Error| Error::Esplora(Box::new(e.into()));
|
||||
let mut builder = Builder::new(config.base_url.as_str());
|
||||
|
||||
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain.url_client.concurrency = concurrency;
|
||||
}
|
||||
let mut builder = Client::builder();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(proxy) = &config.proxy {
|
||||
builder = builder.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(timeout) = config.timeout {
|
||||
builder = builder.timeout(core::time::Duration::from_secs(timeout));
|
||||
builder = builder.timeout(timeout);
|
||||
}
|
||||
|
||||
blockchain.url_client.client = builder.build().map_err(map_e)?;
|
||||
if let Some(proxy) = &config.proxy {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
let mut blockchain =
|
||||
EsploraBlockchain::from_client(builder.build_async()?, config.stop_gap);
|
||||
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain = blockchain.with_concurrency(concurrency);
|
||||
}
|
||||
|
||||
Ok(blockchain)
|
||||
}
|
||||
238
src/blockchain/esplora/blocking.rs
Normal file
238
src/blockchain/esplora/blocking.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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.
|
||||
|
||||
//! Esplora by way of `ureq` HTTP client.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use bitcoin::{Transaction, Txid};
|
||||
|
||||
use esplora_client::{convert_fee_rate, BlockingClient, Builder, Tx};
|
||||
|
||||
use crate::blockchain::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
|
||||
/// Structure that implements the logic to sync with Esplora
|
||||
///
|
||||
/// ## Example
|
||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||
#[derive(Debug)]
|
||||
pub struct EsploraBlockchain {
|
||||
url_client: BlockingClient,
|
||||
stop_gap: usize,
|
||||
concurrency: u8,
|
||||
}
|
||||
|
||||
impl EsploraBlockchain {
|
||||
/// Create a new instance of the client from a base URL and the `stop_gap`.
|
||||
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||
let url_client = Builder::new(base_url)
|
||||
.build_blocking()
|
||||
.expect("Should never fail with no proxy and timeout");
|
||||
|
||||
Self::from_client(url_client, stop_gap)
|
||||
}
|
||||
|
||||
/// Build a new instance given a client
|
||||
pub fn from_client(url_client: BlockingClient, stop_gap: usize) -> Self {
|
||||
EsploraBlockchain {
|
||||
url_client,
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
stop_gap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the number of parallel requests the client can make.
|
||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||
self.concurrency = concurrency;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Blockchain for EsploraBlockchain {
|
||||
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||
vec![
|
||||
Capability::FullHistory,
|
||||
Capability::GetAnyTx,
|
||||
Capability::AccurateFees,
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
self.url_client.broadcast(tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
let estimates = self.url_client.get_fee_estimates()?;
|
||||
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
|
||||
target, estimates,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EsploraBlockchain {
|
||||
type Target = BlockingClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.url_client
|
||||
}
|
||||
}
|
||||
|
||||
impl StatelessBlockchain for EsploraBlockchain {}
|
||||
|
||||
impl GetHeight for EsploraBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(self.url_client.get_height()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetTx for EsploraBlockchain {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.url_client.get_tx(txid)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
Ok(self.url_client.get_block_hash(height as u32)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSync for EsploraBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
_progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
|
||||
let batch_update = loop {
|
||||
request = match request {
|
||||
Request::Script(script_req) => {
|
||||
let scripts = script_req
|
||||
.request()
|
||||
.take(self.concurrency as usize)
|
||||
.cloned();
|
||||
|
||||
let mut handles = vec![];
|
||||
for script in scripts {
|
||||
let client = self.url_client.clone();
|
||||
// make each request in its own thread.
|
||||
handles.push(std::thread::spawn(move || {
|
||||
let mut related_txs: Vec<Tx> = client.scripthash_txs(&script, None)?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
// esplora pages on 25 confirmed transactions. If there's 25 or more we
|
||||
// keep requesting to see if there's more.
|
||||
if n_confirmed >= 25 {
|
||||
loop {
|
||||
let new_related_txs: Vec<Tx> = client.scripthash_txs(
|
||||
&script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)?;
|
||||
let n = new_related_txs.len();
|
||||
related_txs.extend(new_related_txs);
|
||||
// we've reached the end
|
||||
if n < 25 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::<_, Error>::Ok(related_txs)
|
||||
}));
|
||||
}
|
||||
|
||||
let txs_per_script: Vec<Vec<Tx>> = handles
|
||||
.into_iter()
|
||||
.map(|handle| handle.join().unwrap())
|
||||
.collect::<Result<_, _>>()?;
|
||||
let mut satisfaction = vec![];
|
||||
|
||||
for txs in txs_per_script {
|
||||
satisfaction.push(
|
||||
txs.iter()
|
||||
.map(|tx| (tx.txid, tx.status.block_height))
|
||||
.collect(),
|
||||
);
|
||||
for tx in txs {
|
||||
tx_index.insert(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
|
||||
script_req.satisfy(satisfaction)?
|
||||
}
|
||||
Request::Conftime(conftime_req) => {
|
||||
let conftimes = conftime_req
|
||||
.request()
|
||||
.map(|txid| {
|
||||
tx_index
|
||||
.get(txid)
|
||||
.expect("must be in index")
|
||||
.confirmation_time()
|
||||
.map(Into::into)
|
||||
})
|
||||
.collect();
|
||||
conftime_req.satisfy(conftimes)?
|
||||
}
|
||||
Request::Tx(tx_req) => {
|
||||
let full_txs = tx_req
|
||||
.request()
|
||||
.map(|txid| {
|
||||
let tx = tx_index.get(txid).expect("must be in index");
|
||||
Ok((tx.previous_outputs(), tx.to_tx()))
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
tx_req.satisfy(full_txs)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
}
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
type Config = super::EsploraBlockchainConfig;
|
||||
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let mut builder = Builder::new(config.base_url.as_str());
|
||||
|
||||
if let Some(timeout) = config.timeout {
|
||||
builder = builder.timeout(timeout);
|
||||
}
|
||||
|
||||
if let Some(proxy) = &config.proxy {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
let mut blockchain =
|
||||
EsploraBlockchain::from_client(builder.build_blocking()?, config.stop_gap);
|
||||
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain = blockchain.with_concurrency(concurrency);
|
||||
}
|
||||
|
||||
Ok(blockchain)
|
||||
}
|
||||
}
|
||||
@@ -15,89 +15,25 @@
|
||||
//! depending on your needs (blocking or async respectively).
|
||||
//!
|
||||
//! Please note, to configure the Esplora HTTP client correctly use one of:
|
||||
//! Blocking: --features='esplora,ureq'
|
||||
//! Async: --features='async-interface,esplora,reqwest' --no-default-features
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
//! Blocking: --features='use-esplora-blocking'
|
||||
//! Async: --features='async-interface,use-esplora-async' --no-default-features
|
||||
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::{BlockHash, Txid};
|
||||
pub use esplora_client::Error as EsploraError;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
#[cfg(feature = "use-esplora-async")]
|
||||
mod r#async;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
mod reqwest;
|
||||
#[cfg(feature = "use-esplora-async")]
|
||||
pub use self::r#async::*;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use self::reqwest::*;
|
||||
#[cfg(feature = "use-esplora-blocking")]
|
||||
mod blocking;
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
mod ureq;
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
pub use self::ureq::*;
|
||||
|
||||
mod api;
|
||||
|
||||
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
|
||||
let fee_val = {
|
||||
let mut pairs = estimates
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
|
||||
.collect::<Vec<_>>();
|
||||
pairs.sort_unstable_by_key(|(k, _)| std::cmp::Reverse(*k));
|
||||
pairs
|
||||
.into_iter()
|
||||
.find(|(k, _)| k <= &target)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or(1.0)
|
||||
};
|
||||
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
|
||||
}
|
||||
|
||||
/// Errors that can happen during a sync with [`EsploraBlockchain`]
|
||||
#[derive(Debug)]
|
||||
pub enum EsploraError {
|
||||
/// Error during ureq HTTP request
|
||||
#[cfg(feature = "ureq")]
|
||||
Ureq(::ureq::Error),
|
||||
/// Transport error during the ureq HTTP call
|
||||
#[cfg(feature = "ureq")]
|
||||
UreqTransport(::ureq::Transport),
|
||||
/// Error during reqwest HTTP request
|
||||
#[cfg(feature = "reqwest")]
|
||||
Reqwest(::reqwest::Error),
|
||||
/// HTTP response error
|
||||
HttpResponse(u16),
|
||||
/// IO error during ureq response read
|
||||
Io(io::Error),
|
||||
/// No header found in ureq response
|
||||
NoHeader,
|
||||
/// Invalid number returned
|
||||
Parsing(std::num::ParseIntError),
|
||||
/// Invalid Bitcoin data returned
|
||||
BitcoinEncoding(bitcoin::consensus::encode::Error),
|
||||
/// Invalid Hex data returned
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
|
||||
/// Transaction not found
|
||||
TransactionNotFound(Txid),
|
||||
/// Header height not found
|
||||
HeaderHeightNotFound(u32),
|
||||
/// Header hash not found
|
||||
HeaderHashNotFound(BlockHash),
|
||||
}
|
||||
|
||||
impl fmt::Display for EsploraError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "use-esplora-blocking")]
|
||||
pub use self::blocking::*;
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service
|
||||
///
|
||||
@@ -138,16 +74,11 @@ impl EsploraBlockchainConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EsploraError {}
|
||||
|
||||
#[cfg(feature = "ureq")]
|
||||
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
|
||||
#[cfg(feature = "reqwest")]
|
||||
impl_error!(::reqwest::Error, Reqwest, EsploraError);
|
||||
impl_error!(io::Error, Io, EsploraError);
|
||||
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
|
||||
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
|
||||
impl From<esplora_client::BlockTime> for crate::BlockTime {
|
||||
fn from(esplora_client::BlockTime { timestamp, height }: esplora_client::BlockTime) -> Self {
|
||||
Self { timestamp, height }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-esplora")]
|
||||
@@ -161,52 +92,39 @@ const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn feerate_parsing() {
|
||||
let esplora_fees = serde_json::from_str::<HashMap<String, f64>>(
|
||||
r#"{
|
||||
"25": 1.015,
|
||||
"5": 2.3280000000000003,
|
||||
"12": 2.0109999999999997,
|
||||
"15": 1.018,
|
||||
"17": 1.018,
|
||||
"11": 2.0109999999999997,
|
||||
"3": 3.01,
|
||||
"2": 4.9830000000000005,
|
||||
"6": 2.2359999999999998,
|
||||
"21": 1.018,
|
||||
"13": 1.081,
|
||||
"7": 2.2359999999999998,
|
||||
"8": 2.2359999999999998,
|
||||
"16": 1.018,
|
||||
"20": 1.018,
|
||||
"22": 1.017,
|
||||
"23": 1.017,
|
||||
"504": 1,
|
||||
"9": 2.2359999999999998,
|
||||
"14": 1.018,
|
||||
"10": 2.0109999999999997,
|
||||
"24": 1.017,
|
||||
"1008": 1,
|
||||
"1": 4.9830000000000005,
|
||||
"4": 2.3280000000000003,
|
||||
"19": 1.018,
|
||||
"144": 1,
|
||||
"18": 1.018
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
into_fee_rate(6, esplora_fees.clone()).unwrap(),
|
||||
FeeRate::from_sat_per_vb(2.236)
|
||||
);
|
||||
assert_eq!(
|
||||
into_fee_rate(26, esplora_fees).unwrap(),
|
||||
FeeRate::from_sat_per_vb(1.015),
|
||||
"should inherit from value for 25"
|
||||
);
|
||||
#[cfg(feature = "test-esplora")]
|
||||
fn test_esplora_with_variable_configs() {
|
||||
use super::*;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Esplora by way of `ureq` HTTP client.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use ureq::{Agent, Proxy, Response};
|
||||
|
||||
use bitcoin::consensus::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
|
||||
use super::api::Tx;
|
||||
use crate::blockchain::esplora::EsploraError;
|
||||
use crate::blockchain::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UrlClient {
|
||||
url: String,
|
||||
agent: Agent,
|
||||
}
|
||||
|
||||
/// Structure that implements the logic to sync with Esplora
|
||||
///
|
||||
/// ## Example
|
||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||
#[derive(Debug)]
|
||||
pub struct EsploraBlockchain {
|
||||
url_client: UrlClient,
|
||||
stop_gap: usize,
|
||||
concurrency: u8,
|
||||
}
|
||||
|
||||
impl EsploraBlockchain {
|
||||
/// Create a new instance of the client from a base URL and the `stop_gap`.
|
||||
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||
EsploraBlockchain {
|
||||
url_client: UrlClient {
|
||||
url: base_url.to_string(),
|
||||
agent: Agent::new(),
|
||||
},
|
||||
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
|
||||
stop_gap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the inner `ureq` agent.
|
||||
pub fn with_agent(mut self, agent: Agent) -> Self {
|
||||
self.url_client.agent = agent;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the number of parallel requests the client can make.
|
||||
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||
self.concurrency = concurrency;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Blockchain for EsploraBlockchain {
|
||||
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||
vec![
|
||||
Capability::FullHistory,
|
||||
Capability::GetAnyTx,
|
||||
Capability::AccurateFees,
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
|
||||
let batch_update = loop {
|
||||
request = match request {
|
||||
Request::Script(script_req) => {
|
||||
let scripts = script_req
|
||||
.request()
|
||||
.take(self.concurrency as usize)
|
||||
.cloned();
|
||||
|
||||
let handles = scripts.map(move |script| {
|
||||
let client = self.url_client.clone();
|
||||
// make each request in its own thread.
|
||||
std::thread::spawn(move || {
|
||||
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
// esplora pages on 25 confirmed transactions. If there's 25 or more we
|
||||
// keep requesting to see if there's more.
|
||||
if n_confirmed >= 25 {
|
||||
loop {
|
||||
let new_related_txs: Vec<Tx> = client._scripthash_txs(
|
||||
&script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)?;
|
||||
let n = new_related_txs.len();
|
||||
related_txs.extend(new_related_txs);
|
||||
// we've reached the end
|
||||
if n < 25 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::<_, Error>::Ok(related_txs)
|
||||
})
|
||||
});
|
||||
|
||||
let txs_per_script: Vec<Vec<Tx>> = handles
|
||||
.map(|handle| handle.join().unwrap())
|
||||
.collect::<Result<_, _>>()?;
|
||||
let mut satisfaction = vec![];
|
||||
|
||||
for txs in txs_per_script {
|
||||
satisfaction.push(
|
||||
txs.iter()
|
||||
.map(|tx| (tx.txid, tx.status.block_height))
|
||||
.collect(),
|
||||
);
|
||||
for tx in txs {
|
||||
tx_index.insert(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
|
||||
script_req.satisfy(satisfaction)?
|
||||
}
|
||||
Request::Conftime(conftime_req) => {
|
||||
let conftimes = conftime_req
|
||||
.request()
|
||||
.map(|txid| {
|
||||
tx_index
|
||||
.get(txid)
|
||||
.expect("must be in index")
|
||||
.confirmation_time()
|
||||
})
|
||||
.collect();
|
||||
conftime_req.satisfy(conftimes)?
|
||||
}
|
||||
Request::Tx(tx_req) => {
|
||||
let full_txs = tx_req
|
||||
.request()
|
||||
.map(|txid| {
|
||||
let tx = tx_index.get(txid).expect("must be in index");
|
||||
Ok((tx.previous_outputs(), tx.to_tx()))
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
tx_req.satisfy(full_txs)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
}
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(self.url_client._get_tx(txid)?)
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
let _txid = self.url_client._broadcast(tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(self.url_client._get_height()?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
let estimates = self.url_client._get_fee_estimates()?;
|
||||
super::into_fee_rate(target, estimates)
|
||||
}
|
||||
}
|
||||
|
||||
impl UrlClient {
|
||||
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!("{}/tx/{}/raw", self.url, txid))
|
||||
.call();
|
||||
|
||||
match resp {
|
||||
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
|
||||
Err(ureq::Error::Status(code, _)) => {
|
||||
if is_status_not_found(code) {
|
||||
return Ok(None);
|
||||
}
|
||||
Err(EsploraError::HttpResponse(code))
|
||||
}
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
|
||||
match self._get_tx(txid) {
|
||||
Ok(Some(tx)) => Ok(tx),
|
||||
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!("{}/block-height/{}", self.url, block_height))
|
||||
.call();
|
||||
|
||||
let bytes = match resp {
|
||||
Ok(resp) => Ok(into_bytes(resp)?),
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}?;
|
||||
|
||||
let hash = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
|
||||
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!("{}/block/{}/header", self.url, hash))
|
||||
.call();
|
||||
|
||||
match resp {
|
||||
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
.post(&format!("{}/tx", self.url))
|
||||
.send_string(&serialize(transaction).to_hex());
|
||||
|
||||
match resp {
|
||||
Ok(_) => Ok(()), // We do not return the txid?
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_height(&self) -> Result<u32, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!("{}/blocks/tip/height", self.url))
|
||||
.call();
|
||||
|
||||
match resp {
|
||||
Ok(resp) => Ok(resp.into_string()?.parse()?),
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
||||
let resp = self
|
||||
.agent
|
||||
.get(&format!("{}/fee-estimates", self.url,))
|
||||
.call();
|
||||
|
||||
let map = match resp {
|
||||
Ok(resp) => {
|
||||
let map: HashMap<String, f64> = resp.into_json()?;
|
||||
Ok(map)
|
||||
}
|
||||
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||
Err(e) => Err(EsploraError::Ureq(e)),
|
||||
}?;
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn _scripthash_txs(
|
||||
&self,
|
||||
script: &Script,
|
||||
last_seen: Option<Txid>,
|
||||
) -> Result<Vec<Tx>, EsploraError> {
|
||||
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
|
||||
let url = match last_seen {
|
||||
Some(last_seen) => format!(
|
||||
"{}/scripthash/{}/txs/chain/{}",
|
||||
self.url, script_hash, last_seen
|
||||
),
|
||||
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
|
||||
};
|
||||
Ok(self.agent.get(&url).call()?.into_json()?)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_status_not_found(status: u16) -> bool {
|
||||
status == 404
|
||||
}
|
||||
|
||||
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
|
||||
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
|
||||
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
resp.into_reader()
|
||||
.take((BYTES_LIMIT + 1) as u64)
|
||||
.read_to_end(&mut buf)?;
|
||||
if buf.len() > BYTES_LIMIT {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"response too big for into_bytes",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||
type Config = super::EsploraBlockchainConfig;
|
||||
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let mut agent_builder = ureq::AgentBuilder::new();
|
||||
|
||||
if let Some(timeout) = config.timeout {
|
||||
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
|
||||
}
|
||||
|
||||
if let Some(proxy) = &config.proxy {
|
||||
agent_builder = agent_builder
|
||||
.proxy(Proxy::new(proxy).map_err(|e| Error::Esplora(Box::new(e.into())))?);
|
||||
}
|
||||
|
||||
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
|
||||
.with_agent(agent_builder.build());
|
||||
|
||||
if let Some(concurrency) = config.concurrency {
|
||||
blockchain = blockchain.with_concurrency(concurrency);
|
||||
}
|
||||
|
||||
Ok(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ureq::Error> for EsploraError {
|
||||
fn from(e: ureq::Error) -> Self {
|
||||
match e {
|
||||
ureq::Error::Status(code, _) => EsploraError::HttpResponse(code),
|
||||
e => EsploraError::Ureq(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,12 @@ use std::ops::Deref;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitcoin::{Transaction, Txid};
|
||||
use bitcoin::{BlockHash, Transaction, Txid};
|
||||
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
use crate::FeeRate;
|
||||
use crate::wallet::{wallet_name_from_descriptor, Wallet};
|
||||
use crate::{FeeRate, KeychainKind};
|
||||
|
||||
#[cfg(any(
|
||||
feature = "electrum",
|
||||
@@ -86,28 +87,57 @@ pub enum Capability {
|
||||
|
||||
/// Trait that defines the actions that must be supported by a blockchain backend
|
||||
#[maybe_async]
|
||||
pub trait Blockchain {
|
||||
pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash {
|
||||
/// Return the set of [`Capability`] supported by this backend
|
||||
fn get_capabilities(&self) -> HashSet<Capability>;
|
||||
/// Broadcast a transaction
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
|
||||
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
|
||||
}
|
||||
|
||||
/// Trait for getting the current height of the blockchain.
|
||||
#[maybe_async]
|
||||
pub trait GetHeight {
|
||||
/// Return the current height
|
||||
fn get_height(&self) -> Result<u32, Error>;
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
/// Trait for getting a transaction by txid
|
||||
pub trait GetTx {
|
||||
/// Fetch a transaction given its txid
|
||||
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.
|
||||
#[maybe_async]
|
||||
pub trait WalletSync {
|
||||
/// Setup the backend and populate the internal database for the first time
|
||||
///
|
||||
/// This method is the equivalent of [`Blockchain::sync`], but it's guaranteed to only be
|
||||
/// This method is the equivalent of [`Self::wallet_sync`], but it's guaranteed to only be
|
||||
/// called once, at the first [`Wallet::sync`](crate::wallet::Wallet::sync).
|
||||
///
|
||||
/// The rationale behind the distinction between `sync` and `setup` is that some custom backends
|
||||
/// might need to perform specific actions only the first time they are synced.
|
||||
///
|
||||
/// For types that do not have that distinction, only this method can be implemented, since
|
||||
/// [`Blockchain::sync`] defaults to calling this internally if not overridden.
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
/// [`WalletSync::wallet_sync`] defaults to calling this internally if not overridden.
|
||||
/// Populate the internal database with transactions and UTXOs
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error>;
|
||||
/// Populate the internal database with transactions and UTXOs
|
||||
///
|
||||
/// If not overridden, it defaults to calling [`Blockchain::setup`] internally.
|
||||
|
||||
/// If not overridden, it defaults to calling [`Self::wallet_setup`] internally.
|
||||
///
|
||||
/// This method should implement the logic required to iterate over the list of the wallet's
|
||||
/// script_pubkeys using [`Database::iter_script_pubkeys`] and look for relevant transactions
|
||||
@@ -124,23 +154,13 @@ pub trait Blockchain {
|
||||
/// [`BatchOperations::set_tx`]: crate::database::BatchOperations::set_tx
|
||||
/// [`BatchOperations::set_utxo`]: crate::database::BatchOperations::set_utxo
|
||||
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
|
||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||
fn wallet_sync<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self.setup(database, progress_update))
|
||||
maybe_await!(self.wallet_setup(database, progress_update))
|
||||
}
|
||||
|
||||
/// Fetch a transaction from the blockchain given its txid
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
||||
/// Broadcast a transaction
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
|
||||
|
||||
/// Return the current height
|
||||
fn get_height(&self) -> Result<u32, Error>;
|
||||
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
|
||||
}
|
||||
|
||||
/// Trait for [`Blockchain`] types that can be created given a configuration
|
||||
@@ -152,12 +172,112 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
|
||||
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`]
|
||||
pub type ProgressData = (f32, Option<String>);
|
||||
|
||||
/// Trait for types that can receive and process progress updates during [`Blockchain::sync`] and
|
||||
/// [`Blockchain::setup`]
|
||||
pub trait Progress: Send {
|
||||
/// Trait for types that can receive and process progress updates during [`WalletSync::wallet_sync`] and
|
||||
/// [`WalletSync::wallet_setup`]
|
||||
pub trait Progress: Send + 'static + core::fmt::Debug {
|
||||
/// Send a new progress update
|
||||
///
|
||||
/// The `progress` value should be in the range 0.0 - 100.0, and the `message` value is an
|
||||
@@ -182,7 +302,7 @@ impl Progress for Sender<ProgressData> {
|
||||
}
|
||||
|
||||
/// Type that implements [`Progress`] and drops every update received
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct NoopProgress;
|
||||
|
||||
/// Create a new instance of [`NoopProgress`]
|
||||
@@ -197,7 +317,7 @@ impl Progress for NoopProgress {
|
||||
}
|
||||
|
||||
/// Type that implements [`Progress`] and logs at level `INFO` every update received
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct LogProgress;
|
||||
|
||||
/// Create a new instance of [`LogProgress`]
|
||||
@@ -223,33 +343,51 @@ impl<T: Blockchain> Blockchain for Arc<T> {
|
||||
maybe_await!(self.deref().get_capabilities())
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self.deref().setup(database, progress_update))
|
||||
}
|
||||
|
||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self.deref().sync(database, progress_update))
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
maybe_await!(self.deref().get_tx(txid))
|
||||
}
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
maybe_await!(self.deref().broadcast(tx))
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
maybe_await!(self.deref().get_height())
|
||||
}
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
maybe_await!(self.deref().estimate_fee(target))
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl<T: GetTx> GetTx for Arc<T> {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
maybe_await!(self.deref().get_tx(txid))
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl<T: GetHeight> GetHeight for Arc<T> {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
maybe_await!(self.deref().get_height())
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
impl<T: WalletSync> WalletSync for Arc<T> {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self.deref().wallet_setup(database, progress_update))
|
||||
}
|
||||
|
||||
fn wallet_sync<D: BatchDatabase>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
progress_update: Box<dyn Progress>,
|
||||
) -> Result<(), Error> {
|
||||
maybe_await!(self.deref().wallet_sync(database, progress_update))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
|
||||
#![allow(dead_code)]
|
||||
use crate::{
|
||||
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
||||
error::MissingCachedScripts,
|
||||
wallet::time::Instant,
|
||||
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
|
||||
.iter_script_pubkeys(Some(keychain))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
.collect::<VecDeque<_>>();
|
||||
let state = State::new(db);
|
||||
|
||||
Ok(Request::Script(ScriptReq {
|
||||
state,
|
||||
initial_scripts_needed: scripts_needed.len(),
|
||||
scripts_needed,
|
||||
script_index: 0,
|
||||
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> {
|
||||
state: State<'a, D>,
|
||||
script_index: usize,
|
||||
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
|
||||
scripts_needed: VecDeque<Script>,
|
||||
stop_gap: usize,
|
||||
keychain: KeychainKind,
|
||||
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
|
||||
self.script_index += 1;
|
||||
}
|
||||
|
||||
for _ in txids {
|
||||
self.scripts_needed.pop_front();
|
||||
}
|
||||
self.scripts_needed.drain(..txids.len());
|
||||
|
||||
let last_active_index = self
|
||||
// last active index: 0 => No last active
|
||||
let last = self
|
||||
.state
|
||||
.last_active_index
|
||||
.get(&self.keychain)
|
||||
.map(|x| x + 1)
|
||||
.unwrap_or(0); // so no addresses active maps to 0
|
||||
.map(|&l| l + 1)
|
||||
.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(
|
||||
if self.script_index > last_active_index + self.stop_gap
|
||||
|| self.scripts_needed.is_empty()
|
||||
{
|
||||
debug!(
|
||||
"finished scanning for transactions for keychain {:?} at index {}",
|
||||
self.keychain, last_active_index
|
||||
);
|
||||
// we're done here -- check if we need to do the next keychain
|
||||
if let Some(keychain) = self.next_keychains.pop() {
|
||||
self.keychain = keychain;
|
||||
self.script_index = 0;
|
||||
self.scripts_needed = self
|
||||
.state
|
||||
.db
|
||||
.iter_script_pubkeys(Some(keychain))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
Request::Script(self)
|
||||
} else {
|
||||
Request::Tx(TxReq { state: self.state })
|
||||
}
|
||||
} else {
|
||||
Request::Script(self)
|
||||
},
|
||||
)
|
||||
// this is a hack to check whether the scripts are coming from a derivable descriptor
|
||||
// we assume for non-derivable descriptors, the initial script count is always 1
|
||||
let is_derivable = self.initial_scripts_needed > 1;
|
||||
|
||||
debug!(
|
||||
"sync: last={}, remaining={}, diff={}, stop_gap={}",
|
||||
last, remaining, current_gap, self.stop_gap
|
||||
);
|
||||
|
||||
if is_derivable {
|
||||
if remaining > 0 {
|
||||
// we still have scriptPubKeys to do requests for
|
||||
return Ok(Request::Script(self));
|
||||
}
|
||||
|
||||
if last > 0 && current_gap < self.stop_gap {
|
||||
// current gap is not large enough to stop, but we are unable to keep checking since
|
||||
// we have exhausted cached scriptPubKeys, so return error
|
||||
let err = MissingCachedScripts {
|
||||
last_count: self.script_index,
|
||||
missing_count: self.stop_gap - current_gap,
|
||||
};
|
||||
return Err(Error::MissingCachedScripts(err));
|
||||
}
|
||||
|
||||
// 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>,
|
||||
/// The start of the sync
|
||||
start_time: Instant,
|
||||
/// Missing number of scripts to cache per keychain
|
||||
missing_script_counts: HashMap<KeychainKind, usize>,
|
||||
}
|
||||
|
||||
impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
@@ -305,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
tx_needed: BTreeSet::default(),
|
||||
tx_missing_conftime: BTreeMap::default(),
|
||||
start_time: Instant::new(),
|
||||
missing_script_counts: HashMap::default(),
|
||||
}
|
||||
}
|
||||
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 observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
|
||||
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();
|
||||
|
||||
// Delete old txs that no longer exist
|
||||
@@ -332,7 +382,23 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
batch.del_tx(txid, true)?;
|
||||
}
|
||||
|
||||
// Set every tx we observed
|
||||
let mut spent_utxos = HashSet::new();
|
||||
|
||||
// track all the spent utxos
|
||||
for finished_tx in &finished_txs {
|
||||
let tx = finished_tx
|
||||
.transaction
|
||||
.as_ref()
|
||||
.expect("transaction will always be present here");
|
||||
for input in &tx.input {
|
||||
spent_utxos.insert(&input.previous_output);
|
||||
}
|
||||
}
|
||||
|
||||
// set every utxo we observed, unless it's already spent
|
||||
// we don't do this in the loop above as we want to know all the spent outputs before
|
||||
// adding the non-spent to the batch in case there are new tranasactions
|
||||
// that spend form each other.
|
||||
for finished_tx in &finished_txs {
|
||||
let tx = finished_tx
|
||||
.transaction
|
||||
@@ -343,34 +409,28 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
|
||||
{
|
||||
// add utxos we own from the new transactions we've seen.
|
||||
let outpoint = OutPoint {
|
||||
txid: finished_tx.txid,
|
||||
vout: i as u32,
|
||||
};
|
||||
|
||||
batch.set_utxo(&LocalUtxo {
|
||||
outpoint: OutPoint {
|
||||
txid: finished_tx.txid,
|
||||
vout: i as u32,
|
||||
},
|
||||
outpoint,
|
||||
txout: output.clone(),
|
||||
keychain,
|
||||
// Is this UTXO in the spent_utxos set?
|
||||
is_spent: spent_utxos.get(&outpoint).is_some(),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
batch.set_tx(finished_tx)?;
|
||||
}
|
||||
|
||||
// we don't do this in the loop above since we may want to delete some of the utxos we
|
||||
// just added in case there are new tranasactions that spend form each other.
|
||||
for finished_tx in &finished_txs {
|
||||
let tx = finished_tx
|
||||
.transaction
|
||||
.as_ref()
|
||||
.expect("transaction will always be present here");
|
||||
for input in &tx.input {
|
||||
// Delete any spent utxos
|
||||
batch.del_utxo(&input.previous_output)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (keychain, last_active_index) in self.last_active_index {
|
||||
batch.set_last_index(keychain, last_active_index as u32)?;
|
||||
// apply index updates
|
||||
for (keychain, new_index) in index_updates {
|
||||
debug!("updating index ({}, {})", keychain.as_byte(), new_index);
|
||||
batch.set_last_index(keychain, new_index)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
//! # use bdk::database::{AnyDatabase, MemoryDatabase};
|
||||
//! # use bdk::{Wallet};
|
||||
//! let memory = MemoryDatabase::default();
|
||||
//! let wallet_memory = Wallet::new_offline("...", None, Network::Testnet, memory)?;
|
||||
//! let wallet_memory = Wallet::new("...", None, Network::Testnet, memory)?;
|
||||
//!
|
||||
//! # #[cfg(feature = "key-value-db")]
|
||||
//! # {
|
||||
//! let sled = sled::open("my-database")?.open_tree("default_tree")?;
|
||||
//! let wallet_sled = Wallet::new_offline("...", None, Network::Testnet, sled)?;
|
||||
//! let wallet_sled = Wallet::new("...", None, Network::Testnet, sled)?;
|
||||
//! # }
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
@@ -42,7 +42,7 @@
|
||||
//! # use bdk::{Wallet};
|
||||
//! let config = serde_json::from_str("...")?;
|
||||
//! let database = AnyDatabase::from_config(&config)?;
|
||||
//! let wallet = Wallet::new_offline("...", None, Network::Testnet, database)?;
|
||||
//! let wallet = Wallet::new("...", None, Network::Testnet, database)?;
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
|
||||
@@ -61,6 +61,7 @@ macro_rules! impl_from {
|
||||
|
||||
macro_rules! impl_inner_method {
|
||||
( $enum_name:ident, $self:expr, $name:ident $(, $args:expr)* ) => {
|
||||
#[allow(deprecated)]
|
||||
match $self {
|
||||
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
|
||||
#[cfg(feature = "key-value-db")]
|
||||
@@ -254,10 +255,6 @@ impl Database for AnyDatabase {
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
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 {
|
||||
|
||||
@@ -43,6 +43,7 @@ macro_rules! impl_batch_operations {
|
||||
let value = json!({
|
||||
"t": utxo.txout,
|
||||
"i": utxo.keychain,
|
||||
"s": utxo.is_spent,
|
||||
});
|
||||
self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*;
|
||||
|
||||
@@ -125,8 +126,9 @@ macro_rules! impl_batch_operations {
|
||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||
let txout = serde_json::from_value(val["t"].take())?;
|
||||
let keychain = serde_json::from_value(val["i"].take())?;
|
||||
let is_spent = val.get_mut("s").and_then(|s| s.take().as_bool()).unwrap_or(false);
|
||||
|
||||
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain }))
|
||||
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain, is_spent, }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,16 +166,9 @@ macro_rules! impl_batch_operations {
|
||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
let res = self.remove(key);
|
||||
let res = $process_delete!(res);
|
||||
|
||||
match res {
|
||||
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))
|
||||
}
|
||||
}
|
||||
$process_delete!(res)
|
||||
.map(ivec_to_u32)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
@@ -246,11 +241,16 @@ impl Database for Tree {
|
||||
let mut val: serde_json::Value = serde_json::from_slice(&v)?;
|
||||
let txout = serde_json::from_value(val["t"].take())?;
|
||||
let keychain = serde_json::from_value(val["i"].take())?;
|
||||
let is_spent = val
|
||||
.get_mut("s")
|
||||
.and_then(|s| s.take().as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(LocalUtxo {
|
||||
outpoint,
|
||||
txout,
|
||||
keychain,
|
||||
is_spent,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -314,11 +314,16 @@ impl Database for Tree {
|
||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||
let txout = serde_json::from_value(val["t"].take())?;
|
||||
let keychain = serde_json::from_value(val["i"].take())?;
|
||||
let is_spent = val
|
||||
.get_mut("s")
|
||||
.and_then(|s| s.take().as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(LocalUtxo {
|
||||
outpoint: *outpoint,
|
||||
txout,
|
||||
keychain,
|
||||
is_spent,
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
@@ -345,16 +350,7 @@ impl Database for Tree {
|
||||
|
||||
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
self.get(key)?
|
||||
.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()
|
||||
self.get(key)?.map(ivec_to_u32).transpose()
|
||||
}
|
||||
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
@@ -381,19 +377,17 @@ impl Database for Tree {
|
||||
|
||||
Some(new.to_be_bytes().to_vec())
|
||||
})?
|
||||
.map_or(Ok(0), |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)
|
||||
})
|
||||
.map_or(Ok(0), ivec_to_u32)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(Tree::flush(self).map(|_| ())?)
|
||||
}
|
||||
fn ivec_to_u32(b: sled::IVec) -> Result<u32, 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)
|
||||
}
|
||||
|
||||
impl BatchDatabase for Tree {
|
||||
@@ -498,4 +492,44 @@ mod test {
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_raw_txs() {
|
||||
crate::database::test::test_iter_raw_txs(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_path_from_script_pubkey() {
|
||||
crate::database::test::test_del_path_from_script_pubkey(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_script_pubkeys() {
|
||||
crate::database::test::test_iter_script_pubkeys(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_utxo() {
|
||||
crate::database::test::test_del_utxo(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_raw_tx() {
|
||||
crate::database::test::test_del_raw_tx(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_tx() {
|
||||
crate::database::test::test_del_tx(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_last_index() {
|
||||
crate::database::test::test_del_last_index(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_descriptor_checksum() {
|
||||
crate::database::test::test_check_descriptor_checksum(get_tree());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,8 +150,10 @@ impl BatchOperations for MemoryDatabase {
|
||||
|
||||
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||
self.map
|
||||
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
||||
self.map.insert(
|
||||
key,
|
||||
Box::new((utxo.txout.clone(), utxo.keychain, utxo.is_spent)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -228,11 +230,12 @@ impl BatchOperations for MemoryDatabase {
|
||||
match res {
|
||||
None => Ok(None),
|
||||
Some(b) => {
|
||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
||||
Ok(Some(LocalUtxo {
|
||||
outpoint: *outpoint,
|
||||
txout,
|
||||
keychain,
|
||||
is_spent,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -326,11 +329,12 @@ impl Database for MemoryDatabase {
|
||||
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
||||
.map(|(k, v)| {
|
||||
let outpoint = deserialize(&k[1..]).unwrap();
|
||||
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
|
||||
let (txout, keychain, is_spent) = v.downcast_ref().cloned().unwrap();
|
||||
Ok(LocalUtxo {
|
||||
outpoint,
|
||||
txout,
|
||||
keychain,
|
||||
is_spent,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -389,11 +393,12 @@ impl Database for MemoryDatabase {
|
||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||
Ok(self.map.get(&key).map(|b| {
|
||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
||||
LocalUtxo {
|
||||
outpoint: *outpoint,
|
||||
txout,
|
||||
keychain,
|
||||
is_spent,
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -444,10 +449,6 @@ impl Database for MemoryDatabase {
|
||||
|
||||
Ok(*value)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for MemoryDatabase {
|
||||
@@ -481,15 +482,23 @@ impl ConfigurableDatabase for MemoryDatabase {
|
||||
/// don't have `test` set.
|
||||
macro_rules! populate_test_db {
|
||||
($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 $crate::database::BatchOperations;
|
||||
use $crate::database::SyncTime;
|
||||
use $crate::database::{BatchOperations, Database};
|
||||
let mut db = $db;
|
||||
let tx_meta = $tx_meta;
|
||||
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 {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
input: vec![],
|
||||
lock_time: bitcoin::PackedLockTime(0),
|
||||
input,
|
||||
output: tx_meta
|
||||
.output
|
||||
.iter()
|
||||
@@ -503,10 +512,31 @@ macro_rules! populate_test_db {
|
||||
};
|
||||
|
||||
let txid = tx.txid();
|
||||
let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||
timestamp: 0,
|
||||
});
|
||||
// Set Confirmation time only if current height is provided.
|
||||
// panics if `tx_meta.min_confirmation` is Some, and current_height is None.
|
||||
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 {
|
||||
transaction: Some(tx.clone()),
|
||||
@@ -526,6 +556,7 @@ macro_rules! populate_test_db {
|
||||
vout: vout as u32,
|
||||
},
|
||||
keychain: $crate::KeychainKind::External,
|
||||
is_spent: false,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -554,7 +585,7 @@ macro_rules! doctest_wallet {
|
||||
Some(100),
|
||||
);
|
||||
|
||||
$crate::Wallet::new_offline(
|
||||
$crate::Wallet::new(
|
||||
&descriptors.0,
|
||||
descriptors.1.as_ref(),
|
||||
Network::Regtest,
|
||||
@@ -616,4 +647,44 @@ mod test {
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_raw_txs() {
|
||||
crate::database::test::test_iter_raw_txs(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_path_from_script_pubkey() {
|
||||
crate::database::test::test_del_path_from_script_pubkey(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_script_pubkeys() {
|
||||
crate::database::test::test_iter_script_pubkeys(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_utxo() {
|
||||
crate::database::test::test_del_utxo(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_raw_tx() {
|
||||
crate::database::test::test_del_raw_tx(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_tx() {
|
||||
crate::database::test::test_del_tx(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_last_index() {
|
||||
crate::database::test::test_del_last_index(get_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_descriptor_checksum() {
|
||||
crate::database::test::test_check_descriptor_checksum(get_tree());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,6 @@ pub trait Database: BatchOperations {
|
||||
///
|
||||
/// It should insert and return `0` if not present in the database
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
||||
|
||||
/// Force changes to be written to disk
|
||||
fn flush(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Trait for a database that supports batch operations
|
||||
@@ -196,8 +193,7 @@ pub(crate) trait DatabaseUtils: Database {
|
||||
D: FnOnce() -> Result<Option<Transaction>, Error>,
|
||||
{
|
||||
self.get_tx(txid, true)?
|
||||
.map(|t| t.transaction)
|
||||
.flatten()
|
||||
.and_then(|t| t.transaction)
|
||||
.map_or_else(default, |t| Ok(Some(t)))
|
||||
}
|
||||
|
||||
@@ -221,32 +217,33 @@ pub mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::consensus::serialize;
|
||||
use bitcoin::hashes::hex::*;
|
||||
use bitcoin::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn test_script_pubkey<D: Database>(mut tree: D) {
|
||||
pub fn test_script_pubkey<D: Database>(mut db: D) {
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
tree.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
db.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
Some(script.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tree.get_path_from_script_pubkey(&script).unwrap(),
|
||||
db.get_path_from_script_pubkey(&script).unwrap(),
|
||||
Some((keychain, path))
|
||||
);
|
||||
}
|
||||
|
||||
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut tree: D) {
|
||||
let mut batch = tree.begin_batch();
|
||||
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut db: D) {
|
||||
let mut batch = db.begin_batch();
|
||||
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
@@ -257,50 +254,50 @@ pub mod test {
|
||||
batch.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
db.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None);
|
||||
assert_eq!(db.get_path_from_script_pubkey(&script).unwrap(), None);
|
||||
|
||||
tree.commit_batch(batch).unwrap();
|
||||
db.commit_batch(batch).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
db.get_script_pubkey_from_path(keychain, path).unwrap(),
|
||||
Some(script.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
tree.get_path_from_script_pubkey(&script).unwrap(),
|
||||
db.get_path_from_script_pubkey(&script).unwrap(),
|
||||
Some((keychain, path))
|
||||
);
|
||||
}
|
||||
|
||||
pub fn test_iter_script_pubkey<D: Database>(mut tree: D) {
|
||||
pub fn test_iter_script_pubkey<D: Database>(mut db: D) {
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
tree.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
pub fn test_del_script_pubkey<D: Database>(mut tree: D) {
|
||||
pub fn test_del_script_pubkey<D: Database>(mut db: D) {
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
tree.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
|
||||
tree.del_script_pubkey_from_path(keychain, path).unwrap();
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0);
|
||||
db.del_script_pubkey_from_path(keychain, path).unwrap();
|
||||
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
pub fn test_utxo<D: Database>(mut tree: D) {
|
||||
pub fn test_utxo<D: Database>(mut db: D) {
|
||||
let outpoint = OutPoint::from_str(
|
||||
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
|
||||
)
|
||||
@@ -316,25 +313,43 @@ pub mod test {
|
||||
txout,
|
||||
outpoint,
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: true,
|
||||
};
|
||||
|
||||
tree.set_utxo(&utxo).unwrap();
|
||||
|
||||
assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo));
|
||||
db.set_utxo(&utxo).unwrap();
|
||||
db.set_utxo(&utxo).unwrap();
|
||||
assert_eq!(db.iter_utxos().unwrap().len(), 1);
|
||||
assert_eq!(db.get_utxo(&outpoint).unwrap(), Some(utxo));
|
||||
}
|
||||
|
||||
pub fn test_raw_tx<D: Database>(mut tree: D) {
|
||||
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
|
||||
let tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
pub fn test_raw_tx<D: Database>(mut db: D) {
|
||||
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
|
||||
let mut tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
|
||||
tree.set_raw_tx(&tx).unwrap();
|
||||
db.set_raw_tx(&tx).unwrap();
|
||||
|
||||
let txid = tx.txid();
|
||||
|
||||
assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx));
|
||||
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx.clone()));
|
||||
|
||||
// mutate transaction's witnesses
|
||||
for tx_in in tx.input.iter_mut() {
|
||||
tx_in.witness = Witness::new();
|
||||
}
|
||||
|
||||
let updated_hex_tx = serialize(&tx);
|
||||
|
||||
// verify that mutation was successful
|
||||
assert_ne!(hex_tx, updated_hex_tx);
|
||||
|
||||
db.set_raw_tx(&tx).unwrap();
|
||||
|
||||
let txid = tx.txid();
|
||||
|
||||
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx));
|
||||
}
|
||||
|
||||
pub fn test_tx<D: Database>(mut tree: D) {
|
||||
pub fn test_tx<D: Database>(mut db: D) {
|
||||
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
|
||||
let tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
let txid = tx.txid();
|
||||
@@ -350,55 +365,80 @@ pub mod test {
|
||||
}),
|
||||
};
|
||||
|
||||
tree.set_tx(&tx_details).unwrap();
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
|
||||
// get with raw tx too
|
||||
assert_eq!(
|
||||
tree.get_tx(&tx_details.txid, true).unwrap(),
|
||||
db.get_tx(&tx_details.txid, true).unwrap(),
|
||||
Some(tx_details.clone())
|
||||
);
|
||||
// get only raw_tx
|
||||
assert_eq!(
|
||||
tree.get_raw_tx(&tx_details.txid).unwrap(),
|
||||
db.get_raw_tx(&tx_details.txid).unwrap(),
|
||||
tx_details.transaction
|
||||
);
|
||||
|
||||
// now get without raw_tx
|
||||
tx_details.transaction = None;
|
||||
assert_eq!(
|
||||
tree.get_tx(&tx_details.txid, false).unwrap(),
|
||||
db.get_tx(&tx_details.txid, false).unwrap(),
|
||||
Some(tx_details)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn test_last_index<D: Database>(mut tree: D) {
|
||||
tree.set_last_index(KeychainKind::External, 1337).unwrap();
|
||||
pub fn test_list_transaction<D: Database>(mut db: 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,
|
||||
}),
|
||||
};
|
||||
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
|
||||
// get raw tx
|
||||
assert_eq!(db.iter_txs(true).unwrap(), vec![tx_details.clone()]);
|
||||
|
||||
// now get without raw tx
|
||||
tx_details.transaction = None;
|
||||
|
||||
// get not raw tx
|
||||
assert_eq!(db.iter_txs(false).unwrap(), vec![tx_details.clone()]);
|
||||
}
|
||||
|
||||
pub fn test_last_index<D: Database>(mut db: D) {
|
||||
db.set_last_index(KeychainKind::External, 1337).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.get_last_index(KeychainKind::External).unwrap(),
|
||||
db.get_last_index(KeychainKind::External).unwrap(),
|
||||
Some(1337)
|
||||
);
|
||||
assert_eq!(tree.get_last_index(KeychainKind::Internal).unwrap(), None);
|
||||
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), None);
|
||||
|
||||
let res = tree.increment_last_index(KeychainKind::External).unwrap();
|
||||
let res = db.increment_last_index(KeychainKind::External).unwrap();
|
||||
assert_eq!(res, 1338);
|
||||
let res = tree.increment_last_index(KeychainKind::Internal).unwrap();
|
||||
let res = db.increment_last_index(KeychainKind::Internal).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
assert_eq!(
|
||||
tree.get_last_index(KeychainKind::External).unwrap(),
|
||||
db.get_last_index(KeychainKind::External).unwrap(),
|
||||
Some(1338)
|
||||
);
|
||||
assert_eq!(
|
||||
tree.get_last_index(KeychainKind::Internal).unwrap(),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), Some(0));
|
||||
}
|
||||
|
||||
pub fn test_sync_time<D: Database>(mut tree: D) {
|
||||
assert!(tree.get_sync_time().unwrap().is_none());
|
||||
pub fn test_sync_time<D: Database>(mut db: D) {
|
||||
assert!(db.get_sync_time().unwrap().is_none());
|
||||
|
||||
tree.set_sync_time(SyncTime {
|
||||
db.set_sync_time(SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: 100,
|
||||
timestamp: 1000,
|
||||
@@ -406,13 +446,211 @@ pub mod test {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let extracted = tree.get_sync_time().unwrap();
|
||||
let extracted = db.get_sync_time().unwrap();
|
||||
assert!(extracted.is_some());
|
||||
assert_eq!(extracted.as_ref().unwrap().block_time.height, 100);
|
||||
assert_eq!(extracted.as_ref().unwrap().block_time.timestamp, 1000);
|
||||
|
||||
tree.del_sync_time().unwrap();
|
||||
assert!(tree.get_sync_time().unwrap().is_none());
|
||||
db.del_sync_time().unwrap();
|
||||
assert!(db.get_sync_time().unwrap().is_none());
|
||||
}
|
||||
|
||||
pub fn test_iter_raw_txs<D: Database>(mut db: D) {
|
||||
let txs = db.iter_raw_txs().unwrap();
|
||||
assert!(txs.is_empty());
|
||||
|
||||
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
|
||||
let first_tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
|
||||
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
|
||||
let second_tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
|
||||
db.set_raw_tx(&first_tx).unwrap();
|
||||
db.set_raw_tx(&second_tx).unwrap();
|
||||
|
||||
let txs = db.iter_raw_txs().unwrap();
|
||||
|
||||
assert!(txs.contains(&first_tx));
|
||||
assert!(txs.contains(&second_tx));
|
||||
assert_eq!(txs.len(), 2);
|
||||
}
|
||||
|
||||
pub fn test_del_path_from_script_pubkey<D: Database>(mut db: D) {
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
|
||||
let res = db.del_path_from_script_pubkey(&script).unwrap();
|
||||
|
||||
assert!(res.is_none());
|
||||
|
||||
let _res = db.set_script_pubkey(&script, keychain, path);
|
||||
let (chain, child) = db.del_path_from_script_pubkey(&script).unwrap().unwrap();
|
||||
|
||||
assert_eq!(chain, keychain);
|
||||
assert_eq!(child, path);
|
||||
|
||||
let res = db.get_path_from_script_pubkey(&script).unwrap();
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
pub fn test_iter_script_pubkeys<D: Database>(mut db: D) {
|
||||
let keychain = KeychainKind::External;
|
||||
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
|
||||
assert!(scripts.is_empty());
|
||||
|
||||
let first_script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
|
||||
db.set_script_pubkey(&first_script, keychain, path).unwrap();
|
||||
|
||||
let second_script = Script::from(
|
||||
Vec::<u8>::from_hex("00145c9a1816d38db5cbdd4b067b689dc19eb7d930e2").unwrap(),
|
||||
);
|
||||
let path = 57;
|
||||
|
||||
db.set_script_pubkey(&second_script, keychain, path)
|
||||
.unwrap();
|
||||
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
|
||||
|
||||
assert!(scripts.contains(&first_script));
|
||||
assert!(scripts.contains(&second_script));
|
||||
assert_eq!(scripts.len(), 2);
|
||||
}
|
||||
|
||||
pub fn test_del_utxo<D: Database>(mut db: D) {
|
||||
let outpoint = OutPoint::from_str(
|
||||
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
|
||||
)
|
||||
.unwrap();
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let txout = TxOut {
|
||||
value: 133742,
|
||||
script_pubkey: script,
|
||||
};
|
||||
let utxo = LocalUtxo {
|
||||
txout,
|
||||
outpoint,
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: true,
|
||||
};
|
||||
|
||||
let res = db.del_utxo(&outpoint).unwrap();
|
||||
assert!(res.is_none());
|
||||
|
||||
db.set_utxo(&utxo).unwrap();
|
||||
|
||||
let res = db.del_utxo(&outpoint).unwrap();
|
||||
|
||||
assert_eq!(res.unwrap(), utxo);
|
||||
|
||||
let res = db.get_utxo(&outpoint).unwrap();
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
pub fn test_del_raw_tx<D: Database>(mut db: D) {
|
||||
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
|
||||
let tx: Transaction = deserialize(&hex_tx).unwrap();
|
||||
|
||||
let res = db.del_raw_tx(&tx.txid()).unwrap();
|
||||
|
||||
assert!(res.is_none());
|
||||
|
||||
db.set_raw_tx(&tx).unwrap();
|
||||
|
||||
let res = db.del_raw_tx(&tx.txid()).unwrap();
|
||||
|
||||
assert_eq!(res.unwrap(), tx);
|
||||
|
||||
let res = db.get_raw_tx(&tx.txid()).unwrap();
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
pub fn test_del_tx<D: Database>(mut db: 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.clone()),
|
||||
txid,
|
||||
received: 1337,
|
||||
sent: 420420,
|
||||
fee: Some(140),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 123456,
|
||||
height: 1000,
|
||||
}),
|
||||
};
|
||||
|
||||
let res = db.del_tx(&tx.txid(), true).unwrap();
|
||||
|
||||
assert!(res.is_none());
|
||||
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
|
||||
let res = db.del_tx(&tx.txid(), false).unwrap();
|
||||
tx_details.transaction = None;
|
||||
assert_eq!(res.unwrap(), tx_details);
|
||||
|
||||
let res = db.get_tx(&tx.txid(), true).unwrap();
|
||||
assert!(res.is_none());
|
||||
|
||||
let res = db.get_raw_tx(&tx.txid()).unwrap();
|
||||
assert_eq!(res.unwrap(), tx);
|
||||
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
let res = db.del_tx(&tx.txid(), true).unwrap();
|
||||
tx_details.transaction = Some(tx.clone());
|
||||
assert_eq!(res.unwrap(), tx_details);
|
||||
|
||||
let res = db.get_tx(&tx.txid(), true).unwrap();
|
||||
assert!(res.is_none());
|
||||
|
||||
let res = db.get_raw_tx(&tx.txid()).unwrap();
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
pub fn test_del_last_index<D: Database>(mut db: D) {
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
let _res = db.increment_last_index(keychain);
|
||||
|
||||
let res = db.get_last_index(keychain).unwrap().unwrap();
|
||||
|
||||
assert_eq!(res, 0);
|
||||
|
||||
let _res = db.increment_last_index(keychain);
|
||||
|
||||
let res = db.del_last_index(keychain).unwrap().unwrap();
|
||||
|
||||
assert_eq!(res, 1);
|
||||
|
||||
let res = db.get_last_index(keychain).unwrap();
|
||||
assert!(res.is_none());
|
||||
}
|
||||
|
||||
pub fn test_check_descriptor_checksum<D: Database>(mut db: D) {
|
||||
// insert checksum associated to keychain
|
||||
let checksum = "1cead456".as_bytes();
|
||||
let keychain = KeychainKind::External;
|
||||
let _res = db.check_descriptor_checksum(keychain, checksum);
|
||||
|
||||
// check if `check_descriptor_checksum` throws
|
||||
// `Error::ChecksumMismatch` error if the
|
||||
// function is passed a checksum that does
|
||||
// not match the one initially inserted
|
||||
let checksum = "1cead454".as_bytes();
|
||||
let keychain = KeychainKind::External;
|
||||
let res = db.check_descriptor_checksum(keychain, checksum);
|
||||
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
// TODO: more tests...
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hash_types::Txid;
|
||||
@@ -40,6 +42,32 @@ static MIGRATIONS: &[&str] = &[
|
||||
"CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER);",
|
||||
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
|
||||
"DROP TABLE transaction_details_old;",
|
||||
"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);",
|
||||
"ALTER TABLE utxos RENAME TO utxos_old;",
|
||||
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
|
||||
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
|
||||
"DROP TABLE utxos_old;",
|
||||
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
|
||||
// Fix issue https://github.com/bitcoindevkit/bdk/issues/801: drop duplicated script_pubkeys
|
||||
"ALTER TABLE script_pubkeys RENAME TO script_pubkeys_old;",
|
||||
"DROP INDEX idx_keychain_child;",
|
||||
"DROP INDEX idx_script;",
|
||||
"CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
|
||||
"CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
|
||||
"CREATE INDEX idx_script ON script_pubkeys(script);",
|
||||
"CREATE UNIQUE INDEX idx_script_pks_unique ON script_pubkeys(keychain, child);",
|
||||
"INSERT OR REPLACE INTO script_pubkeys SELECT keychain, child, script FROM script_pubkeys_old;",
|
||||
"DROP TABLE script_pubkeys_old;"
|
||||
];
|
||||
|
||||
/// Sqlite database stored on filesystem
|
||||
@@ -49,7 +77,7 @@ static MIGRATIONS: &[&str] = &[
|
||||
#[derive(Debug)]
|
||||
pub struct SqliteDatabase {
|
||||
/// Path on the local filesystem to store the sqlite file
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
/// A rusqlite connection object to the sqlite database
|
||||
pub connection: Connection,
|
||||
}
|
||||
@@ -57,9 +85,12 @@ pub struct SqliteDatabase {
|
||||
impl SqliteDatabase {
|
||||
/// Instantiate a new SqliteDatabase instance by creating a connection
|
||||
/// 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();
|
||||
SqliteDatabase { path, connection }
|
||||
SqliteDatabase {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
connection,
|
||||
}
|
||||
}
|
||||
fn insert_script_pubkey(
|
||||
&self,
|
||||
@@ -67,7 +98,7 @@ impl SqliteDatabase {
|
||||
child: u32,
|
||||
script: &[u8],
|
||||
) -> Result<i64, Error> {
|
||||
let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
||||
let mut statement = self.connection.prepare_cached("INSERT OR REPLACE INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
||||
statement.execute(named_params! {
|
||||
":keychain": keychain,
|
||||
":child": child,
|
||||
@@ -83,14 +114,16 @@ impl SqliteDatabase {
|
||||
vout: u32,
|
||||
txid: &[u8],
|
||||
script: &[u8],
|
||||
is_spent: bool,
|
||||
) -> Result<i64, Error> {
|
||||
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script) VALUES (:value, :keychain, :vout, :txid, :script)")?;
|
||||
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! {
|
||||
":value": value,
|
||||
":keychain": keychain,
|
||||
":vout": vout,
|
||||
":txid": txid,
|
||||
":script": script
|
||||
":script": script,
|
||||
":is_spent": is_spent,
|
||||
})?;
|
||||
|
||||
Ok(self.connection.last_insert_rowid())
|
||||
@@ -291,7 +324,7 @@ impl SqliteDatabase {
|
||||
fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||
let mut statement = self
|
||||
.connection
|
||||
.prepare_cached("SELECT value, keychain, vout, txid, script FROM utxos")?;
|
||||
.prepare_cached("SELECT value, keychain, vout, txid, script, is_spent FROM utxos")?;
|
||||
let mut utxos: Vec<LocalUtxo> = vec![];
|
||||
let mut rows = statement.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
@@ -300,6 +333,7 @@ impl SqliteDatabase {
|
||||
let vout = row.get(2)?;
|
||||
let txid: Vec<u8> = row.get(3)?;
|
||||
let script: Vec<u8> = row.get(4)?;
|
||||
let is_spent: bool = row.get(5)?;
|
||||
|
||||
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
||||
|
||||
@@ -310,19 +344,16 @@ impl SqliteDatabase {
|
||||
script_pubkey: script.into(),
|
||||
},
|
||||
keychain,
|
||||
is_spent,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(utxos)
|
||||
}
|
||||
|
||||
fn select_utxo_by_outpoint(
|
||||
&self,
|
||||
txid: &[u8],
|
||||
vout: u32,
|
||||
) -> Result<Option<(u64, KeychainKind, Script)>, Error> {
|
||||
fn select_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<Option<LocalUtxo>, Error> {
|
||||
let mut statement = self.connection.prepare_cached(
|
||||
"SELECT value, keychain, script FROM utxos WHERE txid=:txid AND vout=:vout",
|
||||
"SELECT value, keychain, script, is_spent FROM utxos WHERE txid=:txid AND vout=:vout",
|
||||
)?;
|
||||
let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
|
||||
match rows.next()? {
|
||||
@@ -331,9 +362,18 @@ impl SqliteDatabase {
|
||||
let keychain: String = row.get(1)?;
|
||||
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
||||
let script: Vec<u8> = row.get(2)?;
|
||||
let script: Script = script.into();
|
||||
let script_pubkey: Script = script.into();
|
||||
let is_spent: bool = row.get(3)?;
|
||||
|
||||
Ok(Some((value, keychain, script)))
|
||||
Ok(Some(LocalUtxo {
|
||||
outpoint: OutPoint::new(deserialize(txid)?, vout),
|
||||
txout: TxOut {
|
||||
value,
|
||||
script_pubkey,
|
||||
},
|
||||
keychain,
|
||||
is_spent,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
@@ -380,7 +420,7 @@ impl SqliteDatabase {
|
||||
let sent: u64 = row.get(3)?;
|
||||
let fee: Option<u64> = row.get(4)?;
|
||||
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 {
|
||||
Some(raw_tx) => {
|
||||
let tx: Transaction = deserialize(&raw_tx)?;
|
||||
@@ -620,6 +660,7 @@ impl BatchOperations for SqliteDatabase {
|
||||
utxo.outpoint.vout,
|
||||
&utxo.outpoint.txid,
|
||||
utxo.txout.script_pubkey.as_bytes(),
|
||||
utxo.is_spent,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -694,16 +735,9 @@ impl BatchOperations for SqliteDatabase {
|
||||
|
||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
||||
Some((value, keychain, script_pubkey)) => {
|
||||
Some(local_utxo) => {
|
||||
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
|
||||
Ok(Some(LocalUtxo {
|
||||
outpoint: *outpoint,
|
||||
txout: TxOut {
|
||||
value,
|
||||
script_pubkey,
|
||||
},
|
||||
keychain,
|
||||
}))
|
||||
Ok(Some(local_utxo))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
@@ -725,11 +759,13 @@ impl BatchOperations for SqliteDatabase {
|
||||
include_raw: bool,
|
||||
) -> Result<Option<TransactionDetails>, Error> {
|
||||
match self.select_transaction_details_by_txid(txid)? {
|
||||
Some(transaction_details) => {
|
||||
Some(mut transaction_details) => {
|
||||
self.delete_transaction_details_by_txid(txid)?;
|
||||
|
||||
if include_raw {
|
||||
self.delete_transaction_by_txid(txid)?;
|
||||
} else {
|
||||
transaction_details.transaction = None;
|
||||
}
|
||||
Ok(Some(transaction_details))
|
||||
}
|
||||
@@ -832,17 +868,7 @@ impl Database for SqliteDatabase {
|
||||
}
|
||||
|
||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
||||
Some((value, keychain, script_pubkey)) => Ok(Some(LocalUtxo {
|
||||
outpoint: *outpoint,
|
||||
txout: TxOut {
|
||||
value,
|
||||
script_pubkey,
|
||||
},
|
||||
keychain,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)
|
||||
}
|
||||
|
||||
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
@@ -887,10 +913,6 @@ impl Database for SqliteDatabase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for SqliteDatabase {
|
||||
@@ -908,9 +930,9 @@ impl BatchDatabase for SqliteDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_connection(path: &str) -> Result<Connection, Error> {
|
||||
let connection = Connection::open(path)?;
|
||||
migrate(&connection)?;
|
||||
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
|
||||
let mut connection = Connection::open(path)?;
|
||||
migrate(&mut connection)?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
@@ -945,28 +967,41 @@ pub fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<u
|
||||
)
|
||||
}
|
||||
|
||||
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
|
||||
pub fn migrate(conn: &mut Connection) -> Result<(), Error> {
|
||||
let version = get_schema_version(conn)?;
|
||||
let stmts = &MIGRATIONS[(version as usize)..];
|
||||
let mut i: i32 = version;
|
||||
|
||||
if version == MIGRATIONS.len() as i32 {
|
||||
// begin transaction, all migration statements and new schema version commit or rollback
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// execute every statement and return `Some` new schema version
|
||||
// if execution fails, return `Error::Rusqlite`
|
||||
// if no statements executed returns `None`
|
||||
let new_version = stmts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|version_stmt| {
|
||||
log::info!(
|
||||
"executing db migration {}: `{}`",
|
||||
version + version_stmt.0 as i32 + 1,
|
||||
version_stmt.1
|
||||
);
|
||||
tx.execute(version_stmt.1, [])
|
||||
// map result value to next migration version
|
||||
.map(|_| version_stmt.0 as i32 + version + 1)
|
||||
})
|
||||
.last()
|
||||
.transpose()?;
|
||||
|
||||
// if `Some` new statement version, set new schema version
|
||||
if let Some(version) = new_version {
|
||||
set_schema_version(&tx, version)?;
|
||||
} else {
|
||||
log::info!("db up to date, no migration needed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for stmt in stmts {
|
||||
let res = conn.execute(stmt, []);
|
||||
if res.is_err() {
|
||||
println!("migration failed on:\n{}\n{:?}", stmt, res);
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
set_schema_version(conn, i)?;
|
||||
|
||||
// commit transaction
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1026,4 +1061,89 @@ pub mod test {
|
||||
fn test_sync_time() {
|
||||
crate::database::test::test_sync_time(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_txs() {
|
||||
crate::database::test::test_list_transaction(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_raw_txs() {
|
||||
crate::database::test::test_iter_raw_txs(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_path_from_script_pubkey() {
|
||||
crate::database::test::test_del_path_from_script_pubkey(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_script_pubkeys() {
|
||||
crate::database::test::test_iter_script_pubkeys(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_utxo() {
|
||||
crate::database::test::test_del_utxo(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_raw_tx() {
|
||||
crate::database::test::test_del_raw_tx(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_tx() {
|
||||
crate::database::test::test_del_tx(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_del_last_index() {
|
||||
crate::database::test::test_del_last_index(get_database());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_descriptor_checksum() {
|
||||
crate::database::test::test_check_descriptor_checksum(get_database());
|
||||
}
|
||||
|
||||
// Issue 801: https://github.com/bitcoindevkit/bdk/issues/801
|
||||
#[test]
|
||||
fn test_unique_spks() {
|
||||
use crate::bitcoin::hashes::hex::FromHex;
|
||||
use crate::database::*;
|
||||
|
||||
let mut db = get_database();
|
||||
|
||||
let script = Script::from(
|
||||
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||
);
|
||||
let path = 42;
|
||||
let keychain = KeychainKind::External;
|
||||
|
||||
for _ in 0..100 {
|
||||
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||
}
|
||||
|
||||
let mut statement = db
|
||||
.connection
|
||||
.prepare_cached(
|
||||
"select keychain,child,count(child) from script_pubkeys group by keychain,child;",
|
||||
)
|
||||
.unwrap();
|
||||
let mut rows = statement.query([]).unwrap();
|
||||
while let Some(row) = rows.next().unwrap() {
|
||||
let keychain: String = row.get(0).unwrap();
|
||||
let child: u32 = row.get(1).unwrap();
|
||||
let count: usize = row.get(2).unwrap();
|
||||
|
||||
assert!(
|
||||
count == 1,
|
||||
"keychain={}, child={}, count={}",
|
||||
keychain,
|
||||
child,
|
||||
count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
||||
//! checksum of a descriptor
|
||||
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use crate::descriptor::DescriptorError;
|
||||
|
||||
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
let c0 = c >> 35;
|
||||
@@ -43,15 +41,29 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
/// Computes the checksum bytes of a descriptor.
|
||||
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
|
||||
pub(crate) fn calc_checksum_bytes_internal(
|
||||
mut desc: &str,
|
||||
exclude_hash: bool,
|
||||
) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
for ch in desc.chars() {
|
||||
|
||||
let mut original_checksum = None;
|
||||
if exclude_hash {
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
let pos = INPUT_CHARSET
|
||||
.find(ch)
|
||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
|
||||
.iter()
|
||||
.position(|b| b == ch)
|
||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
|
||||
c = poly_mod(c, pos & 31);
|
||||
cls = cls * 3 + (pos >> 5);
|
||||
clscount += 1;
|
||||
@@ -67,47 +79,102 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||
c ^= 1;
|
||||
|
||||
let mut chars = Vec::with_capacity(8);
|
||||
let mut checksum = [0_u8; 8];
|
||||
for j in 0..8 {
|
||||
chars.push(
|
||||
CHECKSUM_CHARSET
|
||||
.chars()
|
||||
.nth(((c >> (5 * (7 - j))) & 31) as usize)
|
||||
.unwrap(),
|
||||
);
|
||||
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||
}
|
||||
|
||||
Ok(String::from_iter(chars))
|
||||
// if input data already had a checksum, check calculated checksum against original checksum
|
||||
if let Some(original_checksum) = original_checksum {
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
|
||||
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
|
||||
|
||||
/// Compute the checksum bytes of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::descriptor::get_checksum;
|
||||
use crate::descriptor::calc_checksum;
|
||||
|
||||
// test get_checksum() function; it should return the same value as Bitcoin Core
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core
|
||||
#[test]
|
||||
fn test_get_checksum() {
|
||||
fn test_calc_checksum() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
|
||||
assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62");
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
|
||||
assert_eq!(get_checksum(desc).unwrap(), "lasegmfs");
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
}
|
||||
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core even if the
|
||||
// descriptor string includes a checksum hash
|
||||
#[test]
|
||||
fn test_calc_checksum_with_checksum_hash() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
|
||||
assert!(matches!(
|
||||
calc_checksum(desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
|
||||
assert!(matches!(
|
||||
calc_checksum(desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_checksum_invalid_character() {
|
||||
let sparkle_heart = vec![240, 159, 146, 150];
|
||||
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
|
||||
.unwrap()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap();
|
||||
fn test_calc_checksum_invalid_character() {
|
||||
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||
|
||||
assert!(matches!(
|
||||
get_checksum(&invalid_desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
|
||||
calc_checksum(&invalid_desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Derived descriptor keys
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
|
||||
use bitcoin::hashes::hash160;
|
||||
use bitcoin::PublicKey;
|
||||
|
||||
pub use miniscript::{
|
||||
descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript,
|
||||
ScriptContext, Segwitv0,
|
||||
};
|
||||
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
|
||||
|
||||
use crate::wallet::utils::SecpCtx;
|
||||
|
||||
/// Extended [`DescriptorPublicKey`] that has been derived
|
||||
///
|
||||
/// Derived keys are guaranteed to never contain wildcards of any kind
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx);
|
||||
|
||||
impl<'s> DerivedDescriptorKey<'s> {
|
||||
/// Construct a new derived key
|
||||
///
|
||||
/// Panics if the key is wildcard
|
||||
pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> {
|
||||
if let DescriptorPublicKey::XPub(xpub) = &key {
|
||||
assert!(xpub.wildcard == Wildcard::None)
|
||||
}
|
||||
|
||||
DerivedDescriptorKey(key, secp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Deref for DerivedDescriptorKey<'s> {
|
||||
type Target = DescriptorPublicKey;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> PartialEq for DerivedDescriptorKey<'s> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Eq for DerivedDescriptorKey<'s> {}
|
||||
|
||||
impl<'s> PartialOrd for DerivedDescriptorKey<'s> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Ord for DerivedDescriptorKey<'s> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> fmt::Display for DerivedDescriptorKey<'s> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Hash for DerivedDescriptorKey<'s> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
|
||||
type Hash = Self;
|
||||
|
||||
fn to_pubkeyhash(&self) -> Self::Hash {
|
||||
DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1)
|
||||
}
|
||||
|
||||
fn is_uncompressed(&self) -> bool {
|
||||
self.0.is_uncompressed()
|
||||
}
|
||||
fn serialized_len(&self) -> usize {
|
||||
self.0.serialized_len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
||||
fn to_public_key(&self) -> PublicKey {
|
||||
match &self.0 {
|
||||
DescriptorPublicKey::SinglePub(ref spub) => spub.key.to_public_key(),
|
||||
DescriptorPublicKey::XPub(ref xpub) => {
|
||||
xpub.xkey
|
||||
.derive_pub(self.1, &xpub.derivation_path)
|
||||
.expect("Shouldn't fail, only normal derivations")
|
||||
.public_key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash {
|
||||
hash.to_public_key().to_pubkeyhash()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait AsDerived {
|
||||
// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
|
||||
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
|
||||
-> Descriptor<DerivedDescriptorKey<'s>>;
|
||||
|
||||
// Transform the keys into `DerivedDescriptorKey`.
|
||||
//
|
||||
// Panics if the descriptor is not "fixed", i.e. if it's derivable
|
||||
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
|
||||
}
|
||||
|
||||
impl AsDerived for Descriptor<DescriptorPublicKey> {
|
||||
fn as_derived<'s>(
|
||||
&self,
|
||||
index: u32,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Descriptor<DerivedDescriptorKey<'s>> {
|
||||
self.derive(index).translate_pk_infallible(
|
||||
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
||||
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
||||
)
|
||||
}
|
||||
|
||||
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>> {
|
||||
assert!(!self.is_deriveable());
|
||||
|
||||
self.as_derived(0, secp)
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
#[macro_export]
|
||||
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)]
|
||||
#[macro_export]
|
||||
macro_rules! apply_modifier {
|
||||
@@ -336,7 +434,7 @@ macro_rules! apply_modifier {
|
||||
/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
|
||||
/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
|
||||
///
|
||||
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sdv:older(...)))`
|
||||
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))`
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
@@ -349,7 +447,7 @@ macro_rules! apply_modifier {
|
||||
///
|
||||
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
|
||||
/// wsh (
|
||||
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:d:v:older(my_timelock))
|
||||
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
|
||||
/// )
|
||||
/// }?;
|
||||
///
|
||||
@@ -357,7 +455,7 @@ macro_rules! apply_modifier {
|
||||
/// let b_items = vec![
|
||||
/// bdk::fragment!(pk(my_key_1))?,
|
||||
/// bdk::fragment!(s:pk(my_key_2))?,
|
||||
/// bdk::fragment!(s:d:v:older(my_timelock))?,
|
||||
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
|
||||
/// ];
|
||||
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||
///
|
||||
@@ -441,6 +539,15 @@ macro_rules! descriptor {
|
||||
( wsh ( $( $minisc:tt )* ) ) => ({
|
||||
$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)]
|
||||
@@ -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)]
|
||||
#[macro_export]
|
||||
macro_rules! fragment_internal {
|
||||
@@ -576,10 +700,10 @@ macro_rules! fragment {
|
||||
$crate::keys::make_pkh($key, &secp)
|
||||
});
|
||||
( after ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(After, $value)
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302
|
||||
});
|
||||
( older ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Older, $value)
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||
});
|
||||
( sha256 ( $hash:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
||||
@@ -640,21 +764,22 @@ macro_rules! fragment {
|
||||
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
|
||||
});
|
||||
( 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 keys = vec![
|
||||
$(
|
||||
$key.into_descriptor_key(),
|
||||
)*
|
||||
];
|
||||
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
|
||||
});
|
||||
( 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<_>, _>>()
|
||||
.map_err($crate::descriptor::DescriptorError::Key)
|
||||
.and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp))
|
||||
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $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
|
||||
@@ -670,7 +795,7 @@ macro_rules! fragment {
|
||||
mod test {
|
||||
use bitcoin::hashes::hex::ToHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::{Descriptor, Legacy, Segwitv0};
|
||||
|
||||
use std::str::FromStr;
|
||||
@@ -681,8 +806,6 @@ mod test {
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::PrivateKey;
|
||||
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
|
||||
// test the descriptor!() macro
|
||||
|
||||
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
|
||||
@@ -692,17 +815,15 @@ mod test {
|
||||
is_fixed: bool,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||
for i in 0..expected.len() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
desc.at_derivation_index(index)
|
||||
};
|
||||
let address = child_desc.address(Regtest);
|
||||
if let Ok(address) = address {
|
||||
@@ -714,7 +835,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()
|
||||
|
||||
// expected script for pk and bare manually created
|
||||
@@ -1048,13 +1169,15 @@ mod test {
|
||||
let private_key =
|
||||
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
||||
let (descriptor, _, _) =
|
||||
descriptor!(wsh(thresh(2,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
||||
descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
||||
|
||||
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#cfdcqs3s")
|
||||
assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
|
||||
#[should_panic(
|
||||
expected = "Miniscript(ContextError(CompressedOnly(\"04b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a87378ec38ff91d43e8c2092ebda601780485263da089465619e0358a5c1be7ac91f4\")))"
|
||||
)]
|
||||
fn test_dsl_miniscript_checks() {
|
||||
let mut uncompressed_pk =
|
||||
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
||||
@@ -1062,4 +1185,35 @@ mod test {
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,14 @@ pub enum Error {
|
||||
InvalidDescriptorChecksum,
|
||||
/// The descriptor contains hardened derivation steps on public extended keys
|
||||
HardenedDerivationXpub,
|
||||
/// The descriptor contains multiple keys with the same BIP32 fingerprint
|
||||
DuplicatedKeys,
|
||||
|
||||
/// Error thrown while working with [`keys`](crate::keys)
|
||||
Key(crate::keys::KeyError),
|
||||
/// Error while extracting and manipulating policies
|
||||
Policy(crate::descriptor::policy::PolicyError),
|
||||
|
||||
/// Invalid character found in the descriptor checksum
|
||||
InvalidDescriptorCharacter(char),
|
||||
/// Invalid byte found in the descriptor checksum
|
||||
InvalidDescriptorCharacter(u8),
|
||||
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
|
||||
@@ -14,32 +14,33 @@
|
||||
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
||||
//! from [`miniscript`].
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||
use bitcoin::util::psbt;
|
||||
use bitcoin::{Network, PublicKey, Script, TxOut};
|
||||
use bitcoin::util::{psbt, taproot};
|
||||
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::{Network, TxOut};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard,
|
||||
DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey,
|
||||
};
|
||||
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
pub use miniscript::{
|
||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
};
|
||||
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
|
||||
pub mod checksum;
|
||||
pub(crate) mod derived;
|
||||
#[doc(hidden)]
|
||||
pub mod dsl;
|
||||
pub mod error;
|
||||
pub mod policy;
|
||||
pub mod template;
|
||||
|
||||
pub use self::checksum::get_checksum;
|
||||
use self::derived::AsDerived;
|
||||
pub use self::derived::DerivedDescriptorKey;
|
||||
pub use self::checksum::calc_checksum;
|
||||
use self::checksum::calc_checksum_bytes;
|
||||
pub use self::error::Error as DescriptorError;
|
||||
pub use self::policy::Policy;
|
||||
use self::template::DescriptorTemplateOut;
|
||||
@@ -51,14 +52,21 @@ use crate::wallet::utils::SecpCtx;
|
||||
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
||||
|
||||
/// Alias for a [`Descriptor`] that contains extended **derived** keys
|
||||
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
||||
pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
|
||||
|
||||
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
||||
/// [`psbt::Output`]
|
||||
///
|
||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||
/// [`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`]
|
||||
pub trait IntoWalletDescriptor {
|
||||
@@ -76,19 +84,15 @@ impl IntoWalletDescriptor for &str {
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let descriptor = if self.contains('#') {
|
||||
let parts: Vec<&str> = self.splitn(2, '#').collect();
|
||||
if !get_checksum(parts[0])
|
||||
.ok()
|
||||
.map(|computed| computed == parts[1])
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
let descriptor = match self.split_once('#') {
|
||||
Some((desc, original_checksum)) => {
|
||||
let checksum = calc_checksum_bytes(desc)?;
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
desc
|
||||
}
|
||||
|
||||
parts[0]
|
||||
} else {
|
||||
self
|
||||
None => self,
|
||||
};
|
||||
|
||||
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
|
||||
@@ -124,28 +128,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
use crate::keys::DescriptorKey;
|
||||
|
||||
let check_key = |pk: &DescriptorPublicKey| {
|
||||
let (pk, _, networks) = if self.0.is_witness() {
|
||||
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(secp)?
|
||||
} else {
|
||||
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(secp)?
|
||||
};
|
||||
struct Translator<'s, 'd> {
|
||||
secp: &'s SecpCtx,
|
||||
descriptor: &'d ExtendedDescriptor,
|
||||
network: Network,
|
||||
}
|
||||
|
||||
if networks.contains(&network) {
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
impl<'s, 'd>
|
||||
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
|
||||
for Translator<'s, 'd>
|
||||
{
|
||||
fn pk(
|
||||
&mut self,
|
||||
pk: &DescriptorPublicKey,
|
||||
) -> Result<miniscript::DummyKey, DescriptorError> {
|
||||
let secp = &self.secp;
|
||||
|
||||
let (_, _, networks) = if self.descriptor.is_taproot() {
|
||||
let descriptor_key: DescriptorKey<miniscript::Tap> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
} else if self.descriptor.is_witness() {
|
||||
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
} else {
|
||||
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
};
|
||||
|
||||
if networks.contains(&self.network) {
|
||||
Ok(miniscript::DummyKey)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
}
|
||||
};
|
||||
fn sha256(
|
||||
&mut self,
|
||||
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
|
||||
) -> Result<miniscript::DummySha256Hash, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash256(
|
||||
&mut self,
|
||||
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
|
||||
) -> Result<miniscript::DummyHash256Hash, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn ripemd160(
|
||||
&mut self,
|
||||
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
|
||||
) -> Result<miniscript::DummyRipemd160Hash, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash160(
|
||||
&mut self,
|
||||
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
|
||||
) -> Result<miniscript::DummyHash160Hash, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
// check the network for the keys
|
||||
let translated = self.0.translate_pk(check_key, check_key)?;
|
||||
self.0.translate_pk(&mut Translator {
|
||||
secp,
|
||||
network,
|
||||
descriptor: &self.0,
|
||||
})?;
|
||||
|
||||
Ok((translated, self.1))
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +207,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
_secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let valid_networks = &self.2;
|
||||
struct Translator {
|
||||
network: Network,
|
||||
}
|
||||
|
||||
let fix_key = |pk: &DescriptorPublicKey| {
|
||||
if valid_networks.contains(&network) {
|
||||
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
|
||||
for Translator
|
||||
{
|
||||
fn pk(
|
||||
&mut self,
|
||||
pk: &DescriptorPublicKey,
|
||||
) -> Result<DescriptorPublicKey, DescriptorError> {
|
||||
// workaround for xpubs generated by other key types, like bip39: since when the
|
||||
// conversion is made one network has to be chosen, what we generally choose
|
||||
// "mainnet", but then override the set of valid networks to specify that all of
|
||||
@@ -167,7 +226,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
let pk = match pk {
|
||||
DescriptorPublicKey::XPub(ref xpub) => {
|
||||
let mut xpub = xpub.clone();
|
||||
xpub.xkey.network = network;
|
||||
xpub.xkey.network = self.network;
|
||||
|
||||
DescriptorPublicKey::XPub(xpub)
|
||||
}
|
||||
@@ -175,15 +234,42 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
};
|
||||
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
};
|
||||
miniscript::translate_hash_clone!(
|
||||
DescriptorPublicKey,
|
||||
DescriptorPublicKey,
|
||||
DescriptorError
|
||||
);
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it
|
||||
let translated = self.0.translate_pk(fix_key, fix_key)?;
|
||||
let (desc, keymap, networks) = self;
|
||||
|
||||
Ok((translated, self.1))
|
||||
if !networks.contains(&network) {
|
||||
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it in the descriptor
|
||||
let translated = desc.translate_pk(&mut Translator { network })?;
|
||||
// ...and in the key map
|
||||
let fixed_keymap = keymap
|
||||
.into_iter()
|
||||
.map(|(mut k, mut v)| {
|
||||
match (&mut k, &mut v) {
|
||||
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
|
||||
xpub.xkey.network = network;
|
||||
xprv.xkey.network = network;
|
||||
}
|
||||
(_, DescriptorSecretKey::Single(key)) => {
|
||||
key.key.network = network;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(k, v)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((translated, fixed_keymap))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +288,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
derivation_path,
|
||||
wildcard,
|
||||
..
|
||||
}) = k.as_key()
|
||||
}) = k
|
||||
{
|
||||
return *wildcard == Wildcard::Hardened
|
||||
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
||||
@@ -214,23 +300,9 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
return Err(DescriptorError::HardenedDerivationXpub);
|
||||
}
|
||||
|
||||
// Ensure that there are no duplicated keys
|
||||
let mut found_keys = HashSet::new();
|
||||
let descriptor_contains_duplicated_keys = descriptor.for_any_key(|k| {
|
||||
if let DescriptorPublicKey::XPub(xkey) = k.as_key() {
|
||||
let fingerprint = xkey.root_fingerprint(secp);
|
||||
if found_keys.contains(&fingerprint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
found_keys.insert(fingerprint);
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
if descriptor_contains_duplicated_keys {
|
||||
return Err(DescriptorError::DuplicatedKeys);
|
||||
}
|
||||
// Run miniscript's sanity check, which will look for duplicated keys and other potential
|
||||
// issues
|
||||
descriptor.sanity_check()?;
|
||||
|
||||
Ok((descriptor, keymap))
|
||||
}
|
||||
@@ -263,7 +335,6 @@ pub trait ExtractPolicy {
|
||||
}
|
||||
|
||||
pub(crate) trait XKeyUtils {
|
||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||
}
|
||||
|
||||
@@ -271,27 +342,6 @@ impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
{
|
||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||
let full_path = match self.origin {
|
||||
Some((_, ref path)) => path
|
||||
.into_iter()
|
||||
.chain(self.derivation_path.into_iter())
|
||||
.cloned()
|
||||
.collect(),
|
||||
None => self.derivation_path.clone(),
|
||||
};
|
||||
|
||||
if self.wildcard != Wildcard::None {
|
||||
full_path
|
||||
.into_iter()
|
||||
.chain(append.iter())
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
full_path
|
||||
}
|
||||
}
|
||||
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
@@ -300,53 +350,31 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DerivedDescriptorMeta {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError>;
|
||||
}
|
||||
|
||||
pub(crate) trait DescriptorMeta {
|
||||
fn is_witness(&self) -> bool;
|
||||
fn is_taproot(&self) -> bool;
|
||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_tap_key_origins<'s>(
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_input<'s>(
|
||||
&self,
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
}
|
||||
|
||||
pub(crate) trait DescriptorScripts {
|
||||
fn psbt_redeem_script(&self) -> Option<Script>;
|
||||
fn psbt_witness_script(&self) -> Option<Script>;
|
||||
}
|
||||
|
||||
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
||||
fn psbt_redeem_script(&self) -> Option<Script> {
|
||||
match self.desc_type() {
|
||||
DescriptorType::ShWpkh => Some(self.explicit_script()),
|
||||
DescriptorType::ShWsh => Some(self.explicit_script().to_v0_p2wsh()),
|
||||
DescriptorType::Sh => Some(self.explicit_script()),
|
||||
DescriptorType::Bare => Some(self.explicit_script()),
|
||||
DescriptorType::ShSortedMulti => Some(self.explicit_script()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn psbt_witness_script(&self) -> Option<Script> {
|
||||
match self.desc_type() {
|
||||
DescriptorType::Wsh => Some(self.explicit_script()),
|
||||
DescriptorType::ShWsh => Some(self.explicit_script()),
|
||||
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
||||
Some(self.explicit_script())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
) -> Option<DerivedDescriptor>;
|
||||
}
|
||||
|
||||
impl DescriptorMeta for ExtendedDescriptor {
|
||||
@@ -362,11 +390,15 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_taproot(&self) -> bool {
|
||||
self.desc_type() == DescriptorType::Tr
|
||||
}
|
||||
|
||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
self.for_each_key(|pk| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk {
|
||||
answer.push(xpub.clone());
|
||||
}
|
||||
|
||||
@@ -376,59 +408,122 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// 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;
|
||||
self.for_each_key(|key| {
|
||||
if path_found.is_some() {
|
||||
// already found a matching path, we are done
|
||||
return true;
|
||||
}
|
||||
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||
// Check if the key matches one entry in our `index`. If it does, `matches()` will
|
||||
// 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 {
|
||||
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
|
||||
// return the "prefix" that matched, so we remove that prefix from the full path
|
||||
// found in `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.
|
||||
let root_fingerprint = xpub.root_fingerprint(secp);
|
||||
let derivation_path: Option<Vec<ChildNumber>> = index
|
||||
let derive_path = key_origins
|
||||
.get_key_value(&root_fingerprint)
|
||||
.and_then(|(fingerprint, path)| {
|
||||
xpub.matches(&(**fingerprint, (*path).clone()), secp)
|
||||
.and_then(|(fingerprint, (path, expected))| {
|
||||
xpub.matches(&(*fingerprint, (*path).clone()), secp)
|
||||
.zip(Some((path, expected)))
|
||||
})
|
||||
.map(|prefix| {
|
||||
index
|
||||
.get(&xpub.root_fingerprint(secp))
|
||||
.unwrap()
|
||||
.and_then(|(prefix, (full_path, expected))| {
|
||||
let derive_path = full_path
|
||||
.into_iter()
|
||||
.skip(prefix.into_iter().count())
|
||||
.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 => {
|
||||
// Ignore hardened wildcards
|
||||
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() => {
|
||||
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.at_derivation_index(path))
|
||||
}
|
||||
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "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> {
|
||||
// "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>(
|
||||
@@ -436,19 +531,25 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
) -> Option<DerivedDescriptor> {
|
||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if self.is_deriveable() {
|
||||
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if self.has_wildcard() {
|
||||
// We can't try to bruteforce the derivation index, exit here
|
||||
return None;
|
||||
}
|
||||
|
||||
let descriptor = self.as_derived_fixed(secp);
|
||||
let descriptor = self.at_derivation_index(0);
|
||||
match descriptor.desc_type() {
|
||||
// TODO: add pk() here
|
||||
DescriptorType::Pkh | DescriptorType::Wpkh | DescriptorType::ShWpkh
|
||||
DescriptorType::Pkh
|
||||
| DescriptorType::Wpkh
|
||||
| DescriptorType::ShWpkh
|
||||
| DescriptorType::Tr
|
||||
if utxo.is_some()
|
||||
&& descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
|
||||
{
|
||||
@@ -456,7 +557,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
}
|
||||
DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
|
||||
if psbt_input.redeem_script.is_some()
|
||||
&& &descriptor.explicit_script()
|
||||
&& &descriptor.explicit_script().unwrap()
|
||||
== psbt_input.redeem_script.as_ref().unwrap() =>
|
||||
{
|
||||
Some(descriptor)
|
||||
@@ -466,7 +567,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
| DescriptorType::ShWshSortedMulti
|
||||
| DescriptorType::WshSortedMulti
|
||||
if psbt_input.witness_script.is_some()
|
||||
&& &descriptor.explicit_script()
|
||||
&& &descriptor.explicit_script().unwrap()
|
||||
== psbt_input.witness_script.as_ref().unwrap() =>
|
||||
{
|
||||
Some(descriptor)
|
||||
@@ -476,29 +577,6 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError> {
|
||||
let mut answer = BTreeMap::new();
|
||||
self.for_each_key(|key| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||
let derived_pubkey = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, &xpub.derivation_path)
|
||||
.expect("Derivation can't fail");
|
||||
|
||||
answer.insert(
|
||||
derived_pubkey.public_key,
|
||||
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
Ok(answer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
@@ -507,6 +585,7 @@ mod test {
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::{bip32, psbt};
|
||||
use bitcoin::Script;
|
||||
|
||||
use super::*;
|
||||
use crate::psbt::PsbtUtils;
|
||||
@@ -625,23 +704,40 @@ mod test {
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||
// we are using an "xpub"
|
||||
let key = (xpub, path).into_descriptor_key().unwrap();
|
||||
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
// override it with any. this happens in some key conversions, like bip39
|
||||
let key = key.override_valid_networks(any_network());
|
||||
|
||||
// make a descriptor out of it
|
||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||
// this should convert the key that supports "any_network" to the right network (testnet)
|
||||
let (wallet_desc, _) = desc
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
|
||||
let mut xprv_testnet = xprv;
|
||||
xprv_testnet.network = Network::Testnet;
|
||||
|
||||
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
||||
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||
xkey: xpub_testnet,
|
||||
origin: None,
|
||||
derivation_path: path,
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
|
||||
assert_eq!(
|
||||
keymap
|
||||
.get(&desc_pubkey)
|
||||
.map(|key| key.to_public(&secp).unwrap()),
|
||||
Some(desc_pubkey)
|
||||
);
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
||||
@@ -761,13 +857,32 @@ mod test {
|
||||
DescriptorError::HardenedDerivationXpub
|
||||
));
|
||||
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
DescriptorError::DuplicatedKeys
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh_wsh_sortedmulti_redeemscript() {
|
||||
use miniscript::psbt::PsbtInputExt;
|
||||
|
||||
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.at_derivation_index(0);
|
||||
|
||||
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||
|
||||
let mut psbt_input = psbt::Input::default();
|
||||
psbt_input
|
||||
.update_with_descriptor_unchecked(&descriptor)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
|
||||
assert_eq!(psbt_input.witness_script, Some(script));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,18 +40,19 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk::miniscript::Legacy;
|
||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bitcoin::Network;
|
||||
///
|
||||
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(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))?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait DescriptorTemplate {
|
||||
/// 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
|
||||
@@ -62,7 +63,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
self.build()?.into_wallet_descriptor(secp, network)
|
||||
self.build(network)?.into_wallet_descriptor(secp, network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Pkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
@@ -95,7 +96,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub 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))
|
||||
}
|
||||
}
|
||||
@@ -113,7 +114,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Wpkh_P2Sh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
@@ -130,7 +131,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub 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)))
|
||||
}
|
||||
}
|
||||
@@ -148,7 +149,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Wpkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
@@ -164,12 +165,12 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub 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))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
///
|
||||
@@ -186,28 +187,28 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// use bdk::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip44(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@@ -226,7 +227,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
@@ -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);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
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`).
|
||||
///
|
||||
@@ -262,22 +263,22 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// use bdk::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip49(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// 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.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// 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>>(())
|
||||
/// ```
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +303,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
@@ -310,18 +311,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
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`).
|
||||
///
|
||||
@@ -338,22 +339,22 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// use bdk::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip84(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +379,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// let wallet = Wallet::new(
|
||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
@@ -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);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
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,
|
||||
key: K,
|
||||
keychain: KeychainKind,
|
||||
network: Network,
|
||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||
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(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)?);
|
||||
|
||||
match keychain {
|
||||
@@ -458,14 +468,46 @@ mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
#[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)
|
||||
fn check(
|
||||
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
|
||||
@@ -473,17 +515,15 @@ mod test {
|
||||
is_fixed: bool,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||
for i in 0..expected.len() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
desc.at_derivation_index(index)
|
||||
};
|
||||
let address = child_desc.address(Regtest).unwrap();
|
||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||
@@ -497,7 +537,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Pkh(prvkey).build(),
|
||||
P2Pkh(prvkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||
@@ -508,7 +548,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Pkh(pubkey).build(),
|
||||
P2Pkh(pubkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||
@@ -522,7 +562,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh_P2Sh(prvkey).build(),
|
||||
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||
@@ -533,7 +573,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh_P2Sh(pubkey).build(),
|
||||
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||
@@ -547,7 +587,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh(prvkey).build(),
|
||||
P2Wpkh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||
@@ -558,7 +598,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh(pubkey).build(),
|
||||
P2Wpkh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||
@@ -570,7 +610,7 @@ mod test {
|
||||
fn test_bip44_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::External).build(),
|
||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -580,7 +620,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::Internal).build(),
|
||||
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -597,7 +637,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -607,7 +647,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -623,7 +663,7 @@ mod test {
|
||||
fn test_bip49_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::External).build(),
|
||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -633,7 +673,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::Internal).build(),
|
||||
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -650,7 +690,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -660,7 +700,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -676,7 +716,7 @@ mod test {
|
||||
fn test_bip84_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::External).build(),
|
||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -686,7 +726,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::Internal).build(),
|
||||
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -703,7 +743,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -713,7 +753,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
#[doc(include = "../README.md")]
|
||||
#[doc = include_str!("../README.md")]
|
||||
#[cfg(doctest)]
|
||||
pub struct ReadmeDoctests;
|
||||
|
||||
32
src/error.rs
32
src/error.rs
@@ -12,8 +12,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::bitcoin::Network;
|
||||
use crate::{descriptor, wallet, wallet::address_validator};
|
||||
use bitcoin::OutPoint;
|
||||
use crate::{descriptor, wallet};
|
||||
use bitcoin::{OutPoint, Txid};
|
||||
|
||||
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||
#[derive(Debug)]
|
||||
@@ -99,12 +99,12 @@ pub enum Error {
|
||||
|
||||
/// Error related to the parsing and usage of descriptors
|
||||
Descriptor(crate::descriptor::error::Error),
|
||||
/// Error that can be returned to fail the validation of an address
|
||||
AddressValidator(crate::wallet::address_validator::AddressValidatorError),
|
||||
/// Encoding error
|
||||
Encode(bitcoin::consensus::encode::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
/// An ECDSA error
|
||||
@@ -125,6 +125,10 @@ pub enum Error {
|
||||
//DifferentDescriptorStructure,
|
||||
//Uncapable(crate::blockchain::Capability),
|
||||
//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")]
|
||||
/// Electrum client error
|
||||
Electrum(electrum_client::Error),
|
||||
@@ -145,6 +149,24 @@ pub enum Error {
|
||||
Rusqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
/// Errors returned by miniscript when updating inconsistent PSBTs
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MiniscriptPsbtError {
|
||||
Conversion(miniscript::descriptor::ConversionError),
|
||||
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
|
||||
OutputUpdate(miniscript::psbt::OutputUpdateError),
|
||||
}
|
||||
|
||||
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
||||
/// on cached `scriptPubKey`s.
|
||||
#[derive(Debug)]
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
@@ -167,7 +189,6 @@ macro_rules! impl_error {
|
||||
}
|
||||
|
||||
impl_error!(descriptor::error::Error, Descriptor);
|
||||
impl_error!(address_validator::AddressValidatorError, AddressValidator);
|
||||
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
|
||||
impl_error!(wallet::signer::SignerError, Signer);
|
||||
|
||||
@@ -184,6 +205,7 @@ impl From<crate::keys::KeyError> for Error {
|
||||
|
||||
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
||||
impl_error!(miniscript::Error, Miniscript);
|
||||
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||
impl_error!(serde_json::Error, Json);
|
||||
|
||||
@@ -19,7 +19,7 @@ use bitcoin::Network;
|
||||
|
||||
use miniscript::ScriptContext;
|
||||
|
||||
pub use bip39::{Language, Mnemonic};
|
||||
pub use bip39::{Error, Language, Mnemonic};
|
||||
|
||||
type Seed = [u8; 64];
|
||||
|
||||
@@ -94,6 +94,23 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>) {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
let (mnemonic, passphrase) = self;
|
||||
(mnemonic.into_key(), passphrase).into_extended_key()
|
||||
}
|
||||
|
||||
fn into_descriptor_key(
|
||||
self,
|
||||
source: Option<bip32::KeySource>,
|
||||
derivation_path: bip32::DerivationPath,
|
||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let (mnemonic, passphrase) = self;
|
||||
(mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
|
||||
103
src/keys/mod.rs
103
src/keys/mod.rs
@@ -20,11 +20,11 @@ use std::str::FromStr;
|
||||
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::{Network, PrivateKey, PublicKey};
|
||||
use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
|
||||
|
||||
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||
pub use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
||||
DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
|
||||
SortedMultiVec,
|
||||
};
|
||||
pub use miniscript::ScriptContext;
|
||||
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
let mut key_map = KeyMap::with_capacity(1);
|
||||
|
||||
let public = secret
|
||||
.as_public(secp)
|
||||
.to_public(secp)
|
||||
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
|
||||
key_map.insert(public.clone(), secret);
|
||||
|
||||
@@ -127,6 +127,8 @@ pub enum ScriptContextEnum {
|
||||
Legacy,
|
||||
/// Segwitv0 scripts
|
||||
Segwitv0,
|
||||
/// Taproot scripts
|
||||
Tap,
|
||||
}
|
||||
|
||||
impl ScriptContextEnum {
|
||||
@@ -139,6 +141,11 @@ impl ScriptContextEnum {
|
||||
pub fn is_segwit_v0(&self) -> bool {
|
||||
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
|
||||
@@ -155,6 +162,11 @@ pub trait ExtScriptContext: ScriptContext {
|
||||
fn is_segwit_v0() -> bool {
|
||||
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 {
|
||||
@@ -162,6 +174,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
match TypeId::of::<Ctx>() {
|
||||
t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
|
||||
t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
|
||||
t if t == TypeId::of::<miniscript::Tap>() => ScriptContextEnum::Tap,
|
||||
_ => unimplemented!("Unknown ScriptContext type"),
|
||||
}
|
||||
}
|
||||
@@ -211,8 +224,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||
/// IntoDescriptorKey, KeyError, ScriptContext,
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||
/// ScriptContext, SinglePub, SinglePubKey,
|
||||
/// };
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
@@ -222,9 +235,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
/// Ok(DescriptorKey::from_public(
|
||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
/// DescriptorPublicKey::Single(SinglePub {
|
||||
/// origin: None,
|
||||
/// key: self.pubkey,
|
||||
/// key: SinglePubKey::FullKey(self.pubkey),
|
||||
/// }),
|
||||
/// mainnet_network(),
|
||||
/// ))
|
||||
@@ -319,7 +332,6 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
match self {
|
||||
ExtendedKey::Private((mut xprv, _)) => {
|
||||
xprv.network = network;
|
||||
xprv.private_key.network = network;
|
||||
Some(xprv)
|
||||
}
|
||||
ExtendedKey::Public(_) => None,
|
||||
@@ -334,7 +346,7 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
secp: &Secp256k1<C>,
|
||||
) -> bip32::ExtendedPubKey {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -388,7 +400,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// network: self.network,
|
||||
/// depth: 0,
|
||||
/// 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()),
|
||||
/// 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
|
||||
/// depth: 0,
|
||||
/// 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()),
|
||||
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||
/// };
|
||||
@@ -548,6 +560,16 @@ impl<K, Ctx: ScriptContext> Deref for GeneratedKey<K, Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone, Ctx: ScriptContext> Clone for GeneratedKey<K, Ctx> {
|
||||
fn clone(&self) -> GeneratedKey<K, Ctx> {
|
||||
GeneratedKey {
|
||||
key: self.key.clone(),
|
||||
valid_networks: self.valid_networks.clone(),
|
||||
phantom: self.phantom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
|
||||
// right `valid_networks`.
|
||||
impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
|
||||
@@ -688,11 +710,11 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
// 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 {
|
||||
compressed: options.compressed,
|
||||
network: Network::Bitcoin,
|
||||
key,
|
||||
inner,
|
||||
};
|
||||
|
||||
Ok(GeneratedKey::new(private_key, any_network()))
|
||||
@@ -770,13 +792,18 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
|
||||
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
||||
#[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,
|
||||
variant: V,
|
||||
pks: Vec<Pk>,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
|
||||
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()?;
|
||||
|
||||
@@ -815,7 +842,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match self {
|
||||
DescriptorPublicKey::SinglePub(_) => any_network(),
|
||||
DescriptorPublicKey::Single(_) => any_network(),
|
||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||
if xkey.network == Network::Bitcoin =>
|
||||
{
|
||||
@@ -830,8 +857,18 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
key: self,
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
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::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(self),
|
||||
origin: None,
|
||||
})
|
||||
.into_descriptor_key()
|
||||
@@ -841,7 +878,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match &self {
|
||||
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
||||
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
|
||||
mainnet_network()
|
||||
}
|
||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
||||
@@ -866,7 +903,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self,
|
||||
origin: None,
|
||||
})
|
||||
@@ -933,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")]
|
||||
#[test]
|
||||
fn test_keys_wif_network_bip39() {
|
||||
@@ -967,8 +981,7 @@ pub mod test {
|
||||
.into_extended_key()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
56
src/lib.rs
56
src/lib.rs
@@ -43,32 +43,29 @@
|
||||
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.16.0"
|
||||
//! ```
|
||||
//! # Examples
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
doc = r##"
|
||||
## Sync the balance of a descriptor
|
||||
|
||||
### Example
|
||||
```no_run
|
||||
use bdk::Wallet;
|
||||
use bdk::{Wallet, SyncOptions};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::electrum_client::Client;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
let blockchain = ElectrumBlockchain::from(client);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||
|
||||
@@ -87,7 +84,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
//! use bdk::wallet::AddressIndex::New;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! let wallet = Wallet::new(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
//! bitcoin::Network::Testnet,
|
||||
@@ -106,11 +103,10 @@ fn main() -> Result<(), bdk::Error> {
|
||||
doc = r##"
|
||||
## Create a transaction
|
||||
|
||||
### Example
|
||||
```no_run
|
||||
use bdk::{FeeRate, Wallet};
|
||||
use bdk::{FeeRate, Wallet, SyncOptions};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::electrum_client::Client;
|
||||
|
||||
use bitcoin::consensus::serialize;
|
||||
@@ -123,10 +119,10 @@ fn main() -> Result<(), bdk::Error> {
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
let blockchain = ElectrumBlockchain::from(client);
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
let send_to = wallet.get_address(New)?;
|
||||
let (psbt, details) = {
|
||||
@@ -150,7 +146,6 @@ fn main() -> Result<(), bdk::Error> {
|
||||
//!
|
||||
//! ## Sign a transaction
|
||||
//!
|
||||
//! ### Example
|
||||
//! ```no_run
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
@@ -160,7 +155,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! let wallet = Wallet::new(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
|
||||
//! bitcoin::Network::Testnet,
|
||||
@@ -192,7 +187,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
//! * `async-interface`: async functions in bdk traits
|
||||
//! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys
|
||||
//!
|
||||
//! ## Internal features
|
||||
//! # Internal features
|
||||
//!
|
||||
//! These features do not expose any new API, but influence internal implementation aspects of
|
||||
//! BDK.
|
||||
@@ -208,6 +203,8 @@ pub extern crate miniscript;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
pub extern crate hwi;
|
||||
|
||||
#[cfg(all(feature = "reqwest", feature = "ureq"))]
|
||||
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
|
||||
@@ -236,21 +233,29 @@ extern crate async_trait;
|
||||
#[macro_use]
|
||||
extern crate bdk_macros;
|
||||
|
||||
#[cfg(feature = "compact_filters")]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
pub extern crate bitcoincore_rpc;
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
pub extern crate electrum_client;
|
||||
|
||||
#[cfg(feature = "esplora")]
|
||||
pub extern crate esplora_client;
|
||||
|
||||
#[cfg(feature = "key-value-db")]
|
||||
pub extern crate sled;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
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)]
|
||||
#[macro_use]
|
||||
pub(crate) mod error;
|
||||
@@ -260,7 +265,7 @@ pub mod descriptor;
|
||||
#[cfg(feature = "test-md-docs")]
|
||||
mod doctest;
|
||||
pub mod keys;
|
||||
pub(crate) mod psbt;
|
||||
pub mod psbt;
|
||||
pub(crate) mod types;
|
||||
pub mod wallet;
|
||||
|
||||
@@ -268,20 +273,13 @@ pub use descriptor::template;
|
||||
pub use descriptor::HdKeyPaths;
|
||||
pub use error::Error;
|
||||
pub use types::*;
|
||||
pub use wallet::address_validator;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
pub use wallet::tx_builder::TxBuilder;
|
||||
pub use wallet::SyncOptions;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
/// Get the version of BDK at runtime
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION", "unknown")
|
||||
}
|
||||
|
||||
// We should consider putting this under a feature flag but we need the macro in 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;
|
||||
|
||||
129
src/psbt/mod.rs
129
src/psbt/mod.rs
@@ -9,17 +9,34 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||
|
||||
use crate::FeeRate;
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
// TODO upstream the functions here to `rust-bitcoin`?
|
||||
|
||||
/// Trait to add functions to extract utxos and calculate fees.
|
||||
pub trait PsbtUtils {
|
||||
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||
|
||||
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_amount(&self) -> Option<u64>;
|
||||
|
||||
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
|
||||
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
|
||||
/// transaction.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_rate(&self) -> Option<FeeRate>;
|
||||
}
|
||||
|
||||
impl PsbtUtils for Psbt {
|
||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.global.unsigned_tx;
|
||||
let tx = &self.unsigned_tx;
|
||||
|
||||
if input_index >= tx.input.len() {
|
||||
return None;
|
||||
@@ -37,6 +54,27 @@ impl PsbtUtils for Psbt {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn fee_amount(&self) -> Option<u64> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
|
||||
|
||||
utxos.map(|inputs| {
|
||||
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
|
||||
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
|
||||
input_amount
|
||||
.checked_sub(output_amount)
|
||||
.expect("input amount must be greater than output amount")
|
||||
})
|
||||
}
|
||||
|
||||
fn fee_rate(&self) -> Option<FeeRate> {
|
||||
let fee_amount = self.fee_amount();
|
||||
fee_amount.map(|fee| {
|
||||
let weight = self.clone().extract_tx().weight();
|
||||
FeeRate::from_wu(fee, weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -44,8 +82,9 @@ mod test {
|
||||
use crate::bitcoin::TxIn;
|
||||
use crate::psbt::Psbt;
|
||||
use crate::wallet::AddressIndex;
|
||||
use crate::wallet::AddressIndex::New;
|
||||
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
|
||||
use crate::SignOptions;
|
||||
use crate::{psbt, FeeRate, SignOptions};
|
||||
use std::str::FromStr;
|
||||
|
||||
// from bip 174
|
||||
@@ -93,7 +132,7 @@ mod test {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
psbt.global.unsigned_tx.input.push(TxIn::default());
|
||||
psbt.unsigned_tx.input.push(TxIn::default());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
@@ -112,11 +151,89 @@ mod test {
|
||||
|
||||
// add a finalized input
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
psbt.global
|
||||
.unsigned_tx
|
||||
psbt.unsigned_tx
|
||||
.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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
|
||||
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_missing_txout() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (wpkh_wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wpkh_wallet.get_address(New).unwrap();
|
||||
let mut builder = wpkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let (mut wpkh_psbt, _) = builder.finish().unwrap();
|
||||
|
||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||
wpkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
assert!(wpkh_psbt.fee_amount().is_none());
|
||||
assert!(wpkh_psbt.fee_rate().is_none());
|
||||
|
||||
let (pkh_wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = pkh_wallet.get_address(New).unwrap();
|
||||
let mut builder = pkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let (mut pkh_psbt, _) = builder.finish().unwrap();
|
||||
|
||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
assert!(pkh_psbt.fee_amount().is_none());
|
||||
assert!(pkh_psbt.fee_rate().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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")]
|
||||
pub mod blockchain_tests;
|
||||
|
||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||
use bitcoin::{Address, PublicKey, Txid};
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
pub mod configurable_blockchain_tests;
|
||||
|
||||
use miniscript::descriptor::DescriptorPublicKey;
|
||||
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
||||
use bitcoin::{Address, Txid};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingInput {
|
||||
@@ -96,63 +96,26 @@ 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)]
|
||||
#[macro_export]
|
||||
macro_rules! testutils {
|
||||
( @external $descriptors:expr, $child:expr ) => ({
|
||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::testutils::TranslateDescriptor;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
parsed.at_derivation_index($child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @internal $descriptors:expr, $child:expr ) => ({
|
||||
use $crate::bitcoin::secp256k1::Secp256k1;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::testutils::TranslateDescriptor;
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||
parsed.derive_translated(&secp, $child).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
||||
parsed.at_derivation_index($child).address($crate::bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||
@@ -219,49 +182,50 @@ macro_rules! testutils {
|
||||
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use $crate::miniscript::descriptor::Descriptor;
|
||||
use $crate::miniscript::TranslatePk;
|
||||
|
||||
struct Translator {
|
||||
keys: HashMap<&'static str, (String, Option<String>, Option<String>)>,
|
||||
is_internal: bool,
|
||||
}
|
||||
|
||||
impl $crate::miniscript::Translator<String, String, Infallible> for Translator {
|
||||
fn pk(&mut self, pk: &String) -> Result<String, Infallible> {
|
||||
match self.keys.get(pk.as_str()) {
|
||||
Some((key, ext_path, int_path)) => {
|
||||
let path = if self.is_internal { int_path } else { ext_path };
|
||||
Ok(format!("{}{}", key, path.clone().unwrap_or_default()))
|
||||
}
|
||||
None => Ok(pk.clone()),
|
||||
}
|
||||
}
|
||||
fn sha256(&mut self, sha256: &String) -> Result<String, Infallible> { Ok(sha256.clone()) }
|
||||
fn hash256(&mut self, hash256: &String) -> Result<String, Infallible> { Ok(hash256.clone()) }
|
||||
fn ripemd160(&mut self, ripemd160: &String) -> Result<String, Infallible> { Ok(ripemd160.clone()) }
|
||||
fn hash160(&mut self, hash160: &String) -> Result<String, Infallible> { Ok(hash160.clone()) }
|
||||
}
|
||||
|
||||
#[allow(unused_assignments, unused_mut)]
|
||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
||||
let mut keys = HashMap::new();
|
||||
$(
|
||||
keys = testutils!{ @keys $( $keys )* };
|
||||
)*
|
||||
|
||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
let mut translator = Translator { keys, is_internal: false };
|
||||
|
||||
});
|
||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||
let external = external.translate_pk(&mut translator).expect("Infallible conversion");
|
||||
let external = external.to_string();
|
||||
|
||||
let internal = None::<String>$(.or({
|
||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
translator.is_internal = true;
|
||||
|
||||
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
});
|
||||
Some(string_internal.to_string())
|
||||
let internal = None::<String>$(.or({
|
||||
let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
|
||||
Some(internal.to_string())
|
||||
}))?;
|
||||
|
||||
(external, internal)
|
||||
|
||||
171
src/types.rs
171
src/types.rs
@@ -51,14 +51,44 @@ impl AsRef<[u8]> for KeychainKind {
|
||||
pub struct FeeRate(f32);
|
||||
|
||||
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
|
||||
///
|
||||
/// ## 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 {
|
||||
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
|
||||
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
|
||||
@@ -78,7 +108,7 @@ impl FeeRate {
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/vbyte
|
||||
pub fn as_sat_vb(&self) -> f32 {
|
||||
pub fn as_sat_per_vb(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
@@ -89,7 +119,7 @@ impl FeeRate {
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +161,12 @@ pub struct LocalUtxo {
|
||||
pub txout: TxOut,
|
||||
/// Type of keychain
|
||||
pub keychain: KeychainKind,
|
||||
/// Whether this UTXO is spent or not
|
||||
pub is_spent: bool,
|
||||
}
|
||||
|
||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WeightedUtxo {
|
||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
@@ -145,7 +177,7 @@ pub struct WeightedUtxo {
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
@@ -192,7 +224,7 @@ impl Utxo {
|
||||
}
|
||||
|
||||
/// A wallet transaction
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TransactionDetails {
|
||||
/// Optional transaction
|
||||
pub transaction: Option<Transaction>,
|
||||
@@ -200,10 +232,12 @@ pub struct TransactionDetails {
|
||||
pub txid: Txid,
|
||||
|
||||
/// Received value (sats)
|
||||
/// Sum of owned outputs of this transaction.
|
||||
pub received: u64,
|
||||
/// Sent value (sats)
|
||||
/// Sum of owned inputs of this transaction.
|
||||
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
|
||||
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||
/// funds while offline.
|
||||
@@ -238,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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
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();
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Address validation callbacks
|
||||
//!
|
||||
//! The typical usage of those callbacks is for displaying the newly-generated address on a
|
||||
//! hardware wallet, so that the user can cross-check its correctness.
|
||||
//!
|
||||
//! More generally speaking though, these callbacks can also be used to "do something" every time
|
||||
//! an address is generated, without necessarily checking or validating it.
|
||||
//!
|
||||
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
|
||||
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
|
||||
//! whenever a new address is generated (either explicitly by the user with
|
||||
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
|
||||
//! address) all the attached validators will be polled, in sequence. All of them must complete
|
||||
//! successfully to continue.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::address_validator::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! #[derive(Debug)]
|
||||
//! struct PrintAddressAndContinue;
|
||||
//!
|
||||
//! impl AddressValidator for PrintAddressAndContinue {
|
||||
//! fn validate(
|
||||
//! &self,
|
||||
//! keychain: KeychainKind,
|
||||
//! hd_keypaths: &HdKeyPaths,
|
||||
//! script: &Script
|
||||
//! ) -> Result<(), AddressValidatorError> {
|
||||
//! let address = Address::from_script(script, Network::Testnet)
|
||||
//! .as_ref()
|
||||
//! .map(Address::to_string)
|
||||
//! .unwrap_or(script.to_string());
|
||||
//! println!("New address of type {:?}: {}", keychain, address);
|
||||
//! println!("HD keypaths: {:#?}", hd_keypaths);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
|
||||
//!
|
||||
//! let address = wallet.get_address(New)?;
|
||||
//! println!("Address: {}", address);
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use bitcoin::Script;
|
||||
|
||||
use crate::descriptor::HdKeyPaths;
|
||||
use crate::types::KeychainKind;
|
||||
|
||||
/// Errors that can be returned to fail the validation of an address
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AddressValidatorError {
|
||||
/// User rejected the address
|
||||
UserRejected,
|
||||
/// Network connection error
|
||||
ConnectionError,
|
||||
/// Network request timeout error
|
||||
TimeoutError,
|
||||
/// Invalid script
|
||||
InvalidScript,
|
||||
/// A custom error message
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AddressValidatorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AddressValidatorError {}
|
||||
|
||||
/// Trait to build address validators
|
||||
///
|
||||
/// All the address validators attached to a wallet with [`Wallet::add_address_validator`](super::Wallet::add_address_validator) will be polled
|
||||
/// every time an address (external or internal) is generated by the wallet. Errors returned in the
|
||||
/// validator will be propagated up to the original caller that triggered the address generation.
|
||||
///
|
||||
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
||||
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
||||
/// Validate or inspect an address
|
||||
fn validate(
|
||||
&self,
|
||||
keychain: KeychainKind,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
script: &Script,
|
||||
) -> Result<(), AddressValidatorError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::wallet::AddressIndex::New;
|
||||
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestValidator;
|
||||
impl AddressValidator for TestValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
_keychain: KeychainKind,
|
||||
_hd_keypaths: &HdKeyPaths,
|
||||
_script: &bitcoin::Script,
|
||||
) -> Result<(), AddressValidatorError> {
|
||||
Err(AddressValidatorError::InvalidScript)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InvalidScript")]
|
||||
fn test_address_validator_external() {
|
||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
wallet.get_address(New).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InvalidScript")]
|
||||
fn test_address_validator_internal() {
|
||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
let addr = crate::testutils!(@external descriptors, 10);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,8 @@
|
||||
//! "label":"testnet"
|
||||
//! }"#;
|
||||
//!
|
||||
//! let import = WalletExport::from_str(import)?;
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! let import = FullyNodedExport::from_str(import)?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! &import.descriptor(),
|
||||
//! import.change_descriptor().as_ref(),
|
||||
//! Network::Testnet,
|
||||
@@ -45,13 +45,13 @@
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! let wallet = Wallet::new(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default()
|
||||
//! )?;
|
||||
//! let export = WalletExport::export_wallet(&wallet, "exported wallet", true)
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
//! .map_err(ToString::to_string)
|
||||
//! .map_err(bdk::Error::Generic)?;
|
||||
//!
|
||||
@@ -64,16 +64,21 @@ use std::str::FromStr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use miniscript::descriptor::{ShInner, WshInner};
|
||||
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
|
||||
use miniscript::{Descriptor, ScriptContext, Terminal};
|
||||
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::types::KeychainKind;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Alias for [`FullyNodedExport`]
|
||||
#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")]
|
||||
pub type WalletExport = FullyNodedExport;
|
||||
|
||||
/// Structure that contains the export of a wallet
|
||||
///
|
||||
/// For a usage example see [this module](crate::wallet::export)'s documentation.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WalletExport {
|
||||
pub struct FullyNodedExport {
|
||||
descriptor: String,
|
||||
/// Earliest block to rescan when looking for the wallet's transactions
|
||||
pub blockheight: u32,
|
||||
@@ -81,13 +86,13 @@ pub struct WalletExport {
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl ToString for WalletExport {
|
||||
impl ToString for FullyNodedExport {
|
||||
fn to_string(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for WalletExport {
|
||||
impl FromStr for FullyNodedExport {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
@@ -96,10 +101,10 @@ impl FromStr for WalletExport {
|
||||
}
|
||||
|
||||
fn remove_checksum(s: String) -> String {
|
||||
s.splitn(2, '#').next().map(String::from).unwrap()
|
||||
s.split_once('#').map(|(a, _)| String::from(a)).unwrap()
|
||||
}
|
||||
|
||||
impl WalletExport {
|
||||
impl FullyNodedExport {
|
||||
/// Export a wallet
|
||||
///
|
||||
/// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
|
||||
@@ -111,43 +116,55 @@ impl WalletExport {
|
||||
///
|
||||
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
|
||||
/// returned will be `0`.
|
||||
pub fn export_wallet<B, D: BatchDatabase>(
|
||||
wallet: &Wallet<B, D>,
|
||||
pub fn export_wallet<D: BatchDatabase>(
|
||||
wallet: &Wallet<D>,
|
||||
label: &str,
|
||||
include_blockheight: bool,
|
||||
) -> Result<Self, &'static str> {
|
||||
let descriptor = wallet
|
||||
.descriptor
|
||||
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::External)
|
||||
.as_key_map(wallet.secp_ctx()),
|
||||
);
|
||||
let descriptor = remove_checksum(descriptor);
|
||||
Self::is_compatible_with_core(&descriptor)?;
|
||||
|
||||
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
||||
_ if !include_blockheight => 0,
|
||||
Err(_) => 0,
|
||||
Ok(txs) => {
|
||||
let mut heights = txs
|
||||
.into_iter()
|
||||
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
||||
.collect::<Vec<_>>();
|
||||
heights.sort_unstable();
|
||||
|
||||
*heights.last().unwrap_or(&0)
|
||||
}
|
||||
Ok(txs) => txs
|
||||
.into_iter()
|
||||
.filter_map(|tx| tx.confirmation_time.map(|c| c.height))
|
||||
.min()
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
let export = WalletExport {
|
||||
let export = FullyNodedExport {
|
||||
descriptor,
|
||||
label: label.into(),
|
||||
blockheight,
|
||||
};
|
||||
|
||||
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
|
||||
let descriptor =
|
||||
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()));
|
||||
remove_checksum(descriptor)
|
||||
let change_descriptor = match wallet
|
||||
.public_descriptor(KeychainKind::Internal)
|
||||
.map_err(|_| "Invalid change descriptor")?
|
||||
.is_some()
|
||||
{
|
||||
false => None,
|
||||
true => {
|
||||
let descriptor = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::Internal)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::Internal)
|
||||
.as_key_map(wallet.secp_ctx()),
|
||||
);
|
||||
Some(remove_checksum(descriptor))
|
||||
}
|
||||
};
|
||||
if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) {
|
||||
if export.change_descriptor() != change_descriptor {
|
||||
return Err("Incompatible change descriptor");
|
||||
}
|
||||
|
||||
@@ -228,6 +245,22 @@ mod test {
|
||||
fee: Some(500),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345678,
|
||||
height: 5001,
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
db.set_tx(&TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_str(
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
)
|
||||
.unwrap(),
|
||||
received: 25_000,
|
||||
sent: 0,
|
||||
fee: Some(300),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345677,
|
||||
height: 5000,
|
||||
}),
|
||||
})
|
||||
@@ -241,14 +274,14 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||
@@ -265,9 +298,8 @@ mod test {
|
||||
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
|
||||
let wallet =
|
||||
Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
|
||||
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,14 +311,14 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,14 +334,14 @@ mod test {
|
||||
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
|
||||
))";
|
||||
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Testnet,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||
@@ -322,14 +354,14 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new_offline(
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
|
||||
}
|
||||
@@ -340,7 +372,7 @@ mod test {
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
|
||||
let export = WalletExport::from_str(import_str).unwrap();
|
||||
let export = FullyNodedExport::from_str(import_str).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||
|
||||
97
src/wallet/hardwaresigner.rs
Normal file
97
src/wallet/hardwaresigner.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 HWISigner, an implementation of a [TransactionSigner] to be
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::database::MemoryDatabase;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
|
||||
//! # use hwi::{types::HWIChain, HWIClient};
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let devices = HWIClient::enumerate()?;
|
||||
//! let first_device = devices.first().expect("No devices found!");
|
||||
//! let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
//! # MemoryDatabase::default(),
|
||||
//! # )?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(custom_signer),
|
||||
//! );
|
||||
//!
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::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(())
|
||||
}
|
||||
}
|
||||
2655
src/wallet/mod.rs
2655
src/wallet/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct 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(())
|
||||
//! # }
|
||||
//! # fn connect() -> Self {
|
||||
@@ -47,32 +47,30 @@
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl Signer 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(())
|
||||
//! }
|
||||
//!
|
||||
//! impl SignerCommon for CustomSigner {
|
||||
//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||
//! self.device.get_id()
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn sign_whole_tx(&self) -> bool {
|
||||
//! false
|
||||
//! impl InputSigner for CustomSigner {
|
||||
//! 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(())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! let custom_signer = CustomSigner::connect();
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
@@ -85,22 +83,27 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::ops::Bound::Included;
|
||||
use std::ops::{Bound::Included, Deref};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitcoin::blockdata::opcodes;
|
||||
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
||||
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::{bip143, psbt};
|
||||
use bitcoin::{PrivateKey, Script, SigHash, SigHashType};
|
||||
use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot};
|
||||
use bitcoin::{secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
||||
|
||||
use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, KeyMap};
|
||||
use miniscript::{Legacy, MiniscriptKey, Segwitv0};
|
||||
use miniscript::descriptor::{
|
||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
|
||||
SinglePubKey,
|
||||
};
|
||||
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
|
||||
|
||||
use super::utils::SecpCtx;
|
||||
use crate::descriptor::XKeyUtils;
|
||||
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||
use crate::psbt::PsbtUtils;
|
||||
|
||||
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
|
||||
/// multiple of them
|
||||
@@ -153,6 +156,26 @@ pub enum SignerError {
|
||||
/// To enable signing transactions with non-standard sighashes set
|
||||
/// [`SignOptions::allow_all_sighashes`] to `true`.
|
||||
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 {
|
||||
@@ -163,27 +186,46 @@ impl fmt::Display 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
|
||||
/// [`this module`](crate::wallet::signer)'s documentation.
|
||||
pub trait Signer: fmt::Debug + Send + Sync {
|
||||
/// Sign a PSBT
|
||||
///
|
||||
/// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole
|
||||
/// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and
|
||||
/// can be ignored.
|
||||
fn sign(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: Option<usize>,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError>;
|
||||
/// Used by our software signers to determine the type of signatures to make
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SignerContext {
|
||||
/// Legacy context
|
||||
Legacy,
|
||||
/// Segwit v0 context (BIP 143)
|
||||
Segwitv0,
|
||||
/// Taproot context (BIP 340)
|
||||
Tap {
|
||||
/// Whether the signer can sign for the internal key or not
|
||||
is_internal_key: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Return whether or not the signer signs the whole transaction in one go instead of every
|
||||
/// input individually
|
||||
fn sign_whole_tx(&self) -> bool;
|
||||
/// Wrapper structure to pair a signer with its context
|
||||
#[derive(Debug, Clone)]
|
||||
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
|
||||
///
|
||||
/// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
|
||||
@@ -200,14 +242,69 @@ pub trait Signer: fmt::Debug + Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
fn sign(
|
||||
/// PSBT Input signer
|
||||
///
|
||||
/// 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,
|
||||
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,
|
||||
) -> Result<(), SignerError> {
|
||||
let input_index = input_index.unwrap();
|
||||
if input_index >= psbt.inputs.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
@@ -218,19 +315,23 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
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]
|
||||
.bip32_derivation
|
||||
.iter()
|
||||
.filter_map(|(pk, &(fingerprint, ref path))| {
|
||||
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
|
||||
Some((pk, path))
|
||||
.map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
|
||||
.chain(tap_key_origins)
|
||||
.find_map(|(pk, keysource)| {
|
||||
if self.matches(keysource, secp).is_some() {
|
||||
Some((pk, keysource.1.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
{
|
||||
Some((pk, full_path)) => (pk, full_path.clone()),
|
||||
}) {
|
||||
Some((pk, full_path)) => (pk, full_path),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
@@ -245,35 +346,49 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
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)
|
||||
} 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 {
|
||||
fn sign(
|
||||
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
Some(DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self.signer,
|
||||
origin: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: Option<usize>,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
let input_index = input_index.unwrap();
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
@@ -283,49 +398,142 @@ impl Signer for PrivateKey {
|
||||
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) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// FIXME: use the presence of `witness_utxo` as an indication that we should make a bip143
|
||||
// sig. Does this make sense? Should we add an extra argument to explicitly switch between
|
||||
// these? The original idea was to declare sign() as sign<Ctx: ScriptContex>() and use Ctx,
|
||||
// but that violates the rules for trait-objects, so we can't do it.
|
||||
let (hash, sighash) = match psbt.inputs[input_index].witness_utxo {
|
||||
Some(_) => Segwitv0::sighash(psbt, input_index)?,
|
||||
None => Legacy::sighash(psbt, input_index)?,
|
||||
let (hash, hash_ty) = match self.ctx {
|
||||
SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?,
|
||||
SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?,
|
||||
_ => return Ok(()), // handled above
|
||||
};
|
||||
|
||||
let signature = secp.sign(
|
||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
||||
&self.key,
|
||||
sign_psbt_ecdsa(
|
||||
&self.inner,
|
||||
pubkey,
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
sign_options.allow_grinding,
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_whole_tx(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn sign_psbt_ecdsa(
|
||||
secret_key: &secp256k1::SecretKey,
|
||||
pubkey: PublicKey,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: bitcoin::Sighash,
|
||||
hash_ty: EcdsaSighashType,
|
||||
secp: &SecpCtx,
|
||||
allow_grinding: bool,
|
||||
) {
|
||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||
let sig = if allow_grinding {
|
||||
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||
} else {
|
||||
secp.sign_ecdsa(msg, secret_key)
|
||||
};
|
||||
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||
.expect("invalid or corrupted ecdsa signature");
|
||||
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
||||
}
|
||||
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
|
||||
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
key: *self,
|
||||
origin: None,
|
||||
}))
|
||||
// Calling this with `leaf_hash` = `None` will sign for key-spend
|
||||
fn sign_psbt_schnorr(
|
||||
secret_key: &secp256k1::SecretKey,
|
||||
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)
|
||||
.to_inner(),
|
||||
Some(_) => keypair, // no tweak for script spend
|
||||
};
|
||||
|
||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||
let sig = secp.sign_schnorr(msg, &keypair);
|
||||
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||
.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 +568,7 @@ impl From<(SignerId, SignerOrdering)> for SignersContainerKey {
|
||||
|
||||
/// Container for multiple signers
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn Signer>>);
|
||||
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn TransactionSigner>>);
|
||||
|
||||
impl SignersContainer {
|
||||
/// Create a map of public keys to secret keys
|
||||
@@ -368,27 +576,45 @@ impl SignersContainer {
|
||||
self.0
|
||||
.values()
|
||||
.filter_map(|signer| signer.descriptor_secret_key())
|
||||
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
|
||||
.filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyMap> for SignersContainer {
|
||||
fn from(keymap: KeyMap) -> SignersContainer {
|
||||
let secp = Secp256k1::new();
|
||||
/// Build a new signer container from a [`KeyMap`]
|
||||
///
|
||||
/// 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();
|
||||
|
||||
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 {
|
||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
||||
SignerId::from(private_key.key.public_key(&secp).to_pubkeyhash()),
|
||||
DescriptorSecretKey::Single(private_key) => container.add_external(
|
||||
SignerId::from(
|
||||
private_key
|
||||
.key
|
||||
.public_key(secp)
|
||||
.to_pubkeyhash(SigType::Ecdsa),
|
||||
),
|
||||
SignerOrdering::default(),
|
||||
Arc::new(private_key.key),
|
||||
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
||||
),
|
||||
DescriptorSecretKey::XPrv(xprv) => container.add_external(
|
||||
SignerId::from(xprv.root_fingerprint(&secp)),
|
||||
SignerId::from(xprv.root_fingerprint(secp)),
|
||||
SignerOrdering::default(),
|
||||
Arc::new(xprv),
|
||||
Arc::new(SignerWrapper::new(xprv, ctx)),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -409,13 +635,17 @@ impl SignersContainer {
|
||||
&mut self,
|
||||
id: SignerId,
|
||||
ordering: SignerOrdering,
|
||||
signer: Arc<dyn Signer>,
|
||||
) -> Option<Arc<dyn Signer>> {
|
||||
signer: Arc<dyn TransactionSigner>,
|
||||
) -> Option<Arc<dyn TransactionSigner>> {
|
||||
self.0.insert((id, ordering).into(), signer)
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
@@ -428,12 +658,12 @@ impl SignersContainer {
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// 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
|
||||
.range((
|
||||
Included(&(id.clone(), SignerOrdering(0)).into()),
|
||||
@@ -477,38 +707,105 @@ pub struct SignOptions {
|
||||
///
|
||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||
pub allow_all_sighashes: bool,
|
||||
|
||||
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
|
||||
///
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try finalizing 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,
|
||||
|
||||
/// Whether we should grind ECDSA signature to ensure signing with low r
|
||||
/// or not.
|
||||
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
|
||||
pub allow_grinding: bool,
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
trust_witness_utxo: false,
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
allow_grinding: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ComputeSighash {
|
||||
type Extra;
|
||||
type Sighash;
|
||||
type SighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> Result<(SigHash, SigHashType), SignerError>;
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||
}
|
||||
|
||||
impl ComputeSighash for Legacy {
|
||||
type Extra = ();
|
||||
type Sighash = bitcoin::Sighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), 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 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 {
|
||||
Some(ref redeem_script) => redeem_script.clone(),
|
||||
None => {
|
||||
@@ -526,9 +823,11 @@ impl ComputeSighash for Legacy {
|
||||
};
|
||||
|
||||
Ok((
|
||||
psbt.global
|
||||
.unsigned_tx
|
||||
.signature_hash(input_index, &script, sighash.as_u32()),
|
||||
sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
|
||||
input_index,
|
||||
&script,
|
||||
sighash.to_u32(),
|
||||
)?,
|
||||
sighash,
|
||||
))
|
||||
}
|
||||
@@ -545,18 +844,27 @@ fn p2wpkh_script_code(script: &Script) -> Script {
|
||||
}
|
||||
|
||||
impl ComputeSighash for Segwitv0 {
|
||||
type Extra = ();
|
||||
type Sighash = bitcoin::Sighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), 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 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
|
||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
@@ -599,17 +907,69 @@ impl ComputeSighash for Segwitv0 {
|
||||
};
|
||||
|
||||
Ok((
|
||||
bip143::SigHashCache::new(&psbt.global.unsigned_tx).signature_hash(
|
||||
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
|
||||
input_index,
|
||||
&script,
|
||||
value,
|
||||
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 = (0..psbt.inputs.len())
|
||||
.map(|i| psbt.get_utxo_for(i))
|
||||
.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 {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
@@ -640,12 +1000,11 @@ mod signers_container_tests {
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::Network;
|
||||
use miniscript::ScriptContext;
|
||||
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();
|
||||
this.id(&secp) == that.id(&secp)
|
||||
}
|
||||
@@ -660,11 +1019,11 @@ mod signers_container_tests {
|
||||
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
|
||||
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
|
||||
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
|
||||
let (_, keymap) = desc
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
let signers = SignersContainer::from(keymap);
|
||||
let signers = SignersContainer::build(keymap, &wallet_desc, &secp);
|
||||
assert_eq!(signers.ids().len(), 2);
|
||||
|
||||
let signers = signers.signers();
|
||||
@@ -726,22 +1085,20 @@ mod signers_container_tests {
|
||||
number: u64,
|
||||
}
|
||||
|
||||
impl Signer for DummySigner {
|
||||
fn sign(
|
||||
&self,
|
||||
_psbt: &mut PartiallySignedTransaction,
|
||||
_input_index: Option<usize>,
|
||||
_secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SignerCommon for DummySigner {
|
||||
fn id(&self, _secp: &SecpCtx) -> SignerId {
|
||||
SignerId::Dummy(self.number)
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_whole_tx(&self) -> bool {
|
||||
true
|
||||
impl TransactionSigner for DummySigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||
_sign_options: &SignOptions,
|
||||
_secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +1113,7 @@ mod signers_container_tests {
|
||||
let secp: Secp256k1<All> = Secp256k1::new();
|
||||
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
|
||||
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
|
||||
let fingerprint = tprv.fingerprint(&secp);
|
||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -42,9 +42,7 @@ use std::default::Default;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
||||
|
||||
use miniscript::descriptor::DescriptorTrait;
|
||||
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||
@@ -103,10 +101,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// psbt1.global.unsigned_tx.output[..2],
|
||||
/// psbt2.global.unsigned_tx.output[..2]
|
||||
/// );
|
||||
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -120,8 +115,8 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`coin_selection`]: Self::coin_selection
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
|
||||
pub(crate) wallet: &'a Wallet<B, D>,
|
||||
pub struct TxBuilder<'a, D, Cs, Ctx> {
|
||||
pub(crate) wallet: &'a Wallet<D>,
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
@@ -140,9 +135,9 @@ pub(crate) struct TxParams {
|
||||
pub(crate) utxos: Vec<WeightedUtxo>,
|
||||
pub(crate) unspendable: HashSet<OutPoint>,
|
||||
pub(crate) manually_selected_only: bool,
|
||||
pub(crate) sighash: Option<SigHashType>,
|
||||
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
||||
pub(crate) ordering: TxOrdering,
|
||||
pub(crate) locktime: Option<u32>,
|
||||
pub(crate) locktime: Option<LockTime>,
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
@@ -150,6 +145,8 @@ pub(crate) struct TxParams {
|
||||
pub(crate) add_global_xpubs: bool,
|
||||
pub(crate) include_output_redeem_witness_script: bool,
|
||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||
pub(crate) current_height: Option<LockTime>,
|
||||
pub(crate) allow_dust: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -170,7 +167,7 @@ impl std::default::Default for FeePolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
|
||||
impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
fn clone(&self) -> Self {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
@@ -182,8 +179,8 @@ impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
TxBuilder<'a, B, D, Cs, Ctx>
|
||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
TxBuilder<'a, D, Cs, Ctx>
|
||||
{
|
||||
/// Set a custom fee rate
|
||||
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
|
||||
@@ -337,8 +334,9 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
|
||||
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
|
||||
///
|
||||
/// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
|
||||
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
|
||||
/// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
|
||||
/// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
|
||||
/// is called.
|
||||
///
|
||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||
/// [`finish`]: Self::finish
|
||||
@@ -412,7 +410,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// Sign with a specific sig hash
|
||||
///
|
||||
/// **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
|
||||
}
|
||||
@@ -426,7 +424,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// Use a specific nLockTime while creating the transaction
|
||||
///
|
||||
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
||||
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self {
|
||||
pub fn nlocktime(&mut self, locktime: LockTime) -> &mut Self {
|
||||
self.params.locktime = Some(locktime);
|
||||
self
|
||||
}
|
||||
@@ -508,7 +506,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
|
||||
self,
|
||||
coin_selection: P,
|
||||
) -> TxBuilder<'a, B, D, P, Ctx> {
|
||||
) -> TxBuilder<'a, D, P, Ctx> {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
params: self.params,
|
||||
@@ -517,7 +515,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the building the transaction.
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||
///
|
||||
@@ -541,13 +539,37 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
///
|
||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||
/// be a valid nSequence to signal RBF.
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
|
||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||
self
|
||||
}
|
||||
|
||||
/// 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(LockTime::from_height(height).expect("Invalid 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, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D, Cs, CreateTx> {
|
||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self {
|
||||
self.params.recipients = recipients;
|
||||
@@ -576,6 +598,9 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
||||
/// 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).
|
||||
///
|
||||
/// 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
|
||||
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||
///
|
||||
@@ -606,6 +631,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
||||
///
|
||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
@@ -614,8 +640,8 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
|
||||
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// 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
|
||||
/// will attempt to find a change output to shrink instead.
|
||||
///
|
||||
@@ -675,7 +701,7 @@ impl TxOrdering {
|
||||
#[cfg(not(test))]
|
||||
let mut rng = rand::thread_rng();
|
||||
#[cfg(test)]
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
|
||||
|
||||
tx.output.shuffle(&mut rng);
|
||||
}
|
||||
@@ -708,13 +734,13 @@ impl Default for Version {
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub(crate) enum RbfValue {
|
||||
Default,
|
||||
Value(u32),
|
||||
Value(Sequence),
|
||||
}
|
||||
|
||||
impl RbfValue {
|
||||
pub(crate) fn get_value(&self) -> u32 {
|
||||
pub(crate) fn get_value(&self) -> Sequence {
|
||||
match self {
|
||||
RbfValue::Default => 0xFFFFFFFD,
|
||||
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
RbfValue::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
@@ -830,22 +856,26 @@ mod test {
|
||||
}
|
||||
|
||||
fn get_test_utxos() -> Vec<LocalUtxo> {
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
vec![
|
||||
LocalUtxo {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||
vout: 0,
|
||||
},
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
},
|
||||
LocalUtxo {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||
vout: 1,
|
||||
},
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::Internal,
|
||||
is_spent: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,23 +9,11 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bitcoin::blockdata::script::Script;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::{LockTime, Script, Sequence};
|
||||
|
||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||
|
||||
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
|
||||
// spending using CSV in order to enforce CSV rules
|
||||
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
|
||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
// Mask for the bits used to express the timelock
|
||||
pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
|
||||
|
||||
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
|
||||
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
|
||||
|
||||
/// Trait to check if a value is below the dust limit.
|
||||
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
||||
/// keep it compatible with network dust rate
|
||||
@@ -38,7 +26,7 @@ pub trait IsDust {
|
||||
|
||||
impl IsDust for u64 {
|
||||
fn is_dust(&self, script: &Script) -> bool {
|
||||
*self < script.dust_value().as_sat()
|
||||
*self < script.dust_value().to_sat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,19 +44,15 @@ impl After {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
||||
// This flag cannot be set in the nSequence when spending using OP_CSV
|
||||
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
||||
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
|
||||
// The RBF value must enable relative timelocks
|
||||
if !rbf.is_relative_lock_time() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mask = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK;
|
||||
let rbf = rbf & mask;
|
||||
let csv = csv & mask;
|
||||
|
||||
// Both values should be represented in the same unit (either time-based or
|
||||
// block-height based)
|
||||
if (rbf < SEQUENCE_LOCKTIME_TYPE_FLAG) != (csv < SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
||||
if rbf.is_time_locked() != csv.is_time_locked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
|
||||
// Both values should be expressed in the same unit
|
||||
if (nlocktime < BLOCKS_TIMELOCK_THRESHOLD) != (required < BLOCKS_TIMELOCK_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The value should be at least `required`
|
||||
if nlocktime < required {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
||||
fn check_after(&self, n: u32) -> bool {
|
||||
fn check_after(&self, n: LockTime) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
current_height >= n
|
||||
current_height >= n.to_consensus_u32()
|
||||
} else {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -125,10 +95,15 @@ impl Older {
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||
fn check_older(&self, n: u32) -> bool {
|
||||
fn check_older(&self, n: Sequence) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
// TODO: test >= / >
|
||||
current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64
|
||||
current_height
|
||||
>= self
|
||||
.create_height
|
||||
.unwrap_or(0)
|
||||
.checked_add(n.to_consensus_u32())
|
||||
.expect("Overflowing addition")
|
||||
} else {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -139,12 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
||||
};
|
||||
use crate::bitcoin::Address;
|
||||
use crate::types::FeeRate;
|
||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
|
||||
use super::{check_nsequence_rbf, IsDust};
|
||||
use crate::bitcoin::{Address, Sequence};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
@@ -164,86 +139,42 @@ mod test {
|
||||
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]
|
||||
fn test_check_nsequence_rbf_msb_set() {
|
||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_lt_csv() {
|
||||
let result = check_nsequence_rbf(4000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_different_unit() {
|
||||
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
||||
let result =
|
||||
check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_mask() {
|
||||
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_blocks() {
|
||||
let result = check_nsequence_rbf(10_000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_time() {
|
||||
let result = check_nsequence_rbf(
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_lt_cltv() {
|
||||
let result = check_nlocktime(4000, 5000);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_different_unit() {
|
||||
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_same_unit_blocks() {
|
||||
let result = check_nlocktime(10_000, 5000);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_same_unit_time() {
|
||||
let result = check_nlocktime(
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::fmt;
|
||||
use bitcoin::consensus::serialize;
|
||||
use bitcoin::{OutPoint, Transaction, Txid};
|
||||
|
||||
use crate::blockchain::Blockchain;
|
||||
use crate::blockchain::GetTx;
|
||||
use crate::database::Database;
|
||||
use crate::error::Error;
|
||||
|
||||
@@ -29,7 +29,9 @@ use crate::error::Error;
|
||||
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
||||
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
||||
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
||||
pub fn verify_tx<D: Database, B: Blockchain>(
|
||||
///
|
||||
/// [`Blockchain`]: crate::blockchain::Blockchain
|
||||
pub fn verify_tx<D: Database, B: GetTx>(
|
||||
tx: &Transaction,
|
||||
database: &D,
|
||||
blockchain: &B,
|
||||
@@ -104,43 +106,18 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
use crate::database::{BatchOperations, MemoryDatabase};
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::{Transaction, Txid};
|
||||
|
||||
use crate::blockchain::{Blockchain, Capability, Progress};
|
||||
use crate::database::{BatchDatabase, BatchOperations, MemoryDatabase};
|
||||
use crate::FeeRate;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct DummyBlockchain;
|
||||
|
||||
impl Blockchain for DummyBlockchain {
|
||||
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||
Default::default()
|
||||
}
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
_database: &mut D,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
impl GetTx for DummyBlockchain {
|
||||
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(None)
|
||||
}
|
||||
fn broadcast(&self, _tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(42)
|
||||
}
|
||||
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
|
||||
Ok(FeeRate::default_min_relay_fee())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user