Compare commits

..

1 Commits

Author SHA1 Message Date
Alekos Filini
5493379806 Bump version to 0.21.0 2022-08-11 14:39:18 +02:00
40 changed files with 932 additions and 1562 deletions

View File

@@ -1,98 +0,0 @@
---
name: Minor Release
about: Create a new minor release [for release managers only]
title: 'Release MAJOR.MINOR+1.0'
labels: 'release'
assignees: ''
---
## Create a new minor release
### Summary
<--release summary to be used in announcements-->
### Commit
<--latest commit ID to include in this release-->
### Changelog
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
### Checklist
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
branch **development** version is *MAJOR.MINOR.0*.
#### On the day of the feature freeze
Change the `master` branch to the next MINOR+1 version:
- [ ] Switch to the `master` branch.
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR+1`, eg. `bump_dev_0_22`.
- [ ] Bump the `bump_dev_MAJOR_MINOR+1` branch to the next development MINOR+1 version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR+1` branch to `master`.
- Title PR "Bump version to MAJOR.MINOR+1.0".
Create a new release branch and release candidate tag:
- [ ] Double check that your local `master` is up-to-date with the upstream repo.
- [ ] Create a new branch called `release/MAJOR.MINOR+1` from `master`.
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.1`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.1".
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
- The tag name should be `vMAJOR.MINOR+1.0-rc.1`
- Use message "Release MAJOR.MINOR+1.0 rc.1".
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
- [ ] Push the `release/MAJOR.MINOR` branch and new tag to the `bitcoindevkit/bdk` repo.
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.1` tag.
If any issues need to be fixed before the *MAJOR.MINOR+1.0* version is released:
- [ ] Merge fix PRs to the `master` branch.
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR+1` branch.
- [ ] Verify fixes in `release/MAJOR.MINOR+1` branch.
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.x+1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.x+1`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.x+1".
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
- The tag name should be `vMAJOR.MINOR+1.0-rc.x+1`, where x is the current release candidate number.
- Use tag message "Release MAJOR.MINOR+1.0 rc.x+1".
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.x+1` tag.
#### On the day of the release
Tag and publish new release:
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
- The tag name should be `vMAJOR.MINOR+1.0`
- The first line of the tag message should be "Release MAJOR.MINOR+1.0".
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
- [ ] Wait for the CI to finish one last time.
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
- [ ] Publish **all** the updated crates to crates.io.
- [ ] Create the release on GitHub.
- Go to "tags", click on the dots on the right and select "Create Release".
- Set the title to `Release MAJOR.MINOR+1.0`.
- In the release notes body put the **Summary** and **Changelog**.
- Use the "+ Auto-generate release notes" button to add details from included PRs.
- Until we reach a `1.0.0` release check the "Pre-release" box.
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
- [ ] Celebrate 🎉
[Semantic Versioning]: https://semver.org/
[crates.io]: https://crates.io/crates/bdk
[docs.rs]: https://docs.rs/bdk/latest/bdk
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/

View File

@@ -1,70 +0,0 @@
---
name: Patch Release
about: Create a new patch release [for release managers only]
title: 'Release MAJOR.MINOR.PATCH+1'
labels: 'release'
assignees: ''
---
## Create a new patch release
### Summary
<--release summary to be used in announcements-->
### Commit
<--latest commit ID to include in this release-->
### Changelog
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
### Checklist
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
branch **development** version is *MAJOR.MINOR.PATCH*.
### On the day of the patch release
Change the `master` branch to the new PATCH+1 version:
- [ ] Switch to the `master` branch.
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR_PATCH+1`, eg. `bump_dev_0_22_1`.
- [ ] Bump the `bump_dev_MAJOR_MINOR` branch to the next development PATCH+1 version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR.PATCH+1`.
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR_PATCH+1` branch to `master`.
- Title PR "Bump version to MAJOR.MINOR.PATCH+1".
Cherry-pick, tag and publish new PATCH+1 release:
- [ ] Merge fix PRs to the `master` branch.
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR` branch to be patched.
- [ ] Verify fixes in `release/MAJOR.MINOR` branch.
- [ ] Bump the `release/MAJOR.MINOR.PATCH+1` branch to `MAJOR.MINOR.PATCH+1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR.MINOR.PATCH+1`.
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR` branch.
- The tag name should be `vMAJOR.MINOR.PATCH+1`
- The first line of the tag message should be "Release MAJOR.MINOR.PATCH+1".
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
- [ ] Wait for the CI to finish one last time.
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
- [ ] Publish **all** the updated crates to crates.io.
- [ ] Create the release on GitHub.
- Go to "tags", click on the dots on the right and select "Create Release".
- Set the title to `Release MAJOR.MINOR.PATCH+1`.
- In the release notes body put the **Summary** and **Changelog**.
- Use the "+ Auto-generate release notes" button to add details from included PRs.
- Until we reach a `1.0.0` release check the "Pre-release" box.
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
- [ ] Celebrate 🎉
[Semantic Versioning]: https://semver.org/
[crates.io]: https://crates.io/crates/bdk
[docs.rs]: https://docs.rs/bdk/latest/bdk
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/

View File

@@ -9,11 +9,6 @@
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
of the PR were done in a specific way -->
### Changelog notice
<!-- Notice the release manager should include in the release tag message changelog -->
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
### Checklists
#### All Submissions:
@@ -26,6 +21,7 @@ of the PR were done in a specific way -->
* [ ] I've added tests for the new feature
* [ ] I've added docs for the new feature
* [ ] I've updated `CHANGELOG.md`
#### Bugfixes:

View File

@@ -1,61 +1,37 @@
on: [push, pull_request]
on: [push]
name: Code Coverage
jobs:
Codecov:
name: Code Coverage
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-Cinstrument-coverage"
RUSTDOCFLAGS: "-Cinstrument-coverage"
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install lcov tools
run: sudo apt-get install lcov -y
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Add llvm tools
run: rustup component add llvm-tools-preview
- name: Update toolchain
run: rustup update
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
- name: Test
# WARNING: this is not testing the following features: test-esplora, test-hardware-signer, async-interface
# This is because some of our features are mutually exclusive, and generating various reports and
# merging them doesn't seem to be working very well.
# For more info, see:
# - https://github.com/bitcoindevkit/bdk/issues/696
# - https://github.com/bitcoindevkit/bdk/pull/748#issuecomment-1242721040
run: cargo test --features all-keys,compact_filters,compiler,key-value-db,sqlite,sqlite-bundled,test-electrum,test-rpc,verify
- name: Run grcov
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
- name: Generate HTML coverage report
run: genhtml -o coverage-report.html ./coverage/lcov.info
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
- id: coverage
name: Generate coverage
uses: actions-rs/grcov@v0.1.5
- name: Coveralls upload
uses: coverallsapp/github-action@master
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: coverage-report
path: coverage-report.html
file: ${{ steps.coverage.outputs.report }}
directory: ./coverage/reports/

View File

@@ -17,16 +17,16 @@ jobs:
- default
- minimal
- all-keys
- minimal,use-esplora-blocking
- minimal,use-esplora-ureq
- key-value-db
- electrum
- compact_filters
- use-esplora-blocking,key-value-db,electrum
- esplora,ureq,key-value-db,electrum
- compiler
- rpc
- verify
- async-interface
- use-esplora-async
- use-esplora-reqwest
- sqlite
- sqlite-bundled
steps:
@@ -100,10 +100,10 @@ jobs:
features: test-rpc-legacy
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-async,verify
features: test-esplora,use-esplora-reqwest,verify
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-blocking,verify
features: test-esplora,use-esplora-ureq,verify
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -154,7 +154,7 @@ jobs:
- name: Update toolchain
run: rustup update
- name: Check
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async --no-default-features
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
fmt:
name: Rust fmt
@@ -172,32 +172,3 @@ jobs:
run: rustup update
- name: Check fmt
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

View File

@@ -24,7 +24,7 @@ jobs:
- name: Update toolchain
run: rustup update
- name: Build docs
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,use-esplora-blocking,compact_filters,rpc,key-value-db,sqlite,all-keys,verify,hardware-signer -- --cfg docsrs -Dwarnings
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
- name: Upload artifact
uses: actions/upload-artifact@v2
with:

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
/target
Cargo.lock
/.vscode
*.swp
.idea

View File

