Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
476fa3fd7d | ||
|
|
2755b09e7b | ||
|
|
5e6286a493 | ||
|
|
67714adc80 | ||
|
|
9ff86ea37c | ||
|
|
ceeb3a40cf | ||
|
|
e3316aee4c | ||
|
|
c2567b61aa | ||
|
|
e1a77b87ab | ||
|
|
5bf758b03a | ||
|
|
0bbfa5f989 | ||
|
|
18254110c6 | ||
|
|
44217539e5 | ||
|
|
33b45ebe82 | ||
|
|
2faed425ed | ||
|
|
2cc05c07a5 | ||
|
|
fe371f9d92 | ||
|
|
12de13b95c | ||
|
|
9205295332 | ||
|
|
3b446c9e14 | ||
|
|
378167efca | ||
|
|
224be27aa8 | ||
|
|
4a23070cc8 | ||
|
|
ba2e3042cc | ||
|
|
f8117c0f9f | ||
|
|
1639984b56 | ||
|
|
ab54a17eb7 | ||
|
|
ae5aa06586 | ||
|
|
ab98283159 | ||
|
|
81851190f0 | ||
|
|
e1b037a921 | ||
|
|
9b7ed08891 | ||
|
|
dffb753ce3 | ||
|
|
0b969657cd | ||
|
|
bfef2e3cfe | ||
|
|
0ec064ef13 | ||
|
|
6b60914ca1 | ||
|
|
881ca8d1e3 | ||
|
|
5633475ce8 | ||
|
|
ea8488b2a7 | ||
|
|
d2a981efee | ||
|
|
4c92daf517 | ||
|
|
aba2a05d83 | ||
|
|
5b194c268d | ||
|
|
00bdf08f2a | ||
|
|
38b0470b14 | ||
|
|
d60c5003bf | ||
|
|
fcae5adabd | ||
|
|
9f04a9d82d | ||
|
|
465ef6e674 | ||
|
|
aaa9943a5f | ||
|
|
3897e29740 | ||
|
|
8f06e45872 | ||
|
|
766570abfd | ||
|
|
934ec366d9 | ||
|
|
d0733e9496 | ||
|
|
3c7a1f5918 | ||
|
|
85aadaccd2 | ||
|
|
fad0fe9f30 | ||
|
|
6546b77c08 | ||
|
|
e1066e955c | ||
|
|
7f06dc3330 | ||
|
|
de40351710 | ||
|
|
de811bea30 | ||
|
|
74cc80d127 | ||
|
|
009f68a06a | ||
|
|
47f26447da | ||
|
|
12641b9e8f | ||
|
|
aa3707b5b4 | ||
|
|
f6631e35b8 | ||
|
|
3608ff9f14 | ||
|
|
7fdb98e147 | ||
|
|
9aea90bd81 | ||
|
|
898dfe6cf1 | ||
|
|
7961ae7f8e | ||
|
|
8bf77c8f07 | ||
|
|
3c7bae9ce9 | ||
|
|
17bcd8ed7d | ||
|
|
b5e9589803 | ||
|
|
1d628d84b5 | ||
|
|
b84fd6ea5c | ||
|
|
8fe4222c33 | ||
|
|
e626f2e255 | ||
|
|
5a0c150ff9 | ||
|
|
00f07818f9 | ||
|
|
136a4bddb2 | ||
|
|
ff7b74ec27 | ||
|
|
8c00326990 | ||
|
|
afcd26032d | ||
|
|
8f422a1bf9 | ||
|
|
45983d2166 | ||
|
|
89cb4de7f6 | ||
|
|
7ca0e0e2bd | ||
|
|
2bddd9baed | ||
|
|
0135ba29c5 | ||
|
|
549cd24812 | ||
|
|
a841b5d635 | ||
|
|
16ceb6cb30 | ||
|
|
edfd7d454c | ||
|
|
1d874e50c2 | ||
|
|
98127cc5da | ||
|
|
e243107bb6 | ||
|
|
237a8d4e69 | ||
|
|
7f4042ba1b | ||
|
|
3ed44ce8cf | ||
|
|
8e7d8312a9 | ||
|
|
4da7488dc4 | ||
|
|
e37680af96 | ||
|
|
5f873ae500 | ||
|
|
2380634496 | ||
|
|
af98b8da06 | ||
|
|
b68ec050e2 | ||
|
|
ac7df09200 | ||
|
|
192965413c | ||
|
|
745be7bea8 | ||
|
|
b6007e05c1 | ||
|
|
f53654d9f4 | ||
|
|
e5ecc7f541 | ||
|
|
882a9c27cc | ||
|
|
1e6b8e12b2 | ||
|
|
b226658977 | ||
|
|
6d6776eb58 | ||
|
|
f1f844a5b6 | ||
|
|
a3e45358de | ||
|
|
07e79f6e8a | ||
|
|
d94b8f87a3 | ||
|
|
fdb895d26c | ||
|
|
7041e96737 | ||
|
|
199f716ebb | ||
|
|
b12e358c1d | ||
|
|
f786f0e624 | ||
|
|
71e0472dc9 | ||
|
|
c456a252f8 | ||
|
|
d837a762fc | ||
|
|
e82dfa971e | ||
|
|
cc17ac8859 | ||
|
|
3798b4d115 | ||
|
|
2d0f6c4ec5 | ||
|
|
f3b475ff0e | ||
|
|
41ae202d02 | ||
|
|
fef6176275 | ||
|
|
14ae64e09d | ||
|
|
48215675b0 | ||
|
|
37fa35b24a | ||
|
|
23ec9c3ba0 |
34
.github/workflows/code_coverage.yml
vendored
34
.github/workflows/code_coverage.yml
vendored
@@ -3,25 +3,35 @@ on: [push]
|
||||
name: Code Coverage
|
||||
|
||||
jobs:
|
||||
tarpaulin-codecov:
|
||||
name: Tarpaulin to codecov.io
|
||||
|
||||
Codecov:
|
||||
name: Code Coverage
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
|
||||
|
||||
- id: coverage
|
||||
name: Generate coverage
|
||||
uses: actions-rs/grcov@v0.1.5
|
||||
|
||||
- name: Install tarpaulin
|
||||
run: cargo install cargo-tarpaulin
|
||||
- name: Tarpaulin
|
||||
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
|
||||
|
||||
- name: Publish to codecov.io
|
||||
uses: codecov/codecov-action@v1.0.15
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
file: ./cobertura.xml
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
directory: ./coverage/reports/
|
||||
|
||||
48
.github/workflows/cont_integration.yml
vendored
48
.github/workflows/cont_integration.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- 1.50.0 # STABLE
|
||||
- 1.45.0 # MSRV
|
||||
- 1.53.0 # STABLE
|
||||
- 1.46.0 # MSRV
|
||||
features:
|
||||
- default
|
||||
- minimal
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
- compact_filters
|
||||
- esplora,key-value-db,electrum
|
||||
- compiler
|
||||
- rpc
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
- name: Build
|
||||
run: cargo build --features ${{ matrix.features }} --no-default-features
|
||||
- name: Clippy
|
||||
run: cargo clippy --features ${{ matrix.features }} --no-default-features -- -D warnings
|
||||
run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
|
||||
- name: Test
|
||||
run: cargo test --features ${{ matrix.features }} --no-default-features
|
||||
|
||||
@@ -73,17 +74,30 @@ jobs:
|
||||
- name: Test
|
||||
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
|
||||
|
||||
test-electrum:
|
||||
name: Test electrum
|
||||
test-blockchains:
|
||||
name: Test ${{ matrix.blockchain.name }}
|
||||
runs-on: ubuntu-16.04
|
||||
container: bitcoindevkit/electrs:0.2.0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
blockchain:
|
||||
- name: electrum
|
||||
container: bitcoindevkit/electrs:0.4.0
|
||||
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
|
||||
- name: esplora
|
||||
container: bitcoindevkit/esplora:0.4.0
|
||||
start: /root/electrs --network regtest -vvv --daemon-dir $GITHUB_WORKSPACE/.bitcoin --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002
|
||||
- name: rpc
|
||||
container: bitcoindevkit/electrs:0.4.0
|
||||
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
|
||||
container: ${{ matrix.blockchain.container }}
|
||||
env:
|
||||
BDK_RPC_AUTH: USER_PASS
|
||||
BDK_RPC_USER: admin
|
||||
BDK_RPC_PASS: passw
|
||||
BDK_RPC_AUTH: COOKIEFILE
|
||||
BDK_RPC_COOKIEFILE: ${{ github.workspace }}/.bitcoin/regtest/.cookie
|
||||
BDK_RPC_URL: 127.0.0.1:18443
|
||||
BDK_RPC_WALLET: bdk-test
|
||||
BDK_ELECTRUM_URL: tcp://127.0.0.1:60401
|
||||
BDK_ESPLORA_URL: http://127.0.0.1:3002
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -95,18 +109,22 @@ jobs:
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: get pkg-config # running esplora tests seems to need this
|
||||
run: apt update && apt install -y --fix-missing pkg-config libssl-dev
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: $HOME/.cargo/bin/rustup default 1.50.0 # STABLE
|
||||
run: $HOME/.cargo/bin/rustup default 1.53.0 # STABLE
|
||||
- name: Set profile
|
||||
run: $HOME/.cargo/bin/rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: $HOME/.cargo/bin/rustup update
|
||||
- name: Start core
|
||||
run: ./ci/start-core.sh
|
||||
- name: start ${{ matrix.blockchain.name }}
|
||||
run: nohup ${{ matrix.blockchain.start }} & sleep 5
|
||||
- name: Test
|
||||
run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features
|
||||
run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||
|
||||
check-wasm:
|
||||
name: Check WASM
|
||||
@@ -131,7 +149,7 @@ jobs:
|
||||
- run: sudo apt-get update || exit 1
|
||||
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||
- name: Set default toolchain
|
||||
run: rustup default 1.50.0 # STABLE
|
||||
run: rustup default 1.53.0 # STABLE
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add target wasm32
|
||||
@@ -148,12 +166,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set default toolchain
|
||||
run: rustup default 1.50.0 # STABLE
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add clippy
|
||||
- name: Add rustfmt
|
||||
run: rustup component add rustfmt
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Check fmt
|
||||
run: cargo fmt --all -- --check
|
||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||
|
||||
17
.github/workflows/nightly_docs.yml
vendored
17
.github/workflows/nightly_docs.yml
vendored
@@ -17,17 +17,14 @@ jobs:
|
||||
~/.cargo/git
|
||||
target
|
||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Install nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Build docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: rustdoc
|
||||
args: --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
|
||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -6,6 +6,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Wallet
|
||||
#### Added
|
||||
- Bitcoin core RPC added as blockchain backend
|
||||
|
||||
## [v0.8.0] - [v0.7.0]
|
||||
|
||||
### Wallet
|
||||
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
|
||||
#### Changed
|
||||
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
|
||||
|
||||
## [v0.7.0] - [v0.6.0]
|
||||
|
||||
### Policy
|
||||
#### Changed
|
||||
Removed `fill_satisfaction` method in favor of enum parameter in `extract_policy` method
|
||||
|
||||
#### Added
|
||||
Timelocks are considered (optionally) in building the `satisfaction` field
|
||||
|
||||
### Wallet
|
||||
|
||||
- Changed `Wallet::{sign, finalize_psbt}` now take a `&mut psbt` rather than consuming it.
|
||||
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
|
||||
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
|
||||
|
||||
## [v0.6.0] - [v0.5.1]
|
||||
|
||||
### Misc
|
||||
#### Changed
|
||||
- New minimum supported rust version is 1.46.0
|
||||
- Changed `AnyBlockchainConfig` to use serde tagged representation.
|
||||
|
||||
### Descriptor
|
||||
#### Added
|
||||
- Added ability to analyze a `PSBT` to check which and how many signatures are already available
|
||||
|
||||
### Wallet
|
||||
#### Changed
|
||||
- `get_new_address()` refactored to `get_address(AddressIndex::New)` to support different `get_address()` index selection strategies
|
||||
|
||||
#### Added
|
||||
- Added `get_address(AddressIndex::LastUnused)` which returns the last derived address if it has not been used or if used in a received transaction returns a new address
|
||||
- Added `get_address(AddressIndex::Peek(u32))` which returns a derived address for a specified descriptor index but does not change the current index
|
||||
- Added `get_address(AddressIndex::Reset(u32))` which returns a derived address for a specified descriptor index and resets current index to the given value
|
||||
- Added `get_psbt_input` to create the corresponding psbt input for a local utxo.
|
||||
|
||||
#### Fixed
|
||||
- Fixed `coin_select` calculation for UTXOs where `value < fee` that caused over-/underflow errors.
|
||||
|
||||
## [v0.5.1] - [v0.5.0]
|
||||
|
||||
### Misc
|
||||
#### Changed
|
||||
- Pin `hyper` to `=0.14.4` to make it compile on Rust 1.45
|
||||
|
||||
## [v0.5.0] - [v0.4.0]
|
||||
|
||||
### Misc
|
||||
@@ -287,9 +343,13 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Use `MemoryDatabase` in the compiler example
|
||||
- Make the REPL return JSON
|
||||
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...HEAD
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...HEAD
|
||||
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
||||
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
|
||||
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
||||
[v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0
|
||||
[v0.5.0]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...v0.5.0
|
||||
[v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
|
||||
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
|
||||
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
|
||||
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0
|
||||
|
||||
@@ -46,7 +46,7 @@ Every new feature should be covered by functional tests where possible.
|
||||
When refactoring, structure your PR to make it easy to review and don't
|
||||
hesitate to split it into multiple small, focused PRs.
|
||||
|
||||
The Minimal Supported Rust Version is 1.45 (enforced by our CI).
|
||||
The Minimal Supported Rust Version is 1.46 (enforced by our CI).
|
||||
|
||||
Commits should cover both the issue fixed and the solution's rationale.
|
||||
These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind.
|
||||
|
||||
25
Cargo.toml
25
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.5.0"
|
||||
version = "0.8.1-dev"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0"
|
||||
bdk-macros = "^0.4"
|
||||
log = "^0.4"
|
||||
miniscript = "5.1"
|
||||
bitcoin = { version = "^0.26", features = ["use-serde"] }
|
||||
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.7"
|
||||
@@ -32,6 +32,10 @@ socks = { version = "0.3", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
tiny-bip39 = { version = "^0.8", optional = true }
|
||||
|
||||
# Needed by bdk_blockchain_tests macro
|
||||
bitcoincore-rpc = { version = "0.13", optional = true }
|
||||
serial_test = { version = "0.4", optional = true }
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
@@ -52,20 +56,22 @@ key-value-db = ["sled"]
|
||||
async-interface = ["async-trait"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["tiny-bip39"]
|
||||
rpc = ["bitcoincore-rpc"]
|
||||
|
||||
|
||||
# Debug/Test features
|
||||
debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"]
|
||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||
test-electrum = ["electrum"]
|
||||
test-rpc = ["rpc"]
|
||||
test-esplora = ["esplora"]
|
||||
test-md-docs = ["electrum"]
|
||||
|
||||
[dev-dependencies]
|
||||
bdk-testutils = "0.4"
|
||||
bdk-testutils-macros = "0.4"
|
||||
serial_test = "0.4"
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
base64 = "^0.11"
|
||||
clap = "2.33"
|
||||
serial_test = "0.4"
|
||||
bitcoind = "0.10.0"
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
@@ -79,10 +85,7 @@ path = "examples/compiler.rs"
|
||||
required-features = ["compiler"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros", "testutils", "testutils-macros"]
|
||||
|
||||
# Generate docs with nightly to add the "features required" badge
|
||||
# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
|
||||
members = ["macros"]
|
||||
[package.metadata.docs.rs]
|
||||
features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
|
||||
# defines the configuration attribute `docsrs`
|
||||
|
||||
@@ -20,7 +20,7 @@ As soon as the release is tagged and published, the `release` branch will be mer
|
||||
|
||||
## Making the Release
|
||||
|
||||
What follows are notes and procedures that maintaners 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).
|
||||
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.
|
||||
|
||||
@@ -39,7 +39,8 @@ Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordin
|
||||
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. 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.
|
||||
15. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
||||
16. Announce the release on Twitter, Discord and Telegram.
|
||||
17. Celebrate :tada:
|
||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros`, `bdk-testutils`, or `bdk-testutils-macros` crates, change the `bdk` Cargo.toml `[dev-dependencies]` to point to the local path (ie. `bdk-testutils-macros = { path = "./testutils-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:
|
||||
|
||||
20
README.md
20
README.md
@@ -13,7 +13,7 @@
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html"><img alt="Rustc Version 1.45+" src="https://img.shields.io/badge/rustc-1.45%2B-lightgrey.svg"/></a>
|
||||
<a href="https://blog.rust-lang.org/2020/08/27/Rust-1.46.0.html"><img alt="Rustc Version 1.46+" src="https://img.shields.io/badge/rustc-1.46%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
@@ -67,6 +67,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
|
||||
```rust
|
||||
use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new_offline(
|
||||
@@ -76,9 +77,9 @@ fn main() -> Result<(), bdk::Error> {
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
println!("Address #0: {}", wallet.get_new_address()?);
|
||||
println!("Address #1: {}", wallet.get_new_address()?);
|
||||
println!("Address #2: {}", wallet.get_new_address()?);
|
||||
println!("Address #0: {}", wallet.get_address(New)?);
|
||||
println!("Address #1: {}", wallet.get_address(New)?);
|
||||
println!("Address #2: {}", wallet.get_address(New)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -92,6 +93,7 @@ use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
use bitcoin::consensus::serialize;
|
||||
|
||||
@@ -107,7 +109,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
|
||||
let send_to = wallet.get_new_address()?;
|
||||
let send_to = wallet.get_address(New)?;
|
||||
let (psbt, details) = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
@@ -128,7 +130,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
### Sign a transaction
|
||||
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use bitcoin::consensus::deserialize;
|
||||
|
||||
@@ -141,9 +143,9 @@ fn main() -> Result<(), bdk::Error> {
|
||||
)?;
|
||||
|
||||
let psbt = "...";
|
||||
let psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||
|
||||
let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -164,4 +166,4 @@ at your option.
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echo "Starting bitcoin node."
|
||||
/root/bitcoind -regtest -server -daemon -fallbackfee=0.0002 -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
|
||||
mkdir $GITHUB_WORKSPACE/.bitcoin
|
||||
/root/bitcoind -regtest -server -daemon -datadir=$GITHUB_WORKSPACE/.bitcoin -fallbackfee=0.0002 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
|
||||
|
||||
echo "Waiting for bitcoin node."
|
||||
until /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS getblockchaininfo; do
|
||||
until /root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin getblockchaininfo; do
|
||||
sleep 1
|
||||
done
|
||||
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS createwallet $BDK_RPC_WALLET
|
||||
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin createwallet $BDK_RPC_WALLET
|
||||
echo "Generating 150 bitcoin blocks."
|
||||
ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress)
|
||||
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR
|
||||
|
||||
echo "Starting electrs node."
|
||||
nohup /root/electrs --network regtest --jsonrpc-import &
|
||||
sleep 5
|
||||
ADDR=$(/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin -rpcwallet=$BDK_RPC_WALLET getnewaddress)
|
||||
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin generatetoaddress 150 $ADDR
|
||||
|
||||
13
codecov.yaml
Normal file
13
codecov.yaml
Normal 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
|
||||
@@ -13,11 +13,12 @@ use std::sync::Arc;
|
||||
|
||||
use bdk::bitcoin;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::descriptor::HDKeyPaths;
|
||||
use bdk::descriptor::HdKeyPaths;
|
||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
||||
use bdk::KeychainKind;
|
||||
use bdk::Wallet;
|
||||
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::{Network, Script};
|
||||
@@ -28,7 +29,7 @@ impl AddressValidator for DummyValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
keychain: KeychainKind,
|
||||
hd_keypaths: &HDKeyPaths,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
script: &Script,
|
||||
) -> Result<(), AddressValidatorError> {
|
||||
let (_, path) = hd_keypaths
|
||||
@@ -52,9 +53,9 @@ fn main() -> Result<(), bdk::Error> {
|
||||
|
||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
||||
|
||||
wallet.get_new_address()?;
|
||||
wallet.get_new_address()?;
|
||||
wallet.get_new_address()?;
|
||||
wallet.get_address(New)?;
|
||||
wallet.get_address(New)?;
|
||||
wallet.get_address(New)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::database::memory::MemoryDatabase;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -90,7 +91,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.unwrap_or(Network::Testnet);
|
||||
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database)?;
|
||||
|
||||
info!("... First address: {}", wallet.get_new_address()?);
|
||||
info!("... First address: {}", wallet.get_address(New)?);
|
||||
|
||||
if matches.is_present("parsed_policy") {
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
|
||||
71
run_blockchain_tests.sh
Executable file
71
run_blockchain_tests.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker.
|
||||
|
||||
Usage: ./run_blockchain_tests.sh [esplora|electrum|rpc] [test name].
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
eprintln(){
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if test "$id"; then
|
||||
eprintln "cleaning up $blockchain docker container $id";
|
||||
docker rm -fv "$id" > /dev/null;
|
||||
rm /tmp/regtest-"$id".cookie;
|
||||
fi
|
||||
trap - EXIT INT
|
||||
}
|
||||
|
||||
# Makes sure we clean up the container at the end or if ^C
|
||||
trap 'rc=$?; cleanup; exit $rc' EXIT INT
|
||||
|
||||
blockchain="$1"
|
||||
test_name="$2"
|
||||
|
||||
case "$blockchain" in
|
||||
electrum)
|
||||
eprintln "starting electrs docker container"
|
||||
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
|
||||
;;
|
||||
esplora)
|
||||
eprintln "starting esplora docker container"
|
||||
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora:0.4.0)"
|
||||
export BDK_ESPLORA_URL=http://127.0.0.1:3002
|
||||
;;
|
||||
rpc)
|
||||
eprintln "starting bitcoind docker container (via electrs container)"
|
||||
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
|
||||
;;
|
||||
*)
|
||||
usage;
|
||||
exit 1;
|
||||
;;
|
||||
esac
|
||||
|
||||
# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box
|
||||
export BDK_RPC_AUTH=COOKIEFILE
|
||||
export BDK_RPC_COOKIEFILE=/tmp/regtest-"$id".cookie
|
||||
export BDK_RPC_URL=127.0.0.1:18443
|
||||
export BDK_RPC_WALLET=bdk-test
|
||||
export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401
|
||||
|
||||
cli(){
|
||||
docker exec -it "$id" /root/bitcoin-cli -regtest -datadir=/root/.bitcoin $@
|
||||
}
|
||||
|
||||
#eprintln "running getwalletinfo until bitcoind seems to be alive"
|
||||
while ! cli getwalletinfo >/dev/null; do sleep 1; done
|
||||
|
||||
# sleep again for good measure!
|
||||
sleep 1;
|
||||
|
||||
# copy bitcoind cookie file to /tmp
|
||||
docker cp "$id":/root/.bitcoin/regtest/.cookie /tmp/regtest-"$id".cookie
|
||||
|
||||
cargo test --features "test-blockchains,test-$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name"
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Run various invocations of cargo check
|
||||
|
||||
features=( "default" "compiler" "electrum" "esplora" "compact_filters" "key-value-db" "async-interface" "all-keys" "keys-bip39" )
|
||||
toolchains=( "+stable" "+1.45" "+nightly" )
|
||||
|
||||
main() {
|
||||
check_src
|
||||
check_all_targets
|
||||
}
|
||||
|
||||
# Check with all features, with various toolchains.
|
||||
check_src() {
|
||||
for toolchain in "${toolchains[@]}"; do
|
||||
cmd="cargo $toolchain clippy --all-targets --no-default-features"
|
||||
|
||||
for feature in "${features[@]}"; do
|
||||
touch_files
|
||||
$cmd --features "$feature"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Touch files to prevent cached warnings from not showing up.
|
||||
touch_files() {
|
||||
touch $(find . -name *.rs)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
||||
@@ -177,7 +177,34 @@ impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilt
|
||||
/// This allows storing a single configuration that can be loaded into an [`AnyBlockchain`]
|
||||
/// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime
|
||||
/// will find this particularly useful.
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
///
|
||||
/// This type can be serialized from a JSON object like:
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(feature = "electrum")]
|
||||
/// # {
|
||||
/// use bdk::blockchain::{electrum::ElectrumBlockchainConfig, AnyBlockchainConfig};
|
||||
/// let config: AnyBlockchainConfig = serde_json::from_str(
|
||||
/// r#"{
|
||||
/// "type" : "electrum",
|
||||
/// "url" : "ssl://electrum.blockstream.info:50002",
|
||||
/// "retry": 2
|
||||
/// }"#,
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// assert_eq!(
|
||||
/// config,
|
||||
/// AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
|
||||
/// url: "ssl://electrum.blockstream.info:50002".into(),
|
||||
/// retry: 2,
|
||||
/// socks5: None,
|
||||
/// timeout: None
|
||||
/// })
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AnyBlockchainConfig {
|
||||
#[cfg(feature = "electrum")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
||||
|
||||
@@ -71,7 +71,7 @@ use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::FeeRate;
|
||||
use crate::{ConfirmationTime, FeeRate};
|
||||
|
||||
use peer::*;
|
||||
use store::*;
|
||||
@@ -146,7 +146,7 @@ impl CompactFiltersBlockchain {
|
||||
database: &mut D,
|
||||
tx: &Transaction,
|
||||
height: Option<u32>,
|
||||
timestamp: u64,
|
||||
timestamp: Option<u64>,
|
||||
internal_max_deriv: &mut Option<u32>,
|
||||
external_max_deriv: &mut Option<u32>,
|
||||
) -> Result<(), Error> {
|
||||
@@ -206,9 +206,8 @@ impl CompactFiltersBlockchain {
|
||||
transaction: Some(tx.clone()),
|
||||
received: incoming,
|
||||
sent: outgoing,
|
||||
height,
|
||||
timestamp,
|
||||
fees: inputs_sum.saturating_sub(outputs_sum),
|
||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||
};
|
||||
|
||||
info!("Saving tx {}", tx.txid);
|
||||
@@ -237,7 +236,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
|
||||
let skip_blocks = self.skip_blocks.unwrap_or(0);
|
||||
|
||||
let cf_sync = Arc::new(CFSync::new(Arc::clone(&self.headers), skip_blocks, 0x00)?);
|
||||
let cf_sync = Arc::new(CfSync::new(Arc::clone(&self.headers), skip_blocks, 0x00)?);
|
||||
|
||||
let initial_height = self.headers.get_height()?;
|
||||
let total_bundles = (first_peer.get_version().start_height as usize)
|
||||
@@ -364,8 +363,8 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
);
|
||||
let mut updates = database.begin_batch();
|
||||
for details in database.iter_txs(false)? {
|
||||
match details.height {
|
||||
Some(height) if (height as usize) < last_synced_block => continue,
|
||||
match details.confirmation_time {
|
||||
Some(c) if (c.height as usize) < last_synced_block => continue,
|
||||
_ => updates.del_tx(&details.txid, false)?,
|
||||
};
|
||||
}
|
||||
@@ -387,7 +386,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
database,
|
||||
tx,
|
||||
Some(height as u32),
|
||||
0,
|
||||
None,
|
||||
&mut internal_max_deriv,
|
||||
&mut external_max_deriv,
|
||||
)?;
|
||||
@@ -398,7 +397,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
database,
|
||||
tx,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
&mut internal_max_deriv,
|
||||
&mut external_max_deriv,
|
||||
)?;
|
||||
@@ -456,7 +455,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
}
|
||||
|
||||
/// Data to connect to a Bitcoin P2P peer
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct BitcoinPeerConfig {
|
||||
/// Peer address such as 127.0.0.1:18333
|
||||
pub address: String,
|
||||
@@ -467,7 +466,7 @@ pub struct BitcoinPeerConfig {
|
||||
}
|
||||
|
||||
/// Configuration for a [`CompactFiltersBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct CompactFiltersBlockchainConfig {
|
||||
/// List of peers to try to connect to for asking headers and filters
|
||||
pub peers: Vec<BitcoinPeerConfig>,
|
||||
@@ -537,11 +536,11 @@ pub enum CompactFiltersError {
|
||||
NoPeers,
|
||||
|
||||
/// Internal database error
|
||||
DB(rocksdb::Error),
|
||||
Db(rocksdb::Error),
|
||||
/// Internal I/O error
|
||||
IO(std::io::Error),
|
||||
Io(std::io::Error),
|
||||
/// Invalid BIP158 filter
|
||||
BIP158(bitcoin::util::bip158::Error),
|
||||
Bip158(bitcoin::util::bip158::Error),
|
||||
/// Internal system time error
|
||||
Time(std::time::SystemTimeError),
|
||||
|
||||
@@ -557,9 +556,9 @@ impl fmt::Display for CompactFiltersError {
|
||||
|
||||
impl std::error::Error for CompactFiltersError {}
|
||||
|
||||
impl_error!(rocksdb::Error, DB, CompactFiltersError);
|
||||
impl_error!(std::io::Error, IO, CompactFiltersError);
|
||||
impl_error!(bitcoin::util::bip158::Error, BIP158, CompactFiltersError);
|
||||
impl_error!(rocksdb::Error, Db, CompactFiltersError);
|
||||
impl_error!(std::io::Error, Io, CompactFiltersError);
|
||||
impl_error!(bitcoin::util::bip158::Error, Bip158, CompactFiltersError);
|
||||
impl_error!(std::time::SystemTimeError, Time, CompactFiltersError);
|
||||
|
||||
impl From<crate::error::Error> for CompactFiltersError {
|
||||
|
||||
@@ -227,12 +227,12 @@ impl Peer {
|
||||
|
||||
Ok(Peer {
|
||||
writer,
|
||||
reader_thread,
|
||||
responses,
|
||||
reader_thread,
|
||||
connected,
|
||||
mempool,
|
||||
network,
|
||||
version,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ impl Encodable for BundleStatus {
|
||||
BundleStatus::Init => {
|
||||
written += 0x00u8.consensus_encode(&mut e)?;
|
||||
}
|
||||
BundleStatus::CFHeaders { cf_headers } => {
|
||||
BundleStatus::CfHeaders { cf_headers } => {
|
||||
written += 0x01u8.consensus_encode(&mut e)?;
|
||||
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
||||
for header in cf_headers {
|
||||
@@ -171,7 +171,7 @@ impl Decodable for BundleStatus {
|
||||
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
|
||||
}
|
||||
|
||||
Ok(BundleStatus::CFHeaders { cf_headers })
|
||||
Ok(BundleStatus::CfHeaders { cf_headers })
|
||||
}
|
||||
0x02 => {
|
||||
let num = VarInt::consensus_decode(&mut d)?;
|
||||
@@ -623,26 +623,26 @@ impl<T: StoreType> fmt::Debug for ChainStore<T> {
|
||||
|
||||
pub enum BundleStatus {
|
||||
Init,
|
||||
CFHeaders { cf_headers: Vec<FilterHeader> },
|
||||
CfHeaders { cf_headers: Vec<FilterHeader> },
|
||||
CFilters { cf_filters: Vec<Vec<u8>> },
|
||||
Processed { cf_filters: Vec<Vec<u8>> },
|
||||
Tip { cf_filters: Vec<Vec<u8>> },
|
||||
Pruned,
|
||||
}
|
||||
|
||||
pub struct CFStore {
|
||||
pub struct CfStore {
|
||||
store: Arc<RwLock<DB>>,
|
||||
filter_type: u8,
|
||||
}
|
||||
|
||||
type BundleEntry = (BundleStatus, FilterHeader);
|
||||
|
||||
impl CFStore {
|
||||
impl CfStore {
|
||||
pub fn new(
|
||||
headers_store: &ChainStore<Full>,
|
||||
filter_type: u8,
|
||||
) -> Result<Self, CompactFiltersError> {
|
||||
let cf_store = CFStore {
|
||||
let cf_store = CfStore {
|
||||
store: Arc::clone(&headers_store.store),
|
||||
filter_type,
|
||||
};
|
||||
@@ -782,7 +782,7 @@ impl CFStore {
|
||||
}
|
||||
|
||||
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
||||
let value = (BundleStatus::CFHeaders { cf_headers }, checkpoint);
|
||||
let value = (BundleStatus::CfHeaders { cf_headers }, checkpoint);
|
||||
|
||||
read_store.put(key, value.serialize())?;
|
||||
|
||||
|
||||
@@ -25,22 +25,22 @@ use crate::error::Error;
|
||||
|
||||
pub(crate) const BURIED_CONFIRMATIONS: usize = 100;
|
||||
|
||||
pub struct CFSync {
|
||||
pub struct CfSync {
|
||||
headers_store: Arc<ChainStore<Full>>,
|
||||
cf_store: Arc<CFStore>,
|
||||
cf_store: Arc<CfStore>,
|
||||
skip_blocks: usize,
|
||||
bundles: Mutex<VecDeque<(BundleStatus, FilterHeader, usize)>>,
|
||||
}
|
||||
|
||||
impl CFSync {
|
||||
impl CfSync {
|
||||
pub fn new(
|
||||
headers_store: Arc<ChainStore<Full>>,
|
||||
skip_blocks: usize,
|
||||
filter_type: u8,
|
||||
) -> Result<Self, CompactFiltersError> {
|
||||
let cf_store = Arc::new(CFStore::new(&headers_store, filter_type)?);
|
||||
let cf_store = Arc::new(CfStore::new(&headers_store, filter_type)?);
|
||||
|
||||
Ok(CFSync {
|
||||
Ok(CfSync {
|
||||
headers_store,
|
||||
cf_store,
|
||||
skip_blocks,
|
||||
@@ -151,7 +151,7 @@ impl CFSync {
|
||||
checkpoint,
|
||||
headers_resp.filter_hashes,
|
||||
)? {
|
||||
BundleStatus::CFHeaders { cf_headers } => cf_headers,
|
||||
BundleStatus::CfHeaders { cf_headers } => cf_headers,
|
||||
_ => return Err(CompactFiltersError::InvalidResponse),
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@ impl CFSync {
|
||||
.cf_store
|
||||
.advance_to_cf_filters(index, checkpoint, cf_headers, filters)?;
|
||||
}
|
||||
if let BundleStatus::CFHeaders { cf_headers } = status {
|
||||
if let BundleStatus::CfHeaders { cf_headers } = status {
|
||||
log::trace!("status: CFHeaders");
|
||||
|
||||
peer.get_cf_filters(
|
||||
|
||||
@@ -33,7 +33,7 @@ use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||
|
||||
use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config};
|
||||
|
||||
use self::utils::{ELSGetHistoryRes, ElectrumLikeSync};
|
||||
use self::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||
use super::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
@@ -45,13 +45,6 @@ use crate::FeeRate;
|
||||
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
|
||||
pub struct ElectrumBlockchain(Client);
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-electrum")]
|
||||
#[bdk_blockchain_tests(crate)]
|
||||
fn local_electrs() -> ElectrumBlockchain {
|
||||
ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap())
|
||||
}
|
||||
|
||||
impl std::convert::From<Client> for ElectrumBlockchain {
|
||||
fn from(client: Client) -> Self {
|
||||
ElectrumBlockchain(client)
|
||||
@@ -107,7 +100,7 @@ impl ElectrumLikeSync for Client {
|
||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error> {
|
||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||
self.batch_script_get_history(scripts)
|
||||
.map(|v| {
|
||||
v.into_iter()
|
||||
@@ -116,7 +109,7 @@ impl ElectrumLikeSync for Client {
|
||||
.map(
|
||||
|electrum_client::GetHistoryRes {
|
||||
height, tx_hash, ..
|
||||
}| ELSGetHistoryRes {
|
||||
}| ElsGetHistoryRes {
|
||||
height,
|
||||
tx_hash,
|
||||
},
|
||||
@@ -144,7 +137,7 @@ impl ElectrumLikeSync for Client {
|
||||
}
|
||||
|
||||
/// Configuration for an [`ElectrumBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct ElectrumBlockchainConfig {
|
||||
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
||||
///
|
||||
@@ -175,3 +168,10 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
crate::bdk_blockchain_tests! {
|
||||
fn test_instance() -> ElectrumBlockchain {
|
||||
ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::{BlockHash, BlockHeader, Script, Transaction, Txid};
|
||||
|
||||
use self::utils::{ELSGetHistoryRes, ElectrumLikeSync};
|
||||
use self::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||
use super::*;
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
@@ -210,7 +210,7 @@ impl UrlClient {
|
||||
async fn _script_get_history(
|
||||
&self,
|
||||
script: &Script,
|
||||
) -> Result<Vec<ELSGetHistoryRes>, EsploraError> {
|
||||
) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
|
||||
let mut result = Vec::new();
|
||||
let scripthash = Self::script_to_scripthash(script);
|
||||
|
||||
@@ -227,7 +227,7 @@ impl UrlClient {
|
||||
.json::<Vec<EsploraGetHistory>>()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| ELSGetHistoryRes {
|
||||
.map(|x| ElsGetHistoryRes {
|
||||
tx_hash: x.txid,
|
||||
height: x.status.block_height.unwrap_or(0) as i32,
|
||||
}),
|
||||
@@ -261,7 +261,7 @@ impl UrlClient {
|
||||
|
||||
debug!("... adding {} confirmed transactions", len);
|
||||
|
||||
result.extend(response.into_iter().map(|x| ELSGetHistoryRes {
|
||||
result.extend(response.into_iter().map(|x| ElsGetHistoryRes {
|
||||
tx_hash: x.txid,
|
||||
height: x.status.block_height.unwrap_or(0) as i32,
|
||||
}));
|
||||
@@ -291,7 +291,7 @@ impl ElectrumLikeSync for UrlClient {
|
||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error> {
|
||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||
let future = async {
|
||||
let mut results = vec![];
|
||||
for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
|
||||
@@ -299,7 +299,7 @@ impl ElectrumLikeSync for UrlClient {
|
||||
for script in chunk {
|
||||
futs.push(self._script_get_history(&script));
|
||||
}
|
||||
let partial_results: Vec<Vec<ELSGetHistoryRes>> = futs.try_collect().await?;
|
||||
let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?;
|
||||
results.extend(partial_results);
|
||||
}
|
||||
Ok(stream::iter(results).collect().await)
|
||||
@@ -361,7 +361,7 @@ struct EsploraGetHistory {
|
||||
}
|
||||
|
||||
/// Configuration for an [`EsploraBlockchain`]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||
pub struct EsploraBlockchainConfig {
|
||||
/// Base URL of the esplora service
|
||||
///
|
||||
@@ -414,3 +414,10 @@ impl_error!(reqwest::Error, Reqwest, EsploraError);
|
||||
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
|
||||
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
|
||||
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
crate::bdk_blockchain_tests! {
|
||||
fn test_instance() -> EsploraBlockchain {
|
||||
EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ pub use self::electrum::ElectrumBlockchain;
|
||||
#[cfg(feature = "electrum")]
|
||||
pub use self::electrum::ElectrumBlockchainConfig;
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
pub mod rpc;
|
||||
#[cfg(feature = "rpc")]
|
||||
pub use self::rpc::RpcBlockchain;
|
||||
#[cfg(feature = "rpc")]
|
||||
pub use self::rpc::RpcConfig;
|
||||
|
||||
#[cfg(feature = "esplora")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
||||
pub mod esplora;
|
||||
@@ -52,6 +59,7 @@ pub use self::esplora::EsploraBlockchain;
|
||||
#[cfg(feature = "compact_filters")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||
pub mod compact_filters;
|
||||
|
||||
#[cfg(feature = "compact_filters")]
|
||||
pub use self::compact_filters::CompactFiltersBlockchain;
|
||||
|
||||
@@ -166,7 +174,7 @@ impl Progress for Sender<ProgressData> {
|
||||
}
|
||||
|
||||
/// Type that implements [`Progress`] and drops every update received
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct NoopProgress;
|
||||
|
||||
/// Create a new instance of [`NoopProgress`]
|
||||
@@ -181,7 +189,7 @@ impl Progress for NoopProgress {
|
||||
}
|
||||
|
||||
/// Type that implements [`Progress`] and logs at level `INFO` every update received
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct LogProgress;
|
||||
|
||||
/// Create a nwe instance of [`LogProgress`]
|
||||
|
||||
673
src/blockchain/rpc.rs
Normal file
673
src/blockchain/rpc.rs
Normal file
@@ -0,0 +1,673 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2021 by Riccardo Casatta <riccardo@casatta.it>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Rpc Blockchain
|
||||
//!
|
||||
//! Backend that gets blockchain data from Bitcoin Core RPC
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain};
|
||||
//! let config = RpcConfig {
|
||||
//! url: "127.0.0.1:18332".to_string(),
|
||||
//! auth: bitcoincore_rpc::Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
|
||||
//! network: bdk::bitcoin::Network::Testnet,
|
||||
//! wallet_name: "wallet_name".to_string(),
|
||||
//! skip_blocks: None,
|
||||
//! };
|
||||
//! let blockchain = RpcBlockchain::from_config(&config);
|
||||
//! ```
|
||||
|
||||
use crate::bitcoin::consensus::deserialize;
|
||||
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
|
||||
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||
use crate::database::{BatchDatabase, DatabaseUtils};
|
||||
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
|
||||
use crate::wallet::utils::SecpCtx;
|
||||
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use bitcoincore_rpc::json::{
|
||||
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
|
||||
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
||||
};
|
||||
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
|
||||
#[derive(Debug)]
|
||||
pub struct RpcBlockchain {
|
||||
/// Rpc client to the node, includes the wallet name
|
||||
client: Client,
|
||||
/// Network used
|
||||
network: Network,
|
||||
/// Blockchain capabilities, cached here at startup
|
||||
capabilities: HashSet<Capability>,
|
||||
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
||||
skip_blocks: Option<u32>,
|
||||
|
||||
/// This is a fixed Address used as a hack key to store information on the node
|
||||
_storage_address: Address,
|
||||
}
|
||||
|
||||
/// RpcBlockchain configuration options
|
||||
#[derive(Debug)]
|
||||
pub struct RpcConfig {
|
||||
/// The bitcoin node url
|
||||
pub url: String,
|
||||
/// The bitcoin node authentication mechanism
|
||||
pub auth: Auth,
|
||||
/// The network we are using (it will be checked the bitcoin node network matches this)
|
||||
pub network: Network,
|
||||
/// The wallet name in the bitcoin node, consider using [wallet_name_from_descriptor] for this
|
||||
pub wallet_name: String,
|
||||
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
||||
pub skip_blocks: Option<u32>,
|
||||
}
|
||||
|
||||
impl RpcBlockchain {
|
||||
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
||||
let info = self.client.get_address_info(&self._storage_address)?;
|
||||
if let Some(GetAddressInfoResultLabel::Simple(label)) = info.labels.first() {
|
||||
Ok(label
|
||||
.parse::<u32>()
|
||||
.unwrap_or_else(|_| self.skip_blocks.unwrap_or(0)))
|
||||
} else {
|
||||
Ok(self.skip_blocks.unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the synced height in the core node by using a label of a fixed address so that
|
||||
/// another client with the same descriptor doesn't rescan the blockchain
|
||||
fn set_node_synced_height(&self, height: u32) -> Result<(), Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.set_label(&self._storage_address, &height.to_string())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Blockchain for RpcBlockchain {
|
||||
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||
self.capabilities.clone()
|
||||
}
|
||||
|
||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
stop_gap: Option<usize>,
|
||||
database: &mut D,
|
||||
progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
|
||||
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
|
||||
debug!(
|
||||
"importing {} script_pubkeys (some maybe already imported)",
|
||||
scripts_pubkeys.len()
|
||||
);
|
||||
let requests: Vec<_> = scripts_pubkeys
|
||||
.iter()
|
||||
.map(|s| ImportMultiRequest {
|
||||
timestamp: ImportMultiRescanSince::Timestamp(0),
|
||||
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(&s)),
|
||||
watchonly: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
let options = ImportMultiOptions {
|
||||
rescan: Some(false),
|
||||
};
|
||||
// Note we use import_multi because as of bitcoin core 0.21.0 many descriptors are not supported
|
||||
// https://bitcoindevkit.org/descriptors/#compatibility-matrix
|
||||
//TODO maybe convenient using import_descriptor for compatible descriptor and import_multi as fallback
|
||||
self.client.import_multi(&requests, Some(&options))?;
|
||||
|
||||
let current_height = self.get_height()?;
|
||||
|
||||
// min because block invalidate may cause height to go down
|
||||
let node_synced = self.get_node_synced_height()?.min(current_height);
|
||||
|
||||
//TODO call rescan in chunks (updating node_synced_height) so that in case of
|
||||
// interruption work can be partially recovered
|
||||
debug!(
|
||||
"rescan_blockchain from:{} to:{}",
|
||||
node_synced, current_height
|
||||
);
|
||||
self.client
|
||||
.rescan_blockchain(Some(node_synced as usize), Some(current_height as usize))?;
|
||||
progress_update.update(1.0, None)?;
|
||||
|
||||
self.set_node_synced_height(current_height)?;
|
||||
|
||||
self.sync(stop_gap, database, progress_update)
|
||||
}
|
||||
|
||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||
&self,
|
||||
_stop_gap: Option<usize>,
|
||||
db: &mut D,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
let mut indexes = HashMap::new();
|
||||
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
|
||||
indexes.insert(*keykind, db.get_last_index(*keykind)?.unwrap_or(0));
|
||||
}
|
||||
|
||||
let mut known_txs: HashMap<_, _> = db
|
||||
.iter_txs(true)?
|
||||
.into_iter()
|
||||
.map(|tx| (tx.txid, tx))
|
||||
.collect();
|
||||
let known_utxos: HashSet<_> = db.iter_utxos()?.into_iter().collect();
|
||||
|
||||
//TODO list_since_blocks would be more efficient
|
||||
let current_utxo = self
|
||||
.client
|
||||
.list_unspent(Some(0), None, None, Some(true), None)?;
|
||||
debug!("current_utxo len {}", current_utxo.len());
|
||||
|
||||
//TODO supported up to 1_000 txs, should use since_blocks or do paging
|
||||
let list_txs = self
|
||||
.client
|
||||
.list_transactions(None, Some(1_000), None, Some(true))?;
|
||||
let mut list_txs_ids = HashSet::new();
|
||||
|
||||
for tx_result in list_txs.iter().filter(|t| {
|
||||
// list_txs returns all conflicting tx we want to
|
||||
// filter out replaced tx => unconfirmed and not in the mempool
|
||||
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
|
||||
}) {
|
||||
let txid = tx_result.info.txid;
|
||||
list_txs_ids.insert(txid);
|
||||
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
|
||||
let confirmation_time =
|
||||
ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
|
||||
if confirmation_time != known_tx.confirmation_time {
|
||||
// reorg may change tx height
|
||||
debug!(
|
||||
"updating tx({}) confirmation time to: {:?}",
|
||||
txid, confirmation_time
|
||||
);
|
||||
known_tx.confirmation_time = confirmation_time;
|
||||
db.set_tx(&known_tx)?;
|
||||
}
|
||||
} else {
|
||||
//TODO check there is already the raw tx in db?
|
||||
let tx_result = self.client.get_transaction(&txid, Some(true))?;
|
||||
let tx: Transaction = deserialize(&tx_result.hex)?;
|
||||
let mut received = 0u64;
|
||||
let mut sent = 0u64;
|
||||
for output in tx.output.iter() {
|
||||
if let Ok(Some((kind, index))) =
|
||||
db.get_path_from_script_pubkey(&output.script_pubkey)
|
||||
{
|
||||
if index > *indexes.get(&kind).unwrap() {
|
||||
indexes.insert(kind, index);
|
||||
}
|
||||
received += output.value;
|
||||
}
|
||||
}
|
||||
|
||||
for input in tx.input.iter() {
|
||||
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
|
||||
sent += previous_output.value;
|
||||
}
|
||||
}
|
||||
|
||||
let td = TransactionDetails {
|
||||
transaction: Some(tx),
|
||||
txid: tx_result.info.txid,
|
||||
confirmation_time: ConfirmationTime::new(
|
||||
tx_result.info.blockheight,
|
||||
tx_result.info.blocktime,
|
||||
),
|
||||
received,
|
||||
sent,
|
||||
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||
};
|
||||
debug!(
|
||||
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
||||
td.txid, tx_result.fee, td.fee
|
||||
);
|
||||
db.set_tx(&td)?;
|
||||
}
|
||||
}
|
||||
|
||||
for known_txid in known_txs.keys() {
|
||||
if !list_txs_ids.contains(known_txid) {
|
||||
debug!("removing tx: {}", known_txid);
|
||||
db.del_tx(known_txid, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
let current_utxos: HashSet<_> = current_utxo
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
Ok(LocalUtxo {
|
||||
outpoint: OutPoint::new(u.txid, u.vout),
|
||||
keychain: db
|
||||
.get_path_from_script_pubkey(&u.script_pub_key)?
|
||||
.ok_or(Error::TransactionNotFound)?
|
||||
.0,
|
||||
txout: TxOut {
|
||||
value: u.amount.as_sat(),
|
||||
script_pubkey: u.script_pub_key,
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
||||
for s in spent {
|
||||
debug!("removing utxo: {:?}", s);
|
||||
db.del_utxo(&s.outpoint)?;
|
||||
}
|
||||
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
|
||||
for s in received {
|
||||
debug!("adding utxo: {:?}", s);
|
||||
db.set_utxo(s)?;
|
||||
}
|
||||
|
||||
for (keykind, index) in indexes {
|
||||
debug!("{:?} max {}", keykind, index);
|
||||
db.set_last_index(keykind, index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||
Ok(Some(self.client.get_raw_transaction(txid, None)?))
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
|
||||
}
|
||||
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||
let sat_per_kb = self
|
||||
.client
|
||||
.estimate_smart_fee(target as u16, None)?
|
||||
.fee_rate
|
||||
.ok_or(Error::FeeRateUnavailable)?
|
||||
.as_sat() as f64;
|
||||
|
||||
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurableBlockchain for RpcBlockchain {
|
||||
type Config = RpcConfig;
|
||||
|
||||
/// Returns RpcBlockchain backend creating an RPC client to a specific wallet named as the descriptor's checksum
|
||||
/// if it's the first time it creates the wallet in the node and upon return is granted the wallet is loaded
|
||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||
let wallet_name = config.wallet_name.clone();
|
||||
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
||||
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
||||
|
||||
let client = Client::new(wallet_url, config.auth.clone())?;
|
||||
let loaded_wallets = client.list_wallets()?;
|
||||
if loaded_wallets.contains(&wallet_name) {
|
||||
debug!("wallet already loaded {:?}", wallet_name);
|
||||
} else {
|
||||
let existing_wallets = list_wallet_dir(&client)?;
|
||||
if existing_wallets.contains(&wallet_name) {
|
||||
client.load_wallet(&wallet_name)?;
|
||||
debug!("wallet loaded {:?}", wallet_name);
|
||||
} else {
|
||||
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
|
||||
debug!("wallet created {:?}", wallet_name);
|
||||
}
|
||||
}
|
||||
|
||||
let blockchain_info = client.get_blockchain_info()?;
|
||||
let network = match blockchain_info.chain.as_str() {
|
||||
"main" => Network::Bitcoin,
|
||||
"test" => Network::Testnet,
|
||||
"regtest" => Network::Regtest,
|
||||
"signet" => Network::Signet,
|
||||
_ => return Err(Error::Generic("Invalid network".to_string())),
|
||||
};
|
||||
if network != config.network {
|
||||
return Err(Error::InvalidNetwork {
|
||||
requested: config.network,
|
||||
found: network,
|
||||
});
|
||||
}
|
||||
|
||||
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
|
||||
let rpc_version = client.version()?;
|
||||
if rpc_version >= 210_000 {
|
||||
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
|
||||
if info.contains_key("txindex") {
|
||||
capabilities.insert(Capability::GetAnyTx);
|
||||
capabilities.insert(Capability::AccurateFees);
|
||||
}
|
||||
}
|
||||
|
||||
// this is just a fixed address used only to store a label containing the synced height in the node
|
||||
let mut storage_address =
|
||||
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
|
||||
storage_address.network = network;
|
||||
|
||||
Ok(RpcBlockchain {
|
||||
client,
|
||||
network,
|
||||
capabilities,
|
||||
_storage_address: storage_address,
|
||||
skip_blocks: config.skip_blocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministically generate a unique name given the descriptors defining the wallet
|
||||
pub fn wallet_name_from_descriptor<T>(
|
||||
descriptor: T,
|
||||
change_descriptor: Option<T>,
|
||||
network: Network,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
T: IntoWalletDescriptor,
|
||||
{
|
||||
//TODO check descriptors contains only public keys
|
||||
let descriptor = descriptor
|
||||
.into_wallet_descriptor(&secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
|
||||
if let Some(change_descriptor) = change_descriptor {
|
||||
let change_descriptor = change_descriptor
|
||||
.into_wallet_descriptor(&secp, network)?
|
||||
.0
|
||||
.to_string();
|
||||
wallet_name.push_str(
|
||||
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(wallet_name)
|
||||
}
|
||||
|
||||
/// return the wallets available in default wallet directory
|
||||
//TODO use bitcoincore_rpc method when PR #179 lands
|
||||
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
||||
#[derive(Deserialize)]
|
||||
struct Name {
|
||||
name: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct CallResult {
|
||||
wallets: Vec<Name>,
|
||||
}
|
||||
|
||||
let result: CallResult = client.call("listwalletdir", &[])?;
|
||||
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
crate::bdk_blockchain_tests! {
|
||||
|
||||
fn test_instance() -> RpcBlockchain {
|
||||
let url = std::env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
|
||||
let url = format!("http://{}", url);
|
||||
|
||||
// TODO same code in `fn get_auth` in testutils, make it public there
|
||||
let auth = match std::env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
|
||||
Ok("USER_PASS") => Auth::UserPass(
|
||||
std::env::var("BDK_RPC_USER").unwrap(),
|
||||
std::env::var("BDK_RPC_PASS").unwrap(),
|
||||
),
|
||||
_ => Auth::CookieFile(std::path::PathBuf::from(
|
||||
std::env::var("BDK_RPC_COOKIEFILE")
|
||||
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
|
||||
)),
|
||||
};
|
||||
let config = RpcConfig {
|
||||
url,
|
||||
auth,
|
||||
network: Network::Regtest,
|
||||
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
|
||||
skip_blocks: None,
|
||||
};
|
||||
RpcBlockchain::from_config(&config).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-rpc")]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{RpcBlockchain, RpcConfig};
|
||||
use crate::bitcoin::consensus::deserialize;
|
||||
use crate::bitcoin::{Address, Amount, Network, Transaction};
|
||||
use crate::blockchain::rpc::wallet_name_from_descriptor;
|
||||
use crate::blockchain::{noop_progress, Blockchain, Capability, ConfigurableBlockchain};
|
||||
use crate::database::MemoryDatabase;
|
||||
use crate::wallet::AddressIndex;
|
||||
use crate::Wallet;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::Txid;
|
||||
use bitcoincore_rpc::json::CreateRawTransactionInput;
|
||||
use bitcoincore_rpc::RawTx;
|
||||
use bitcoincore_rpc::{Auth, RpcApi};
|
||||
use bitcoind::BitcoinD;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn create_rpc(
|
||||
bitcoind: &BitcoinD,
|
||||
desc: &str,
|
||||
network: Network,
|
||||
) -> Result<RpcBlockchain, crate::Error> {
|
||||
let secp = Secp256k1::new();
|
||||
let wallet_name = wallet_name_from_descriptor(desc, None, network, &secp).unwrap();
|
||||
|
||||
let config = RpcConfig {
|
||||
url: bitcoind.rpc_url(),
|
||||
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
|
||||
network,
|
||||
wallet_name,
|
||||
skip_blocks: None,
|
||||
};
|
||||
RpcBlockchain::from_config(&config)
|
||||
}
|
||||
fn create_bitcoind(args: Vec<String>) -> BitcoinD {
|
||||
let exe = std::env::var("BITCOIND_EXE").unwrap();
|
||||
bitcoind::BitcoinD::with_args(exe, args, false, bitcoind::P2P::No).unwrap()
|
||||
}
|
||||
|
||||
const DESCRIPTOR_PUB: &'static str = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
|
||||
const DESCRIPTOR_PRIV: &'static str = "wpkh(tprv8ZgxMBicQKsPdZxBDUcvTSMEaLwCTzTc6gmw8KBKwa3BJzWzec4g6VUbQBHJcutDH6mMEmBeVyN27H1NF3Nu8isZ1Sts4SufWyfLE6Mf1MB/*)";
|
||||
|
||||
#[test]
|
||||
fn test_rpc_wallet_setup() {
|
||||
env_logger::try_init().unwrap();
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let node_address = bitcoind.client.get_new_address(None, None).unwrap();
|
||||
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
let db = MemoryDatabase::new();
|
||||
let wallet = Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
generate(&bitcoind, 101);
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap();
|
||||
let expected_address = "bcrt1q8dyvgt4vhr8ald4xuwewcxhdjha9a5k78wxm5t";
|
||||
assert_eq!(expected_address, address.to_string());
|
||||
send_to_address(&bitcoind, &address, 100_000);
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_address.script_pubkey(), 50_000);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
wallet.broadcast(tx).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(
|
||||
wallet.get_balance().unwrap(),
|
||||
100_000 - 50_000 - details.fee.unwrap_or(0)
|
||||
);
|
||||
drop(wallet);
|
||||
|
||||
// test skip_blocks
|
||||
generate(&bitcoind, 5);
|
||||
let config = RpcConfig {
|
||||
url: bitcoind.rpc_url(),
|
||||
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
|
||||
network: Network::Regtest,
|
||||
wallet_name: "another-name".to_string(),
|
||||
skip_blocks: Some(103),
|
||||
};
|
||||
let blockchain_skip = RpcBlockchain::from_config(&config).unwrap();
|
||||
let db = MemoryDatabase::new();
|
||||
let wallet_skip =
|
||||
Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain_skip).unwrap();
|
||||
wallet_skip.sync(noop_progress(), None).unwrap();
|
||||
send_to_address(&bitcoind, &address, 100_000);
|
||||
wallet_skip.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet_skip.get_balance().unwrap(), 100_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_from_config() {
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest);
|
||||
assert!(blockchain.is_ok());
|
||||
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Testnet);
|
||||
assert!(blockchain.is_err(), "wrong network doesn't error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_capabilities_get_tx() {
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
let capabilities = rpc.get_capabilities();
|
||||
assert!(capabilities.contains(&Capability::FullHistory) && capabilities.len() == 1);
|
||||
let bitcoind_indexed = create_bitcoind(vec!["-txindex".to_string()]);
|
||||
let rpc_indexed = create_rpc(&bitcoind_indexed, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
assert_eq!(rpc_indexed.get_capabilities().len(), 3);
|
||||
let address = generate(&bitcoind_indexed, 101);
|
||||
let txid = send_to_address(&bitcoind_indexed, &address, 100_000);
|
||||
assert!(rpc_indexed.get_tx(&txid).unwrap().is_some());
|
||||
assert!(rpc.get_tx(&txid).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_estimate_fee_get_height() {
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
let result = rpc.estimate_fee(2);
|
||||
assert!(result.is_err());
|
||||
let address = generate(&bitcoind, 100);
|
||||
// create enough tx so that core give some fee estimation
|
||||
for _ in 0..15 {
|
||||
let _ = bitcoind.client.generate_to_address(1, &address).unwrap();
|
||||
for _ in 0..2 {
|
||||
send_to_address(&bitcoind, &address, 100_000);
|
||||
}
|
||||
}
|
||||
let result = rpc.estimate_fee(2);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(rpc.get_height().unwrap(), 115);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_node_synced_height() {
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
let synced_height = rpc.get_node_synced_height().unwrap();
|
||||
|
||||
assert_eq!(synced_height, 0);
|
||||
rpc.set_node_synced_height(1).unwrap();
|
||||
|
||||
let synced_height = rpc.get_node_synced_height().unwrap();
|
||||
assert_eq!(synced_height, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_broadcast() {
|
||||
let bitcoind = create_bitcoind(vec![]);
|
||||
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
|
||||
let address = generate(&bitcoind, 101);
|
||||
let utxo = bitcoind
|
||||
.client
|
||||
.list_unspent(None, None, None, None, None)
|
||||
.unwrap();
|
||||
let input = CreateRawTransactionInput {
|
||||
txid: utxo[0].txid,
|
||||
vout: utxo[0].vout,
|
||||
sequence: None,
|
||||
};
|
||||
|
||||
let out: HashMap<_, _> = vec![(
|
||||
address.to_string(),
|
||||
utxo[0].amount - Amount::from_sat(100_000),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let tx = bitcoind
|
||||
.client
|
||||
.create_raw_transaction(&[input], &out, None, None)
|
||||
.unwrap();
|
||||
let signed_tx = bitcoind
|
||||
.client
|
||||
.sign_raw_transaction_with_wallet(tx.raw_hex(), None, None)
|
||||
.unwrap();
|
||||
let parsed_tx: Transaction = deserialize(&signed_tx.hex).unwrap();
|
||||
rpc.broadcast(&parsed_tx).unwrap();
|
||||
assert!(bitcoind
|
||||
.client
|
||||
.get_raw_mempool()
|
||||
.unwrap()
|
||||
.contains(&tx.txid()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_wallet_name() {
|
||||
let secp = Secp256k1::new();
|
||||
let name =
|
||||
wallet_name_from_descriptor(DESCRIPTOR_PUB, None, Network::Regtest, &secp).unwrap();
|
||||
assert_eq!("tmg7aqay", name);
|
||||
}
|
||||
|
||||
fn generate(bitcoind: &BitcoinD, blocks: u64) -> Address {
|
||||
let address = bitcoind.client.get_new_address(None, None).unwrap();
|
||||
bitcoind
|
||||
.client
|
||||
.generate_to_address(blocks, &address)
|
||||
.unwrap();
|
||||
address
|
||||
}
|
||||
|
||||
fn send_to_address(bitcoind: &BitcoinD, address: &Address, amount: u64) -> Txid {
|
||||
bitcoind
|
||||
.client
|
||||
.send_to_address(
|
||||
&address,
|
||||
Amount::from_sat(amount),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid};
|
||||
use super::*;
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::types::{ConfirmationTime, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use crate::wallet::time::Instant;
|
||||
use crate::wallet::utils::ChunksIterator;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ELSGetHistoryRes {
|
||||
pub struct ElsGetHistoryRes {
|
||||
pub height: i32,
|
||||
pub tx_hash: Txid,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub trait ElectrumLikeSync {
|
||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error>;
|
||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error>;
|
||||
|
||||
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid> + Clone>(
|
||||
&self,
|
||||
@@ -77,7 +77,7 @@ pub trait ElectrumLikeSync {
|
||||
|
||||
for (i, chunk) in ChunksIterator::new(script_iter, stop_gap).enumerate() {
|
||||
// TODO if i == last, should create another chunk of addresses in db
|
||||
let call_result: Vec<Vec<ELSGetHistoryRes>> =
|
||||
let call_result: Vec<Vec<ElsGetHistoryRes>> =
|
||||
maybe_await!(self.els_batch_script_get_history(chunk.iter()))?;
|
||||
let max_index = call_result
|
||||
.iter()
|
||||
@@ -87,7 +87,7 @@ pub trait ElectrumLikeSync {
|
||||
if let Some(max) = max_index {
|
||||
max_indexes.insert(keychain, max + (i * chunk_size) as u32);
|
||||
}
|
||||
let flattened: Vec<ELSGetHistoryRes> = call_result.into_iter().flatten().collect();
|
||||
let flattened: Vec<ElsGetHistoryRes> = call_result.into_iter().flatten().collect();
|
||||
debug!("#{} of {:?} results:{}", i, keychain, flattened.len());
|
||||
if flattened.is_empty() {
|
||||
// Didn't find anything in the last `stop_gap` script_pubkeys, breaking
|
||||
@@ -147,18 +147,19 @@ pub trait ElectrumLikeSync {
|
||||
// save any tx details not in db but in history_txs_id or with different height/timestamp
|
||||
for txid in history_txs_id.iter() {
|
||||
let height = txid_height.get(txid).cloned().flatten();
|
||||
let timestamp = *new_timestamps.get(txid).unwrap_or(&0u64);
|
||||
let timestamp = new_timestamps.get(txid).cloned();
|
||||
if let Some(tx_details) = txs_details_in_db.get(txid) {
|
||||
// check if height matches, otherwise updates it
|
||||
if tx_details.height != height {
|
||||
// check if tx height matches, otherwise updates it. timestamp is not in the if clause
|
||||
// because we are not asking headers for confirmed tx we know about
|
||||
if tx_details.confirmation_time.as_ref().map(|c| c.height) != height {
|
||||
let confirmation_time = ConfirmationTime::new(height, timestamp);
|
||||
let mut new_tx_details = tx_details.clone();
|
||||
new_tx_details.height = height;
|
||||
new_tx_details.timestamp = timestamp;
|
||||
new_tx_details.confirmation_time = confirmation_time;
|
||||
batch.set_tx(&new_tx_details)?;
|
||||
}
|
||||
} else {
|
||||
save_transaction_details_and_utxos(
|
||||
&txid,
|
||||
txid,
|
||||
db,
|
||||
timestamp,
|
||||
height,
|
||||
@@ -171,7 +172,7 @@ pub trait ElectrumLikeSync {
|
||||
// remove any tx details in db but not in history_txs_id
|
||||
for txid in txs_details_in_db.keys() {
|
||||
if !history_txs_id.contains(txid) {
|
||||
batch.del_tx(&txid, false)?;
|
||||
batch.del_tx(txid, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,9 +239,13 @@ pub trait ElectrumLikeSync {
|
||||
chunk_size: usize,
|
||||
) -> Result<HashMap<Txid, u64>, Error> {
|
||||
let mut txid_timestamp = HashMap::new();
|
||||
let txid_in_db_with_conf: HashSet<_> = txs_details_in_db
|
||||
.values()
|
||||
.filter_map(|details| details.confirmation_time.as_ref().map(|_| details.txid))
|
||||
.collect();
|
||||
let needed_txid_height: HashMap<&Txid, u32> = txid_height
|
||||
.iter()
|
||||
.filter(|(t, _)| txs_details_in_db.get(*t).is_none())
|
||||
.filter(|(t, _)| !txid_in_db_with_conf.contains(*t))
|
||||
.filter_map(|(t, o)| o.map(|h| (t, h)))
|
||||
.collect();
|
||||
let needed_heights: HashSet<u32> = needed_txid_height.values().cloned().collect();
|
||||
@@ -292,7 +297,7 @@ pub trait ElectrumLikeSync {
|
||||
fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||
txid: &Txid,
|
||||
db: &mut D,
|
||||
timestamp: u64,
|
||||
timestamp: Option<u64>,
|
||||
height: Option<u32>,
|
||||
updates: &mut dyn BatchOperations,
|
||||
utxo_deps: &HashMap<OutPoint, OutPoint>,
|
||||
@@ -329,7 +334,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||
|
||||
// removes conflicting UTXO if any (generated from same inputs, like for example RBF)
|
||||
if let Some(outpoint) = utxo_deps.get(&input.previous_output) {
|
||||
updates.del_utxo(&outpoint)?;
|
||||
updates.del_utxo(outpoint)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,9 +360,8 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||
transaction: Some(tx),
|
||||
received: incoming,
|
||||
sent: outgoing,
|
||||
height,
|
||||
timestamp,
|
||||
fees: inputs_sum.saturating_sub(outputs_sum), /* if the tx is a coinbase, fees would be negative */
|
||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
|
||||
};
|
||||
updates.set_tx(&tx_details)?;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ macro_rules! impl_batch_operations {
|
||||
}
|
||||
|
||||
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||
let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||
let value = json!({
|
||||
"t": utxo.txout,
|
||||
"i": utxo.keychain,
|
||||
@@ -108,7 +108,7 @@ macro_rules! impl_batch_operations {
|
||||
}
|
||||
|
||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||
let res = self.remove(key);
|
||||
let res = $process_delete!(res);
|
||||
|
||||
@@ -222,7 +222,7 @@ impl Database for Tree {
|
||||
}
|
||||
|
||||
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(None).as_map_key();
|
||||
let key = MapKey::Utxo(None).as_map_key();
|
||||
self.scan_prefix(key)
|
||||
.map(|x| -> Result<_, Error> {
|
||||
let (k, v) = x?;
|
||||
@@ -293,7 +293,7 @@ impl Database for Tree {
|
||||
}
|
||||
|
||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||
self.get(key)?
|
||||
.map(|b| -> Result<_, Error> {
|
||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||
@@ -320,7 +320,7 @@ impl Database for Tree {
|
||||
.map(|b| -> Result<_, Error> {
|
||||
let mut txdetails: TransactionDetails = serde_json::from_slice(&b)?;
|
||||
if include_raw {
|
||||
txdetails.transaction = self.get_raw_tx(&txid)?;
|
||||
txdetails.transaction = self.get_raw_tx(txid)?;
|
||||
}
|
||||
|
||||
Ok(txdetails)
|
||||
|
||||
@@ -36,7 +36,7 @@ use crate::types::*;
|
||||
pub(crate) enum MapKey<'a> {
|
||||
Path((Option<KeychainKind>, Option<u32>)),
|
||||
Script(Option<&'a Script>),
|
||||
UTXO(Option<&'a OutPoint>),
|
||||
Utxo(Option<&'a OutPoint>),
|
||||
RawTx(Option<&'a Txid>),
|
||||
Transaction(Option<&'a Txid>),
|
||||
LastIndex(KeychainKind),
|
||||
@@ -54,7 +54,7 @@ impl MapKey<'_> {
|
||||
v
|
||||
}
|
||||
MapKey::Script(_) => b"s".to_vec(),
|
||||
MapKey::UTXO(_) => b"u".to_vec(),
|
||||
MapKey::Utxo(_) => b"u".to_vec(),
|
||||
MapKey::RawTx(_) => b"r".to_vec(),
|
||||
MapKey::Transaction(_) => b"t".to_vec(),
|
||||
MapKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
||||
@@ -66,7 +66,7 @@ impl MapKey<'_> {
|
||||
match self {
|
||||
MapKey::Path((_, Some(child))) => child.to_be_bytes().to_vec(),
|
||||
MapKey::Script(Some(s)) => serialize(*s),
|
||||
MapKey::UTXO(Some(s)) => serialize(*s),
|
||||
MapKey::Utxo(Some(s)) => serialize(*s),
|
||||
MapKey::RawTx(Some(s)) => serialize(*s),
|
||||
MapKey::Transaction(Some(s)) => serialize(*s),
|
||||
_ => vec![],
|
||||
@@ -145,7 +145,7 @@ impl BatchOperations for MemoryDatabase {
|
||||
}
|
||||
|
||||
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||
let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||
self.map
|
||||
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
||||
|
||||
@@ -211,7 +211,7 @@ impl BatchOperations for MemoryDatabase {
|
||||
}
|
||||
}
|
||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||
let res = self.map.remove(&key);
|
||||
self.deleted_keys.push(key);
|
||||
|
||||
@@ -304,7 +304,7 @@ impl Database for MemoryDatabase {
|
||||
}
|
||||
|
||||
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(None).as_map_key();
|
||||
let key = MapKey::Utxo(None).as_map_key();
|
||||
self.map
|
||||
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
||||
.map(|(k, v)| {
|
||||
@@ -370,7 +370,7 @@ impl Database for MemoryDatabase {
|
||||
}
|
||||
|
||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||
Ok(self.map.get(&key).map(|b| {
|
||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||
LocalUtxo {
|
||||
@@ -394,7 +394,7 @@ impl Database for MemoryDatabase {
|
||||
Ok(self.map.get(&key).map(|b| {
|
||||
let mut txdetails: TransactionDetails = b.downcast_ref().cloned().unwrap();
|
||||
if include_raw {
|
||||
txdetails.transaction = self.get_raw_tx(&txid).unwrap();
|
||||
txdetails.transaction = self.get_raw_tx(txid).unwrap();
|
||||
}
|
||||
|
||||
txdetails
|
||||
@@ -429,8 +429,8 @@ impl BatchDatabase for MemoryDatabase {
|
||||
}
|
||||
|
||||
fn commit_batch(&mut self, mut batch: Self::Batch) -> Result<(), Error> {
|
||||
for key in batch.deleted_keys {
|
||||
self.map.remove(&key);
|
||||
for key in batch.deleted_keys.iter() {
|
||||
self.map.remove(key);
|
||||
}
|
||||
self.map.append(&mut batch.map);
|
||||
Ok(())
|
||||
@@ -473,18 +473,18 @@ macro_rules! populate_test_db {
|
||||
};
|
||||
|
||||
let txid = tx.txid();
|
||||
let height = tx_meta
|
||||
.min_confirmations
|
||||
.map(|conf| current_height.unwrap().checked_sub(conf as u32).unwrap());
|
||||
let confirmation_time = tx_meta.min_confirmations.map(|conf| ConfirmationTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
let tx_details = TransactionDetails {
|
||||
transaction: Some(tx.clone()),
|
||||
txid,
|
||||
timestamp: 0,
|
||||
height,
|
||||
fee: Some(0),
|
||||
received: 0,
|
||||
sent: 0,
|
||||
fees: 0,
|
||||
confirmation_time,
|
||||
};
|
||||
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
@@ -511,7 +511,7 @@ macro_rules! doctest_wallet {
|
||||
() => {{
|
||||
use $crate::bitcoin::Network;
|
||||
use $crate::database::MemoryDatabase;
|
||||
use testutils::testutils;
|
||||
use $crate::testutils;
|
||||
let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
|
||||
let descriptors = testutils!(@descriptors (descriptor) (descriptor));
|
||||
|
||||
|
||||
@@ -164,14 +164,14 @@ pub(crate) trait DatabaseUtils: Database {
|
||||
.map(|o| o.is_some())
|
||||
}
|
||||
|
||||
fn get_raw_tx_or<F>(&self, txid: &Txid, f: F) -> Result<Option<Transaction>, Error>
|
||||
fn get_raw_tx_or<D>(&self, txid: &Txid, default: D) -> Result<Option<Transaction>, Error>
|
||||
where
|
||||
F: FnOnce() -> Result<Option<Transaction>, Error>,
|
||||
D: FnOnce() -> Result<Option<Transaction>, Error>,
|
||||
{
|
||||
self.get_tx(txid, true)?
|
||||
.map(|t| t.transaction)
|
||||
.flatten()
|
||||
.map_or_else(f, |t| Ok(Some(t)))
|
||||
.map_or_else(default, |t| Ok(Some(t)))
|
||||
}
|
||||
|
||||
fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {
|
||||
@@ -314,11 +314,13 @@ pub mod test {
|
||||
let mut tx_details = TransactionDetails {
|
||||
transaction: Some(tx),
|
||||
txid,
|
||||
timestamp: 123456,
|
||||
received: 1337,
|
||||
sent: 420420,
|
||||
fees: 140,
|
||||
height: Some(1000),
|
||||
fee: Some(140),
|
||||
confirmation_time: Some(ConfirmationTime {
|
||||
timestamp: 123456,
|
||||
height: 1000,
|
||||
}),
|
||||
};
|
||||
|
||||
tree.set_tx(&tx_details).unwrap();
|
||||
|
||||
@@ -535,9 +535,7 @@ macro_rules! fragment_internal {
|
||||
( @t , $( $tail:tt )* ) => ({
|
||||
$crate::fragment_internal!( @t $( $tail )* )
|
||||
});
|
||||
( @t ) => ({
|
||||
()
|
||||
});
|
||||
( @t ) => ({});
|
||||
|
||||
// Fallback to calling `fragment!()`
|
||||
( $( $tokens:tt )* ) => ({
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid HD Key path, such as having a wildcard but a length != 1
|
||||
InvalidHDKeyPath,
|
||||
InvalidHdKeyPath,
|
||||
/// The provided descriptor doesn't match its checksum
|
||||
InvalidDescriptorChecksum,
|
||||
/// The descriptor contains hardened derivation steps on public extended keys
|
||||
@@ -32,11 +32,11 @@ pub enum Error {
|
||||
InvalidDescriptorCharacter(char),
|
||||
|
||||
/// BIP32 error
|
||||
BIP32(bitcoin::util::bip32::Error),
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
/// Error during base58 decoding
|
||||
Base58(bitcoin::util::base58::Error),
|
||||
/// Key-related error
|
||||
PK(bitcoin::util::key::Error),
|
||||
Pk(bitcoin::util::key::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// Hex decoding error
|
||||
@@ -47,7 +47,7 @@ impl From<crate::keys::KeyError> for Error {
|
||||
fn from(key_error: crate::keys::KeyError) -> Error {
|
||||
match key_error {
|
||||
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
||||
crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner),
|
||||
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
|
||||
e => Error::Key(e),
|
||||
}
|
||||
}
|
||||
@@ -61,9 +61,9 @@ impl std::fmt::Display for Error {
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl_error!(bitcoin::util::bip32::Error, BIP32);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||
impl_error!(bitcoin::util::base58::Error, Base58);
|
||||
impl_error!(bitcoin::util::key::Error, PK);
|
||||
impl_error!(bitcoin::util::key::Error, Pk);
|
||||
impl_error!(miniscript::Error, Miniscript);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||
impl_error!(crate::descriptor::policy::PolicyError, Policy);
|
||||
|
||||
@@ -27,6 +27,8 @@ use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey
|
||||
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
|
||||
pub mod checksum;
|
||||
pub(crate) mod derived;
|
||||
#[doc(hidden)]
|
||||
@@ -56,7 +58,7 @@ pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
||||
///
|
||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||
pub type HDKeyPaths = BTreeMap<PublicKey, KeySource>;
|
||||
pub type HdKeyPaths = BTreeMap<PublicKey, KeySource>;
|
||||
|
||||
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
||||
pub trait IntoWalletDescriptor {
|
||||
@@ -126,11 +128,11 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
let (pk, _, networks) = if self.0.is_witness() {
|
||||
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(&secp)?
|
||||
desciptor_key.extract(secp)?
|
||||
} else {
|
||||
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(&secp)?
|
||||
desciptor_key.extract(secp)?
|
||||
};
|
||||
|
||||
if networks.contains(&network) {
|
||||
@@ -255,6 +257,7 @@ pub trait ExtractPolicy {
|
||||
fn extract_policy(
|
||||
&self,
|
||||
signers: &SignersContainer,
|
||||
psbt: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, DescriptorError>;
|
||||
}
|
||||
@@ -329,7 +332,7 @@ impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
|
||||
}
|
||||
|
||||
pub(crate) trait DerivedDescriptorMeta {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError>;
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError>;
|
||||
}
|
||||
|
||||
pub(crate) trait DescriptorMeta {
|
||||
@@ -337,7 +340,7 @@ pub(crate) trait DescriptorMeta {
|
||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
&self,
|
||||
hd_keypaths: &HDKeyPaths,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
fn derive_from_psbt_input<'s>(
|
||||
@@ -406,7 +409,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
&self,
|
||||
hd_keypaths: &HDKeyPaths,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
|
||||
@@ -505,7 +508,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
}
|
||||
|
||||
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError> {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError> {
|
||||
let mut answer = BTreeMap::new();
|
||||
self.for_each_key(|key| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||
@@ -537,7 +540,7 @@ mod test {
|
||||
use bitcoin::util::{bip32, psbt};
|
||||
|
||||
use super::*;
|
||||
use crate::psbt::PSBTUtils;
|
||||
use crate::psbt::PsbtUtils;
|
||||
|
||||
#[test]
|
||||
fn test_derive_from_psbt_input_wpkh_wif() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::descriptor::error::Error as DescriptorError;
|
||||
/// use bdk::keys::{KeyError, IntoDescriptorKey};
|
||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk::miniscript::Legacy;
|
||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
///
|
||||
@@ -74,26 +74,27 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::P2PKH;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Pkh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// P2PKH(key),
|
||||
/// P2Pkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_new_address()?.to_string(),
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct P2PKH<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
|
||||
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(pkh(self.0))
|
||||
}
|
||||
@@ -107,27 +108,28 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::P2WPKH_P2SH;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Wpkh_P2Sh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// P2WPKH_P2SH(key),
|
||||
/// P2Wpkh_P2Sh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_new_address()?.to_string(),
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct P2WPKH_P2SH<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(sh(wpkh(self.0)))
|
||||
}
|
||||
@@ -141,26 +143,27 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::P2WPKH;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Wpkh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// P2WPKH(key),
|
||||
/// P2Wpkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_new_address()?.to_string(),
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct P2WPKH<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(wpkh(self.0))
|
||||
}
|
||||
@@ -170,7 +173,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
/// See [`BIP44Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -179,25 +182,26 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP44;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP44(key.clone(), KeychainKind::External),
|
||||
/// Some(BIP44(key, KeychainKind::Internal)),
|
||||
/// Bip44(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2PKH(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +211,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
/// See [`BIP44`] for a template that does the full derivation, but requires private data
|
||||
/// See [`Bip44`] for a template that does the full derivation, but requires private data
|
||||
/// for the key.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -217,26 +221,27 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP44Public;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(BIP44Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2PKH(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +249,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
/// See [`BIP49Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -253,25 +258,26 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP49;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP49(key.clone(), KeychainKind::External),
|
||||
/// Some(BIP49(key, KeychainKind::Internal)),
|
||||
/// Bip49(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2WPKH_P2SH(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +287,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
/// See [`BIP49`] for a template that does the full derivation, but requires private data
|
||||
/// See [`Bip49`] for a template that does the full derivation, but requires private data
|
||||
/// for the key.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -291,26 +297,27 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP49Public;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(BIP49Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2WPKH_P2SH(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +325,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
/// See [`BIP84Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -327,25 +334,26 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP84;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP84(key.clone(), KeychainKind::External),
|
||||
/// Some(BIP84(key, KeychainKind::Internal)),
|
||||
/// Bip84(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2WPKH(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +363,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
/// See [`BIP84`] for a template that does the full derivation, but requires private data
|
||||
/// See [`Bip84`] for a template that does the full derivation, but requires private data
|
||||
/// for the key.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -365,26 +373,27 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// use bdk::template::BIP84Public;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new_offline(
|
||||
/// BIP84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(BIP84Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct BIP84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84Public<K> {
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2WPKH(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,11 +455,12 @@ expand_make_bipxx!(segwit_v0, Segwitv0);
|
||||
mod test {
|
||||
// test existing descriptor templates, make sure they are expanded to the right descriptors
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use bitcoin::hashes::core::str::FromStr;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
@@ -487,7 +497,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2PKH(prvkey).build(),
|
||||
P2Pkh(prvkey).build(),
|
||||
false,
|
||||
true,
|
||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||
@@ -498,7 +508,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2PKH(pubkey).build(),
|
||||
P2Pkh(pubkey).build(),
|
||||
false,
|
||||
true,
|
||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||
@@ -512,7 +522,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2WPKH_P2SH(prvkey).build(),
|
||||
P2Wpkh_P2Sh(prvkey).build(),
|
||||
true,
|
||||
true,
|
||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||
@@ -523,7 +533,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2WPKH_P2SH(pubkey).build(),
|
||||
P2Wpkh_P2Sh(pubkey).build(),
|
||||
true,
|
||||
true,
|
||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||
@@ -537,7 +547,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2WPKH(prvkey).build(),
|
||||
P2Wpkh(prvkey).build(),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||
@@ -548,7 +558,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2WPKH(pubkey).build(),
|
||||
P2Wpkh(pubkey).build(),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||
@@ -560,7 +570,7 @@ mod test {
|
||||
fn test_bip44_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
BIP44(prvkey, KeychainKind::External).build(),
|
||||
Bip44(prvkey, KeychainKind::External).build(),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -570,7 +580,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP44(prvkey, KeychainKind::Internal).build(),
|
||||
Bip44(prvkey, KeychainKind::Internal).build(),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -587,7 +597,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
BIP44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -597,7 +607,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -613,7 +623,7 @@ mod test {
|
||||
fn test_bip49_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
BIP49(prvkey, KeychainKind::External).build(),
|
||||
Bip49(prvkey, KeychainKind::External).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -623,7 +633,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP49(prvkey, KeychainKind::Internal).build(),
|
||||
Bip49(prvkey, KeychainKind::Internal).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -640,7 +650,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
BIP49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -650,7 +660,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -666,7 +676,7 @@ mod test {
|
||||
fn test_bip84_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
BIP84(prvkey, KeychainKind::External).build(),
|
||||
Bip84(prvkey, KeychainKind::External).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -676,7 +686,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP84(prvkey, KeychainKind::Internal).build(),
|
||||
Bip84(prvkey, KeychainKind::Internal).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -693,7 +703,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
BIP84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -703,7 +713,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
BIP84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
|
||||
35
src/error.rs
35
src/error.rs
@@ -11,6 +11,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::bitcoin::Network;
|
||||
use crate::{descriptor, wallet, wallet::address_validator};
|
||||
use bitcoin::OutPoint;
|
||||
|
||||
@@ -47,7 +48,7 @@ pub enum Error {
|
||||
/// the desired outputs plus fee, if there is not such combination this error is thrown
|
||||
BnBNoExactMatch,
|
||||
/// Happens when trying to spend an UTXO that is not in the internal database
|
||||
UnknownUTXO,
|
||||
UnknownUtxo,
|
||||
/// Thrown when a tx is not found in the internal database
|
||||
TransactionNotFound,
|
||||
/// Happens when trying to bump a transaction that is already confirmed
|
||||
@@ -64,6 +65,8 @@ pub enum Error {
|
||||
/// Required fee absolute value (satoshi)
|
||||
required: u64,
|
||||
},
|
||||
/// Node doesn't have data to estimate a fee rate
|
||||
FeeRateUnavailable,
|
||||
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
|
||||
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
|
||||
/// explicit origin provided
|
||||
@@ -80,7 +83,13 @@ pub enum Error {
|
||||
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
||||
/// Signing error
|
||||
Signer(crate::wallet::signer::SignerError),
|
||||
|
||||
/// Invalid network
|
||||
InvalidNetwork {
|
||||
/// requested network, for example what is given as bdk-cli option
|
||||
requested: Network,
|
||||
/// found network, for example the network of the bitcoin node
|
||||
found: Network,
|
||||
},
|
||||
/// Progress value must be between `0.0` (included) and `100.0` (included)
|
||||
InvalidProgressValue(f32),
|
||||
/// Progress update error (maybe the channel has been closed)
|
||||
@@ -97,15 +106,17 @@ pub enum Error {
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// BIP32 error
|
||||
BIP32(bitcoin::util::bip32::Error),
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
/// An ECDSA error
|
||||
Secp256k1(bitcoin::secp256k1::Error),
|
||||
/// Error serializing or deserializing JSON data
|
||||
JSON(serde_json::Error),
|
||||
Json(serde_json::Error),
|
||||
/// Hex decoding error
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
/// Partially signed bitcoin transaction error
|
||||
PSBT(bitcoin::util::psbt::Error),
|
||||
Psbt(bitcoin::util::psbt::Error),
|
||||
/// Partially signed bitcoin transaction parseerror
|
||||
PsbtParse(bitcoin::util::psbt::PsbtParseError),
|
||||
|
||||
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
|
||||
//MissingInputUTXO(usize),
|
||||
@@ -126,6 +137,9 @@ pub enum Error {
|
||||
#[cfg(feature = "key-value-db")]
|
||||
/// Sled database error
|
||||
Sled(sled::Error),
|
||||
#[cfg(feature = "rpc")]
|
||||
/// Rpc client error
|
||||
Rpc(bitcoincore_rpc::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
@@ -158,7 +172,7 @@ impl From<crate::keys::KeyError> for Error {
|
||||
fn from(key_error: crate::keys::KeyError) -> Error {
|
||||
match key_error {
|
||||
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
||||
crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner),
|
||||
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
|
||||
crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
|
||||
e => Error::Key(e),
|
||||
}
|
||||
@@ -167,11 +181,12 @@ impl From<crate::keys::KeyError> for Error {
|
||||
|
||||
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
||||
impl_error!(miniscript::Error, Miniscript);
|
||||
impl_error!(bitcoin::util::bip32::Error, BIP32);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||
impl_error!(serde_json::Error, JSON);
|
||||
impl_error!(serde_json::Error, Json);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||
impl_error!(bitcoin::util::psbt::Error, PSBT);
|
||||
impl_error!(bitcoin::util::psbt::Error, Psbt);
|
||||
impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
impl_error!(electrum_client::Error, Electrum);
|
||||
@@ -179,6 +194,8 @@ impl_error!(electrum_client::Error, Electrum);
|
||||
impl_error!(crate::blockchain::esplora::EsploraError, Esplora);
|
||||
#[cfg(feature = "key-value-db")]
|
||||
impl_error!(sled::Error, Sled);
|
||||
#[cfg(feature = "rpc")]
|
||||
impl_error!(bitcoincore_rpc::Error, Rpc);
|
||||
|
||||
#[cfg(feature = "compact_filters")]
|
||||
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
||||
|
||||
@@ -192,7 +192,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// ```
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, KeyError, ScriptContext, IntoDescriptorKey};
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
@@ -211,8 +211,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub, KeyError,
|
||||
/// ScriptContext, IntoDescriptorKey,
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||
/// IntoDescriptorKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
@@ -237,7 +237,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// ```
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, ExtScriptContext, KeyError, ScriptContext, IntoDescriptorKey};
|
||||
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
/// is_legacy: bool,
|
||||
@@ -266,7 +266,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, KeyError, IntoDescriptorKey};
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
///
|
||||
/// pub struct MySegwitOnlyKeyType {
|
||||
/// pubkey: PublicKey,
|
||||
@@ -873,13 +873,13 @@ pub enum KeyError {
|
||||
Message(String),
|
||||
|
||||
/// BIP32 error
|
||||
BIP32(bitcoin::util::bip32::Error),
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
}
|
||||
|
||||
impl_error!(miniscript::Error, Miniscript, KeyError);
|
||||
impl_error!(bitcoin::util::bip32::Error, BIP32, KeyError);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError);
|
||||
|
||||
impl std::fmt::Display for KeyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
195
src/lib.rs
195
src/lib.rs
@@ -14,9 +14,6 @@
|
||||
// only enables the `doc_cfg` feature when
|
||||
// the `docsrs` configuration attribute is defined
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
// only enables the nightly `external_doc` feature when
|
||||
// `test-md-docs` is enabled
|
||||
#![cfg_attr(feature = "test-md-docs", feature(external_doc))]
|
||||
|
||||
//! A modern, lightweight, descriptor-based wallet library written in Rust.
|
||||
//!
|
||||
@@ -43,36 +40,39 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.5.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Sync the balance of a descriptor
|
||||
//!
|
||||
//! ### Example
|
||||
//! ```ignore
|
||||
//! use bdk::Wallet;
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
//!
|
||||
//! use bdk::electrum_client::Client;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
//! bitcoin::Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! ElectrumBlockchain::from(client)
|
||||
//! )?;
|
||||
//!
|
||||
//! wallet.sync(noop_progress(), None)?;
|
||||
//!
|
||||
//! println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! bdk = "0.8.0"
|
||||
//! ```
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
doc = r##"
|
||||
## Sync the balance of a descriptor
|
||||
|
||||
### Example
|
||||
```no_run
|
||||
use bdk::Wallet;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
use bdk::electrum_client::Client;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
|
||||
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
"##
|
||||
)]
|
||||
//!
|
||||
//! ## Generate a few addresses
|
||||
//!
|
||||
@@ -80,72 +80,80 @@
|
||||
//! ```
|
||||
//! use bdk::{Wallet};
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//! use bdk::wallet::AddressIndex::New;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! let wallet = Wallet::new_offline(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
//! bitcoin::Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! )?;
|
||||
//!
|
||||
//! println!("Address #0: {}", wallet.get_new_address()?);
|
||||
//! println!("Address #1: {}", wallet.get_new_address()?);
|
||||
//! println!("Address #2: {}", wallet.get_new_address()?);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Create a transaction
|
||||
//!
|
||||
//! ### Example
|
||||
//! ```ignore
|
||||
//! use base64::decode;
|
||||
//! use bdk::{FeeRate, Wallet};
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
//!
|
||||
//! use bdk::electrum_client::Client;
|
||||
//!
|
||||
//! use bitcoin::consensus::serialize;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
//! bitcoin::Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! ElectrumBlockchain::from(client)
|
||||
//! )?;
|
||||
//!
|
||||
//! wallet.sync(noop_progress(), None)?;
|
||||
//!
|
||||
//! let send_to = wallet.get_new_address()?;
|
||||
//! let (psbt, details) = wallet.build_tx()
|
||||
//! .add_recipient(send_to.script_pubkey(), 50_000)
|
||||
//! .enable_rbf()
|
||||
//! .do_not_spend_change()
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
//! .finish()?;
|
||||
//!
|
||||
//! println!("Transaction details: {:#?}", details);
|
||||
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
||||
//! println!("Address #0: {}", wallet.get_address(New)?);
|
||||
//! println!("Address #1: {}", wallet.get_address(New)?);
|
||||
//! println!("Address #2: {}", wallet.get_address(New)?);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
doc = r##"
|
||||
## Create a transaction
|
||||
|
||||
### Example
|
||||
```no_run
|
||||
use bdk::{FeeRate, Wallet};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||
use bdk::electrum_client::Client;
|
||||
|
||||
use bitcoin::consensus::serialize;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
ElectrumBlockchain::from(client)
|
||||
)?;
|
||||
|
||||
wallet.sync(noop_progress(), None)?;
|
||||
|
||||
let send_to = wallet.get_address(New)?;
|
||||
let (psbt, details) = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(send_to.script_pubkey(), 50_000)
|
||||
.enable_rbf()
|
||||
.do_not_spend_change()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
builder.finish()?
|
||||
};
|
||||
|
||||
println!("Transaction details: {:#?}", details);
|
||||
println!("Unsigned PSBT: {}", &psbt);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
"##
|
||||
)]
|
||||
//!
|
||||
//! ## Sign a transaction
|
||||
//!
|
||||
//! ### Example
|
||||
//! ```ignore
|
||||
//! use base64::decode;
|
||||
//! use bdk::{Wallet};
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//! ```no_run
|
||||
//! use std::str::FromStr;
|
||||
//!
|
||||
//! use bitcoin::consensus::deserialize;
|
||||
//! use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||
//!
|
||||
//! use bdk::{Wallet, SignOptions};
|
||||
//! use bdk::database::MemoryDatabase;
|
||||
//!
|
||||
//! fn main() -> Result<(), bdk::Error> {
|
||||
//! let wallet = Wallet::new_offline(
|
||||
@@ -156,9 +164,9 @@
|
||||
//! )?;
|
||||
//!
|
||||
//! let psbt = "...";
|
||||
//! let psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||
//! let mut psbt = Psbt::from_str(psbt)?;
|
||||
//!
|
||||
//! let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
|
||||
//! let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -214,6 +222,9 @@ extern crate bdk_macros;
|
||||
#[cfg(feature = "compact_filters")]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
pub extern crate bitcoincore_rpc;
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
pub extern crate electrum_client;
|
||||
|
||||
@@ -225,16 +236,10 @@ pub extern crate sled;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate testutils;
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate testutils_macros;
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate serial_test;
|
||||
pub extern crate serial_test;
|
||||
|
||||
#[macro_use]
|
||||
pub(crate) mod error;
|
||||
@@ -249,11 +254,12 @@ pub(crate) mod types;
|
||||
pub mod wallet;
|
||||
|
||||
pub use descriptor::template;
|
||||
pub use descriptor::HDKeyPaths;
|
||||
pub use descriptor::HdKeyPaths;
|
||||
pub use error::Error;
|
||||
pub use types::*;
|
||||
pub use wallet::address_validator;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
pub use wallet::tx_builder::TxBuilder;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
@@ -261,3 +267,10 @@ pub use wallet::Wallet;
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION", "unknown")
|
||||
}
|
||||
|
||||
// We should consider putting this under a feature flag but we need the macro in doctets so we need
|
||||
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
|
||||
//
|
||||
// Stuff in here is too rough to document atm
|
||||
#[doc(hidden)]
|
||||
pub mod testutils;
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
pub trait PSBTUtils {
|
||||
pub trait PsbtUtils {
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||
}
|
||||
|
||||
impl PSBTUtils for PSBT {
|
||||
impl PsbtUtils for Psbt {
|
||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.global.unsigned_tx;
|
||||
|
||||
@@ -37,3 +38,85 @@ impl PSBTUtils for PSBT {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::bitcoin::TxIn;
|
||||
use crate::psbt::Psbt;
|
||||
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||
use crate::wallet::AddressIndex;
|
||||
use crate::SignOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
// from bip 174
|
||||
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_psbt_input_legacy() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_psbt_input_segwit() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[1].clone());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_tx_input() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
psbt.global.unsigned_tx.input.push(TxIn::default());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_sign_with_finalized() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
// add a finalized input
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
psbt.global
|
||||
.unsigned_tx
|
||||
.input
|
||||
.push(psbt_bip.global.unsigned_tx.input[0].clone());
|
||||
|
||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
879
src/testutils/blockchain_tests.rs
Normal file
879
src/testutils/blockchain_tests.rs
Normal file
@@ -0,0 +1,879 @@
|
||||
use crate::testutils::TestIncomingTx;
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::sha256d;
|
||||
use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||
use core::str::FromStr;
|
||||
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct TestClient {
|
||||
client: RpcClient,
|
||||
electrum: ElectrumClient,
|
||||
}
|
||||
|
||||
impl TestClient {
|
||||
pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
|
||||
let client = RpcClient::new(
|
||||
format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
|
||||
get_auth(),
|
||||
)
|
||||
.unwrap();
|
||||
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
|
||||
|
||||
TestClient { client, electrum }
|
||||
}
|
||||
|
||||
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
|
||||
// wait for electrs to index the tx
|
||||
exponential_backoff_poll(|| {
|
||||
trace!("wait_for_tx {}", txid);
|
||||
|
||||
self.electrum
|
||||
.script_get_history(monitor_script)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.position(|entry| entry.tx_hash == txid)
|
||||
});
|
||||
}
|
||||
|
||||
fn wait_for_block(&mut self, min_height: usize) {
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
loop {
|
||||
let header = exponential_backoff_poll(|| {
|
||||
self.electrum.ping().unwrap();
|
||||
self.electrum.block_headers_pop().unwrap()
|
||||
});
|
||||
if header.height >= min_height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
|
||||
assert!(
|
||||
!meta_tx.output.is_empty(),
|
||||
"can't create a transaction with no outputs"
|
||||
);
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
let mut required_balance = 0;
|
||||
for out in &meta_tx.output {
|
||||
required_balance += out.value;
|
||||
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
||||
}
|
||||
|
||||
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
||||
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
||||
}
|
||||
|
||||
// FIXME: core can't create a tx with two outputs to the same address
|
||||
let tx = self
|
||||
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
|
||||
.unwrap();
|
||||
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
||||
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
||||
|
||||
if let Some(true) = meta_tx.replaceable {
|
||||
// for some reason core doesn't set this field right
|
||||
for input in &mut tx.input {
|
||||
input.sequence = 0xFFFFFFFD;
|
||||
}
|
||||
}
|
||||
|
||||
let tx = self
|
||||
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
|
||||
.unwrap();
|
||||
|
||||
// broadcast through electrum so that it caches the tx immediately
|
||||
let txid = self
|
||||
.electrum
|
||||
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
||||
.unwrap();
|
||||
|
||||
if let Some(num) = meta_tx.min_confirmations {
|
||||
self.generate(num, None);
|
||||
}
|
||||
|
||||
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
self.wait_for_tx(txid, &monitor_script);
|
||||
|
||||
debug!("Sent tx: {}", txid);
|
||||
|
||||
txid
|
||||
}
|
||||
|
||||
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
|
||||
let tx = self.get_raw_transaction_info(txid, None).unwrap();
|
||||
assert!(
|
||||
tx.confirmations.is_none(),
|
||||
"Can't bump tx {} because it's already confirmed",
|
||||
txid
|
||||
);
|
||||
|
||||
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
|
||||
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
|
||||
|
||||
let monitor_script =
|
||||
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
|
||||
self.wait_for_tx(new_txid, &monitor_script);
|
||||
|
||||
debug!("Bumped {}, new txid {}", txid, new_txid);
|
||||
|
||||
new_txid
|
||||
}
|
||||
|
||||
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
|
||||
use bitcoin::blockdata::block::{Block, BlockHeader};
|
||||
use bitcoin::blockdata::script::Builder;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
||||
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
||||
|
||||
let block_template: serde_json::Value = self
|
||||
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
||||
.unwrap();
|
||||
trace!("getblocktemplate: {:#?}", block_template);
|
||||
|
||||
let header = BlockHeader {
|
||||
version: block_template["version"].as_i64().unwrap() as i32,
|
||||
prev_blockhash: BlockHash::from_hex(
|
||||
block_template["previousblockhash"].as_str().unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
merkle_root: TxMerkleNode::default(),
|
||||
time: block_template["curtime"].as_u64().unwrap() as u32,
|
||||
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
||||
nonce: 0,
|
||||
};
|
||||
debug!("header: {:#?}", header);
|
||||
|
||||
let height = block_template["height"].as_u64().unwrap() as i64;
|
||||
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
|
||||
// burn block subsidy and fees, not a big deal
|
||||
let mut coinbase_tx = Transaction {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
script_sig: Builder::new().push_int(height).into_script(),
|
||||
sequence: 0xFFFFFFFF,
|
||||
witness: vec![witness_reserved_value],
|
||||
}],
|
||||
output: vec![],
|
||||
};
|
||||
|
||||
let mut txdata = vec![coinbase_tx.clone()];
|
||||
txdata.extend_from_slice(&txs);
|
||||
|
||||
let mut block = Block { header, txdata };
|
||||
|
||||
let witness_root = block.witness_root();
|
||||
let witness_commitment =
|
||||
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
|
||||
|
||||
// now update and replace the coinbase tx
|
||||
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
||||
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
||||
|
||||
coinbase_tx.output.push(TxOut {
|
||||
value: 0,
|
||||
script_pubkey: coinbase_witness_commitment_script.into(),
|
||||
});
|
||||
block.txdata[0] = coinbase_tx;
|
||||
|
||||
// set merkle root
|
||||
let merkle_root = block.merkle_root();
|
||||
block.header.merkle_root = merkle_root;
|
||||
|
||||
assert!(block.check_merkle_root());
|
||||
assert!(block.check_witness_commitment());
|
||||
|
||||
// now do PoW :)
|
||||
let target = block.header.target();
|
||||
while block.header.validate_pow(&target).is_err() {
|
||||
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
|
||||
}
|
||||
|
||||
let block_hex: String = serialize(&block).to_hex();
|
||||
debug!("generated block hex: {}", block_hex);
|
||||
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
let submit_result: serde_json::Value =
|
||||
self.call("submitblock", &[block_hex.into()]).unwrap();
|
||||
debug!("submitblock: {:?}", submit_result);
|
||||
assert!(
|
||||
submit_result.is_null(),
|
||||
"submitblock error: {:?}",
|
||||
submit_result.as_str()
|
||||
);
|
||||
|
||||
self.wait_for_block(height as usize);
|
||||
|
||||
block.header.block_hash().to_hex()
|
||||
}
|
||||
|
||||
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
|
||||
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
|
||||
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
|
||||
let best_hash = hashes.last().unwrap();
|
||||
let height = self.get_block_info(best_hash).unwrap().height;
|
||||
|
||||
self.wait_for_block(height);
|
||||
|
||||
debug!("Generated blocks to new height {}", height);
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, num_blocks: u64) {
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
let best_hash = self.get_best_block_hash().unwrap();
|
||||
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
||||
|
||||
let mut to_invalidate = best_hash;
|
||||
for i in 1..=num_blocks {
|
||||
trace!(
|
||||
"Invalidating block {}/{} ({})",
|
||||
i,
|
||||
num_blocks,
|
||||
to_invalidate
|
||||
);
|
||||
|
||||
self.invalidate_block(&to_invalidate).unwrap();
|
||||
to_invalidate = self.get_best_block_hash().unwrap();
|
||||
}
|
||||
|
||||
self.wait_for_block(initial_height - num_blocks as usize);
|
||||
|
||||
debug!(
|
||||
"Invalidated {} blocks to new height of {}",
|
||||
num_blocks,
|
||||
initial_height - num_blocks as usize
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reorg(&mut self, num_blocks: u64) {
|
||||
self.invalidate(num_blocks);
|
||||
self.generate(num_blocks, None);
|
||||
}
|
||||
|
||||
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
|
||||
Address::from_str(
|
||||
&self
|
||||
.get_new_address(None, address_type)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_electrum_url() -> String {
|
||||
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
|
||||
}
|
||||
|
||||
impl Deref for TestClient {
|
||||
type Target = RpcClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestClient {
|
||||
fn default() -> Self {
|
||||
let rpc_host_and_port =
|
||||
env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
|
||||
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
|
||||
Self::new(rpc_host_and_port, wallet)
|
||||
}
|
||||
}
|
||||
|
||||
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
|
||||
where
|
||||
F: FnMut() -> Option<T>,
|
||||
{
|
||||
let mut delay = Duration::from_millis(64);
|
||||
loop {
|
||||
match poll() {
|
||||
Some(data) => break data,
|
||||
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
|
||||
None => {}
|
||||
}
|
||||
|
||||
std::thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we currently only support env vars, we could also parse a toml file
|
||||
fn get_auth() -> Auth {
|
||||
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
|
||||
Ok("USER_PASS") => Auth::UserPass(
|
||||
env::var("BDK_RPC_USER").unwrap(),
|
||||
env::var("BDK_RPC_PASS").unwrap(),
|
||||
),
|
||||
_ => Auth::CookieFile(PathBuf::from(
|
||||
env::var("BDK_RPC_COOKIEFILE")
|
||||
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
|
||||
/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
|
||||
/// the setup required to run the tests yourself.
|
||||
#[macro_export]
|
||||
macro_rules! bdk_blockchain_tests {
|
||||
(
|
||||
fn test_instance() -> $blockchain:ty $block:block) => {
|
||||
#[cfg(test)]
|
||||
mod bdk_blockchain_tests {
|
||||
use $crate::bitcoin::Network;
|
||||
use $crate::testutils::blockchain_tests::TestClient;
|
||||
use $crate::blockchain::noop_progress;
|
||||
use $crate::database::MemoryDatabase;
|
||||
use $crate::types::KeychainKind;
|
||||
use $crate::{Wallet, FeeRate};
|
||||
use $crate::testutils;
|
||||
use $crate::serial_test::serial;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_blockchain() -> $blockchain {
|
||||
$block
|
||||
}
|
||||
|
||||
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
|
||||
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
|
||||
}
|
||||
|
||||
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let descriptors = testutils! {
|
||||
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
||||
};
|
||||
|
||||
let test_client = TestClient::default();
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
|
||||
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
|
||||
#[cfg(feature = "test-rpc")]
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
(wallet, descriptors, test_client)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_simple() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let tx = testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
};
|
||||
println!("{:?}", tx);
|
||||
let txid = test_client.receive(tx);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
|
||||
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_stop_gap_20() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 5) => 50_000 )
|
||||
});
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 25) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_before_and_after_receive() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_multiple_outputs_same_tx() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||
assert_eq!(list_tx_item.received, 105_000, "incorrect received");
|
||||
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_multi() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 5) => 25_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_address_reuse() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 25_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_rbf_replaced() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
|
||||
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
|
||||
|
||||
let new_txid = test_client.bump_fee(&txid);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, new_txid, "incorrect txid after bump");
|
||||
assert_eq!(list_tx_item.received, 50_000, "incorrect received after bump");
|
||||
assert_eq!(list_tx_item.sent, 0, "incorrect sent after bump");
|
||||
assert_eq!(list_tx_item.confirmation_time, None, "incorrect height after bump");
|
||||
}
|
||||
|
||||
// FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
|
||||
// doesn't work for some reason.
|
||||
#[cfg(not(feature = "esplora"))]
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_reorg_block() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||
assert!(list_tx_item.confirmation_time.is_some(), "incorrect confirmation_time");
|
||||
|
||||
// Invalidate 1 block
|
||||
test_client.invalidate(1);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
|
||||
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time after invalidate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_after_send() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
println!("{}", descriptors.0);
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||
wallet.broadcast(tx).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
||||
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_update_confirmation_time_after_generate() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
println!("{}", descriptors.0);
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
let received_txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
let details = tx_map.get(&received_txid).unwrap();
|
||||
assert!(details.confirmation_time.is_none());
|
||||
|
||||
test_client.generate(1, Some(node_addr));
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
let details = tx_map.get(&received_txid).unwrap();
|
||||
assert!(details.confirmation_time.is_some());
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_outgoing_from_scratch() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
let received_txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
||||
|
||||
// empty wallet
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
|
||||
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||
test_client.generate(1, Some(node_addr));
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
let received = tx_map.get(&received_txid).unwrap();
|
||||
assert_eq!(received.received, 50_000, "incorrect received from receiver");
|
||||
assert_eq!(received.sent, 0, "incorrect sent from receiver");
|
||||
|
||||
let sent = tx_map.get(&sent_txid).unwrap();
|
||||
assert_eq!(sent.received, details.received, "incorrect received from sender");
|
||||
assert_eq!(sent.sent, details.sent, "incorrect sent from sender");
|
||||
assert_eq!(sent.fee.unwrap_or(0), details.fee.unwrap_or(0), "incorrect fees from sender");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_long_change_chain() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..5 {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 5_000);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
total_sent += 5_000 + details.fee.unwrap_or(0);
|
||||
}
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
|
||||
|
||||
// empty wallet
|
||||
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
|
||||
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||
test_client.generate(1, Some(node_addr));
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_basic() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
|
||||
|
||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_remove_change() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
||||
|
||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_add_input_simple() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_add_input_no_change() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
println!("{:#?}", new_details);
|
||||
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_coinbase() {
|
||||
let (wallet, _, mut test_client) = init_single_sig();
|
||||
|
||||
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
|
||||
|
||||
test_client.generate(1, Some(wallet_addr));
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
{
|
||||
// rpc consider coinbase only when mature (100 blocks)
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
test_client.generate(100, Some(node_addr));
|
||||
}
|
||||
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
src/testutils/mod.rs
Normal file
230
src/testutils/mod.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
pub mod blockchain_tests;
|
||||
|
||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||
use bitcoin::{Address, PublicKey};
|
||||
|
||||
use miniscript::descriptor::DescriptorPublicKey;
|
||||
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingOutput {
|
||||
pub value: u64,
|
||||
pub to_address: String,
|
||||
}
|
||||
|
||||
impl TestIncomingOutput {
|
||||
pub fn new(value: u64, to_address: Address) -> Self {
|
||||
Self {
|
||||
value,
|
||||
to_address: to_address.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingTx {
|
||||
pub output: Vec<TestIncomingOutput>,
|
||||
pub min_confirmations: Option<u64>,
|
||||
pub locktime: Option<i64>,
|
||||
pub replaceable: Option<bool>,
|
||||
}
|
||||
|
||||
impl TestIncomingTx {
|
||||
pub fn new(
|
||||
output: Vec<TestIncomingOutput>,
|
||||
min_confirmations: Option<u64>,
|
||||
locktime: Option<i64>,
|
||||
replaceable: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output,
|
||||
min_confirmations,
|
||||
locktime,
|
||||
replaceable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
||||
self.output.push(output);
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait TranslateDescriptor {
|
||||
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
|
||||
fn derive_translated<C: Verification>(
|
||||
&self,
|
||||
secp: &Secp256k1<C>,
|
||||
index: u32,
|
||||
) -> Descriptor<PublicKey>;
|
||||
}
|
||||
|
||||
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
|
||||
fn derive_translated<C: Verification>(
|
||||
&self,
|
||||
secp: &Secp256k1<C>,
|
||||
index: u32,
|
||||
) -> Descriptor<PublicKey> {
|
||||
let translate = |key: &DescriptorPublicKey| -> PublicKey {
|
||||
match key {
|
||||
DescriptorPublicKey::XPub(xpub) => {
|
||||
xpub.xkey
|
||||
.derive_pub(secp, &xpub.derivation_path)
|
||||
.expect("hardened derivation steps")
|
||||
.public_key
|
||||
}
|
||||
DescriptorPublicKey::SinglePub(key) => key.key,
|
||||
}
|
||||
};
|
||||
|
||||
self.derive(index)
|
||||
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! testutils {
|
||||
( @external $descriptors:expr, $child:expr ) => ({
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::testutils::TranslateDescriptor;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @internal $descriptors:expr, $child:expr ) => ({
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::testutils::TranslateDescriptor;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||
|
||||
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
|
||||
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
|
||||
|
||||
let locktime = None::<i64>$(.or(Some($locktime)))?;
|
||||
|
||||
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
|
||||
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
|
||||
|
||||
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
|
||||
});
|
||||
|
||||
( @literal $key:expr ) => ({
|
||||
let key = $key.to_string();
|
||||
(key, None::<String>, None::<String>)
|
||||
});
|
||||
( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({
|
||||
use rand::Rng;
|
||||
|
||||
let mut seed = [0u8; 32];
|
||||
rand::thread_rng().fill(&mut seed[..]);
|
||||
|
||||
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
|
||||
bitcoin::Network::Testnet,
|
||||
&seed,
|
||||
);
|
||||
|
||||
let external_path = None::<String>$(.or(Some($external_path.to_string())))?;
|
||||
let internal_path = None::<String>$(.or(Some($internal_path.to_string())))?;
|
||||
|
||||
(key.unwrap().to_string(), external_path, internal_path)
|
||||
});
|
||||
( @generate_wif ) => ({
|
||||
use rand::Rng;
|
||||
|
||||
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
|
||||
rand::thread_rng().fill(&mut key[..]);
|
||||
|
||||
(bitcoin::PrivateKey {
|
||||
compressed: true,
|
||||
network: bitcoin::Network::Testnet,
|
||||
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
|
||||
}.to_string(), None::<String>, None::<String>)
|
||||
});
|
||||
|
||||
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
|
||||
let mut map = std::collections::HashMap::new();
|
||||
$(
|
||||
let alias: &str = $alias;
|
||||
map.insert(alias, testutils!( $($key_type)* ));
|
||||
)+
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use miniscript::descriptor::Descriptor;
|
||||
use miniscript::TranslatePk;
|
||||
|
||||
#[allow(unused_assignments, unused_mut)]
|
||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
||||
$(
|
||||
keys = testutils!{ @keys $( $keys )* };
|
||||
)*
|
||||
|
||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
|
||||
});
|
||||
let external = external.to_string();
|
||||
|
||||
let internal = None::<String>$(.or({
|
||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
|
||||
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
});
|
||||
Some(string_internal.to_string())
|
||||
}))?;
|
||||
|
||||
(external, internal)
|
||||
})
|
||||
}
|
||||
35
src/types.rs
35
src/types.rs
@@ -80,7 +80,7 @@ impl std::default::Default for FeeRate {
|
||||
/// An unspent output owned by a [`Wallet`].
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LocalUtxo {
|
||||
/// Reference to a transaction output
|
||||
pub outpoint: OutPoint,
|
||||
@@ -139,7 +139,7 @@ impl Utxo {
|
||||
}
|
||||
|
||||
if let Some(txout) = &psbt_input.witness_utxo {
|
||||
return &txout;
|
||||
return txout;
|
||||
}
|
||||
|
||||
unreachable!("Foreign UTXOs will always have one of these set")
|
||||
@@ -155,16 +155,35 @@ pub struct TransactionDetails {
|
||||
pub transaction: Option<Transaction>,
|
||||
/// Transaction id
|
||||
pub txid: Txid,
|
||||
/// Timestamp
|
||||
pub timestamp: u64,
|
||||
|
||||
/// Received value (sats)
|
||||
pub received: u64,
|
||||
/// Sent value (sats)
|
||||
pub sent: u64,
|
||||
/// Fee value (sats)
|
||||
pub fees: u64,
|
||||
/// Confirmed in block height, `None` means unconfirmed
|
||||
pub height: Option<u32>,
|
||||
/// Fee value (sats) if available
|
||||
pub fee: Option<u64>,
|
||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||
/// transaction, unconfirmed transaction contains `None`.
|
||||
pub confirmation_time: Option<ConfirmationTime>,
|
||||
}
|
||||
|
||||
/// Block height and timestamp of the block containing the confirmed transaction
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ConfirmationTime {
|
||||
/// confirmation block height
|
||||
pub height: u32,
|
||||
/// confirmation block timestamp
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
impl ConfirmationTime {
|
||||
/// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some`
|
||||
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
|
||||
match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
|
||||
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
|
||||
//! whenever a new address is generated (either explicitly by the user with
|
||||
//! [`Wallet::get_new_address`](super::Wallet::get_new_address) or internally to create a change
|
||||
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
|
||||
//! address) all the attached validators will be polled, in sequence. All of them must complete
|
||||
//! successfully to continue.
|
||||
//!
|
||||
@@ -32,6 +32,7 @@
|
||||
//! # use bdk::address_validator::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! #[derive(Debug)]
|
||||
//! struct PrintAddressAndContinue;
|
||||
//!
|
||||
@@ -39,7 +40,7 @@
|
||||
//! fn validate(
|
||||
//! &self,
|
||||
//! keychain: KeychainKind,
|
||||
//! hd_keypaths: &HDKeyPaths,
|
||||
//! hd_keypaths: &HdKeyPaths,
|
||||
//! script: &Script
|
||||
//! ) -> Result<(), AddressValidatorError> {
|
||||
//! let address = Address::from_script(script, Network::Testnet)
|
||||
@@ -57,7 +58,7 @@
|
||||
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
|
||||
//!
|
||||
//! let address = wallet.get_new_address()?;
|
||||
//! let address = wallet.get_address(New)?;
|
||||
//! println!("Address: {}", address);
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
@@ -66,7 +67,7 @@ use std::fmt;
|
||||
|
||||
use bitcoin::Script;
|
||||
|
||||
use crate::descriptor::HDKeyPaths;
|
||||
use crate::descriptor::HdKeyPaths;
|
||||
use crate::types::KeychainKind;
|
||||
|
||||
/// Errors that can be returned to fail the validation of an address
|
||||
@@ -104,7 +105,7 @@ pub trait AddressValidator: Send + Sync + fmt::Debug {
|
||||
fn validate(
|
||||
&self,
|
||||
keychain: KeychainKind,
|
||||
hd_keypaths: &HDKeyPaths,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
script: &Script,
|
||||
) -> Result<(), AddressValidatorError>;
|
||||
}
|
||||
@@ -115,6 +116,7 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||
use crate::wallet::AddressIndex::New;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestValidator;
|
||||
@@ -122,7 +124,7 @@ mod test {
|
||||
fn validate(
|
||||
&self,
|
||||
_keychain: KeychainKind,
|
||||
_hd_keypaths: &HDKeyPaths,
|
||||
_hd_keypaths: &HdKeyPaths,
|
||||
_script: &bitcoin::Script,
|
||||
) -> Result<(), AddressValidatorError> {
|
||||
Err(AddressValidatorError::InvalidScript)
|
||||
@@ -135,7 +137,7 @@ mod test {
|
||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
wallet.get_new_address().unwrap();
|
||||
wallet.get_address(New).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -144,7 +146,7 @@ mod test {
|
||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
let addr = testutils!(@external descriptors, 10);
|
||||
let addr = crate::testutils!(@external descriptors, 10);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
builder.finish().unwrap();
|
||||
|
||||
@@ -46,17 +46,25 @@
|
||||
//! let mut selected_amount = 0;
|
||||
//! let mut additional_weight = 0;
|
||||
//! let all_utxos_selected = required_utxos
|
||||
//! .into_iter().chain(optional_utxos)
|
||||
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| {
|
||||
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
||||
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
|
||||
//! Some(weighted_utxo.utxo)
|
||||
//! })
|
||||
//! .into_iter()
|
||||
//! .chain(optional_utxos)
|
||||
//! .scan(
|
||||
//! (&mut selected_amount, &mut additional_weight),
|
||||
//! |(selected_amount, additional_weight), weighted_utxo| {
|
||||
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
||||
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
|
||||
//! Some(weighted_utxo.utxo)
|
||||
//! },
|
||||
//! )
|
||||
//! .collect::<Vec<_>>();
|
||||
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
|
||||
//! let amount_needed_with_fees = (fee_amount + additional_fees).ceil() as u64 + amount_needed;
|
||||
//! if amount_needed_with_fees > selected_amount {
|
||||
//! return Err(bdk::Error::InsufficientFunds{ needed: amount_needed_with_fees, available: selected_amount });
|
||||
//! let amount_needed_with_fees =
|
||||
//! (fee_amount + additional_fees).ceil() as u64 + amount_needed;
|
||||
//! if amount_needed_with_fees > selected_amount {
|
||||
//! return Err(bdk::Error::InsufficientFunds {
|
||||
//! needed: amount_needed_with_fees,
|
||||
//! available: selected_amount,
|
||||
//! });
|
||||
//! }
|
||||
//!
|
||||
//! Ok(CoinSelectionResult {
|
||||
@@ -72,8 +80,7 @@
|
||||
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
//! let (psbt, details) = {
|
||||
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
|
||||
//! builder
|
||||
//! .add_recipient(to_address.script_pubkey(), 50_000);
|
||||
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
|
||||
//! builder.finish()?
|
||||
//! };
|
||||
//!
|
||||
@@ -91,6 +98,7 @@ use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
#[cfg(test)]
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
|
||||
/// overridden
|
||||
@@ -254,8 +262,8 @@ impl OutputGroup {
|
||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
|
||||
OutputGroup {
|
||||
weighted_utxo,
|
||||
effective_value,
|
||||
fee,
|
||||
effective_value,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,32 +311,39 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
||||
.collect();
|
||||
|
||||
// Mapping every (UTXO, usize) to an output group.
|
||||
// Filtering UTXOs with an effective_value < 0, as the fee paid for
|
||||
// adding them is more than their value
|
||||
let optional_utxos: Vec<OutputGroup> = optional_utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.filter(|u| u.effective_value > 0)
|
||||
.collect();
|
||||
|
||||
let curr_value = required_utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let curr_available_value = optional_utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let actual_target = fee_amount.ceil() as u64 + amount_needed;
|
||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
|
||||
|
||||
if curr_available_value + curr_value < actual_target {
|
||||
let expected = (curr_available_value + curr_value)
|
||||
.try_into()
|
||||
.map_err(|_| {
|
||||
Error::Generic("Sum of UTXO spendable values does not fit into u64".to_string())
|
||||
})?;
|
||||
|
||||
if expected < actual_target {
|
||||
return Err(Error::InsufficientFunds {
|
||||
needed: actual_target,
|
||||
available: curr_available_value + curr_value,
|
||||
available: expected,
|
||||
});
|
||||
}
|
||||
|
||||
let actual_target = actual_target
|
||||
.try_into()
|
||||
.expect("Bitcoin amount to fit into i64");
|
||||
|
||||
Ok(self
|
||||
.bnb(
|
||||
required_utxos.clone(),
|
||||
@@ -359,9 +374,9 @@ impl BranchAndBoundCoinSelection {
|
||||
&self,
|
||||
required_utxos: Vec<OutputGroup>,
|
||||
mut optional_utxos: Vec<OutputGroup>,
|
||||
mut curr_value: u64,
|
||||
mut curr_available_value: u64,
|
||||
actual_target: u64,
|
||||
mut curr_value: i64,
|
||||
mut curr_available_value: i64,
|
||||
actual_target: i64,
|
||||
fee_amount: f32,
|
||||
cost_of_change: f32,
|
||||
) -> Result<CoinSelectionResult, Error> {
|
||||
@@ -387,7 +402,7 @@ impl BranchAndBoundCoinSelection {
|
||||
// or the selected value is out of range.
|
||||
// Go back and try other branch
|
||||
if curr_value + curr_available_value < actual_target
|
||||
|| curr_value > actual_target + cost_of_change as u64
|
||||
|| curr_value > actual_target + cost_of_change as i64
|
||||
{
|
||||
backtrack = true;
|
||||
} else if curr_value >= actual_target {
|
||||
@@ -413,8 +428,7 @@ impl BranchAndBoundCoinSelection {
|
||||
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
|
||||
while let Some(false) = current_selection.last() {
|
||||
current_selection.pop();
|
||||
curr_available_value +=
|
||||
optional_utxos[current_selection.len()].effective_value as u64;
|
||||
curr_available_value += optional_utxos[current_selection.len()].effective_value;
|
||||
}
|
||||
|
||||
if current_selection.last_mut().is_none() {
|
||||
@@ -432,17 +446,17 @@ impl BranchAndBoundCoinSelection {
|
||||
}
|
||||
|
||||
let utxo = &optional_utxos[current_selection.len() - 1];
|
||||
curr_value -= utxo.effective_value as u64;
|
||||
curr_value -= utxo.effective_value;
|
||||
} else {
|
||||
// Moving forwards, continuing down this branch
|
||||
let utxo = &optional_utxos[current_selection.len()];
|
||||
|
||||
// Remove this utxo from the curr_available_value utxo amount
|
||||
curr_available_value -= utxo.effective_value as u64;
|
||||
curr_available_value -= utxo.effective_value;
|
||||
|
||||
// Inclusion branch first (Largest First Exploration)
|
||||
current_selection.push(true);
|
||||
curr_value += utxo.effective_value as u64;
|
||||
curr_value += utxo.effective_value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,8 +483,8 @@ impl BranchAndBoundCoinSelection {
|
||||
&self,
|
||||
required_utxos: Vec<OutputGroup>,
|
||||
mut optional_utxos: Vec<OutputGroup>,
|
||||
curr_value: u64,
|
||||
actual_target: u64,
|
||||
curr_value: i64,
|
||||
actual_target: i64,
|
||||
fee_amount: f32,
|
||||
) -> CoinSelectionResult {
|
||||
#[cfg(not(test))]
|
||||
@@ -488,7 +502,7 @@ impl BranchAndBoundCoinSelection {
|
||||
if *curr_value >= actual_target {
|
||||
None
|
||||
} else {
|
||||
*curr_value += utxo.effective_value as u64;
|
||||
*curr_value += utxo.effective_value;
|
||||
Some(utxo)
|
||||
}
|
||||
})
|
||||
@@ -532,13 +546,15 @@ mod test {
|
||||
|
||||
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
||||
|
||||
const FEE_AMOUNT: f32 = 50.0;
|
||||
|
||||
fn get_test_utxos() -> Vec<WeightedUtxo> {
|
||||
vec![
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
@@ -552,7 +568,21 @@ mod test {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
|
||||
"0000000000000000000000000000000000000000000000000000000000000001:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
value: FEE_AMOUNT as u64 - 40,
|
||||
script_pubkey: Script::new(),
|
||||
},
|
||||
keychain: KeychainKind::External,
|
||||
}),
|
||||
},
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||
utxo: Utxo::Local(LocalUtxo {
|
||||
outpoint: OutPoint::from_str(
|
||||
"0000000000000000000000000000000000000000000000000000000000000002:0",
|
||||
)
|
||||
.unwrap(),
|
||||
txout: TxOut {
|
||||
@@ -629,9 +659,9 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 2);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert_eq!(result.fee_amount, 186.0);
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -650,9 +680,9 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 2);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert_eq!(result.fee_amount, 186.0);
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -673,7 +703,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 1);
|
||||
assert_eq!(result.selected_amount(), 200_000);
|
||||
assert_eq!(result.fee_amount, 118.0);
|
||||
assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -733,7 +763,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -748,13 +778,34 @@ mod test {
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
20_000,
|
||||
50.0,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 2);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert_eq!(result.fee_amount, 186.0);
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bnb_coin_selection_optional_are_enough() {
|
||||
let utxos = get_test_utxos();
|
||||
let database = MemoryDatabase::default();
|
||||
|
||||
let result = BranchAndBoundCoinSelection::default()
|
||||
.coin_select(
|
||||
&database,
|
||||
vec![],
|
||||
utxos,
|
||||
FeeRate::from_sat_per_vb(1.0),
|
||||
299756,
|
||||
FEE_AMOUNT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300010);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -848,9 +899,7 @@ mod test {
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.collect();
|
||||
|
||||
let curr_available_value = utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
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_vb();
|
||||
@@ -876,9 +925,7 @@ mod test {
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.collect();
|
||||
|
||||
let curr_available_value = utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
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_vb();
|
||||
@@ -911,13 +958,11 @@ mod test {
|
||||
|
||||
let curr_value = 0;
|
||||
|
||||
let curr_available_value = utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||
// cost_of_change + 5.
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as u64 + 5;
|
||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
||||
|
||||
let result = BranchAndBoundCoinSelection::new(size_of_change)
|
||||
.bnb(
|
||||
@@ -930,7 +975,7 @@ mod test {
|
||||
cost_of_change,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.fee_amount, 186.0);
|
||||
assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
}
|
||||
|
||||
@@ -951,10 +996,10 @@ mod test {
|
||||
|
||||
let curr_available_value = optional_utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
let target_amount = optional_utxos[3].effective_value as u64
|
||||
+ optional_utxos[23].effective_value as u64;
|
||||
let target_amount =
|
||||
optional_utxos[3].effective_value + optional_utxos[23].effective_value;
|
||||
|
||||
let result = BranchAndBoundCoinSelection::new(0)
|
||||
.bnb(
|
||||
@@ -967,7 +1012,7 @@ mod test {
|
||||
0.0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.selected_amount(), target_amount);
|
||||
assert_eq!(result.selected_amount(), target_amount as u64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,14 +1033,13 @@ mod test {
|
||||
vec![],
|
||||
utxos,
|
||||
0,
|
||||
target_amount,
|
||||
target_amount as i64,
|
||||
50.0,
|
||||
);
|
||||
|
||||
assert!(result.selected_amount() > target_amount);
|
||||
assert_eq!(
|
||||
result.fee_amount,
|
||||
50.0 + result.selected.len() as f32 * 68.0
|
||||
assert!(
|
||||
(result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ impl WalletExport {
|
||||
Ok(txs) => {
|
||||
let mut heights = txs
|
||||
.into_iter()
|
||||
.map(|tx| tx.height.unwrap_or(0))
|
||||
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
||||
.collect::<Vec<_>>();
|
||||
heights.sort_unstable();
|
||||
|
||||
@@ -212,6 +212,7 @@ mod test {
|
||||
use crate::database::{memory::MemoryDatabase, BatchOperations};
|
||||
use crate::types::TransactionDetails;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::ConfirmationTime;
|
||||
|
||||
fn get_test_db() -> MemoryDatabase {
|
||||
let mut db = MemoryDatabase::new();
|
||||
@@ -221,11 +222,14 @@ mod test {
|
||||
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
|
||||
)
|
||||
.unwrap(),
|
||||
timestamp: 12345678,
|
||||
|
||||
received: 100_000,
|
||||
sent: 0,
|
||||
fees: 500,
|
||||
height: Some(5000),
|
||||
fee: Some(500),
|
||||
confirmation_time: Some(ConfirmationTime {
|
||||
timestamp: 12345678,
|
||||
height: 5000,
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
1018
src/wallet/mod.rs
1018
src/wallet/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -146,7 +146,13 @@ pub enum SignerError {
|
||||
/// The `witness_script` field of the transaction is requied to sign this input
|
||||
MissingWitnessScript,
|
||||
/// The fingerprint and derivation path are missing from the psbt input
|
||||
MissingHDKeypath,
|
||||
MissingHdKeypath,
|
||||
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
|
||||
/// explicitly allowed them
|
||||
///
|
||||
/// To enable signing transactions with non-standard sighashes set
|
||||
/// [`SignOptions::allow_all_sighashes`] to `true`.
|
||||
NonStandardSighash,
|
||||
}
|
||||
|
||||
impl fmt::Display for SignerError {
|
||||
@@ -206,11 +212,17 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
if psbt.inputs[input_index].final_script_sig.is_some()
|
||||
|| psbt.inputs[input_index].final_script_witness.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (public_key, full_path) = match psbt.inputs[input_index]
|
||||
.bip32_derivation
|
||||
.iter()
|
||||
.filter_map(|(pk, &(fingerprint, ref path))| {
|
||||
if self.matches(&(fingerprint, path.clone()), &secp).is_some() {
|
||||
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
|
||||
Some((pk, path))
|
||||
} else {
|
||||
None
|
||||
@@ -228,12 +240,12 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
&full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
|
||||
[origin_path.len()..],
|
||||
);
|
||||
self.xkey.derive_priv(&secp, &deriv_path).unwrap()
|
||||
self.xkey.derive_priv(secp, &deriv_path).unwrap()
|
||||
}
|
||||
None => self.xkey.derive_priv(&secp, &full_path).unwrap(),
|
||||
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
|
||||
};
|
||||
|
||||
if &derived_key.private_key.public_key(&secp) != public_key {
|
||||
if &derived_key.private_key.public_key(secp) != public_key {
|
||||
Err(SignerError::InvalidKey)
|
||||
} else {
|
||||
derived_key.private_key.sign(psbt, Some(input_index), secp)
|
||||
@@ -245,7 +257,7 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
}
|
||||
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.root_fingerprint(&secp))
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
@@ -261,11 +273,17 @@ impl Signer for PrivateKey {
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
let input_index = input_index.unwrap();
|
||||
if input_index >= psbt.inputs.len() {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
let pubkey = self.public_key(&secp);
|
||||
if psbt.inputs[input_index].final_script_sig.is_some()
|
||||
|| psbt.inputs[input_index].final_script_witness.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pubkey = self.public_key(secp);
|
||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -427,6 +445,50 @@ impl SignersContainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for a software signer
|
||||
///
|
||||
/// Adjust the behavior of our software signers and the way a transaction is finalized
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignOptions {
|
||||
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
|
||||
/// provided
|
||||
///
|
||||
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into
|
||||
/// paying a fee larger than expected.
|
||||
///
|
||||
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
|
||||
/// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
|
||||
/// should correctly produce a signature, at the expense of an increased trust in the creator
|
||||
/// of the PSBT.
|
||||
///
|
||||
/// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
|
||||
pub trust_witness_utxo: bool,
|
||||
|
||||
/// Whether the wallet should assume a specific height has been reached when trying to finalize
|
||||
/// a transaction
|
||||
///
|
||||
/// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
|
||||
/// timelock height has already been reached. This option allows overriding the "current height" to let the
|
||||
/// wallet use timelocks in the future to spend a coin.
|
||||
pub assume_height: Option<u32>,
|
||||
|
||||
/// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
|
||||
/// what its value is
|
||||
///
|
||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||
pub allow_all_sighashes: bool,
|
||||
}
|
||||
|
||||
impl Default for SignOptions {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
trust_witness_utxo: false,
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ComputeSighash {
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
@@ -439,7 +501,7 @@ impl ComputeSighash for Legacy {
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
@@ -487,32 +549,49 @@ impl ComputeSighash for Segwitv0 {
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
let tx_input = &psbt.global.unsigned_tx.input[input_index];
|
||||
|
||||
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
|
||||
|
||||
let witness_utxo = psbt_input
|
||||
.witness_utxo
|
||||
.as_ref()
|
||||
.ok_or(SignerError::MissingNonWitnessUtxo)?;
|
||||
let value = witness_utxo.value;
|
||||
// Always try first with the non-witness utxo
|
||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
// Check the provided prev-tx
|
||||
if prev_tx.txid() != tx_input.previous_output.txid {
|
||||
return Err(SignerError::InvalidNonWitnessUtxo);
|
||||
}
|
||||
|
||||
// The output should be present, if it's missing the `non_witness_utxo` is invalid
|
||||
prev_tx
|
||||
.output
|
||||
.get(tx_input.previous_output.vout as usize)
|
||||
.ok_or(SignerError::InvalidNonWitnessUtxo)?
|
||||
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
|
||||
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
|
||||
// before we get to this point
|
||||
witness_utxo
|
||||
} else {
|
||||
// Nothing has been provided
|
||||
return Err(SignerError::MissingNonWitnessUtxo);
|
||||
};
|
||||
let value = utxo.value;
|
||||
|
||||
let script = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => witness_script.clone(),
|
||||
None => {
|
||||
if witness_utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&witness_utxo.script_pubkey)
|
||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&utxo.script_pubkey)
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.map(Script::is_v0_p2wpkh)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
p2wpkh_script_code(&psbt_input.redeem_script.as_ref().unwrap())
|
||||
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessScript);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ use std::collections::HashSet;
|
||||
use std::default::Default;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as PSBT};
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
||||
|
||||
use miniscript::descriptor::DescriptorTrait;
|
||||
@@ -87,9 +87,9 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// let (psbt1, details) = {
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder
|
||||
/// .ordering(TxOrdering::Untouched)
|
||||
/// .add_recipient(addr1.script_pubkey(), 50_000)
|
||||
/// .add_recipient(addr2.script_pubkey(), 50_000);
|
||||
/// .ordering(TxOrdering::Untouched)
|
||||
/// .add_recipient(addr1.script_pubkey(), 50_000)
|
||||
/// .add_recipient(addr2.script_pubkey(), 50_000);
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
@@ -103,7 +103,10 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(psbt1.global.unsigned_tx.output[..2], psbt2.global.unsigned_tx.output[..2]);
|
||||
/// assert_eq!(
|
||||
/// psbt1.global.unsigned_tx.output[..2],
|
||||
/// psbt2.global.unsigned_tx.output[..2]
|
||||
/// );
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// ```
|
||||
///
|
||||
@@ -119,9 +122,6 @@ impl TxBuilderContext for BumpFee {}
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
|
||||
pub(crate) wallet: &'a Wallet<B, D>,
|
||||
// params and coin_selection are Options not becasue they are optionally set (they are always
|
||||
// there) but because `.finish()` uses `Option::take` to get an owned value from a &mut self.
|
||||
// They are only `None` after `.finish()` is called.
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
@@ -143,10 +143,10 @@ pub(crate) struct TxParams {
|
||||
pub(crate) sighash: Option<SigHashType>,
|
||||
pub(crate) ordering: TxOrdering,
|
||||
pub(crate) locktime: Option<u32>,
|
||||
pub(crate) rbf: Option<RBFValue>,
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
pub(crate) force_non_witness_utxo: bool,
|
||||
pub(crate) only_witness_utxo: bool,
|
||||
pub(crate) add_global_xpubs: bool,
|
||||
pub(crate) include_output_redeem_witness_script: bool,
|
||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||
@@ -249,7 +249,8 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// let mut path = BTreeMap::new();
|
||||
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
|
||||
///
|
||||
/// let builder = wallet.build_tx()
|
||||
/// let builder = wallet
|
||||
/// .build_tx()
|
||||
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
/// .policy_path(path, KeychainKind::External);
|
||||
///
|
||||
@@ -278,7 +279,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> {
|
||||
let utxos = outpoints
|
||||
.iter()
|
||||
.map(|outpoint| self.wallet.get_utxo(*outpoint)?.ok_or(Error::UnknownUTXO))
|
||||
.map(|outpoint| self.wallet.get_utxo(*outpoint)?.ok_or(Error::UnknownUtxo))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for utxo in utxos {
|
||||
@@ -336,10 +337,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
|
||||
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
|
||||
///
|
||||
/// Note if you set [`force_non_witness_utxo`] any `psbt_input` you pass to this method must
|
||||
/// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
|
||||
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
|
||||
///
|
||||
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo
|
||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||
pub fn add_foreign_utxo(
|
||||
@@ -464,12 +465,13 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit
|
||||
/// descriptors.
|
||||
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
|
||||
/// SegWit descriptors.
|
||||
///
|
||||
/// This is useful for signers which always require it, like Trezor hardware wallets.
|
||||
pub fn force_non_witness_utxo(&mut self) -> &mut Self {
|
||||
self.params.force_non_witness_utxo = true;
|
||||
/// This reduces the size of the PSBT, but some signers might reject them due to the lack of
|
||||
/// the `non_witness_utxo`.
|
||||
pub fn only_witness_utxo(&mut self) -> &mut Self {
|
||||
self.params.only_witness_utxo = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -520,9 +522,29 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<(PSBT, TransactionDetails), Error> {
|
||||
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
|
||||
self.wallet.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
pub fn enable_rbf(&mut self) -> &mut Self {
|
||||
self.params.rbf = Some(RbfValue::Default);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable signaling RBF with a specific nSequence value
|
||||
///
|
||||
/// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
|
||||
/// and the given `nsequence` is lower than the CSV value.
|
||||
///
|
||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||
/// be a valid nSequence to signal RBF.
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D, Cs, CreateTx> {
|
||||
@@ -558,26 +580,6 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
pub fn enable_rbf(&mut self) -> &mut Self {
|
||||
self.params.rbf = Some(RBFValue::Default);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable signaling RBF with a specific nSequence value
|
||||
///
|
||||
/// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
|
||||
/// and the given `nsequence` is lower than the CSV value.
|
||||
///
|
||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||
/// be a valid nSequence to signal RBF.
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
||||
self.params.rbf = Some(RBFValue::Value(nsequence));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
@@ -612,7 +614,7 @@ pub enum TxOrdering {
|
||||
/// Unchanged
|
||||
Untouched,
|
||||
/// BIP69 / Lexicographic
|
||||
BIP69Lexicographic,
|
||||
Bip69Lexicographic,
|
||||
}
|
||||
|
||||
impl Default for TxOrdering {
|
||||
@@ -638,7 +640,7 @@ impl TxOrdering {
|
||||
|
||||
tx.output.shuffle(&mut rng);
|
||||
}
|
||||
TxOrdering::BIP69Lexicographic => {
|
||||
TxOrdering::Bip69Lexicographic => {
|
||||
tx.input.sort_unstable_by_key(|txin| {
|
||||
(txin.previous_output.txid, txin.previous_output.vout)
|
||||
});
|
||||
@@ -665,16 +667,16 @@ impl Default for Version {
|
||||
///
|
||||
/// Has a default value of `0xFFFFFFFD`
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub(crate) enum RBFValue {
|
||||
pub(crate) enum RbfValue {
|
||||
Default,
|
||||
Value(u32),
|
||||
}
|
||||
|
||||
impl RBFValue {
|
||||
impl RbfValue {
|
||||
pub(crate) fn get_value(&self) -> u32 {
|
||||
match self {
|
||||
RBFValue::Default => 0xFFFFFFFD,
|
||||
RBFValue::Value(v) => *v,
|
||||
RbfValue::Default => 0xFFFFFFFD,
|
||||
RbfValue::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -759,7 +761,7 @@ mod test {
|
||||
let original_tx = ordering_test_tx!();
|
||||
let mut tx = original_tx;
|
||||
|
||||
TxOrdering::BIP69Lexicographic.sort_tx(&mut tx);
|
||||
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
|
||||
|
||||
assert_eq!(
|
||||
tx.input[0].previous_output,
|
||||
|
||||
@@ -201,31 +201,31 @@ mod test {
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_msb_set() {
|
||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_lt_csv() {
|
||||
let result = check_nsequence_rbf(4000, 5000);
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_different_unit() {
|
||||
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_mask() {
|
||||
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_blocks() {
|
||||
let result = check_nsequence_rbf(10_000, 5000);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -234,25 +234,25 @@ mod test {
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
||||
);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_lt_cltv() {
|
||||
let result = check_nlocktime(4000, 5000);
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_different_unit() {
|
||||
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_same_unit_blocks() {
|
||||
let result = check_nlocktime(10_000, 5000);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -261,6 +261,6 @@ mod test {
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
||||
);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "bdk-testutils-macros"
|
||||
version = "0.4.0"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk-testutils-macros"
|
||||
description = "Supporting testing macros for `bdk`"
|
||||
keywords = ["bdk"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
name = "testutils_macros"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0", features = ["parsing", "full"] }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
||||
[features]
|
||||
debug = ["syn/extra-traits"]
|
||||
@@ -1,552 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
#[macro_use]
|
||||
extern crate quote;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{parse, parse2, Ident, ReturnType};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let root_ident = if !attr.is_empty() {
|
||||
match parse::<syn::ExprPath>(attr) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => {
|
||||
let error_string = e.to_string();
|
||||
return (quote! {
|
||||
compile_error!("Invalid crate path: {:?}", #error_string)
|
||||
})
|
||||
.into();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parse2::<syn::ExprPath>(quote! { bdk }).unwrap()
|
||||
};
|
||||
|
||||
match parse::<syn::ItemFn>(item) {
|
||||
Err(_) => (quote! {
|
||||
compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s")
|
||||
})
|
||||
.into(),
|
||||
Ok(parsed) => {
|
||||
let parsed_sig_ident = parsed.sig.ident.clone();
|
||||
let mod_name = Ident::new(
|
||||
&format!("generated_tests_{}", parsed_sig_ident.to_string()),
|
||||
parsed.span(),
|
||||
);
|
||||
|
||||
let return_type = match parsed.sig.output {
|
||||
ReturnType::Type(_, ref t) => t.clone(),
|
||||
ReturnType::Default => {
|
||||
return (quote! {
|
||||
compile_error!("The tagged function must return a type that impl `Blockchain`")
|
||||
}).into();
|
||||
}
|
||||
};
|
||||
|
||||
let output = quote! {
|
||||
|
||||
#parsed
|
||||
|
||||
mod #mod_name {
|
||||
use bitcoin::Network;
|
||||
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use testutils::{TestClient, serial};
|
||||
|
||||
use #root_ident::blockchain::{Blockchain, noop_progress};
|
||||
use #root_ident::descriptor::ExtendedDescriptor;
|
||||
use #root_ident::database::MemoryDatabase;
|
||||
use #root_ident::types::KeychainKind;
|
||||
use #root_ident::{Wallet, TxBuilder, FeeRate};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_blockchain() -> #return_type {
|
||||
#parsed_sig_ident()
|
||||
}
|
||||
|
||||
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<#return_type, MemoryDatabase> {
|
||||
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
|
||||
}
|
||||
|
||||
fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option<String>), TestClient) {
|
||||
let descriptors = testutils! {
|
||||
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
||||
};
|
||||
|
||||
let test_client = TestClient::new();
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
|
||||
(wallet, descriptors, test_client)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_simple() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let tx = testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
};
|
||||
println!("{:?}", tx);
|
||||
let txid = test_client.receive(tx);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid);
|
||||
assert_eq!(list_tx_item.received, 50_000);
|
||||
assert_eq!(list_tx_item.sent, 0);
|
||||
assert_eq!(list_tx_item.height, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_stop_gap_20() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 5) => 50_000 )
|
||||
});
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 25) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_before_and_after_receive() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_multiple_outputs_same_tx() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 105_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid);
|
||||
assert_eq!(list_tx_item.received, 105_000);
|
||||
assert_eq!(list_tx_item.sent, 0);
|
||||
assert_eq!(list_tx_item.height, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_multi() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 5) => 25_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_address_reuse() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 25_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_rbf_replaced() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid);
|
||||
assert_eq!(list_tx_item.received, 50_000);
|
||||
assert_eq!(list_tx_item.sent, 0);
|
||||
assert_eq!(list_tx_item.height, None);
|
||||
|
||||
let new_txid = test_client.bump_fee(&txid);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, new_txid);
|
||||
assert_eq!(list_tx_item.received, 50_000);
|
||||
assert_eq!(list_tx_item.sent, 0);
|
||||
assert_eq!(list_tx_item.height, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_reorg_block() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid);
|
||||
assert!(list_tx_item.height.is_some());
|
||||
|
||||
// Invalidate 1 block
|
||||
test_client.invalidate(1);
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid);
|
||||
assert_eq!(list_tx_item.height, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_after_send() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
println!("{}", descriptors.0);
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||
wallet.broadcast(tx).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
||||
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_outgoing_from_scratch() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
let received_txid = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
||||
|
||||
// empty wallet
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
|
||||
let received = tx_map.get(&received_txid).unwrap();
|
||||
assert_eq!(received.received, 50_000);
|
||||
assert_eq!(received.sent, 0);
|
||||
|
||||
let sent = tx_map.get(&sent_txid).unwrap();
|
||||
assert_eq!(sent.received, details.received);
|
||||
assert_eq!(sent.sent, details.sent);
|
||||
assert_eq!(sent.fees, details.fees);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_long_change_chain() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..5 {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 5_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
total_sent += 5_000 + details.fees;
|
||||
}
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
|
||||
|
||||
// empty wallet
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
|
||||
|
||||
assert!(new_details.fees > details.fees);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_remove_change() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
assert_eq!(new_details.received, 0);
|
||||
|
||||
assert!(new_details.fees > details.fees);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_add_input() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
|
||||
assert_eq!(details.received, 1_000 - details.fees);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000);
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_bump_fee_add_input_no_change() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
|
||||
assert_eq!(details.received, 1_000 - details.fees);
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
println!("{:#?}", new_details);
|
||||
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000);
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
assert_eq!(new_details.received, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_sync_receive_coinbase() {
|
||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||
let wallet_addr = wallet.get_new_address().unwrap();
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
|
||||
test_client.generate(1, Some(wallet_addr));
|
||||
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
assert!(wallet.get_balance().unwrap() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
2
testutils/.gitignore
vendored
2
testutils/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
target/
|
||||
Cargo.lock
|
||||
@@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "bdk-testutils"
|
||||
version = "0.4.0"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk-testutils"
|
||||
description = "Supporting testing utilities for `bdk`"
|
||||
keywords = ["bdk"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
name = "testutils"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serial_test = "0.4"
|
||||
bitcoin = "0.26"
|
||||
bitcoincore-rpc = "0.13"
|
||||
miniscript = "5.1"
|
||||
electrum-client = "0.6.0"
|
||||
@@ -1,564 +0,0 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
pub use serial_test::serial;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||
use bitcoin::hashes::sha256d;
|
||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||
use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
|
||||
|
||||
use miniscript::descriptor::DescriptorPublicKey;
|
||||
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
||||
|
||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||
|
||||
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
||||
|
||||
// TODO: we currently only support env vars, we could also parse a toml file
|
||||
fn get_auth() -> Auth {
|
||||
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
|
||||
Ok("USER_PASS") => Auth::UserPass(
|
||||
env::var("BDK_RPC_USER").unwrap(),
|
||||
env::var("BDK_RPC_PASS").unwrap(),
|
||||
),
|
||||
_ => Auth::CookieFile(PathBuf::from(
|
||||
env::var("BDK_RPC_COOKIEFILE")
|
||||
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_electrum_url() -> String {
|
||||
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
|
||||
}
|
||||
|
||||
pub struct TestClient {
|
||||
client: RpcClient,
|
||||
electrum: ElectrumClient,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingOutput {
|
||||
pub value: u64,
|
||||
pub to_address: String,
|
||||
}
|
||||
|
||||
impl TestIncomingOutput {
|
||||
pub fn new(value: u64, to_address: Address) -> Self {
|
||||
Self {
|
||||
value,
|
||||
to_address: to_address.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingTx {
|
||||
pub output: Vec<TestIncomingOutput>,
|
||||
pub min_confirmations: Option<u64>,
|
||||
pub locktime: Option<i64>,
|
||||
pub replaceable: Option<bool>,
|
||||
}
|
||||
|
||||
impl TestIncomingTx {
|
||||
pub fn new(
|
||||
output: Vec<TestIncomingOutput>,
|
||||
min_confirmations: Option<u64>,
|
||||
locktime: Option<i64>,
|
||||
replaceable: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output,
|
||||
min_confirmations,
|
||||
locktime,
|
||||
replaceable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
||||
self.output.push(output);
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait TranslateDescriptor {
|
||||
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
|
||||
fn derive_translated<C: Verification>(
|
||||
&self,
|
||||
secp: &Secp256k1<C>,
|
||||
index: u32,
|
||||
) -> Descriptor<PublicKey>;
|
||||
}
|
||||
|
||||
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
|
||||
fn derive_translated<C: Verification>(
|
||||
&self,
|
||||
secp: &Secp256k1<C>,
|
||||
index: u32,
|
||||
) -> Descriptor<PublicKey> {
|
||||
let translate = |key: &DescriptorPublicKey| -> PublicKey {
|
||||
match key {
|
||||
DescriptorPublicKey::XPub(xpub) => {
|
||||
xpub.xkey
|
||||
.derive_pub(secp, &xpub.derivation_path)
|
||||
.expect("hardened derivation steps")
|
||||
.public_key
|
||||
}
|
||||
DescriptorPublicKey::SinglePub(key) => key.key,
|
||||
}
|
||||
};
|
||||
|
||||
self.derive(index)
|
||||
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! testutils {
|
||||
( @external $descriptors:expr, $child:expr ) => ({
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::TranslateDescriptor;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @internal $descriptors:expr, $child:expr ) => ({
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||
|
||||
use $crate::TranslateDescriptor;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||
});
|
||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||
|
||||
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
|
||||
let mut outs = Vec::new();
|
||||
$( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
|
||||
|
||||
let mut locktime = None::<i64>;
|
||||
$( locktime = Some($locktime); )*
|
||||
|
||||
let mut min_confirmations = None::<u64>;
|
||||
$( min_confirmations = Some($confirmations); )*
|
||||
|
||||
let mut replaceable = None::<bool>;
|
||||
$( replaceable = Some($replaceable); )*
|
||||
|
||||
testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
|
||||
});
|
||||
|
||||
( @literal $key:expr ) => ({
|
||||
let key = $key.to_string();
|
||||
(key, None::<String>, None::<String>)
|
||||
});
|
||||
( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({
|
||||
use rand::Rng;
|
||||
|
||||
let mut seed = [0u8; 32];
|
||||
rand::thread_rng().fill(&mut seed[..]);
|
||||
|
||||
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
|
||||
bitcoin::Network::Testnet,
|
||||
&seed,
|
||||
);
|
||||
|
||||
let mut external_path = None::<String>;
|
||||
$( external_path = Some($external_path.to_string()); )*
|
||||
|
||||
let mut internal_path = None::<String>;
|
||||
$( internal_path = Some($internal_path.to_string()); )*
|
||||
|
||||
(key.unwrap().to_string(), external_path, internal_path)
|
||||
});
|
||||
( @generate_wif ) => ({
|
||||
use rand::Rng;
|
||||
|
||||
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
|
||||
rand::thread_rng().fill(&mut key[..]);
|
||||
|
||||
(bitcoin::PrivateKey {
|
||||
compressed: true,
|
||||
network: bitcoin::Network::Testnet,
|
||||
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
|
||||
}.to_string(), None::<String>, None::<String>)
|
||||
});
|
||||
|
||||
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
|
||||
let mut map = std::collections::HashMap::new();
|
||||
$(
|
||||
let alias: &str = $alias;
|
||||
map.insert(alias, testutils!( $($key_type)* ));
|
||||
)+
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
use miniscript::TranslatePk;
|
||||
|
||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
||||
$(
|
||||
keys = testutils!{ @keys $( $keys )* };
|
||||
)*
|
||||
|
||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
|
||||
});
|
||||
let external = external.to_string();
|
||||
|
||||
let mut internal = None::<String>;
|
||||
$(
|
||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||
|
||||
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
k.clone()
|
||||
}
|
||||
}, |kh| {
|
||||
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
||||
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||
} else {
|
||||
kh.clone()
|
||||
}
|
||||
});
|
||||
internal = Some(string_internal.to_string());
|
||||
)*
|
||||
|
||||
(external, internal)
|
||||
})
|
||||
}
|
||||
|
||||
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
|
||||
where
|
||||
F: FnMut() -> Option<T>,
|
||||
{
|
||||
let mut delay = Duration::from_millis(64);
|
||||
loop {
|
||||
match poll() {
|
||||
Some(data) => break data,
|
||||
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
|
||||
None => {}
|
||||
}
|
||||
|
||||
std::thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
impl TestClient {
|
||||
pub fn new() -> Self {
|
||||
let url = env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
|
||||
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
|
||||
let client =
|
||||
RpcClient::new(format!("http://{}/wallet/{}", url, wallet), get_auth()).unwrap();
|
||||
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
|
||||
|
||||
TestClient { client, electrum }
|
||||
}
|
||||
|
||||
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
|
||||
// wait for electrs to index the tx
|
||||
exponential_backoff_poll(|| {
|
||||
trace!("wait_for_tx {}", txid);
|
||||
|
||||
self.electrum
|
||||
.script_get_history(monitor_script)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.position(|entry| entry.tx_hash == txid)
|
||||
});
|
||||
}
|
||||
|
||||
fn wait_for_block(&mut self, min_height: usize) {
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
loop {
|
||||
let header = exponential_backoff_poll(|| {
|
||||
self.electrum.ping().unwrap();
|
||||
self.electrum.block_headers_pop().unwrap()
|
||||
});
|
||||
if header.height >= min_height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
|
||||
assert!(
|
||||
!meta_tx.output.is_empty(),
|
||||
"can't create a transaction with no outputs"
|
||||
);
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
let mut required_balance = 0;
|
||||
for out in &meta_tx.output {
|
||||
required_balance += out.value;
|
||||
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
||||
}
|
||||
|
||||
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
||||
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
||||
}
|
||||
|
||||
// FIXME: core can't create a tx with two outputs to the same address
|
||||
let tx = self
|
||||
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
|
||||
.unwrap();
|
||||
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
||||
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
||||
|
||||
if let Some(true) = meta_tx.replaceable {
|
||||
// for some reason core doesn't set this field right
|
||||
for input in &mut tx.input {
|
||||
input.sequence = 0xFFFFFFFD;
|
||||
}
|
||||
}
|
||||
|
||||
let tx = self
|
||||
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
|
||||
.unwrap();
|
||||
|
||||
// broadcast through electrum so that it caches the tx immediately
|
||||
let txid = self
|
||||
.electrum
|
||||
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
||||
.unwrap();
|
||||
|
||||
if let Some(num) = meta_tx.min_confirmations {
|
||||
self.generate(num, None);
|
||||
}
|
||||
|
||||
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
self.wait_for_tx(txid, &monitor_script);
|
||||
|
||||
debug!("Sent tx: {}", txid);
|
||||
|
||||
txid
|
||||
}
|
||||
|
||||
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
|
||||
let tx = self.get_raw_transaction_info(txid, None).unwrap();
|
||||
assert!(
|
||||
tx.confirmations.is_none(),
|
||||
"Can't bump tx {} because it's already confirmed",
|
||||
txid
|
||||
);
|
||||
|
||||
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
|
||||
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
|
||||
|
||||
let monitor_script =
|
||||
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
|
||||
self.wait_for_tx(new_txid, &monitor_script);
|
||||
|
||||
debug!("Bumped {}, new txid {}", txid, new_txid);
|
||||
|
||||
new_txid
|
||||
}
|
||||
|
||||
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
|
||||
use bitcoin::blockdata::block::{Block, BlockHeader};
|
||||
use bitcoin::blockdata::script::Builder;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
||||
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
||||
|
||||
let block_template: serde_json::Value = self
|
||||
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
||||
.unwrap();
|
||||
trace!("getblocktemplate: {:#?}", block_template);
|
||||
|
||||
let header = BlockHeader {
|
||||
version: block_template["version"].as_i64().unwrap() as i32,
|
||||
prev_blockhash: BlockHash::from_hex(
|
||||
block_template["previousblockhash"].as_str().unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
merkle_root: TxMerkleNode::default(),
|
||||
time: block_template["curtime"].as_u64().unwrap() as u32,
|
||||
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
||||
nonce: 0,
|
||||
};
|
||||
debug!("header: {:#?}", header);
|
||||
|
||||
let height = block_template["height"].as_u64().unwrap() as i64;
|
||||
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
|
||||
// burn block subsidy and fees, not a big deal
|
||||
let mut coinbase_tx = Transaction {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
script_sig: Builder::new().push_int(height).into_script(),
|
||||
sequence: 0xFFFFFFFF,
|
||||
witness: vec![witness_reserved_value],
|
||||
}],
|
||||
output: vec![],
|
||||
};
|
||||
|
||||
let mut txdata = vec![coinbase_tx.clone()];
|
||||
txdata.extend_from_slice(&txs);
|
||||
|
||||
let mut block = Block { header, txdata };
|
||||
|
||||
let witness_root = block.witness_root();
|
||||
let witness_commitment =
|
||||
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
|
||||
|
||||
// now update and replace the coinbase tx
|
||||
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
||||
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
||||
|
||||
coinbase_tx.output.push(TxOut {
|
||||
value: 0,
|
||||
script_pubkey: coinbase_witness_commitment_script.into(),
|
||||
});
|
||||
block.txdata[0] = coinbase_tx;
|
||||
|
||||
// set merkle root
|
||||
let merkle_root = block.merkle_root();
|
||||
block.header.merkle_root = merkle_root;
|
||||
|
||||
assert!(block.check_merkle_root());
|
||||
assert!(block.check_witness_commitment());
|
||||
|
||||
// now do PoW :)
|
||||
let target = block.header.target();
|
||||
while block.header.validate_pow(&target).is_err() {
|
||||
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
|
||||
}
|
||||
|
||||
let block_hex: String = serialize(&block).to_hex();
|
||||
debug!("generated block hex: {}", block_hex);
|
||||
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
let submit_result: serde_json::Value =
|
||||
self.call("submitblock", &[block_hex.into()]).unwrap();
|
||||
debug!("submitblock: {:?}", submit_result);
|
||||
assert!(
|
||||
submit_result.is_null(),
|
||||
"submitblock error: {:?}",
|
||||
submit_result.as_str()
|
||||
);
|
||||
|
||||
self.wait_for_block(height as usize);
|
||||
|
||||
block.header.block_hash().to_hex()
|
||||
}
|
||||
|
||||
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
|
||||
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
|
||||
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
|
||||
let best_hash = hashes.last().unwrap();
|
||||
let height = self.get_block_info(best_hash).unwrap().height;
|
||||
|
||||
self.wait_for_block(height);
|
||||
|
||||
debug!("Generated blocks to new height {}", height);
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, num_blocks: u64) {
|
||||
self.electrum.block_headers_subscribe().unwrap();
|
||||
|
||||
let best_hash = self.get_best_block_hash().unwrap();
|
||||
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
||||
|
||||
let mut to_invalidate = best_hash;
|
||||
for i in 1..=num_blocks {
|
||||
trace!(
|
||||
"Invalidating block {}/{} ({})",
|
||||
i,
|
||||
num_blocks,
|
||||
to_invalidate
|
||||
);
|
||||
|
||||
self.invalidate_block(&to_invalidate).unwrap();
|
||||
to_invalidate = self.get_best_block_hash().unwrap();
|
||||
}
|
||||
|
||||
self.wait_for_block(initial_height - num_blocks as usize);
|
||||
|
||||
debug!(
|
||||
"Invalidated {} blocks to new height of {}",
|
||||
num_blocks,
|
||||
initial_height - num_blocks as usize
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reorg(&mut self, num_blocks: u64) {
|
||||
self.invalidate(num_blocks);
|
||||
self.generate(num_blocks, None);
|
||||
}
|
||||
|
||||
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
|
||||
Address::from_str(
|
||||
&self
|
||||
.get_new_address(None, address_type)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestClient {
|
||||
type Target = RpcClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user