Compare commits
85 Commits
release/0.
...
release/0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32db387104 | ||
|
|
4f7d567f47 | ||
|
|
5c7b2af0bc | ||
|
|
a7589c5baa | ||
|
|
0010ecd94a | ||
|
|
690411722e | ||
|
|
f2e12d0ccd | ||
|
|
7001b14b4c | ||
|
|
13cf72ffa7 | ||
|
|
d7163c3a97 | ||
|
|
cf13c80991 | ||
|
|
7c57965999 | ||
|
|
3d69f1c291 | ||
|
|
3451d1c12e | ||
|
|
4fbd8520e6 | ||
|
|
bfd7b2f65d | ||
|
|
061f15af00 | ||
|
|
2bff4e5e56 | ||
|
|
138acc3b7d | ||
|
|
d6e1dd1040 | ||
|
|
76034772cb | ||
|
|
12507c707f | ||
|
|
de358f8cdc | ||
|
|
08668ac462 | ||
|
|
0a3734ed2b | ||
|
|
a5d1a3d65c | ||
|
|
7bc2980905 | ||
|
|
34e792e193 | ||
|
|
7b1ad1b629 | ||
|
|
a8f9f6c43a | ||
|
|
c9b1b6d076 | ||
|
|
cd078903a7 | ||
|
|
588c17ff69 | ||
|
|
baf7eaace6 | ||
|
|
8026bd9476 | ||
|
|
e2bd96012a | ||
|
|
9be63e66ec | ||
|
|
9f9ffd0efd | ||
|
|
2db881519a | ||
|
|
d9adfbe047 | ||
|
|
74e2c477f1 | ||
|
|
c5952dd09a | ||
|
|
134b19a9cb | ||
|
|
2c01b6118f | ||
|
|
03d3c786f2 | ||
|
|
0f03831274 | ||
|
|
dc7adb7161 | ||
|
|
5eeba6cced | ||
|
|
5eb74af414 | ||
|
|
ac19c19f21 | ||
|
|
ef03da0a76 | ||
|
|
9d85c9667f | ||
|
|
85bd126c6c | ||
|
|
7fdacdbad4 | ||
|
|
9c0a769675 | ||
|
|
11865fddff | ||
|
|
e8df3d2d91 | ||
|
|
a63c51f35d | ||
|
|
1730e0150f | ||
|
|
5a415979af | ||
|
|
a713a5a062 | ||
|
|
419dc248b6 | ||
|
|
632dabaa07 | ||
|
|
2756411ef7 | ||
|
|
50af51da5a | ||
|
|
ae919061e2 | ||
|
|
7ac87b8f99 | ||
|
|
ac051d7ae9 | ||
|
|
00d426b885 | ||
|
|
42fde6d457 | ||
|
|
8e0d00a3ea | ||
|
|
235011feef | ||
|
|
a1477405d1 | ||
|
|
558e37afa7 | ||
|
|
6bae52e6f2 | ||
|
|
32ae95f463 | ||
|
|
3644a452c1 | ||
|
|
5c940c33cb | ||
|
|
277e18f5cb | ||
|
|
8d3b2a9581 | ||
|
|
45a4ae5828 | ||
|
|
6db5b4a094 | ||
|
|
9d2024434e | ||
|
|
9165faef95 | ||
|
|
2c02a44586 |
89
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
89
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: Minor Release
|
||||||
|
about: Create a new minor release [for release managers only]
|
||||||
|
title: 'Release MAJOR.MINOR+1.0'
|
||||||
|
labels: 'release'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a new minor release
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
<--release summary to be used in announcements-->
|
||||||
|
|
||||||
|
### Commit
|
||||||
|
|
||||||
|
<--latest commit ID to include in this release-->
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||||
|
branch **development** version is *MAJOR.MINOR.0*.
|
||||||
|
|
||||||
|
#### On the day of the feature freeze
|
||||||
|
|
||||||
|
Change the `master` branch to the next MINOR+1 version:
|
||||||
|
|
||||||
|
- [ ] Switch to the `master` branch.
|
||||||
|
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR+1`, eg. `bump_dev_0_22`.
|
||||||
|
- [ ] Bump the `bump_dev_MAJOR_MINOR+1` branch to the next development MINOR+1 version.
|
||||||
|
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
|
||||||
|
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
|
||||||
|
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR+1` branch to `master`.
|
||||||
|
- Title PR "Bump version to MAJOR.MINOR+1.0".
|
||||||
|
|
||||||
|
Create a new release branch and release candidate tag:
|
||||||
|
|
||||||
|
- [ ] Double check that your local `master` is up-to-date with the upstream repo.
|
||||||
|
- [ ] Create a new branch called `release/MAJOR.MINOR+1` from `master`.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0-RC.1`
|
||||||
|
- Use message "Release MAJOR.MINOR+1.0 RC.1".
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Push the `release/MAJOR.MINOR` branch and new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-RC.1` tag.
|
||||||
|
|
||||||
|
If any issues need to be fixed before the *MAJOR.MINOR+1.0* version is released:
|
||||||
|
|
||||||
|
- [ ] Merge fix PRs to the `master` branch.
|
||||||
|
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- [ ] Verify fixes in `release/MAJOR.MINOR+1` branch.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0-RC.x+1`, where x is the current release candidate number.
|
||||||
|
- Use tag message "Release MAJOR.MINOR+1.0 RC.x+1".
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-RC.x+1` tag.
|
||||||
|
|
||||||
|
#### On the day of the release
|
||||||
|
|
||||||
|
Tag and publish new release:
|
||||||
|
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR+1.0`
|
||||||
|
- The first line of the tag message should be "Release MAJOR.MINOR+1.0".
|
||||||
|
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Wait for the CI to finish one last time.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- [ ] Publish **all** the updated crates to crates.io.
|
||||||
|
- [ ] Create the release on GitHub.
|
||||||
|
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||||
|
- Set the title to `Release MAJOR.MINOR+1.0`.
|
||||||
|
- In the release notes body put the **Summary** and **Changelog**.
|
||||||
|
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||||
|
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||||
|
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||||
|
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||||
|
- [ ] Celebrate 🎉
|
||||||
|
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
|
[crates.io]: https://crates.io/crates/bdk
|
||||||
|
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||||
|
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||||
67
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
67
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: Patch Release
|
||||||
|
about: Create a new patch release [for release managers only]
|
||||||
|
title: 'Release MAJOR.MINOR.PATCH+1'
|
||||||
|
labels: 'release'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a new patch release
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
<--release summary to be used in announcements-->
|
||||||
|
|
||||||
|
### Commit
|
||||||
|
|
||||||
|
<--latest commit ID to include in this release-->
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||||
|
branch **development** version is *MAJOR.MINOR.PATCH*.
|
||||||
|
|
||||||
|
### On the day of the patch release
|
||||||
|
|
||||||
|
Change the `master` branch to the new PATCH+1 version:
|
||||||
|
|
||||||
|
- [ ] Switch to the `master` branch.
|
||||||
|
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR_PATCH+1`, eg. `bump_dev_0_22_1`.
|
||||||
|
- [ ] Bump the `bump_dev_MAJOR_MINOR` branch to the next development PATCH+1 version.
|
||||||
|
- Change the `Cargo.toml` version value to `MAJOR.MINOR.PATCH+1`.
|
||||||
|
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
|
||||||
|
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR_PATCH+1` branch to `master`.
|
||||||
|
- Title PR "Bump version to MAJOR.MINOR.PATCH+1".
|
||||||
|
|
||||||
|
Cherry-pick, tag and publish new PATCH+1 release:
|
||||||
|
|
||||||
|
- [ ] Merge fix PRs to the `master` branch.
|
||||||
|
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR` branch to be patched.
|
||||||
|
- [ ] Verify fixes in `release/MAJOR.MINOR` branch.
|
||||||
|
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR` branch.
|
||||||
|
- The tag name should be `vMAJOR.MINOR.PATCH+1`
|
||||||
|
- The first line of the tag message should be "Release MAJOR.MINOR.PATCH+1".
|
||||||
|
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||||
|
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
|
- [ ] Wait for the CI to finish one last time.
|
||||||
|
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||||
|
- [ ] Publish **all** the updated crates to crates.io.
|
||||||
|
- [ ] Create the release on GitHub.
|
||||||
|
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||||
|
- Set the title to `Release MAJOR.MINOR.PATCH+1`.
|
||||||
|
- In the release notes body put the **Summary** and **Changelog**.
|
||||||
|
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||||
|
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||||
|
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||||
|
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||||
|
- [ ] Celebrate 🎉
|
||||||
|
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
|
[crates.io]: https://crates.io/crates/bdk
|
||||||
|
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||||
|
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -9,6 +9,11 @@
|
|||||||
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
|
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
|
||||||
of the PR were done in a specific way -->
|
of the PR were done in a specific way -->
|
||||||
|
|
||||||
|
### Changelog notice
|
||||||
|
|
||||||
|
<!-- Notice the release manager should include in the release tag message changelog -->
|
||||||
|
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
|
||||||
|
|
||||||
### Checklists
|
### Checklists
|
||||||
|
|
||||||
#### All Submissions:
|
#### All Submissions:
|
||||||
@@ -21,7 +26,6 @@ of the PR were done in a specific way -->
|
|||||||
|
|
||||||
* [ ] I've added tests for the new feature
|
* [ ] I've added tests for the new feature
|
||||||
* [ ] I've added docs for the new feature
|
* [ ] I've added docs for the new feature
|
||||||
* [ ] I've updated `CHANGELOG.md`
|
|
||||||
|
|
||||||
#### Bugfixes:
|
#### Bugfixes:
|
||||||
|
|
||||||
|
|||||||
48
.github/workflows/code_coverage.yml
vendored
48
.github/workflows/code_coverage.yml
vendored
@@ -3,35 +3,53 @@ on: [push]
|
|||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
Codecov:
|
Codecov:
|
||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: '0'
|
RUSTFLAGS: "-Cinstrument-coverage"
|
||||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
RUSTDOCFLAGS: "-Cinstrument-coverage"
|
||||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Install lcov tools
|
||||||
|
run: sudo apt-get install lcov -y
|
||||||
- name: Install rustup
|
- name: Install rustup
|
||||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default nightly
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
|
- name: Add llvm tools
|
||||||
|
run: rustup component add llvm-tools-preview
|
||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Test
|
- name: Cache cargo
|
||||||
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
uses: actions/cache@v3
|
||||||
|
|
||||||
- id: coverage
|
|
||||||
name: Generate coverage
|
|
||||||
uses: actions-rs/grcov@v0.1.5
|
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ steps.coverage.outputs.report }}
|
path: |
|
||||||
directory: ./coverage/reports/
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
- name: Install grcov
|
||||||
|
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features default,minimal,all-keys,compact_filters,key-value-db,compiler,sqlite,sqlite-bundled,test-electrum,verify,test-rpc
|
||||||
|
- name: Run grcov
|
||||||
|
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
|
||||||
|
- name: Generate HTML coverage report
|
||||||
|
run: genhtml -o coverage-report.html ./coverage/lcov.info
|
||||||
|
|
||||||
|
- name: Coveralls upload
|
||||||
|
uses: coverallsapp/github-action@master
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage-report.html
|
||||||
|
|||||||
29
.github/workflows/cont_integration.yml
vendored
29
.github/workflows/cont_integration.yml
vendored
@@ -172,3 +172,32 @@ jobs:
|
|||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||||
|
|
||||||
|
test_harware_wallet:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust:
|
||||||
|
- version: 1.60.0 # STABLE
|
||||||
|
- version: 1.56.1 # MSRV
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Build simulator image
|
||||||
|
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
|
||||||
|
- name: Run simulator image
|
||||||
|
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
||||||
|
- name: Install Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: Install python dependencies
|
||||||
|
run: pip install hwi==2.1.1 protobuf==3.20.1
|
||||||
|
- name: Set default toolchain
|
||||||
|
run: rustup default ${{ matrix.rust.version }}
|
||||||
|
- name: Set profile
|
||||||
|
run: rustup set profile minimal
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features test-hardware-signer
|
||||||
|
|||||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,11 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project prior to release **0.22.0** are documented in this file. Future
|
||||||
|
changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
|
||||||
|
Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk/releases)
|
||||||
|
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [v0.21.0] - [v0.20.0]
|
||||||
|
|
||||||
|
- Add `descriptor::checksum::get_checksum_bytes` method.
|
||||||
|
- Add `Excess` enum to handle remaining amount after coin selection.
|
||||||
|
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
|
||||||
|
- Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef<Path>
|
||||||
|
- Add the ability to specify which leaves to sign in a taproot transaction through `TapLeavesOptions` in `SignOptions`
|
||||||
|
- Add the ability to specify whether a taproot transaction should be signed using the internal key or not, using `sign_with_tap_internal_key` in `SignOptions`
|
||||||
|
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
|
||||||
|
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
|
||||||
|
- New `RpcBlockchain` implementation with various fixes.
|
||||||
|
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
|
||||||
|
|
||||||
## [v0.20.0] - [v0.19.0]
|
## [v0.20.0] - [v0.19.0]
|
||||||
|
|
||||||
@@ -13,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- 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)
|
- 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.
|
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
|
||||||
- Set coin type in BIP44, BIP49, and BIP84 templates
|
- 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.
|
- 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`
|
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
|
||||||
- Deprecate `AddressValidator`
|
- Deprecate `AddressValidator`
|
||||||
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
|
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
|
||||||
@@ -25,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Unpinned tokio to `1`
|
- Unpinned tokio to `1`
|
||||||
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
|
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
|
||||||
- Upgrade to rust-bitcoin `0.28`
|
- 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
|
- 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
|
- Update `PkOrF` in the policy module to become an enum
|
||||||
- Add experimental support for Taproot, including:
|
- Add experimental support for Taproot, including:
|
||||||
- Support for `tr()` descriptors with complex tapscript trees
|
- Support for `tr()` descriptors with complex tapscript trees
|
||||||
@@ -36,13 +49,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [v0.18.0] - [v0.17.0]
|
## [v0.18.0] - [v0.17.0]
|
||||||
|
|
||||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
|
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, i.e. for mobile platforms.
|
||||||
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
||||||
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
||||||
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
||||||
- Improve key generation traits
|
- Improve key generation traits
|
||||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
||||||
- Bump `miniscript` dependency version to `^6.1`.
|
- Bump `miniscript` dependency version to `^6.1`.
|
||||||
|
|
||||||
## [v0.17.0] - [v0.16.1]
|
## [v0.17.0] - [v0.16.1]
|
||||||
|
|
||||||
@@ -479,4 +492,4 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
||||||
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.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.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
|
||||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...HEAD
|
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.20.1-dev"
|
version = "0.22.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -22,7 +22,7 @@ rand = "^0.7"
|
|||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.10", optional = true }
|
electrum-client = { version = "0.11", optional = true }
|
||||||
rusqlite = { version = "0.27.0", optional = true }
|
rusqlite = { version = "0.27.0", optional = true }
|
||||||
ahash = { version = "0.7.6", optional = true }
|
ahash = { version = "0.7.6", optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
||||||
@@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
|
|||||||
cc = { version = ">=1.0.64", optional = true }
|
cc = { version = ">=1.0.64", optional = true }
|
||||||
socks = { version = "0.3", optional = true }
|
socks = { version = "0.3", optional = true }
|
||||||
lazy_static = { version = "1.4", optional = true }
|
lazy_static = { version = "1.4", optional = true }
|
||||||
|
hwi = { version = "0.2.2", optional = true }
|
||||||
|
|
||||||
bip39 = { version = "1.0.1", optional = true }
|
bip39 = { version = "1.0.1", optional = true }
|
||||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||||
@@ -61,6 +62,7 @@ key-value-db = ["sled"]
|
|||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
keys-bip39 = ["bip39"]
|
keys-bip39 = ["bip39"]
|
||||||
rpc = ["bitcoincore-rpc"]
|
rpc = ["bitcoincore-rpc"]
|
||||||
|
hardware-signer = ["hwi"]
|
||||||
|
|
||||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||||
# blocking except for the `EsploraBlockchain` which can be either async or
|
# blocking except for the `EsploraBlockchain` which can be either async or
|
||||||
@@ -93,12 +95,13 @@ test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-bl
|
|||||||
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_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-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
|
test-hardware-signer = ["hardware-signer"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
electrsd = "0.19.1"
|
electrsd = "0.20"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
@@ -114,11 +117,11 @@ required-features = ["compiler"]
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "rpcwallet"
|
name = "rpcwallet"
|
||||||
path = "examples/rpcwallet.rs"
|
path = "examples/rpcwallet.rs"
|
||||||
required-features = ["keys-bip39", "key-value-db", "rpc"]
|
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
|
features = ["compiler", "electrum", "esplora", "use-esplora-ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
||||||
# defines the configuration attribute `docsrs`
|
# defines the configuration attribute `docsrs`
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|||||||
@@ -1,46 +1,16 @@
|
|||||||
# Development Cycle
|
# Development Cycle
|
||||||
|
|
||||||
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
This project follows a regular releasing schedule similar to the one [used by the Rust language]. In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
||||||
|
|
||||||
|
This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API changes in the changelog info attached to each release tag.
|
||||||
|
|
||||||
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
||||||
|
|
||||||
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
||||||
|
|
||||||
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
||||||
|
|
||||||
```
|
To create a new release a release manager will create a new issue using the `Release` template and follow the template instructions.
|
||||||
master: - - - - * - - - * - - - - - - * - - - * ...
|
|
||||||
| / | |
|
|
||||||
release/0.x.0: * - - # | |
|
|
||||||
| /
|
|
||||||
release/0.y.0: * - - #
|
|
||||||
```
|
|
||||||
|
|
||||||
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
|
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
|
||||||
|
[Semantic Versioning]: https://semver.org/
|
||||||
## Making the Release
|
|
||||||
|
|
||||||
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
|
|
||||||
|
|
||||||
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
|
|
||||||
|
|
||||||
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
|
|
||||||
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
|
|
||||||
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
|
|
||||||
4. During the one week of feature freeze run additional tests on the release branch.
|
|
||||||
5. If a bug is found:
|
|
||||||
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
|
|
||||||
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
|
|
||||||
6. Update the changelog with the new release version.
|
|
||||||
7. Update `src/lib.rs` with the new version (line ~43)
|
|
||||||
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
|
|
||||||
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
|
||||||
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
|
|
||||||
11. Publish **all** the updated crates to crates.io.
|
|
||||||
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
|
||||||
13. Merge the release branch back into `master`.
|
|
||||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (ie. `bdk-macros = { path = "./macros"}`)
|
|
||||||
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
|
||||||
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
|
||||||
17. Announce the release on Twitter, Discord and Telegram.
|
|
||||||
18. Celebrate :tada:
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||||
<a href="https://blog.rust-lang.org/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://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
|
||||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||||
@@ -95,6 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
|
|||||||
use bdk::electrum_client::Client;
|
use bdk::electrum_client::Client;
|
||||||
use bdk::wallet::AddressIndex::New;
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
|
use bitcoin::base64;
|
||||||
use bitcoin::consensus::serialize;
|
use bitcoin::consensus::serialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@@ -131,6 +132,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
```rust,no_run
|
```rust,no_run
|
||||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||||
|
|
||||||
|
use bitcoin::base64;
|
||||||
use bitcoin::consensus::deserialize;
|
use bitcoin::consensus::deserialize;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
@@ -154,7 +156,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
|
|
||||||
### Unit testing
|
### Unit testing
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -162,7 +164,7 @@ cargo test
|
|||||||
|
|
||||||
Integration testing require testing features, for example:
|
Integration testing require testing features, for example:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
cargo test --features test-electrum
|
cargo test --features test-electrum
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
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
|
|
||||||
@@ -103,7 +103,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
auth: bitcoind_auth,
|
auth: bitcoind_auth,
|
||||||
network: Network::Regtest,
|
network: Network::Regtest,
|
||||||
wallet_name,
|
wallet_name,
|
||||||
skip_blocks: None,
|
sync_params: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the above configuration to create a RPC blockchain backend
|
// Use the above configuration to create a RPC blockchain backend
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
@@ -79,6 +80,14 @@ impl Blockchain for ElectrumBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for ElectrumBlockchain {
|
||||||
|
type Target = Client;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StatelessBlockchain for ElectrumBlockchain {}
|
impl StatelessBlockchain for ElectrumBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for ElectrumBlockchain {
|
impl GetHeight for ElectrumBlockchain {
|
||||||
@@ -385,7 +394,7 @@ mod test {
|
|||||||
.sync_wallet(&wallet, None, Default::default())
|
.sync_wallet(&wallet, None, Default::default())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
//! Esplora by way of `reqwest` HTTP client.
|
//! Esplora by way of `reqwest` HTTP client.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::consensus::{deserialize, serialize};
|
use bitcoin::consensus::{deserialize, serialize};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
@@ -31,8 +32,9 @@ use crate::database::BatchDatabase;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
/// Structure encapsulates Esplora client
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UrlClient {
|
pub struct UrlClient {
|
||||||
url: String,
|
url: String,
|
||||||
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
// We use the async client instead of the blocking one because it automatically uses `fetch`
|
||||||
// when the target platform is wasm32.
|
// when the target platform is wasm32.
|
||||||
@@ -101,6 +103,14 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for EsploraBlockchain {
|
||||||
|
type Target = UrlClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.url_client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StatelessBlockchain for EsploraBlockchain {}
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
@@ -213,7 +223,6 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
};
|
};
|
||||||
|
|
||||||
database.commit_batch(batch_update)?;
|
database.commit_batch(batch_update)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
@@ -33,8 +34,9 @@ use crate::database::BatchDatabase;
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::FeeRate;
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
/// Structure encapsulates ureq Esplora client
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct UrlClient {
|
pub struct UrlClient {
|
||||||
url: String,
|
url: String,
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
}
|
}
|
||||||
@@ -98,6 +100,14 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for EsploraBlockchain {
|
||||||
|
type Target = UrlClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.url_client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StatelessBlockchain for EsploraBlockchain {}
|
impl StatelessBlockchain for EsploraBlockchain {}
|
||||||
|
|
||||||
impl GetHeight for EsploraBlockchain {
|
impl GetHeight for EsploraBlockchain {
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ This example shows how to sync multiple walles and return the sum of their balan
|
|||||||
# use bdk::database::*;
|
# use bdk::database::*;
|
||||||
# use bdk::wallet::*;
|
# use bdk::wallet::*;
|
||||||
# use bdk::*;
|
# use bdk::*;
|
||||||
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
|
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
|
||||||
Ok(wallets
|
Ok(wallets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|w| -> Result<_, Error> {
|
.map(|w| -> Result<_, Error> {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
||||||
|
error::MissingCachedScripts,
|
||||||
wallet::time::Instant,
|
wallet::time::Instant,
|
||||||
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
||||||
};
|
};
|
||||||
@@ -34,11 +35,12 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
|||||||
let scripts_needed = db
|
let scripts_needed = db
|
||||||
.iter_script_pubkeys(Some(keychain))?
|
.iter_script_pubkeys(Some(keychain))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect::<VecDeque<_>>();
|
||||||
let state = State::new(db);
|
let state = State::new(db);
|
||||||
|
|
||||||
Ok(Request::Script(ScriptReq {
|
Ok(Request::Script(ScriptReq {
|
||||||
state,
|
state,
|
||||||
|
initial_scripts_needed: scripts_needed.len(),
|
||||||
scripts_needed,
|
scripts_needed,
|
||||||
script_index: 0,
|
script_index: 0,
|
||||||
stop_gap,
|
stop_gap,
|
||||||
@@ -50,6 +52,7 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
|||||||
pub struct ScriptReq<'a, D: BatchDatabase> {
|
pub struct ScriptReq<'a, D: BatchDatabase> {
|
||||||
state: State<'a, D>,
|
state: State<'a, D>,
|
||||||
script_index: usize,
|
script_index: usize,
|
||||||
|
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
|
||||||
scripts_needed: VecDeque<Script>,
|
scripts_needed: VecDeque<Script>,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
|
|||||||
self.script_index += 1;
|
self.script_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in txids {
|
self.scripts_needed.drain(..txids.len());
|
||||||
self.scripts_needed.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_active_index = self
|
// last active index: 0 => No last active
|
||||||
|
let last = self
|
||||||
.state
|
.state
|
||||||
.last_active_index
|
.last_active_index
|
||||||
.get(&self.keychain)
|
.get(&self.keychain)
|
||||||
.map(|x| x + 1)
|
.map(|&l| l + 1)
|
||||||
.unwrap_or(0); // so no addresses active maps to 0
|
.unwrap_or(0);
|
||||||
|
// remaining scripts left to check
|
||||||
|
let remaining = self.scripts_needed.len();
|
||||||
|
// difference between current index and last active index
|
||||||
|
let current_gap = self.script_index - last;
|
||||||
|
|
||||||
Ok(
|
// this is a hack to check whether the scripts are coming from a derivable descriptor
|
||||||
if self.script_index > last_active_index + self.stop_gap
|
// we assume for non-derivable descriptors, the initial script count is always 1
|
||||||
|| self.scripts_needed.is_empty()
|
let is_derivable = self.initial_scripts_needed > 1;
|
||||||
{
|
|
||||||
debug!(
|
debug!(
|
||||||
"finished scanning for transactions for keychain {:?} at index {}",
|
"sync: last={}, remaining={}, diff={}, stop_gap={}",
|
||||||
self.keychain, last_active_index
|
last, remaining, current_gap, self.stop_gap
|
||||||
);
|
);
|
||||||
// we're done here -- check if we need to do the next keychain
|
|
||||||
if let Some(keychain) = self.next_keychains.pop() {
|
if is_derivable {
|
||||||
self.keychain = keychain;
|
if remaining > 0 {
|
||||||
self.script_index = 0;
|
// we still have scriptPubKeys to do requests for
|
||||||
self.scripts_needed = self
|
return Ok(Request::Script(self));
|
||||||
.state
|
}
|
||||||
.db
|
|
||||||
.iter_script_pubkeys(Some(keychain))?
|
if last > 0 && current_gap < self.stop_gap {
|
||||||
.into_iter()
|
// current gap is not large enough to stop, but we are unable to keep checking since
|
||||||
.collect();
|
// we have exhausted cached scriptPubKeys, so return error
|
||||||
Request::Script(self)
|
let err = MissingCachedScripts {
|
||||||
} else {
|
last_count: self.script_index,
|
||||||
Request::Tx(TxReq { state: self.state })
|
missing_count: self.stop_gap - current_gap,
|
||||||
}
|
};
|
||||||
} else {
|
return Err(Error::MissingCachedScripts(err));
|
||||||
Request::Script(self)
|
}
|
||||||
},
|
|
||||||
)
|
// we have exhausted cached scriptPubKeys and found no txs, continue
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"finished scanning for txs of keychain {:?} at index {:?}",
|
||||||
|
self.keychain, last
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(keychain) = self.next_keychains.pop() {
|
||||||
|
// we still have another keychain to request txs with
|
||||||
|
let scripts_needed = self
|
||||||
|
.state
|
||||||
|
.db
|
||||||
|
.iter_script_pubkeys(Some(keychain))?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<VecDeque<_>>();
|
||||||
|
|
||||||
|
self.keychain = keychain;
|
||||||
|
self.script_index = 0;
|
||||||
|
self.initial_scripts_needed = scripts_needed.len();
|
||||||
|
self.scripts_needed = scripts_needed;
|
||||||
|
return Ok(Request::Script(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have finished requesting txids, let's get the actual txs.
|
||||||
|
Ok(Request::Tx(TxReq { state: self.state }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +325,8 @@ struct State<'a, D> {
|
|||||||
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
||||||
/// The start of the sync
|
/// The start of the sync
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
|
/// Missing number of scripts to cache per keychain
|
||||||
|
missing_script_counts: HashMap<KeychainKind, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, D: BatchDatabase> State<'a, D> {
|
impl<'a, D: BatchDatabase> State<'a, D> {
|
||||||
@@ -305,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
tx_needed: BTreeSet::default(),
|
tx_needed: BTreeSet::default(),
|
||||||
tx_missing_conftime: BTreeMap::default(),
|
tx_missing_conftime: BTreeMap::default(),
|
||||||
start_time: Instant::new(),
|
start_time: Instant::new(),
|
||||||
|
missing_script_counts: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn into_db_update(self) -> Result<D::Batch, Error> {
|
fn into_db_update(self) -> Result<D::Batch, Error> {
|
||||||
|
|||||||
@@ -486,15 +486,15 @@ macro_rules! populate_test_db {
|
|||||||
}};
|
}};
|
||||||
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
|
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use $crate::database::BatchOperations;
|
use $crate::database::SyncTime;
|
||||||
|
use $crate::database::{BatchOperations, Database};
|
||||||
let mut db = $db;
|
let mut db = $db;
|
||||||
let tx_meta = $tx_meta;
|
let tx_meta = $tx_meta;
|
||||||
let current_height: Option<u32> = $current_height;
|
let current_height: Option<u32> = $current_height;
|
||||||
let input = if $is_coinbase {
|
let mut input = vec![$crate::bitcoin::TxIn::default()];
|
||||||
vec![$crate::bitcoin::TxIn::default()]
|
if !$is_coinbase {
|
||||||
} else {
|
input[0].previous_output.vout = 0;
|
||||||
vec![]
|
}
|
||||||
};
|
|
||||||
let tx = $crate::bitcoin::Transaction {
|
let tx = $crate::bitcoin::Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: 0,
|
||||||
@@ -512,14 +512,32 @@ macro_rules! populate_test_db {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
|
// 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
|
let confirmation_time = tx_meta
|
||||||
.min_confirmations
|
.min_confirmations
|
||||||
.and_then(|v| if v == 0 { None } else { Some(v) })
|
.and_then(|v| if v == 0 { None } else { Some(v) })
|
||||||
.map(|conf| $crate::BlockTime {
|
.map(|conf| $crate::BlockTime {
|
||||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap() + 1,
|
height: current_height.expect("Current height is needed for testing transaction with min-confirmation values").checked_sub(conf as u32).unwrap() + 1,
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set the database sync_time.
|
||||||
|
// Check if the current_height is less than already known sync height, apply the max
|
||||||
|
// If any of them is None, the other will be applied instead.
|
||||||
|
// If both are None, this will not be set.
|
||||||
|
if let Some(height) = db.get_sync_time().unwrap()
|
||||||
|
.map(|sync_time| sync_time.block_time.height)
|
||||||
|
.max(current_height) {
|
||||||
|
let sync_time = SyncTime {
|
||||||
|
block_time: BlockTime {
|
||||||
|
height,
|
||||||
|
timestamp: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
db.set_sync_time(sync_time).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let tx_details = $crate::TransactionDetails {
|
let tx_details = $crate::TransactionDetails {
|
||||||
transaction: Some(tx.clone()),
|
transaction: Some(tx.clone()),
|
||||||
txid,
|
txid,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
use bitcoin::hash_types::Txid;
|
use bitcoin::hash_types::Txid;
|
||||||
@@ -60,7 +62,7 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SqliteDatabase {
|
pub struct SqliteDatabase {
|
||||||
/// Path on the local filesystem to store the sqlite file
|
/// Path on the local filesystem to store the sqlite file
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
/// A rusqlite connection object to the sqlite database
|
/// A rusqlite connection object to the sqlite database
|
||||||
pub connection: Connection,
|
pub connection: Connection,
|
||||||
}
|
}
|
||||||
@@ -68,9 +70,12 @@ pub struct SqliteDatabase {
|
|||||||
impl SqliteDatabase {
|
impl SqliteDatabase {
|
||||||
/// Instantiate a new SqliteDatabase instance by creating a connection
|
/// Instantiate a new SqliteDatabase instance by creating a connection
|
||||||
/// to the database stored at path
|
/// to the database stored at path
|
||||||
pub fn new(path: String) -> Self {
|
pub fn new<T: AsRef<Path>>(path: T) -> Self {
|
||||||
let connection = get_connection(&path).unwrap();
|
let connection = get_connection(&path).unwrap();
|
||||||
SqliteDatabase { path, connection }
|
SqliteDatabase {
|
||||||
|
path: PathBuf::from(path.as_ref()),
|
||||||
|
connection,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn insert_script_pubkey(
|
fn insert_script_pubkey(
|
||||||
&self,
|
&self,
|
||||||
@@ -908,7 +913,7 @@ impl BatchDatabase for SqliteDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connection(path: &str) -> Result<Connection, Error> {
|
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
|
||||||
let connection = Connection::open(path)?;
|
let connection = Connection::open(path)?;
|
||||||
migrate(&connection)?;
|
migrate(&connection)?;
|
||||||
Ok(connection)
|
Ok(connection)
|
||||||
|
|||||||
@@ -14,12 +14,10 @@
|
|||||||
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
||||||
//! checksum of a descriptor
|
//! checksum of a descriptor
|
||||||
|
|
||||||
use std::iter::FromIterator;
|
|
||||||
|
|
||||||
use crate::descriptor::DescriptorError;
|
use crate::descriptor::DescriptorError;
|
||||||
|
|
||||||
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||||
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||||
let c0 = c >> 35;
|
let c0 = c >> 35;
|
||||||
@@ -43,15 +41,17 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
|||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the checksum of a descriptor
|
/// Computes the checksum bytes of a descriptor
|
||||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||||
let mut c = 1;
|
let mut c = 1;
|
||||||
let mut cls = 0;
|
let mut cls = 0;
|
||||||
let mut clscount = 0;
|
let mut clscount = 0;
|
||||||
for ch in desc.chars() {
|
|
||||||
|
for ch in desc.as_bytes() {
|
||||||
let pos = INPUT_CHARSET
|
let pos = INPUT_CHARSET
|
||||||
.find(ch)
|
.iter()
|
||||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
|
.position(|b| b == ch)
|
||||||
|
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
|
||||||
c = poly_mod(c, pos & 31);
|
c = poly_mod(c, pos & 31);
|
||||||
cls = cls * 3 + (pos >> 5);
|
cls = cls * 3 + (pos >> 5);
|
||||||
clscount += 1;
|
clscount += 1;
|
||||||
@@ -67,17 +67,18 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
|||||||
(0..8).for_each(|_| c = poly_mod(c, 0));
|
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||||
c ^= 1;
|
c ^= 1;
|
||||||
|
|
||||||
let mut chars = Vec::with_capacity(8);
|
let mut checksum = [0_u8; 8];
|
||||||
for j in 0..8 {
|
for j in 0..8 {
|
||||||
chars.push(
|
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||||
CHECKSUM_CHARSET
|
|
||||||
.chars()
|
|
||||||
.nth(((c >> (5 * (7 - j))) & 31) as usize)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_iter(chars))
|
Ok(checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the checksum of a descriptor
|
||||||
|
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||||
|
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||||
|
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -97,17 +98,12 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_checksum_invalid_character() {
|
fn test_get_checksum_invalid_character() {
|
||||||
let sparkle_heart = vec![240, 159, 146, 150];
|
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||||
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
|
|
||||||
.unwrap()
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.unwrap();
|
|
||||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_checksum(&invalid_desc).err(),
|
get_checksum(&invalid_desc).err(),
|
||||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
|
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -839,7 +839,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
// - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
||||||
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
||||||
|
|
||||||
// expected script for pk and bare manually created
|
// expected script for pk and bare manually created
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ pub enum Error {
|
|||||||
/// Error while extracting and manipulating policies
|
/// Error while extracting and manipulating policies
|
||||||
Policy(crate::descriptor::policy::PolicyError),
|
Policy(crate::descriptor::policy::PolicyError),
|
||||||
|
|
||||||
/// Invalid character found in the descriptor checksum
|
/// Invalid byte found in the descriptor checksum
|
||||||
InvalidDescriptorCharacter(char),
|
InvalidDescriptorCharacter(u8),
|
||||||
|
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
Bip32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
#[doc(include = "../README.md")]
|
#[doc = include_str!("../README.md")]
|
||||||
#[cfg(doctest)]
|
#[cfg(doctest)]
|
||||||
pub struct ReadmeDoctests;
|
pub struct ReadmeDoctests;
|
||||||
|
|||||||
16
src/error.rs
16
src/error.rs
@@ -13,7 +13,7 @@ use std::fmt;
|
|||||||
|
|
||||||
use crate::bitcoin::Network;
|
use crate::bitcoin::Network;
|
||||||
use crate::{descriptor, wallet, wallet::address_validator};
|
use crate::{descriptor, wallet, wallet::address_validator};
|
||||||
use bitcoin::OutPoint;
|
use bitcoin::{OutPoint, Txid};
|
||||||
|
|
||||||
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -125,6 +125,10 @@ pub enum Error {
|
|||||||
//DifferentDescriptorStructure,
|
//DifferentDescriptorStructure,
|
||||||
//Uncapable(crate::blockchain::Capability),
|
//Uncapable(crate::blockchain::Capability),
|
||||||
//MissingCachedAddresses,
|
//MissingCachedAddresses,
|
||||||
|
/// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which
|
||||||
|
/// are needed to satisfy `stop_gap`.
|
||||||
|
MissingCachedScripts(MissingCachedScripts),
|
||||||
|
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
/// Electrum client error
|
/// Electrum client error
|
||||||
Electrum(electrum_client::Error),
|
Electrum(electrum_client::Error),
|
||||||
@@ -145,6 +149,16 @@ pub enum Error {
|
|||||||
Rusqlite(rusqlite::Error),
|
Rusqlite(rusqlite::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
||||||
|
/// on cached `scriptPubKey`s.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MissingCachedScripts {
|
||||||
|
/// Number of scripts in which txs were requested during last request.
|
||||||
|
pub last_count: usize,
|
||||||
|
/// Minimum number of scripts to cache more of in order to satisfy `stop_gap`.
|
||||||
|
pub missing_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{:?}", self)
|
||||||
|
|||||||
@@ -43,10 +43,6 @@
|
|||||||
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
|
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
|
||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
|
||||||
//! bdk = "0.20.0"
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! # Examples
|
//! # Examples
|
||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
feature = "electrum",
|
feature = "electrum",
|
||||||
@@ -81,6 +77,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
//!
|
//!
|
||||||
//! ## Generate a few addresses
|
//! ## Generate a few addresses
|
||||||
//!
|
//!
|
||||||
|
//! ### Example
|
||||||
//! ```
|
//! ```
|
||||||
//! use bdk::{Wallet};
|
//! use bdk::{Wallet};
|
||||||
//! use bdk::database::MemoryDatabase;
|
//! use bdk::database::MemoryDatabase;
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
@@ -477,7 +477,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 100_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +486,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
assert_eq!(wallet.get_balance().unwrap().get_total(), 0);
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
@@ -494,8 +494,16 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().confirmed, 100_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -508,7 +516,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 105_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
||||||
|
|
||||||
@@ -532,7 +540,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
||||||
}
|
}
|
||||||
@@ -546,14 +554,14 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 25_000 )
|
@tx ( (@external descriptors, 0) => 25_000 )
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -566,7 +574,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
||||||
|
|
||||||
@@ -580,7 +588,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after bump");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
||||||
|
|
||||||
@@ -603,8 +611,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
|
|
||||||
@@ -617,7 +624,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after invalidate");
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
|
||||||
@@ -635,7 +642,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
@@ -646,7 +653,12 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().trusted_pending, details.received, "incorrect balance after send");
|
||||||
|
|
||||||
|
test_client.generate(1, Some(node_addr));
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap().confirmed, details.received, "incorrect balance after send");
|
||||||
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
@@ -720,7 +732,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
let tx1 = {
|
let tx1 = {
|
||||||
@@ -743,9 +755,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
blockchain.broadcast(&tx1).expect("broadcasting first");
|
||||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
||||||
|
|
||||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
||||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
assert_eq!(receiver_wallet.get_balance().expect("balance").untrusted_pending, 49_000, "should have received coins once and only once");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -771,7 +782,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
let balance = wallet.get_balance().unwrap();
|
||||||
|
assert_eq!(balance.untrusted_pending + balance.get_spendable(), 100_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -785,7 +797,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
let details = tx_map.get(&received_txid).unwrap();
|
let details = tx_map.get(&received_txid).unwrap();
|
||||||
@@ -809,7 +821,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
@@ -821,7 +833,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&sent_tx).unwrap();
|
blockchain.broadcast(&sent_tx).unwrap();
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance after receive");
|
||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||||
@@ -852,7 +864,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut total_sent = 0;
|
let mut total_sent = 0;
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
@@ -869,7 +881,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance after chain");
|
||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
|
|
||||||
@@ -879,7 +891,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +905,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||||
@@ -902,8 +914,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance from received");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||||
@@ -912,8 +924,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance from received after bump");
|
||||||
|
|
||||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
}
|
}
|
||||||
@@ -928,7 +940,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -937,8 +949,8 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect received after send");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
|
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
|
||||||
@@ -947,7 +959,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after change removal");
|
||||||
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
||||||
|
|
||||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
@@ -963,7 +975,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -972,7 +984,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
@@ -983,7 +995,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance after add input");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -996,7 +1008,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
@@ -1005,7 +1017,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
@@ -1018,7 +1030,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after add input");
|
||||||
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,7 +1044,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||||
|
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
let data = [42u8;80];
|
let data = [42u8;80];
|
||||||
@@ -1047,7 +1059,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
|
|
||||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
let _ = tx_map.get(&tx.txid()).unwrap();
|
let _ = tx_map.get(&tx.txid()).unwrap();
|
||||||
@@ -1058,22 +1070,24 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
println!("wallet addr: {}", wallet_addr);
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().immature, 0, "incorrect balance");
|
||||||
|
|
||||||
test_client.generate(1, Some(wallet_addr));
|
test_client.generate(1, Some(wallet_addr));
|
||||||
|
|
||||||
#[cfg(feature = "rpc")]
|
|
||||||
{
|
|
||||||
// rpc consider coinbase only when mature (100 blocks)
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
test_client.generate(100, Some(node_addr));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
|
||||||
|
assert!(wallet.get_balance().unwrap().immature > 0, "incorrect balance after receiving coinbase");
|
||||||
|
|
||||||
|
// make coinbase mature (100 blocks)
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
test_client.generate(100, Some(node_addr));
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
assert!(wallet.get_balance().unwrap().confirmed > 0, "incorrect balance after maturing coinbase");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1150,7 +1164,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "wallet has incorrect balance");
|
||||||
|
|
||||||
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
||||||
|
|
||||||
@@ -1162,7 +1176,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx();
|
||||||
blockchain.broadcast(&tx).unwrap();
|
blockchain.broadcast(&tx).unwrap();
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "wallet has incorrect balance after send");
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
||||||
test_client.generate(1, None);
|
test_client.generate(1, None);
|
||||||
@@ -1268,12 +1282,12 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
let _ = test_client.receive(testutils! {
|
let _ = test_client.receive(testutils! {
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
let tx = {
|
let tx = {
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
@@ -1296,7 +1310,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
});
|
});
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||||
|
|
||||||
let tx = {
|
let tx = {
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
@@ -1317,7 +1331,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
|
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
|
||||||
});
|
});
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000);
|
||||||
|
|
||||||
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
||||||
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();
|
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
|||||||
|
|
||||||
if self.config_with_stop_gap(test_client, 0).is_some() {
|
if self.config_with_stop_gap(test_client, 0).is_some() {
|
||||||
test_wallet_sync_with_stop_gaps(test_client, self);
|
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 {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"{}: Skipped tests requiring config_with_stop_gap.",
|
"{}: Skipped tests requiring config_with_stop_gap.",
|
||||||
@@ -113,16 +115,21 @@ where
|
|||||||
} else {
|
} else {
|
||||||
max_balance
|
max_balance
|
||||||
};
|
};
|
||||||
|
|
||||||
// perform wallet sync
|
|
||||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
|
||||||
|
|
||||||
let wallet_balance = wallet.get_balance().unwrap();
|
|
||||||
|
|
||||||
let details = format!(
|
let details = format!(
|
||||||
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
|
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
|
||||||
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!(
|
assert!(
|
||||||
wallet_balance <= max_balance,
|
wallet_balance <= max_balance,
|
||||||
"wallet balance is greater than received amount: {}",
|
"wallet balance is greater than received amount: {}",
|
||||||
@@ -138,3 +145,113 @@ where
|
|||||||
test_client.generate(1, None);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
161
src/types.rs
161
src/types.rs
@@ -51,14 +51,44 @@ impl AsRef<[u8]> for KeychainKind {
|
|||||||
pub struct FeeRate(f32);
|
pub struct FeeRate(f32);
|
||||||
|
|
||||||
impl FeeRate {
|
impl FeeRate {
|
||||||
|
/// Create a new instance checking the value provided
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
|
fn new_checked(value: f32) -> Self {
|
||||||
|
assert!(value.is_normal() || value == 0.0);
|
||||||
|
assert!(value.is_sign_positive());
|
||||||
|
|
||||||
|
FeeRate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
||||||
|
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
||||||
|
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
||||||
FeeRate(btc_per_kvb * 1e5)
|
FeeRate::new_checked(btc_per_kvb * 1e5)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||||
pub const fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
///
|
||||||
FeeRate(sat_per_vb)
|
/// ## Panics
|
||||||
|
///
|
||||||
|
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||||
|
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||||
|
FeeRate::new_checked(sat_per_vb)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||||
@@ -78,7 +108,7 @@ impl FeeRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value as satoshi/vbyte
|
/// Return the value as satoshi/vbyte
|
||||||
pub fn as_sat_vb(&self) -> f32 {
|
pub fn as_sat_per_vb(&self) -> f32 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +119,7 @@ impl FeeRate {
|
|||||||
|
|
||||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||||
(self.as_sat_vb() * vbytes as f32).ceil() as u64
|
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +237,7 @@ pub struct TransactionDetails {
|
|||||||
/// Sent value (sats)
|
/// Sent value (sats)
|
||||||
/// Sum of owned inputs of this transaction.
|
/// Sum of owned inputs of this transaction.
|
||||||
pub sent: u64,
|
pub sent: u64,
|
||||||
/// Fee value (sats) if available.
|
/// Fee value (sats) if confirmed.
|
||||||
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||||
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||||
/// funds while offline.
|
/// funds while offline.
|
||||||
@@ -242,13 +272,130 @@ impl BlockTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Balance differentiated in various categories
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
|
pub struct Balance {
|
||||||
|
/// All coinbase outputs not yet matured
|
||||||
|
pub immature: u64,
|
||||||
|
/// Unconfirmed UTXOs generated by a wallet tx
|
||||||
|
pub trusted_pending: u64,
|
||||||
|
/// Unconfirmed UTXOs received from an external wallet
|
||||||
|
pub untrusted_pending: u64,
|
||||||
|
/// Confirmed and immediately spendable balance
|
||||||
|
pub confirmed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Balance {
|
||||||
|
/// Get sum of trusted_pending and confirmed coins
|
||||||
|
pub fn get_spendable(&self) -> u64 {
|
||||||
|
self.confirmed + self.trusted_pending
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the whole balance visible to the wallet
|
||||||
|
pub fn get_total(&self) -> u64 {
|
||||||
|
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Balance {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
|
||||||
|
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Balance {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
immature: self.immature + other.immature,
|
||||||
|
trusted_pending: self.trusted_pending + other.trusted_pending,
|
||||||
|
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
|
||||||
|
confirmed: self.confirmed + other.confirmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::iter::Sum for Balance {
|
||||||
|
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
||||||
|
iter.fold(
|
||||||
|
Balance {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|a, b| a + b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_store_feerate_in_const() {
|
fn can_store_feerate_in_const() {
|
||||||
const _MY_RATE: FeeRate = FeeRate::from_sat_per_vb(10.0);
|
|
||||||
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_neg_value() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(-5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_nan() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_invalid_feerate_inf() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_feerate_pos_zero() {
|
||||||
|
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_btc_per_kvb() {
|
||||||
|
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_vbyte() {
|
||||||
|
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_default_min_relay_fee() {
|
||||||
|
let fee = FeeRate::default_min_relay_fee();
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kvb() {
|
||||||
|
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fee_from_sat_per_kwu() {
|
||||||
|
let fee = FeeRate::from_sat_per_kwu(250.0);
|
||||||
|
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
64
src/wallet/hardwaresigner.rs
Normal file
64
src/wallet/hardwaresigner.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
//! HWI Signer
|
||||||
|
//!
|
||||||
|
//! This module contains a simple implementation of a Custom signer for rust-hwi
|
||||||
|
|
||||||
|
use bitcoin::psbt::PartiallySignedTransaction;
|
||||||
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
|
|
||||||
|
use hwi::error::Error;
|
||||||
|
use hwi::types::{HWIChain, HWIDevice};
|
||||||
|
use hwi::HWIClient;
|
||||||
|
|
||||||
|
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Custom signer for Hardware Wallets
|
||||||
|
///
|
||||||
|
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
|
||||||
|
pub struct HWISigner {
|
||||||
|
fingerprint: Fingerprint,
|
||||||
|
client: HWIClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HWISigner {
|
||||||
|
/// Create a instance from the specified device and chain
|
||||||
|
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
|
||||||
|
let client = HWIClient::get_client(device, false, chain)?;
|
||||||
|
Ok(HWISigner {
|
||||||
|
fingerprint: device.fingerprint,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerCommon for HWISigner {
|
||||||
|
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||||
|
SignerId::Fingerprint(self.fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This implementation ignores `sign_options`
|
||||||
|
impl TransactionSigner for HWISigner {
|
||||||
|
fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
psbt: &mut PartiallySignedTransaction,
|
||||||
|
_sign_options: &crate::SignOptions,
|
||||||
|
_secp: &crate::wallet::utils::SecpCtx,
|
||||||
|
) -> Result<(), SignerError> {
|
||||||
|
psbt.combine(self.client.sign_tx(psbt)?.psbt)
|
||||||
|
.expect("Failed to combine HW signed psbt with passed PSBT");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@
|
|||||||
//! &self,
|
//! &self,
|
||||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
//! input_index: usize,
|
//! input_index: usize,
|
||||||
|
//! _sign_options: &SignOptions,
|
||||||
//! _secp: &Secp256k1<All>,
|
//! _secp: &Secp256k1<All>,
|
||||||
//! ) -> Result<(), SignerError> {
|
//! ) -> Result<(), SignerError> {
|
||||||
//! self.device.hsm_sign_input(psbt, input_index)?;
|
//! self.device.hsm_sign_input(psbt, input_index)?;
|
||||||
@@ -158,6 +159,16 @@ pub enum SignerError {
|
|||||||
InvalidSighash,
|
InvalidSighash,
|
||||||
/// Error while computing the hash to sign
|
/// Error while computing the hash to sign
|
||||||
SighashError(sighash::Error),
|
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 {
|
impl From<sighash::Error> for SignerError {
|
||||||
@@ -241,6 +252,7 @@ pub trait InputSigner: SignerCommon {
|
|||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError>;
|
) -> Result<(), SignerError>;
|
||||||
}
|
}
|
||||||
@@ -254,6 +266,7 @@ pub trait TransactionSigner: SignerCommon {
|
|||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError>;
|
) -> Result<(), SignerError>;
|
||||||
}
|
}
|
||||||
@@ -262,10 +275,11 @@ impl<T: InputSigner> TransactionSigner for T {
|
|||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
for input_index in 0..psbt.inputs.len() {
|
for input_index in 0..psbt.inputs.len() {
|
||||||
self.sign_input(psbt, input_index, secp)?;
|
self.sign_input(psbt, input_index, sign_options, secp)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -287,6 +301,7 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
|||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
if input_index >= psbt.inputs.len() {
|
if input_index >= psbt.inputs.len() {
|
||||||
@@ -346,7 +361,7 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
|||||||
inner: derived_key.private_key,
|
inner: derived_key.private_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp)
|
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,6 +384,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
|||||||
&self,
|
&self,
|
||||||
psbt: &mut psbt::PartiallySignedTransaction,
|
psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
|
sign_options: &SignOptions,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||||
@@ -385,7 +401,10 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
|||||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||||
|
|
||||||
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
||||||
if is_internal_key && psbt.inputs[input_index].tap_key_sig.is_none() {
|
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)?;
|
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||||
sign_psbt_schnorr(
|
sign_psbt_schnorr(
|
||||||
&self.inner,
|
&self.inner,
|
||||||
@@ -404,9 +423,18 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
|||||||
let leaf_hashes = leaf_hashes
|
let leaf_hashes = leaf_hashes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|lh| {
|
.filter(|lh| {
|
||||||
!psbt.inputs[input_index]
|
// Removing the leaves we shouldn't sign for
|
||||||
.tap_script_sigs
|
let should_sign = match &sign_options.tap_leaves_options {
|
||||||
.contains_key(&(x_only_pubkey, **lh))
|
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()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -457,10 +485,10 @@ fn sign_psbt_ecdsa(
|
|||||||
hash_ty: EcdsaSighashType,
|
hash_ty: EcdsaSighashType,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) {
|
) {
|
||||||
let sig = secp.sign_ecdsa(
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
let sig = secp.sign_ecdsa(msg, secret_key);
|
||||||
secret_key,
|
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||||
);
|
.expect("invalid or corrupted ecdsa signature");
|
||||||
|
|
||||||
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
|
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
|
||||||
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
||||||
@@ -486,10 +514,10 @@ fn sign_psbt_schnorr(
|
|||||||
Some(_) => keypair, // no tweak for script spend
|
Some(_) => keypair, // no tweak for script spend
|
||||||
};
|
};
|
||||||
|
|
||||||
let sig = secp.sign_schnorr(
|
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
let sig = secp.sign_schnorr(msg, &keypair);
|
||||||
&keypair,
|
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair))
|
||||||
);
|
.expect("invalid or corrupted schnorr signature");
|
||||||
|
|
||||||
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
||||||
|
|
||||||
@@ -677,6 +705,38 @@ pub struct SignOptions {
|
|||||||
///
|
///
|
||||||
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
||||||
pub try_finalize: bool,
|
pub try_finalize: bool,
|
||||||
|
|
||||||
|
/// Specifies which Taproot script-spend leaves we should sign for. This option is
|
||||||
|
/// ignored if we're signing a non-taproot PSBT.
|
||||||
|
///
|
||||||
|
/// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
|
||||||
|
pub tap_leaves_options: TapLeavesOptions,
|
||||||
|
|
||||||
|
/// Whether we should try to sign a taproot transaction with the taproot internal key
|
||||||
|
/// or not. This option is ignored if we're signing a non-taproot PSBT.
|
||||||
|
///
|
||||||
|
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||||
|
pub sign_with_tap_internal_key: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Customize which taproot script-path leaves the signer should sign.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum TapLeavesOptions {
|
||||||
|
/// The signer will sign all the leaves it has a key for.
|
||||||
|
All,
|
||||||
|
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
||||||
|
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||||
|
Include(Vec<taproot::TapLeafHash>),
|
||||||
|
/// The signer won't sign the specified leaves.
|
||||||
|
Exclude(Vec<taproot::TapLeafHash>),
|
||||||
|
/// The signer won't sign any leaf.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TapLeavesOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
TapLeavesOptions::All
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
#[allow(clippy::derivable_impls)]
|
||||||
@@ -688,6 +748,8 @@ impl Default for SignOptions {
|
|||||||
allow_all_sighashes: false,
|
allow_all_sighashes: false,
|
||||||
remove_partial_sigs: true,
|
remove_partial_sigs: true,
|
||||||
try_finalize: true,
|
try_finalize: true,
|
||||||
|
tap_leaves_options: TapLeavesOptions::default(),
|
||||||
|
sign_with_tap_internal_key: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1080,7 @@ mod signers_container_tests {
|
|||||||
fn sign_transaction(
|
fn sign_transaction(
|
||||||
&self,
|
&self,
|
||||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||||
|
_sign_options: &SignOptions,
|
||||||
_secp: &SecpCtx,
|
_secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) include_output_redeem_witness_script: bool,
|
pub(crate) include_output_redeem_witness_script: bool,
|
||||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||||
pub(crate) current_height: Option<u32>,
|
pub(crate) current_height: Option<u32>,
|
||||||
|
pub(crate) allow_dust: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -516,7 +517,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish the building the transaction.
|
/// Finish building the transaction.
|
||||||
///
|
///
|
||||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||||
///
|
///
|
||||||
@@ -560,6 +561,14 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
|||||||
self.params.current_height = Some(height);
|
self.params.current_height = Some(height);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether or not the dust limit is checked.
|
||||||
|
///
|
||||||
|
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
|
||||||
|
pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
|
||||||
|
self.params.allow_dust = allow_dust;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ mod test {
|
|||||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
||||||
};
|
};
|
||||||
use crate::bitcoin::Address;
|
use crate::bitcoin::Address;
|
||||||
use crate::types::FeeRate;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -164,24 +163,6 @@ mod test {
|
|||||||
assert!(!294.is_dust(&script_p2wpkh));
|
assert!(!294.is_dust(&script_p2wpkh));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_btc_per_kb() {
|
|
||||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_from_sats_vbyte() {
|
|
||||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fee_default_min_relay_fee() {
|
|
||||||
let fee = FeeRate::default_min_relay_fee();
|
|
||||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_msb_set() {
|
fn test_check_nsequence_rbf_msb_set() {
|
||||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user