@@ -1,12 +1,11 @@
# Changelog
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.
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [v0.21.0] - [v0.20.0]
- Add `descriptor::checksum::get_checksum_bytes` method.
@@ -26,7 +25,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)
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
- Set coin type in BIP44, BIP49, and BIP84 templates
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
- Deprecate `AddressValidator`
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
@@ -38,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Unpinned tokio to `1`
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
- Upgrade to rust-bitcoin `0.28`
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
- Update `PkOrF` in the policy module to become an enum
- Add experimental support for Taproot, including:
- Support for `tr()` descriptors with complex tapscript trees
@@ -55,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
- Improve key generation traits
- 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]
@@ -493,3 +492,4 @@ final transaction is created by calling `finish` on the builder.
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...HEAD

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.23.0"
version = "0.21.0-rc.1"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -22,16 +22,17 @@ rand = "^0.7"
# Optional dependencies
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.11", optional = true }
esplora-client = { version = "0.1.1", default-features = false, optional = true }
electrum-client = { version = "0.10", optional = true }
rusqlite = { version = "0.27.0", optional = true }
ahash = { version = "0.7.6", optional = true }
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
ureq = { version = "~2.2.0", features = ["json"], optional = true }
futures = { version = "0.3", optional = true }
async-trait = { version = "0.1", optional = true }
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
cc = { version = ">=1.0.64", optional = true }
socks = { version = "0.3", optional = true }
hwi = { version = "0.2.2", optional = true }
lazy_static = { version = "1.4", optional = true }
bip39 = { version = "1.0.1", optional = true }
bitcoinconsensus = { version = "0.19.0-3", optional = true }
@@ -55,12 +56,11 @@ verify = ["bitcoinconsensus"]
default = ["key-value-db", "electrum"]
sqlite = ["rusqlite", "ahash"]
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
compact_filters = ["rocksdb", "socks", "cc"]
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
key-value-db = ["sled"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
rpc = ["bitcoincore-rpc"]
hardware-signer = ["hwi"]
# We currently provide mulitple implementations of `Blockchain`, all are
# blocking except for the `EsploraBlockchain` which can be either async or
@@ -68,26 +68,23 @@ hardware-signer = ["hwi"]
#
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
# access to the asynchronous method implementations. Then, if Esplora is wanted,
# enable the `use-esplora-async` feature.
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
# - Users wanting blocking HTTP calls can use any of the other blockchain
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
# use Esplora should enable the `use-esplora-blocking` feature.
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
#
# WARNING: Please take care with the features below, various combinations will
# fail to build. We cannot currently build `bdk` with `--all-features`.
async-interface = ["async-trait"]
electrum = ["electrum-client"]
# MUST ALSO USE `--no-default-features`.
use-esplora-async = ["esplora", "esplora-client/async", "futures"]
use-esplora-blocking = ["esplora", "esplora-client/blocking"]
# Deprecated aliases
use-esplora-reqwest = ["use-esplora-async"]
use-esplora-ureq = ["use-esplora-blocking"]
use-esplora-reqwest = ["esplora", "reqwest", "reqwest/socks", "futures"]
use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
# Typical configurations will not need to use `esplora` feature directly.
esplora = []
# Use below feature with `use-esplora-async` to enable reqwest default TLS support
reqwest-default-tls = ["esplora-client/async-https"]
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
reqwest-default-tls = ["reqwest/default-tls"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
@@ -96,12 +93,12 @@ 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-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
test-md-docs = ["electrum"]
test-hardware-signer = ["hardware-signer"]
[dev-dependencies]
lazy_static = "1.4"
env_logger = "0.7"
electrsd = "0.20"
clap = "2.33"
electrsd = "0.19.1"
[[example]]
name = "address_validator"
@@ -114,23 +111,14 @@ name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]
[[example]]
name = "policy"
path = "examples/policy.rs"
[[example]]
name = "rpcwallet"
path = "examples/rpcwallet.rs"
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
[[example]]
name = "psbt_signer"
path = "examples/psbt_signer.rs"
required-features = ["electrum"]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]
features = ["compiler", "electrum", "esplora", "use-esplora-blocking", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
# defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -1,16 +1,46 @@
# Development Cycle
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.
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".
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 reaches a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 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**.
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: * - - #
```
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
[Semantic Versioning]: https://semver.org/
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
## Making the Release
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
4. During the one week of feature freeze run additional tests on the release branch.
5. If a bug is found:
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
6. Update the changelog with the new release version.
7. Update `src/lib.rs` with the new version (line ~43)
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
11. Publish **all** the updated crates to crates.io.
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
13. Merge the release branch back into `master`.
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (i.e. `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:

View File

@@ -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://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://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></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://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://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>

View File

@@ -1,9 +0,0 @@
# 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"]

View File

@@ -1,30 +0,0 @@
{
"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 Normal file
View File

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

View File

@@ -11,6 +11,7 @@
extern crate bdk;
extern crate bitcoin;
extern crate clap;
extern crate log;
extern crate miniscript;
extern crate serde_json;
@@ -20,6 +21,8 @@ use std::str::FromStr;
use log::info;
use clap::{App, Arg};
use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
@@ -28,49 +31,75 @@ use bdk::database::memory::MemoryDatabase;
use bdk::wallet::AddressIndex::New;
use bdk::{KeychainKind, Wallet};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
/// can be derived from the policy.
///
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
// We start with a generic miniscript policy string
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
info!("Compiling policy: \n{}", policy_str);
let matches = App::new("Miniscript Compiler")
.arg(
Arg::with_name("POLICY")
.help("Sets the spending policy to compile")
.required(true)
.index(1),
)
.arg(
Arg::with_name("TYPE")
.help("Sets the script type used to embed the compiled policy")
.required(true)
.index(2)
.possible_values(&["sh", "wsh", "sh-wsh"]),
)
.arg(
Arg::with_name("parsed_policy")
.long("parsed_policy")
.short("p")
.help("Also return the parsed spending policy in JSON format"),
)
.arg(
Arg::with_name("network")
.short("n")
.long("network")
.help("Sets the network")
.takes_value(true)
.default_value("testnet")
.possible_values(&["testnet", "regtest", "bitcoin", "signet"]),
)
.get_matches();
let policy_str = matches.value_of("POLICY").unwrap();
info!("Compiling policy: {}", policy_str);
// Parse the string as a [`Concrete`] type miniscript policy.
let policy = Concrete::<String>::from_str(policy_str)?;
// Create a `wsh` type descriptor from the policy.
// `policy.compile()` returns the resulting miniscript from the policy.
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
let descriptor = match matches.value_of("TYPE").unwrap() {
"sh" => Descriptor::new_sh(policy.compile()?)?,
"wsh" => Descriptor::new_wsh(policy.compile()?)?,
"sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?)?,
_ => panic!("Invalid type"),
};
info!("Compiled into following Descriptor: \n{}", descriptor);
info!("... Descriptor: {}", descriptor);
let database = MemoryDatabase::new();
// Create a new wallet from this descriptor
let wallet = Wallet::new(&format!("{}", descriptor), None, Network::Regtest, database)?;
let network = matches
.value_of("network")
.map(Network::from_str)
.transpose()
.unwrap()
.unwrap_or(Network::Testnet);
let wallet = Wallet::new(&format!("{}", descriptor), None, network, database)?;
info!(
"First derived address from the descriptor: \n{}",
wallet.get_address(New)?
);
info!("... First address: {}", wallet.get_address(New)?);
// BDK also has it's own `Policy` structure to represent the spending condition in a more
// human readable json format.
let spending_policy = wallet.policies(KeychainKind::External)?;
info!(
"The BDK spending policy: \n{}",
serde_json::to_string_pretty(&spending_policy)?
);
if matches.is_present("parsed_policy") {
let spending_policy = wallet.policies(KeychainKind::External)?;
info!(
"... Spending policy:\n{}",
serde_json::to_string_pretty(&spending_policy)?
);
}
Ok(())
}

View File

@@ -1,66 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk;
extern crate env_logger;
extern crate log;
use std::error::Error;
use bdk::bitcoin::Network;
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk::wallet::signer::SignersContainer;
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
///
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
/// This is useful to express complex miniscript spending conditions into more human readable form.
/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
/// to spend from.
///
/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
/// one of the Extend Private key.
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let secp = bitcoin::secp256k1::Secp256k1::new();
// The descriptor used in the example
// The form is "wsh(multi(2, <privkey>, <pubkey>))"
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
// Use the descriptor string to derive the full descriptor and a keymap.
// The wallet descriptor can be used to create a new bdk::wallet.
// While the `keymap` can be used to create a `SignerContainer`.
//
// The `SignerContainer` can sign for `PSBT`s.
// a bdk::wallet internally uses these to handle transaction signing.
// But they can be used as independent tools also.
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
log::info!("Example Descriptor for policy analysis : {}", wallet_desc);
// Create the signer with the keymap and descriptor.
let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
// Extract the Policy from the given descriptor and signer.
// Note that Policy is a wallet specific structure. It depends on the the descriptor, and
// what the concerned wallet with a given signer can sign for.
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
.expect("We expect a policy");
log::info!("Derived Policy for the descriptor {:#?}", policy);
Ok(())
}

View File

@@ -1,102 +0,0 @@
// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::wallet::AddressIndex;
use bdk::SyncOptions;
use bdk::{FeeRate, SignOptions, Wallet};
use bitcoin::{Address, Network};
use electrum_client::Client;
use std::error::Error;
use std::str::FromStr;
fn main() -> Result<(), Box<dyn Error>> {
// test keys created with `bdk-cli key generate` and `bdk-cli key derive` commands
let signing_external_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*)";
let signing_internal_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*)";
let watch_only_external_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/0/*)";
let watch_only_internal_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/1/*)";
// create client for Blockstream's testnet electrum server
let blockchain =
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
// create watch only wallet
let watch_only_wallet: Wallet<MemoryDatabase> = Wallet::new(
watch_only_external_descriptor,
Some(watch_only_internal_descriptor),
Network::Testnet,
MemoryDatabase::default(),
)?;
// create signing wallet
let signing_wallet: Wallet<MemoryDatabase> = Wallet::new(
signing_external_descriptor,
Some(signing_internal_descriptor),
Network::Testnet,
MemoryDatabase::default(),
)?;
println!("Syncing watch only wallet.");
watch_only_wallet.sync(&blockchain, SyncOptions::default())?;
// get deposit address
let deposit_address = watch_only_wallet.get_address(AddressIndex::New)?;
let balance = watch_only_wallet.get_balance()?;
println!("Watch only wallet balances in SATs: {}", balance);
if balance.get_total() < 10000 {
println!(
"Send at least 10000 SATs (0.0001 BTC) from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
addr = deposit_address.address
);
} else if balance.get_spendable() < 10000 {
println!(
"Wait for at least 10000 SATs of your wallet transactions to be confirmed...\nBe patient, this could take 10 mins or longer depending on how testnet is behaving."
);
for tx_details in watch_only_wallet
.list_transactions(false)?
.iter()
.filter(|txd| txd.received > 0 && txd.confirmation_time.is_none())
{
println!(
"See unconfirmed tx for {} SATs: https://mempool.space/testnet/tx/{}",
tx_details.received, tx_details.txid
);
}
} else {
println!("Creating a PSBT sending 9800 SATs plus fee to the u01.net testnet faucet return address 'tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt'.");
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
let mut builder = watch_only_wallet.build_tx();
builder
.add_recipient(return_address.script_pubkey(), 9_800)
.enable_rbf()
.fee_rate(FeeRate::from_sat_per_vb(1.0));
let (mut psbt, details) = builder.finish()?;
println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", psbt);
// Sign and finalize the PSBT with the signing wallet
let finalized = signing_wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized, "The PSBT was not finalized!");
println!("The PSBT has been signed and finalized.");
// Broadcast the transaction
let raw_transaction = psbt.extract_tx();
let txid = raw_transaction.txid();
blockchain.broadcast(&raw_transaction)?;
println!("Transaction broadcast! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
}
Ok(())
}

View File

@@ -13,6 +13,7 @@ use std::convert::TryInto;
use std::fmt;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::RwLock;
@@ -21,9 +22,9 @@ use rand::{thread_rng, Rng};
use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch, DB};
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::consensus::{deserialize, encode::VarInt, serialize, Decodable, Encodable};
use bitcoin::hash_types::{FilterHash, FilterHeader};
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::Hash;
use bitcoin::util::bip158::BlockFilter;
use bitcoin::util::uint::Uint256;
@@ -32,8 +33,17 @@ use bitcoin::BlockHash;
use bitcoin::BlockHeader;
use bitcoin::Network;
use lazy_static::lazy_static;
use super::CompactFiltersError;
lazy_static! {
static ref MAINNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A29AB5F49FFFF001D1DAC2B7C0101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref TESTNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF001D1AA4AE180101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref REGTEST_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF7F20020000000101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref SIGNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A008F4D5FAE77031E8AD222030101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
}
pub trait StoreType: Default + fmt::Debug {}
#[derive(Default, Debug)]
@@ -214,7 +224,12 @@ pub struct ChainStore<T: StoreType> {
impl ChainStore<Full> {
pub fn new(store: DB, network: Network) -> Result<Self, CompactFiltersError> {
let genesis = genesis_block(network);
let genesis = match network {
Network::Bitcoin => MAINNET_GENESIS.deref(),
Network::Testnet => TESTNET_GENESIS.deref(),
Network::Regtest => REGTEST_GENESIS.deref(),
Network::Signet => SIGNET_GENESIS.deref(),
};
let cf_name = "default".to_string();
let cf_handle = store.cf_handle(&cf_name).unwrap();
@@ -632,9 +647,14 @@ impl CfStore {
filter_type,
};
let genesis = genesis_block(headers_store.network);
let genesis = match headers_store.network {
Network::Bitcoin => MAINNET_GENESIS.deref(),
Network::Testnet => TESTNET_GENESIS.deref(),
Network::Regtest => REGTEST_GENESIS.deref(),
Network::Signet => SIGNET_GENESIS.deref(),
};
let filter = BlockFilter::new_script_filter(&genesis, |utxo| {
let filter = BlockFilter::new_script_filter(genesis, |utxo| {
Err(bitcoin::util::bip158::Error::UtxoMissing(*utxo))
})?;
let first_key = StoreEntry::CFilterTable((filter_type, Some(0))).get_key();

View File

@@ -25,7 +25,6 @@
//! ```
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
@@ -80,14 +79,6 @@ impl Blockchain for ElectrumBlockchain {
}
}
impl Deref for ElectrumBlockchain {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl StatelessBlockchain for ElectrumBlockchain {}
impl GetHeight for ElectrumBlockchain {

View File

@@ -0,0 +1,117 @@
//! structs from the esplora API
//!
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
use crate::BlockTime;
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness};
#[derive(serde::Deserialize, Clone, Debug)]
pub struct PrevOut {
pub value: u64,
pub scriptpubkey: Script,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Vin {
pub txid: Txid,
pub vout: u32,
// None if coinbase
pub prevout: Option<PrevOut>,
pub scriptsig: Script,
#[serde(deserialize_with = "deserialize_witness", default)]
pub witness: Vec<Vec<u8>>,
pub sequence: u32,
pub is_coinbase: bool,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Vout {
pub value: u64,
pub scriptpubkey: Script,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct TxStatus {
pub confirmed: bool,
pub block_height: Option<u32>,
pub block_time: Option<u64>,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Tx {
pub txid: Txid,
pub version: i32,
pub locktime: u32,
pub vin: Vec<Vin>,
pub vout: Vec<Vout>,
pub status: TxStatus,
pub fee: u64,
}
impl Tx {
pub fn to_tx(&self) -> Transaction {
Transaction {
version: self.version,
lock_time: self.locktime,
input: self
.vin
.iter()
.cloned()
.map(|vin| TxIn {
previous_output: OutPoint {
txid: vin.txid,
vout: vin.vout,
},
script_sig: vin.scriptsig,
sequence: vin.sequence,
witness: Witness::from_vec(vin.witness),
})
.collect(),
output: self
.vout
.iter()
.cloned()
.map(|vout| TxOut {
value: vout.value,
script_pubkey: vout.scriptpubkey,
})
.collect(),
}
}
pub fn confirmation_time(&self) -> Option<BlockTime> {
match self.status {
TxStatus {
confirmed: true,
block_height: Some(height),
block_time: Some(timestamp),
} => Some(BlockTime { timestamp, height }),
_ => None,
}
}
pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
self.vin
.iter()
.cloned()
.map(|vin| {
vin.prevout.map(|po| TxOut {
script_pubkey: po.scriptpubkey,
value: po.value,
})
})
.collect()
}
}
fn deserialize_witness<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
use crate::serde::Deserialize;
use bitcoin::hashes::hex::FromHex;
let list = Vec::<String>::deserialize(d)?;
list.into_iter()
.map(|hex_str| Vec::<u8>::from_hex(&hex_str))
.collect::<Result<Vec<Vec<u8>>, _>>()
.map_err(serde::de::Error::custom)
}

View File

@@ -15,22 +15,86 @@
//! depending on your needs (blocking or async respectively).
//!
//! Please note, to configure the Esplora HTTP client correctly use one of:
//! Blocking: --features='use-esplora-blocking'
//! Async: --features='async-interface,use-esplora-async' --no-default-features
//! Blocking: --features='esplora,ureq'
//! Async: --features='async-interface,esplora,reqwest' --no-default-features
use std::collections::HashMap;
use std::fmt;
use std::io;
pub use esplora_client::Error as EsploraError;
use bitcoin::consensus;
use bitcoin::{BlockHash, Txid};
#[cfg(feature = "use-esplora-async")]
mod r#async;
use crate::error::Error;
use crate::FeeRate;
#[cfg(feature = "use-esplora-async")]
pub use self::r#async::*;
#[cfg(feature = "reqwest")]
mod reqwest;
#[cfg(feature = "use-esplora-blocking")]
mod blocking;
#[cfg(feature = "reqwest")]
pub use self::reqwest::*;
#[cfg(feature = "use-esplora-blocking")]
pub use self::blocking::*;
#[cfg(feature = "ureq")]
mod ureq;
#[cfg(feature = "ureq")]
pub use self::ureq::*;
mod api;
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
let fee_val = {
let mut pairs = estimates
.into_iter()
.filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
.collect::<Vec<_>>();
pairs.sort_unstable_by_key(|(k, _)| std::cmp::Reverse(*k));
pairs
.into_iter()
.find(|(k, _)| k <= &target)
.map(|(_, v)| v)
.unwrap_or(1.0)
};
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
}
/// Errors that can happen during a sync with [`EsploraBlockchain`]
#[derive(Debug)]
pub enum EsploraError {
/// Error during ureq HTTP request
#[cfg(feature = "ureq")]
Ureq(::ureq::Error),
/// Transport error during the ureq HTTP call
#[cfg(feature = "ureq")]
UreqTransport(::ureq::Transport),
/// Error during reqwest HTTP request
#[cfg(feature = "reqwest")]
Reqwest(::reqwest::Error),
/// HTTP response error
HttpResponse(u16),
/// IO error during ureq response read
Io(io::Error),
/// No header found in ureq response
NoHeader,
/// Invalid number returned
Parsing(std::num::ParseIntError),
/// Invalid Bitcoin data returned
BitcoinEncoding(bitcoin::consensus::encode::Error),
/// Invalid Hex data returned
Hex(bitcoin::hashes::hex::Error),
/// Transaction not found
TransactionNotFound(Txid),
/// Header height not found
HeaderHeightNotFound(u32),
/// Header hash not found
HeaderHashNotFound(BlockHash),
}
impl fmt::Display for EsploraError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Configuration for an [`EsploraBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
@@ -74,11 +138,16 @@ impl EsploraBlockchainConfig {
}
}
impl From<esplora_client::BlockTime> for crate::BlockTime {
fn from(esplora_client::BlockTime { timestamp, height }: esplora_client::BlockTime) -> Self {
Self { timestamp, height }
}
}
impl std::error::Error for EsploraError {}
#[cfg(feature = "ureq")]
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
#[cfg(feature = "reqwest")]
impl_error!(::reqwest::Error, Reqwest, EsploraError);
impl_error!(io::Error, Io, EsploraError);
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
#[cfg(test)]
#[cfg(feature = "test-esplora")]
@@ -92,11 +161,58 @@ const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
#[cfg(test)]
mod test {
use super::*;
#[test]
fn feerate_parsing() {
let esplora_fees = serde_json::from_str::<HashMap<String, f64>>(
r#"{
"25": 1.015,
"5": 2.3280000000000003,
"12": 2.0109999999999997,
"15": 1.018,
"17": 1.018,
"11": 2.0109999999999997,
"3": 3.01,
"2": 4.9830000000000005,
"6": 2.2359999999999998,
"21": 1.018,
"13": 1.081,
"7": 2.2359999999999998,
"8": 2.2359999999999998,
"16": 1.018,
"20": 1.018,
"22": 1.017,
"23": 1.017,
"504": 1,
"9": 2.2359999999999998,
"14": 1.018,
"10": 2.0109999999999997,
"24": 1.017,
"1008": 1,
"1": 4.9830000000000005,
"4": 2.3280000000000003,
"19": 1.018,
"144": 1,
"18": 1.018
}
"#,
)
.unwrap();
assert_eq!(
into_fee_rate(6, esplora_fees.clone()).unwrap(),
FeeRate::from_sat_per_vb(2.236)
);
assert_eq!(
into_fee_rate(26, esplora_fees).unwrap(),
FeeRate::from_sat_per_vb(1.015),
"should inherit from value for 25"
);
}
#[test]
#[cfg(feature = "test-esplora")]
fn test_esplora_with_variable_configs() {
use super::*;
use crate::testutils::{
blockchain_tests::TestClient,
configurable_blockchain_tests::ConfigurableBlockchainTester,

View File

@@ -12,38 +12,49 @@
//! Esplora by way of `reqwest` HTTP client.
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use bitcoin::{Transaction, Txid};
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::{BlockHeader, Script, Transaction, Txid};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use esplora_client::{convert_fee_rate, AsyncClient, Builder, Tx};
use ::reqwest::{Client, StatusCode};
use futures::stream::{FuturesOrdered, TryStreamExt};
use super::api::Tx;
use crate::blockchain::esplora::EsploraError;
use crate::blockchain::*;
use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
#[derive(Debug)]
struct UrlClient {
url: String,
// We use the async client instead of the blocking one because it automatically uses `fetch`
// when the target platform is wasm32.
client: Client,
concurrency: u8,
}
/// Structure that implements the logic to sync with Esplora
///
/// ## Example
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
#[derive(Debug)]
pub struct EsploraBlockchain {
url_client: AsyncClient,
url_client: UrlClient,
stop_gap: usize,
concurrency: u8,
}
impl std::convert::From<AsyncClient> for EsploraBlockchain {
fn from(url_client: AsyncClient) -> Self {
impl std::convert::From<UrlClient> for EsploraBlockchain {
fn from(url_client: UrlClient) -> Self {
EsploraBlockchain {
url_client,
stop_gap: 20,
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
}
}
}
@@ -51,25 +62,19 @@ impl std::convert::From<AsyncClient> for EsploraBlockchain {
impl EsploraBlockchain {
/// Create a new instance of the client from a base URL and `stop_gap`.
pub fn new(base_url: &str, stop_gap: usize) -> Self {
let url_client = Builder::new(base_url)
.build_async()
.expect("Should never fail with no proxy and timeout");
Self::from_client(url_client, stop_gap)
}
/// Build a new instance given a client
pub fn from_client(url_client: AsyncClient, stop_gap: usize) -> Self {
EsploraBlockchain {
url_client,
url_client: UrlClient {
url: base_url.to_string(),
client: Client::new(),
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
},
stop_gap,
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
}
}
/// Set the concurrency to use when doing batch queries against the Esplora instance.
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
self.concurrency = concurrency;
self.url_client.concurrency = concurrency;
self
}
}
@@ -87,22 +92,12 @@ impl Blockchain for EsploraBlockchain {
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client.broadcast(tx))?)
Ok(await_or_block!(self.url_client._broadcast(tx))?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = await_or_block!(self.url_client.get_fee_estimates())?;
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
target, estimates,
)?))
}
}
impl Deref for EsploraBlockchain {
type Target = AsyncClient;
fn deref(&self) -> &Self::Target {
&self.url_client
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
super::into_fee_rate(target, estimates)
}
}
@@ -111,21 +106,21 @@ impl StatelessBlockchain for EsploraBlockchain {}
#[maybe_async]
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(await_or_block!(self.url_client.get_height())?)
Ok(await_or_block!(self.url_client._get_height())?)
}
}
#[maybe_async]
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client.get_tx(txid))?)
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
}
#[maybe_async]
impl GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = await_or_block!(self.url_client.get_header(height as u32))?;
let block_header = await_or_block!(self.url_client._get_header(height as u32))?;
Ok(block_header.block_hash())
}
}
@@ -146,10 +141,10 @@ impl WalletSync for EsploraBlockchain {
Request::Script(script_req) => {
let futures: FuturesOrdered<_> = script_req
.request()
.take(self.concurrency as usize)
.take(self.url_client.concurrency as usize)
.map(|script| async move {
let mut related_txs: Vec<Tx> =
self.url_client.scripthash_txs(script, None).await?;
self.url_client._scripthash_txs(script, None).await?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
@@ -159,7 +154,7 @@ impl WalletSync for EsploraBlockchain {
loop {
let new_related_txs: Vec<Tx> = self
.url_client
.scripthash_txs(
._scripthash_txs(
script,
Some(related_txs.last().unwrap().txid),
)
@@ -199,7 +194,6 @@ impl WalletSync for EsploraBlockchain {
.get(txid)
.expect("must be in index")
.confirmation_time()
.map(Into::into)
})
.collect();
conftime_req.satisfy(conftimes)?
@@ -223,26 +217,132 @@ impl WalletSync for EsploraBlockchain {
}
}
impl UrlClient {
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
let resp = self
.client
.get(&format!("{}/tx/{}/raw", self.url, txid))
.send()
.await?;
if let StatusCode::NOT_FOUND = resp.status() {
return Ok(None);
}
Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
}
async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
match self._get_tx(txid).await {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
let resp = self
.client
.get(&format!("{}/block-height/{}", self.url, block_height))
.send()
.await?;
if let StatusCode::NOT_FOUND = resp.status() {
return Err(EsploraError::HeaderHeightNotFound(block_height));
}
let bytes = resp.bytes().await?;
let hash = std::str::from_utf8(&bytes)
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
let resp = self
.client
.get(&format!("{}/block/{}/header", self.url, hash))
.send()
.await?;
let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
Ok(header)
}
async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
self.client
.post(&format!("{}/tx", self.url))
.body(serialize(transaction).to_hex())
.send()
.await?
.error_for_status()?;
Ok(())
}
async fn _get_height(&self) -> Result<u32, EsploraError> {
let req = self
.client
.get(&format!("{}/blocks/tip/height", self.url))
.send()
.await?;
Ok(req.error_for_status()?.text().await?.parse()?)
}
async fn _scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<Tx>, EsploraError> {
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
let url = match last_seen {
Some(last_seen) => format!(
"{}/scripthash/{}/txs/chain/{}",
self.url, script_hash, last_seen
),
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
};
Ok(self
.client
.get(url)
.send()
.await?
.error_for_status()?
.json::<Vec<Tx>>()
.await?)
}
async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
Ok(self
.client
.get(&format!("{}/fee-estimates", self.url,))
.send()
.await?
.error_for_status()?
.json::<HashMap<String, f64>>()
.await?)
}
}
impl ConfigurableBlockchain for EsploraBlockchain {
type Config = super::EsploraBlockchainConfig;
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let mut builder = Builder::new(config.base_url.as_str());
if let Some(timeout) = config.timeout {
builder = builder.timeout(timeout);
}
if let Some(proxy) = &config.proxy {
builder = builder.proxy(proxy);
}
let mut blockchain =
EsploraBlockchain::from_client(builder.build_async()?, config.stop_gap);
let map_e = |e: reqwest::Error| Error::Esplora(Box::new(e.into()));
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
if let Some(concurrency) = config.concurrency {
blockchain = blockchain.with_concurrency(concurrency);
blockchain.url_client.concurrency = concurrency;
}
let mut builder = Client::builder();
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = &config.proxy {
builder = builder.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(timeout) = config.timeout {
builder = builder.timeout(core::time::Duration::from_secs(timeout));
}
blockchain.url_client.client = builder.build().map_err(map_e)?;
Ok(blockchain)
}

View File

@@ -12,26 +12,40 @@
//! Esplora by way of `ureq` HTTP client.
use std::collections::{HashMap, HashSet};
use std::io;
use std::io::Read;
use std::time::Duration;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use bitcoin::{Transaction, Txid};
use ureq::{Agent, Proxy, Response};
use esplora_client::{convert_fee_rate, BlockingClient, Builder, Tx};
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::{BlockHeader, Script, Transaction, Txid};
use super::api::Tx;
use crate::blockchain::esplora::EsploraError;
use crate::blockchain::*;
use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
#[derive(Debug, Clone)]
struct UrlClient {
url: String,
agent: Agent,
}
/// Structure that implements the logic to sync with Esplora
///
/// ## Example
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
#[derive(Debug)]
pub struct EsploraBlockchain {
url_client: BlockingClient,
url_client: UrlClient,
stop_gap: usize,
concurrency: u8,
}
@@ -39,22 +53,22 @@ pub struct EsploraBlockchain {
impl EsploraBlockchain {
/// Create a new instance of the client from a base URL and the `stop_gap`.
pub fn new(base_url: &str, stop_gap: usize) -> Self {
let url_client = Builder::new(base_url)
.build_blocking()
.expect("Should never fail with no proxy and timeout");
Self::from_client(url_client, stop_gap)
}
/// Build a new instance given a client
pub fn from_client(url_client: BlockingClient, stop_gap: usize) -> Self {
EsploraBlockchain {
url_client,
url_client: UrlClient {
url: base_url.to_string(),
agent: Agent::new(),
},
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
stop_gap,
}
}
/// Set the inner `ureq` agent.
pub fn with_agent(mut self, agent: Agent) -> Self {
self.url_client.agent = agent;
self
}
/// Set the number of parallel requests the client can make.
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
self.concurrency = concurrency;
@@ -74,23 +88,13 @@ impl Blockchain for EsploraBlockchain {
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.url_client.broadcast(tx)?;
self.url_client._broadcast(tx)?;
Ok(())
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = self.url_client.get_fee_estimates()?;
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
target, estimates,
)?))
}
}
impl Deref for EsploraBlockchain {
type Target = BlockingClient;
fn deref(&self) -> &Self::Target {
&self.url_client
let estimates = self.url_client._get_fee_estimates()?;
super::into_fee_rate(target, estimates)
}
}
@@ -98,19 +102,19 @@ impl StatelessBlockchain for EsploraBlockchain {}
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client.get_height()?)
Ok(self.url_client._get_height()?)
}
}
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client.get_tx(txid)?)
Ok(self.url_client._get_tx(txid)?)
}
}
impl GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = self.url_client.get_header(height as u32)?;
let block_header = self.url_client._get_header(height as u32)?;
Ok(block_header.block_hash())
}
}
@@ -137,7 +141,7 @@ impl WalletSync for EsploraBlockchain {
let client = self.url_client.clone();
// make each request in its own thread.
handles.push(std::thread::spawn(move || {
let mut related_txs: Vec<Tx> = client.scripthash_txs(&script, None)?;
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
@@ -145,7 +149,7 @@ impl WalletSync for EsploraBlockchain {
// keep requesting to see if there's more.
if n_confirmed >= 25 {
loop {
let new_related_txs: Vec<Tx> = client.scripthash_txs(
let new_related_txs: Vec<Tx> = client._scripthash_txs(
&script,
Some(related_txs.last().unwrap().txid),
)?;
@@ -188,7 +192,6 @@ impl WalletSync for EsploraBlockchain {
.get(txid)
.expect("must be in index")
.confirmation_time()
.map(Into::into)
})
.collect();
conftime_req.satisfy(conftimes)?
@@ -213,22 +216,159 @@ impl WalletSync for EsploraBlockchain {
}
}
impl UrlClient {
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
let resp = self
.agent
.get(&format!("{}/tx/{}/raw", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(EsploraError::HttpResponse(code))
}
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
match self._get_tx(txid) {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
let resp = self
.agent
.get(&format!("{}/block-height/{}", self.url, block_height))
.call();
let bytes = match resp {
Ok(resp) => Ok(into_bytes(resp)?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}?;
let hash = std::str::from_utf8(&bytes)
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
let resp = self
.agent
.get(&format!("{}/block/{}/header", self.url, hash))
.call();
match resp {
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
let resp = self
.agent
.post(&format!("{}/tx", self.url))
.send_string(&serialize(transaction).to_hex());
match resp {
Ok(_) => Ok(()), // We do not return the txid?
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_height(&self) -> Result<u32, EsploraError> {
let resp = self
.agent
.get(&format!("{}/blocks/tip/height", self.url))
.call();
match resp {
Ok(resp) => Ok(resp.into_string()?.parse()?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
let resp = self
.agent
.get(&format!("{}/fee-estimates", self.url,))
.call();
let map = match resp {
Ok(resp) => {
let map: HashMap<String, f64> = resp.into_json()?;
Ok(map)
}
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}?;
Ok(map)
}
fn _scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<Tx>, EsploraError> {
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
let url = match last_seen {
Some(last_seen) => format!(
"{}/scripthash/{}/txs/chain/{}",
self.url, script_hash, last_seen
),
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
};
Ok(self.agent.get(&url).call()?.into_json()?)
}
}
fn is_status_not_found(status: u16) -> bool {
status == 404
}
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
let mut buf: Vec<u8> = vec![];
resp.into_reader()
.take((BYTES_LIMIT + 1) as u64)
.read_to_end(&mut buf)?;
if buf.len() > BYTES_LIMIT {
return Err(io::Error::new(
io::ErrorKind::Other,
"response too big for into_bytes",
));
}
Ok(buf)
}
impl ConfigurableBlockchain for EsploraBlockchain {
type Config = super::EsploraBlockchainConfig;
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let mut builder = Builder::new(config.base_url.as_str());
let mut agent_builder = ureq::AgentBuilder::new();
if let Some(timeout) = config.timeout {
builder = builder.timeout(timeout);
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
}
if let Some(proxy) = &config.proxy {
builder = builder.proxy(proxy);
agent_builder = agent_builder
.proxy(Proxy::new(proxy).map_err(|e| Error::Esplora(Box::new(e.into())))?);
}
let mut blockchain =
EsploraBlockchain::from_client(builder.build_blocking()?, config.stop_gap);
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
.with_agent(agent_builder.build());
if let Some(concurrency) = config.concurrency {
blockchain = blockchain.with_concurrency(concurrency);
@@ -237,3 +377,12 @@ impl ConfigurableBlockchain for EsploraBlockchain {
Ok(blockchain)
}
}
impl From<ureq::Error> for EsploraError {
fn from(e: ureq::Error) -> Self {
match e {
ureq::Error::Status(code, _) => EsploraError::HttpResponse(code),
e => EsploraError::Ureq(e),
}
}
}

View File

@@ -50,7 +50,6 @@ use bitcoincore_rpc::{Client, RpcApi};
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
@@ -68,14 +67,6 @@ pub struct RpcBlockchain {
sync_params: RpcSyncParams,
}
impl Deref for RpcBlockchain {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
/// RpcBlockchain configuration options
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct RpcConfig {

View File

@@ -492,44 +492,4 @@ mod test {
fn test_sync_time() {
crate::database::test::test_sync_time(get_tree());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_tree());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_tree());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_tree());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_tree());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_tree());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_tree());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_tree());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_tree());
}
}

View File

@@ -486,15 +486,15 @@ macro_rules! populate_test_db {
}};
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
use std::str::FromStr;
use $crate::database::SyncTime;
use $crate::database::{BatchOperations, Database};
use $crate::database::BatchOperations;
let mut db = $db;
let tx_meta = $tx_meta;
let current_height: Option<u32> = $current_height;
let mut input = vec![$crate::bitcoin::TxIn::default()];
if !$is_coinbase {
input[0].previous_output.vout = 0;
}
let input = if $is_coinbase {
vec![$crate::bitcoin::TxIn::default()]
} else {
vec![]
};
let tx = $crate::bitcoin::Transaction {
version: 1,
lock_time: 0,
@@ -512,32 +512,14 @@ macro_rules! populate_test_db {
};
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
.min_confirmations
.and_then(|v| if v == 0 { None } else { Some(v) })
.map(|conf| $crate::BlockTime {
height: current_height.expect("Current height is needed for testing transaction with min-confirmation values").checked_sub(conf as u32).unwrap() + 1,
height: current_height.unwrap().checked_sub(conf as u32).unwrap() + 1,
timestamp: 0,
});
// Set the database sync_time.
// Check if the current_height is less than already known sync height, apply the max
// If any of them is None, the other will be applied instead.
// If both are None, this will not be set.
if let Some(height) = db.get_sync_time().unwrap()
.map(|sync_time| sync_time.block_time.height)
.max(current_height) {
let sync_time = SyncTime {
block_time: BlockTime {
height,
timestamp: 0
}
};
db.set_sync_time(sync_time).unwrap();
}
let tx_details = $crate::TransactionDetails {
transaction: Some(tx.clone()),
txid,
@@ -647,44 +629,4 @@ mod test {
fn test_sync_time() {
crate::database::test::test_sync_time(get_tree());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_tree());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_tree());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_tree());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_tree());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_tree());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_tree());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_tree());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_tree());
}
}

View File

@@ -217,33 +217,32 @@ pub mod test {
use std::str::FromStr;
use bitcoin::consensus::encode::deserialize;
use bitcoin::consensus::serialize;
use bitcoin::hashes::hex::*;
use bitcoin::*;
use super::*;
pub fn test_script_pubkey<D: Database>(mut db: D) {
pub fn test_script_pubkey<D: Database>(mut tree: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
db.set_script_pubkey(&script, keychain, path).unwrap();
tree.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(
db.get_script_pubkey_from_path(keychain, path).unwrap(),
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
Some(script.clone())
);
assert_eq!(
db.get_path_from_script_pubkey(&script).unwrap(),
tree.get_path_from_script_pubkey(&script).unwrap(),
Some((keychain, path))
);
}
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut db: D) {
let mut batch = db.begin_batch();
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut tree: D) {
let mut batch = tree.begin_batch();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
@@ -254,50 +253,50 @@ pub mod test {
batch.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(
db.get_script_pubkey_from_path(keychain, path).unwrap(),
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
None
);
assert_eq!(db.get_path_from_script_pubkey(&script).unwrap(), None);
assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None);
db.commit_batch(batch).unwrap();
tree.commit_batch(batch).unwrap();
assert_eq!(
db.get_script_pubkey_from_path(keychain, path).unwrap(),
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
Some(script.clone())
);
assert_eq!(
db.get_path_from_script_pubkey(&script).unwrap(),
tree.get_path_from_script_pubkey(&script).unwrap(),
Some((keychain, path))
);
}
pub fn test_iter_script_pubkey<D: Database>(mut db: D) {
pub fn test_iter_script_pubkey<D: Database>(mut tree: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
db.set_script_pubkey(&script, keychain, path).unwrap();
tree.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
}
pub fn test_del_script_pubkey<D: Database>(mut db: D) {
pub fn test_del_script_pubkey<D: Database>(mut tree: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
db.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
tree.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
db.del_script_pubkey_from_path(keychain, path).unwrap();
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 0);
tree.del_script_pubkey_from_path(keychain, path).unwrap();
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0);
}
pub fn test_utxo<D: Database>(mut db: D) {
pub fn test_utxo<D: Database>(mut tree: D) {
let outpoint = OutPoint::from_str(
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
)
@@ -316,40 +315,24 @@ pub mod test {
is_spent: true,
};
db.set_utxo(&utxo).unwrap();
db.set_utxo(&utxo).unwrap();
assert_eq!(db.iter_utxos().unwrap().len(), 1);
assert_eq!(db.get_utxo(&outpoint).unwrap(), Some(utxo));
tree.set_utxo(&utxo).unwrap();
tree.set_utxo(&utxo).unwrap();
assert_eq!(tree.iter_utxos().unwrap().len(), 1);
assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo));
}
pub fn test_raw_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let mut tx: Transaction = deserialize(&hex_tx).unwrap();
pub fn test_raw_tx<D: Database>(mut tree: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
db.set_raw_tx(&tx).unwrap();
tree.set_raw_tx(&tx).unwrap();
let txid = tx.txid();
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx.clone()));
// mutate transaction's witnesses
for tx_in in tx.input.iter_mut() {
tx_in.witness = Witness::new();
}
let updated_hex_tx = serialize(&tx);
// verify that mutation was successful
assert_ne!(hex_tx, updated_hex_tx);
db.set_raw_tx(&tx).unwrap();
let txid = tx.txid();
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx));
assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx));
}
pub fn test_tx<D: Database>(mut db: D) {
pub fn test_tx<D: Database>(mut tree: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
@@ -365,28 +348,28 @@ pub mod test {
}),
};
db.set_tx(&tx_details).unwrap();
tree.set_tx(&tx_details).unwrap();
// get with raw tx too
assert_eq!(
db.get_tx(&tx_details.txid, true).unwrap(),
tree.get_tx(&tx_details.txid, true).unwrap(),
Some(tx_details.clone())
);
// get only raw_tx
assert_eq!(
db.get_raw_tx(&tx_details.txid).unwrap(),
tree.get_raw_tx(&tx_details.txid).unwrap(),
tx_details.transaction
);
// now get without raw_tx
tx_details.transaction = None;
assert_eq!(
db.get_tx(&tx_details.txid, false).unwrap(),
tree.get_tx(&tx_details.txid, false).unwrap(),
Some(tx_details)
);
}
pub fn test_list_transaction<D: Database>(mut db: D) {
pub fn test_list_transaction<D: Database>(mut tree: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
@@ -402,43 +385,46 @@ pub mod test {
}),
};
db.set_tx(&tx_details).unwrap();
tree.set_tx(&tx_details).unwrap();
// get raw tx
assert_eq!(db.iter_txs(true).unwrap(), vec![tx_details.clone()]);
assert_eq!(tree.iter_txs(true).unwrap(), vec![tx_details.clone()]);
// now get without raw tx
tx_details.transaction = None;
// get not raw tx
assert_eq!(db.iter_txs(false).unwrap(), vec![tx_details.clone()]);
assert_eq!(tree.iter_txs(false).unwrap(), vec![tx_details.clone()]);
}
pub fn test_last_index<D: Database>(mut db: D) {
db.set_last_index(KeychainKind::External, 1337).unwrap();
pub fn test_last_index<D: Database>(mut tree: D) {
tree.set_last_index(KeychainKind::External, 1337).unwrap();
assert_eq!(
db.get_last_index(KeychainKind::External).unwrap(),
tree.get_last_index(KeychainKind::External).unwrap(),
Some(1337)
);
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), None);
assert_eq!(tree.get_last_index(KeychainKind::Internal).unwrap(), None);
let res = db.increment_last_index(KeychainKind::External).unwrap();
let res = tree.increment_last_index(KeychainKind::External).unwrap();
assert_eq!(res, 1338);
let res = db.increment_last_index(KeychainKind::Internal).unwrap();
let res = tree.increment_last_index(KeychainKind::Internal).unwrap();
assert_eq!(res, 0);
assert_eq!(
db.get_last_index(KeychainKind::External).unwrap(),
tree.get_last_index(KeychainKind::External).unwrap(),
Some(1338)
);
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), Some(0));
assert_eq!(
tree.get_last_index(KeychainKind::Internal).unwrap(),
Some(0)
);
}
pub fn test_sync_time<D: Database>(mut db: D) {
assert!(db.get_sync_time().unwrap().is_none());
pub fn test_sync_time<D: Database>(mut tree: D) {
assert!(tree.get_sync_time().unwrap().is_none());
db.set_sync_time(SyncTime {
tree.set_sync_time(SyncTime {
block_time: BlockTime {
height: 100,
timestamp: 1000,
@@ -446,211 +432,13 @@ pub mod test {
})
.unwrap();
let extracted = db.get_sync_time().unwrap();
let extracted = tree.get_sync_time().unwrap();
assert!(extracted.is_some());
assert_eq!(extracted.as_ref().unwrap().block_time.height, 100);
assert_eq!(extracted.as_ref().unwrap().block_time.timestamp, 1000);
db.del_sync_time().unwrap();
assert!(db.get_sync_time().unwrap().is_none());
}
pub fn test_iter_raw_txs<D: Database>(mut db: D) {
let txs = db.iter_raw_txs().unwrap();
assert!(txs.is_empty());
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let first_tx: Transaction = deserialize(&hex_tx).unwrap();
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let second_tx: Transaction = deserialize(&hex_tx).unwrap();
db.set_raw_tx(&first_tx).unwrap();
db.set_raw_tx(&second_tx).unwrap();
let txs = db.iter_raw_txs().unwrap();
assert!(txs.contains(&first_tx));
assert!(txs.contains(&second_tx));
assert_eq!(txs.len(), 2);
}
pub fn test_del_path_from_script_pubkey<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let res = db.del_path_from_script_pubkey(&script).unwrap();
assert!(res.is_none());
let _res = db.set_script_pubkey(&script, keychain, path);
let (chain, child) = db.del_path_from_script_pubkey(&script).unwrap().unwrap();
assert_eq!(chain, keychain);
assert_eq!(child, path);
let res = db.get_path_from_script_pubkey(&script).unwrap();
assert!(res.is_none());
}
pub fn test_iter_script_pubkeys<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
assert!(scripts.is_empty());
let first_script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
db.set_script_pubkey(&first_script, keychain, path).unwrap();
let second_script = Script::from(
Vec::<u8>::from_hex("00145c9a1816d38db5cbdd4b067b689dc19eb7d930e2").unwrap(),
);
let path = 57;
db.set_script_pubkey(&second_script, keychain, path)
.unwrap();
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
assert!(scripts.contains(&first_script));
assert!(scripts.contains(&second_script));
assert_eq!(scripts.len(), 2);
}
pub fn test_del_utxo<D: Database>(mut db: D) {
let outpoint = OutPoint::from_str(
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
)
.unwrap();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let txout = TxOut {
value: 133742,
script_pubkey: script,
};
let utxo = LocalUtxo {
txout,
outpoint,
keychain: KeychainKind::External,
is_spent: true,
};
let res = db.del_utxo(&outpoint).unwrap();
assert!(res.is_none());
db.set_utxo(&utxo).unwrap();
let res = db.del_utxo(&outpoint).unwrap();
assert_eq!(res.unwrap(), utxo);
let res = db.get_utxo(&outpoint).unwrap();
assert!(res.is_none());
}
pub fn test_del_raw_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let res = db.del_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
db.set_raw_tx(&tx).unwrap();
let res = db.del_raw_tx(&tx.txid()).unwrap();
assert_eq!(res.unwrap(), tx);
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
}
pub fn test_del_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
let mut tx_details = TransactionDetails {
transaction: Some(tx.clone()),
txid,
received: 1337,
sent: 420420,
fee: Some(140),
confirmation_time: Some(BlockTime {
timestamp: 123456,
height: 1000,
}),
};
let res = db.del_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
db.set_tx(&tx_details).unwrap();
let res = db.del_tx(&tx.txid(), false).unwrap();
tx_details.transaction = None;
assert_eq!(res.unwrap(), tx_details);
let res = db.get_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert_eq!(res.unwrap(), tx);
db.set_tx(&tx_details).unwrap();
let res = db.del_tx(&tx.txid(), true).unwrap();
tx_details.transaction = Some(tx.clone());
assert_eq!(res.unwrap(), tx_details);
let res = db.get_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
}
pub fn test_del_last_index<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let _res = db.increment_last_index(keychain);
let res = db.get_last_index(keychain).unwrap().unwrap();
assert_eq!(res, 0);
let _res = db.increment_last_index(keychain);
let res = db.del_last_index(keychain).unwrap().unwrap();
assert_eq!(res, 1);
let res = db.get_last_index(keychain).unwrap();
assert!(res.is_none());
}
pub fn test_check_descriptor_checksum<D: Database>(mut db: D) {
// insert checksum associated to keychain
let checksum = "1cead456".as_bytes();
let keychain = KeychainKind::External;
let _res = db.check_descriptor_checksum(keychain, checksum);
// check if `check_descriptor_checksum` throws
// `Error::ChecksumMismatch` error if the
// function is passed a checksum that does
// not match the one initially inserted
let checksum = "1cead454".as_bytes();
let keychain = KeychainKind::External;
let res = db.check_descriptor_checksum(keychain, checksum);
assert!(res.is_err());
tree.del_sync_time().unwrap();
assert!(tree.get_sync_time().unwrap().is_none());
}
// TODO: more tests...

View File

@@ -52,11 +52,6 @@ static MIGRATIONS: &[&str] = &[
"DELETE FROM transactions;",
"DELETE FROM utxos;",
"DROP INDEX idx_txid_vout;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
"ALTER TABLE utxos RENAME TO utxos_old;",
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
"DROP TABLE utxos_old;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
];
@@ -749,13 +744,11 @@ impl BatchOperations for SqliteDatabase {
include_raw: bool,
) -> Result<Option<TransactionDetails>, Error> {
match self.select_transaction_details_by_txid(txid)? {
Some(mut transaction_details) => {
Some(transaction_details) => {
self.delete_transaction_details_by_txid(txid)?;
if include_raw {
self.delete_transaction_by_txid(txid)?;
} else {
transaction_details.transaction = None;
}
Ok(Some(transaction_details))
}
@@ -921,8 +914,8 @@ impl BatchDatabase for SqliteDatabase {
}
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
let mut connection = Connection::open(path)?;
migrate(&mut connection)?;
let connection = Connection::open(path)?;
migrate(&connection)?;
Ok(connection)
}
@@ -957,41 +950,28 @@ pub fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<u
)
}
pub fn migrate(conn: &mut Connection) -> Result<(), Error> {
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
let version = get_schema_version(conn)?;
let stmts = &MIGRATIONS[(version as usize)..];
let mut i: i32 = version;
// begin transaction, all migration statements and new schema version commit or rollback
let tx = conn.transaction()?;
// execute every statement and return `Some` new schema version
// if execution fails, return `Error::Rusqlite`
// if no statements executed returns `None`
let new_version = stmts
.iter()
.enumerate()
.map(|version_stmt| {
log::info!(
"executing db migration {}: `{}`",
version + version_stmt.0 as i32 + 1,
version_stmt.1
);
tx.execute(version_stmt.1, [])
// map result value to next migration version
.map(|_| version_stmt.0 as i32 + version + 1)
})
.last()
.transpose()?;
// if `Some` new statement version, set new schema version
if let Some(version) = new_version {
set_schema_version(&tx, version)?;
} else {
if version == MIGRATIONS.len() as i32 {
log::info!("db up to date, no migration needed");
return Ok(());
}
// commit transaction
tx.commit()?;
for stmt in stmts {
let res = conn.execute(stmt, []);
if res.is_err() {
println!("migration failed on:\n{}\n{:?}", stmt, res);
break;
}
i += 1;
}
set_schema_version(conn, i)?;
Ok(())
}
@@ -1056,44 +1036,4 @@ pub mod test {
fn test_txs() {
crate::database::test::test_list_transaction(get_database());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_database());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_database());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_database());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_database());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_database());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_database());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_database());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_database());
}
}

View File

@@ -20,6 +20,8 @@ pub enum Error {
InvalidDescriptorChecksum,
/// The descriptor contains hardened derivation steps on public extended keys
HardenedDerivationXpub,
/// The descriptor contains multiple keys with the same BIP32 fingerprint
DuplicatedKeys,
/// Error thrown while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),

View File

@@ -14,7 +14,7 @@
//! This module contains generic utilities to work with descriptors, plus some re-exported types
//! from [`miniscript`].
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
@@ -222,9 +222,23 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
return Err(DescriptorError::HardenedDerivationXpub);
}
// Run miniscript's sanity check, which will look for duplicated keys and other potential
// issues
descriptor.sanity_check()?;
// Ensure that there are no duplicated keys
let mut found_keys = HashSet::new();
let descriptor_contains_duplicated_keys = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(xkey) = k.as_key() {
let fingerprint = xkey.root_fingerprint(secp);
if found_keys.contains(&fingerprint) {
return true;
}
found_keys.insert(fingerprint);
}
false
});
if descriptor_contains_duplicated_keys {
return Err(DescriptorError::DuplicatedKeys);
}
Ok((descriptor, keymap))
}
@@ -909,10 +923,14 @@ mod test {
DescriptorError::HardenedDerivationXpub
));
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DescriptorError::DuplicatedKeys
));
}
#[test]

View File

@@ -43,6 +43,10 @@
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
//! interact with the bitcoin P2P network.
//!
//! ```toml
//! bdk = "0.21.0"
//! ```
//!
//! # Examples
#![cfg_attr(
feature = "electrum",
@@ -77,7 +81,6 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ## Generate a few addresses
//!
//! ### Example
//! ```
//! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase;
@@ -231,15 +234,15 @@ extern crate async_trait;
#[macro_use]
extern crate bdk_macros;
#[cfg(feature = "compact_filters")]
extern crate lazy_static;
#[cfg(feature = "rpc")]
pub extern crate bitcoincore_rpc;
#[cfg(feature = "electrum")]
pub extern crate electrum_client;
#[cfg(feature = "esplora")]
pub extern crate esplora_client;
#[cfg(feature = "key-value-db")]
pub extern crate sled;

View File

@@ -9,22 +9,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use crate::FeeRate;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut;
pub trait PsbtUtils {
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_amount(&self) -> Option<u64>;
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
/// transaction.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_rate(&self) -> Option<FeeRate>;
}
impl PsbtUtils for Psbt {
@@ -48,27 +37,6 @@ impl PsbtUtils for Psbt {
None
}
}
fn fee_amount(&self) -> Option<u64> {
let tx = &self.unsigned_tx;
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
utxos.map(|inputs| {
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
input_amount
.checked_sub(output_amount)
.expect("input amount must be greater than output amount")
})
}
fn fee_rate(&self) -> Option<FeeRate> {
let fee_amount = self.fee_amount();
fee_amount.map(|fee| {
let weight = self.clone().extract_tx().weight();
FeeRate::from_wu(fee, weight)
})
}
}
#[cfg(test)]
@@ -76,9 +44,8 @@ mod test {
use crate::bitcoin::TxIn;
use crate::psbt::Psbt;
use crate::wallet::AddressIndex;
use crate::wallet::AddressIndex::New;
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
use crate::{psbt, FeeRate, SignOptions};
use crate::SignOptions;
use std::str::FromStr;
// from bip 174
@@ -151,83 +118,4 @@ mod test {
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
}
#[test]
fn test_psbt_fee_rate_with_witness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut psbt, _) = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_nonwitness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut psbt, _) = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_missing_txout() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wpkh_wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wpkh_wallet.get_address(New).unwrap();
let mut builder = wpkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut wpkh_psbt, _) = builder.finish().unwrap();
wpkh_psbt.inputs[0].witness_utxo = None;
wpkh_psbt.inputs[0].non_witness_utxo = None;
assert!(wpkh_psbt.fee_amount().is_none());
assert!(wpkh_psbt.fee_rate().is_none());
let (pkh_wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = pkh_wallet.get_address(New).unwrap();
let mut builder = pkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut pkh_psbt, _) = builder.finish().unwrap();
pkh_psbt.inputs[0].non_witness_utxo = None;
assert!(pkh_psbt.fee_amount().is_none());
assert!(pkh_psbt.fee_rate().is_none());
}
}

View File

@@ -63,16 +63,6 @@ impl FeeRate {
FeeRate(value)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
///
/// ## Panics
@@ -108,7 +98,7 @@ impl FeeRate {
}
/// Return the value as satoshi/vbyte
pub fn as_sat_per_vb(&self) -> f32 {
pub fn as_sat_vb(&self) -> f32 {
self.0
}
@@ -119,7 +109,7 @@ impl FeeRate {
/// Calculate absolute fee in Satoshis using size in virtual bytes.
pub fn fee_vb(&self, vbytes: usize) -> u64 {
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
(self.as_sat_vb() * vbytes as f32).ceil() as u64
}
}
@@ -368,34 +358,4 @@ mod tests {
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);
}
}

View File

@@ -454,7 +454,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
.iter()
.fold(0, |acc, x| acc + x.effective_value);
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
// `curr_value` and `curr_available_value` are both the sum of *effective_values* of
// the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
@@ -734,9 +734,7 @@ mod test {
use rand::seq::SliceRandom;
use rand::{Rng, SeedableRng};
// n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
// + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
const P2WPKH_SATISFACTION_SIZE: usize = 73 + 33 + 2 + 1;
const FEE_AMOUNT: u64 = 50;
@@ -1360,8 +1358,7 @@ mod test {
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
let size_of_change = 31;
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
BranchAndBoundCoinSelection::new(size_of_change)
@@ -1390,7 +1387,7 @@ mod test {
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
let size_of_change = 31;
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
let target_amount = 20_000 + FEE_AMOUNT;
let drain_script = Script::default();
@@ -1414,7 +1411,7 @@ mod test {
fn test_bnb_function_almost_exact_match_with_fees() {
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let size_of_change = 31;
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
.into_iter()

View File

@@ -1,64 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! 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(())
}
}

View File

@@ -48,10 +48,6 @@ pub(crate) mod utils;
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
pub mod verify;
#[cfg(feature = "hardware-signer")]
#[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))]
pub mod hardwaresigner;
pub use utils::IsDust;
#[allow(deprecated)]
@@ -791,10 +787,7 @@ where
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
for (index, (script_pubkey, value)) in recipients.enumerate() {
if !params.allow_dust
&& value.is_dust(script_pubkey)
&& !script_pubkey.is_provably_unspendable()
{
if value.is_dust(script_pubkey) && !script_pubkey.is_provably_unspendable() {
return Err(Error::OutputBelowDustLimit(index));
}
@@ -1074,7 +1067,7 @@ where
utxos: original_utxos,
bumping_fee: Some(tx_builder::PreviousFee {
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
rate: feerate.as_sat_per_vb(),
rate: feerate.as_sat_vb(),
}),
..Default::default()
};
@@ -1196,12 +1189,7 @@ where
}
}
/// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass
/// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to
/// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer)
/// for further information.
///
/// Returns `true` if the PSBT could be finalized, and `false` otherwise.
/// Try to finalize a PSBT
///
/// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
pub fn finalize_psbt(
@@ -1842,7 +1830,7 @@ where
.to_string()
.split_once('#')
.unwrap()
.1
.0
.to_string()
}
}
@@ -1938,28 +1926,6 @@ pub(crate) mod test {
// OP_PUSH.
const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
#[test]
fn test_descriptor_checksum() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let checksum = wallet.descriptor_checksum(KeychainKind::External);
assert_eq!(checksum.len(), 8);
let raw_descriptor = wallet
.descriptor
.to_string()
.split_once('#')
.unwrap()
.0
.to_string();
assert_eq!(get_checksum(&raw_descriptor).unwrap(), checksum);
}
#[test]
fn test_get_funded_wallet_balance() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
assert_eq!(wallet.get_balance().unwrap().confirmed, 50000);
}
#[test]
fn test_cache_addresses_fixed() {
let db = MemoryDatabase::new();
@@ -2093,10 +2059,6 @@ pub(crate) mod test {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
pub(crate) fn get_test_tr_dup_keys() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
macro_rules! assert_fee_rate {
($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
let psbt = $psbt.clone();
@@ -2133,7 +2095,7 @@ pub(crate) mod test {
let fee_rate = $fee_rate;
if !dust_change {
assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
} else {
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
}
@@ -2211,22 +2173,7 @@ pub(crate) mod test {
#[test]
fn test_create_tx_default_locktime() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
// Add the transaction to our db, but do not sync the db.
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
@@ -2417,23 +2364,7 @@ pub(crate) mod test {
#[test]
fn test_create_tx_default_sequence() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
// Add the transaction to our db, but do not sync the db. Unsynced db
// should trigger the default sequence value for a new transaction as 0xFFFFFFFF
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
@@ -2931,22 +2862,7 @@ pub(crate) mod test {
#[test]
fn test_create_tx_policy_path_no_csv() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
// Add the transaction to our db, but do not sync the db. Unsynced db
// should trigger the default sequence value for a new transaction as 0xFFFFFFFF
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
@@ -4984,29 +4900,6 @@ pub(crate) mod test {
);
}
#[test]
fn test_taproot_sign_using_non_witness_utxo() {
let (wallet, _, prev_txid) = get_funded_wallet(get_test_tr_single_sig());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = wallet.database().get_raw_tx(&prev_txid).unwrap();
assert!(
psbt.inputs[0].non_witness_utxo.is_some(),
"Previous tx should be present in the database"
);
let result = wallet.sign(&mut psbt, Default::default());
assert!(result.is_ok(), "Signing should have worked");
assert!(
result.unwrap(),
"Should finalize the input since we can produce signatures"
);
}
#[test]
fn test_taproot_foreign_utxo() {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
@@ -5460,30 +5353,6 @@ pub(crate) mod test {
builder.finish().unwrap();
}
#[test]
fn test_allow_dust_limit() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 0);
assert!(matches!(
builder.finish().unwrap_err(),
Error::OutputBelowDustLimit(0)
));
let mut builder = wallet.build_tx();
builder
.allow_dust(true)
.add_recipient(addr.script_pubkey(), 0);
assert!(builder.finish().is_ok());
}
#[test]
fn test_fee_rate_sign_no_grinding_high_r() {
// Our goal is to obtain a transaction with a signature with high-R (71 bytes
@@ -5545,48 +5414,4 @@ pub(crate) mod test {
// ...and checking that everything is fine
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
}
#[cfg(feature = "test-hardware-signer")]
#[test]
fn test_create_signer() {
use crate::wallet::hardwaresigner::HWISigner;
use hwi::types::HWIChain;
use hwi::HWIClient;
let devices = HWIClient::enumerate().unwrap();
let device = devices.first().expect("No devices found");
let client = HWIClient::get_client(device, true, HWIChain::Regtest).unwrap();
let descriptors = client.get_descriptors(None).unwrap();
let custom_signer = HWISigner::from_device(device, HWIChain::Regtest).unwrap();
let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]);
wallet.add_signer(
KeychainKind::External,
SignerOrdering(200),
Arc::new(custom_signer),
);
let addr = wallet.get_address(LastUnused).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
}
#[test]
fn test_taproot_load_descriptor_duplicated_keys() {
// Added after issue https://github.com/bitcoindevkit/bdk/issues/760
//
// Having the same key in multiple taproot leaves is safe and should be accepted by BDK
let (wallet, _, _) = get_funded_wallet(get_test_tr_dup_keys());
let addr = wallet.get_address(New).unwrap();
assert_eq!(
addr.to_string(),
"bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5"
);
}
}

View File

@@ -103,7 +103,6 @@ use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils};
use crate::psbt::PsbtUtils;
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
/// multiple of them
@@ -160,16 +159,6 @@ pub enum SignerError {
InvalidSighash,
/// Error while computing the hash to sign
SighashError(sighash::Error),
/// Error while signing using hardware wallets
#[cfg(feature = "hardware-signer")]
HWIError(hwi::error::Error),
}
#[cfg(feature = "hardware-signer")]
impl From<hwi::error::Error> for SignerError {
fn from(e: hwi::error::Error) -> Self {
SignerError::HWIError(e)
}
}
impl From<sighash::Error> for SignerError {
@@ -486,10 +475,10 @@ fn sign_psbt_ecdsa(
hash_ty: EcdsaSighashType,
secp: &SecpCtx,
) {
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_ecdsa(msg, secret_key);
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
.expect("invalid or corrupted ecdsa signature");
let sig = secp.sign_ecdsa(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
secret_key,
);
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
psbt_input.partial_sigs.insert(pubkey, final_signature);
@@ -515,10 +504,10 @@ fn sign_psbt_schnorr(
Some(_) => keypair, // no tweak for script spend
};
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_schnorr(msg, &keypair);
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair))
.expect("invalid or corrupted schnorr signature");
let sig = secp.sign_schnorr(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
&keypair,
);
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
@@ -697,14 +686,14 @@ pub struct SignOptions {
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
pub allow_all_sighashes: bool,
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
/// Whether to remove partial_sigs from psbt inputs while finalizing psbt.
///
/// Defaults to `true` which will remove partial signatures during finalization.
/// Defaults to `true` which will remove partial_sigs after finalizing.
pub remove_partial_sigs: bool,
/// Whether to try finalizing the PSBT after the inputs are signed.
/// Whether to try finalizing psbt input after the inputs are signed.
///
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
pub try_finalize: bool,
/// Specifies which Taproot script-spend leaves we should sign for. This option is
@@ -922,8 +911,11 @@ impl ComputeSighash for Tap {
.unwrap_or_else(|| SchnorrSighashType::Default.into())
.schnorr_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let witness_utxos = (0..psbt.inputs.len())
.map(|i| psbt.get_utxo_for(i))
let witness_utxos = psbt
.inputs
.iter()
.cloned()
.map(|i| i.witness_utxo)
.collect::<Vec<_>>();
let mut all_witness_utxos = vec![];

View File

@@ -148,7 +148,6 @@ pub(crate) struct TxParams {
pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>,
pub(crate) current_height: Option<u32>,
pub(crate) allow_dust: bool,
}
#[derive(Clone, Copy, Debug)]
@@ -561,14 +560,6 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
self.params.current_height = Some(height);
self
}
/// Set whether or not the dust limit is checked.
///
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
self.params.allow_dust = allow_dust;
self
}
}
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {

View File

@@ -144,6 +144,7 @@ mod test {
SEQUENCE_LOCKTIME_TYPE_FLAG,
};
use crate::bitcoin::Address;
use crate::types::FeeRate;
use std::str::FromStr;
#[test]
@@ -163,6 +164,24 @@ mod test {
assert!(!294.is_dust(&script_p2wpkh));
}
#[test]
fn test_fee_from_btc_per_kb() {
let fee = FeeRate::from_btc_per_kvb(1e-5);
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
}
#[test]
fn test_fee_from_sats_vbyte() {
let fee = FeeRate::from_sat_per_vb(1.0);
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
}
#[test]
fn test_fee_default_min_relay_fee() {
let fee = FeeRate::default_min_relay_fee();
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
}
#[test]
fn test_check_nsequence_rbf_msb_set() {
let result = check_nsequence_rbf(0x80000000, 5000);

View File

@@ -29,8 +29,6 @@ use crate::error::Error;
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
/// with unconfirmed transactions that have been evicted from the backend's memory.
///
/// [`Blockchain`]: crate::blockchain::Blockchain
pub fn verify_tx<D: Database, B: GetTx>(
tx: &Transaction,
database: &D,