Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5d3a4d31a | ||
|
|
d03d3c0dbd | ||
|
|
9aba3196ff | ||
|
|
c8593ecf70 | ||
|
|
cbec0b0bcf | ||
|
|
c54e1e9652 | ||
|
|
5cdc5fb58a | ||
|
|
27cd9bbcd6 | ||
|
|
f37e735b43 | ||
|
|
adceafa40c | ||
|
|
2b0c4f0817 | ||
|
|
e9428433a0 | ||
|
|
63592f169f | ||
|
|
27600f4a11 | ||
|
|
77eae76459 | ||
|
|
ad69702aa3 | ||
|
|
fd254536d3 | ||
|
|
c4d5dd14fa | ||
|
|
13bed2667a | ||
|
|
2db24fb8c5 | ||
|
|
d2d37fc06d | ||
|
|
2986fce7c6 | ||
|
|
1dc648508c | ||
|
|
474620e6a5 | ||
|
|
a5919f4ab0 | ||
|
|
7e986fd904 | ||
|
|
77379e9262 | ||
|
|
ea699a6ec1 | ||
|
|
81c1ccb185 | ||
|
|
4f4802b0f3 | ||
|
|
bab9d99a00 | ||
|
|
22f4db0de1 | ||
|
|
a6ce75fa2d | ||
|
|
7597645ed6 | ||
|
|
618e0d3700 | ||
|
|
44d0e8d07c | ||
|
|
7a9b691f68 | ||
|
|
4e813e8869 | ||
|
|
53409ef3ae | ||
|
|
f8a6e1c3f4 | ||
|
|
c1077b95cf | ||
|
|
fa5103b0eb | ||
|
|
e5d4994329 | ||
|
|
d1658a2eda | ||
|
|
879e5cf319 | ||
|
|
928f9c6112 | ||
|
|
814ab4c855 | ||
|
|
58cf46050f | ||
|
|
b6beef77e7 | ||
|
|
7ed0676e44 | ||
|
|
595e1bdbe1 | ||
|
|
7555d3b430 | ||
|
|
fbdee52f2f | ||
|
|
50597fd73f | ||
|
|
975905c8ea | ||
|
|
a67aca32c0 | ||
|
|
7873dd5e40 | ||
|
|
a186d82f9a | ||
|
|
7109f7d9b4 | ||
|
|
f52fda4b4b | ||
|
|
a6be470fe4 | ||
|
|
8e41c4587d | ||
|
|
2ecae348ea | ||
|
|
f4ecfa0d49 | ||
|
|
696647b893 | ||
|
|
18dcda844f | ||
|
|
6394c3e209 | ||
|
|
42adad7dbd | ||
|
|
4498e0f7f8 |
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
|
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
||||||
|
|
||||||
- id: coverage
|
- id: coverage
|
||||||
name: Generate coverage
|
name: Generate coverage
|
||||||
|
|||||||
51
.github/workflows/cont_integration.yml
vendored
51
.github/workflows/cont_integration.yml
vendored
@@ -16,13 +16,16 @@ jobs:
|
|||||||
- default
|
- default
|
||||||
- minimal
|
- minimal
|
||||||
- all-keys
|
- all-keys
|
||||||
- minimal,esplora
|
- minimal,use-esplora-ureq
|
||||||
- key-value-db
|
- key-value-db
|
||||||
- electrum
|
- electrum
|
||||||
- compact_filters
|
- compact_filters
|
||||||
- esplora,key-value-db,electrum
|
- esplora,ureq,key-value-db,electrum
|
||||||
- compiler
|
- compiler
|
||||||
- rpc
|
- rpc
|
||||||
|
- verify
|
||||||
|
- async-interface
|
||||||
|
- use-esplora-reqwest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -76,28 +79,14 @@ jobs:
|
|||||||
|
|
||||||
test-blockchains:
|
test-blockchains:
|
||||||
name: Test ${{ matrix.blockchain.name }}
|
name: Test ${{ matrix.blockchain.name }}
|
||||||
runs-on: ubuntu-16.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
blockchain:
|
blockchain:
|
||||||
- name: electrum
|
- 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
|
- name: rpc
|
||||||
container: bitcoindevkit/electrs:0.4.0
|
- name: esplora
|
||||||
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
|
|
||||||
container: ${{ matrix.blockchain.container }}
|
|
||||||
env:
|
|
||||||
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -106,25 +95,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
|
~/.cargo/bitcoin
|
||||||
|
~/.cargo/electrs
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||||
- name: get pkg-config # running esplora tests seems to need this
|
- name: Setup rust toolchain
|
||||||
run: apt update && apt install -y --fix-missing pkg-config libssl-dev
|
uses: actions-rs/toolchain@v1
|
||||||
- name: Install rustup
|
with:
|
||||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
toolchain: stable
|
||||||
- name: Set default toolchain
|
override: true
|
||||||
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
|
- name: Test
|
||||||
run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
run: cargo test --features test-${{ matrix.blockchain.name }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||||
|
|
||||||
check-wasm:
|
check-wasm:
|
||||||
name: Check WASM
|
name: Check WASM
|
||||||
@@ -157,7 +139,8 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --target wasm32-unknown-unknown --features esplora --no-default-features
|
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
|
||||||
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: Rust fmt
|
name: Rust fmt
|
||||||
|
|||||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
|
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -6,9 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.10.0] - [v0.9.0]
|
||||||
|
|
||||||
|
- Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`)
|
||||||
|
- Removed hard dependency on `tokio`.
|
||||||
|
|
||||||
### Wallet
|
### Wallet
|
||||||
#### Added
|
|
||||||
- Bitcoin core RPC added as blockchain backend
|
- Removed and replaced `set_single_recipient` with more general `drain_to` and replaced `maintain_single_recipient` with `allow_shrinking`.
|
||||||
|
|
||||||
|
### Blockchain
|
||||||
|
|
||||||
|
- Removed `stop_gap` from `Blockchain` trait and added it to only `ElectrumBlockchain` and `EsploraBlockchain` structs.
|
||||||
|
- Added a `ureq` backend for use when not using feature `async-interface` or target WASM. `ureq` is a blocking HTTP client.
|
||||||
|
|
||||||
|
## [v0.9.0] - [v0.8.0]
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
|
||||||
|
- Added Bitcoin core RPC added as blockchain backend
|
||||||
|
- Added a `verify` feature that can be enable to verify the unconfirmed txs we download against the consensus rules
|
||||||
|
|
||||||
## [v0.8.0] - [v0.7.0]
|
## [v0.8.0] - [v0.7.0]
|
||||||
|
|
||||||
@@ -343,7 +360,7 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
- Use `MemoryDatabase` in the compiler example
|
- Use `MemoryDatabase` in the compiler example
|
||||||
- Make the REPL return JSON
|
- Make the REPL return JSON
|
||||||
|
|
||||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...HEAD
|
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.9.0...HEAD
|
||||||
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
[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.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.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
||||||
@@ -353,3 +370,5 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
|
[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.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
|
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0
|
||||||
|
[v0.9.0]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...v0.9.0
|
||||||
|
[v0.10.0]: https://github.com/bitcoindevkit/bdk/compare/v0.9.0...v0.10.0
|
||||||
|
|||||||
50
Cargo.toml
50
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.8.1-dev"
|
version = "0.10.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
|||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk-macros = "^0.4"
|
bdk-macros = "0.5"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
miniscript = "5.1"
|
miniscript = "5.1"
|
||||||
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
|
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
|
||||||
@@ -24,6 +24,7 @@ rand = "^0.7"
|
|||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.7", optional = true }
|
electrum-client = { version = "0.7", optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||||
|
ureq = { version = "2.1", default-features = false, features = ["json"], optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
async-trait = { version = "0.1", optional = true }
|
async-trait = { version = "0.1", optional = true }
|
||||||
rocksdb = { version = "0.14", optional = true }
|
rocksdb = { version = "0.14", optional = true }
|
||||||
@@ -31,14 +32,11 @@ cc = { version = ">=1.0.64", optional = true }
|
|||||||
socks = { version = "0.3", optional = true }
|
socks = { version = "0.3", optional = true }
|
||||||
lazy_static = { version = "1.4", optional = true }
|
lazy_static = { version = "1.4", optional = true }
|
||||||
tiny-bip39 = { version = "^0.8", optional = true }
|
tiny-bip39 = { version = "^0.8", optional = true }
|
||||||
|
zeroize = { version = "<1.4.0", optional = true }
|
||||||
|
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||||
|
|
||||||
# Needed by bdk_blockchain_tests macro
|
# Needed by bdk_blockchain_tests macro
|
||||||
bitcoincore-rpc = { version = "0.13", optional = true }
|
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"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -48,30 +46,48 @@ rand = { version = "^0.7", features = ["wasm-bindgen"] }
|
|||||||
[features]
|
[features]
|
||||||
minimal = []
|
minimal = []
|
||||||
compiler = ["miniscript/compiler"]
|
compiler = ["miniscript/compiler"]
|
||||||
|
verify = ["bitcoinconsensus"]
|
||||||
default = ["key-value-db", "electrum"]
|
default = ["key-value-db", "electrum"]
|
||||||
electrum = ["electrum-client"]
|
|
||||||
esplora = ["reqwest", "futures"]
|
|
||||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||||
key-value-db = ["sled"]
|
key-value-db = ["sled"]
|
||||||
async-interface = ["async-trait"]
|
|
||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
keys-bip39 = ["tiny-bip39"]
|
keys-bip39 = ["tiny-bip39", "zeroize"]
|
||||||
rpc = ["bitcoincore-rpc"]
|
rpc = ["bitcoincore-rpc"]
|
||||||
|
|
||||||
|
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||||
|
# blocking except for the `EsploraBlockchain` which can be either async or
|
||||||
|
# blocking, depending on the HTTP client in use.
|
||||||
|
#
|
||||||
|
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
|
||||||
|
# access to the asynchronous method implementations. Then, if Esplora is wanted,
|
||||||
|
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
|
||||||
|
# - Users wanting blocking HTTP calls can use any of the other blockchain
|
||||||
|
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
|
||||||
|
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
|
||||||
|
#
|
||||||
|
# WARNING: Please take care with the features below, various combinations will
|
||||||
|
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
||||||
|
async-interface = ["async-trait"]
|
||||||
|
electrum = ["electrum-client"]
|
||||||
|
# MUST ALSO USE `--no-default-features`.
|
||||||
|
use-esplora-reqwest = ["async-interface", "esplora", "reqwest", "futures"]
|
||||||
|
use-esplora-ureq = ["esplora", "ureq"]
|
||||||
|
# Typical configurations will not need to use `esplora` feature directly.
|
||||||
|
esplora = []
|
||||||
|
|
||||||
|
|
||||||
# Debug/Test features
|
# Debug/Test features
|
||||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||||
test-electrum = ["electrum"]
|
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||||
test-rpc = ["rpc"]
|
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||||
test-esplora = ["esplora"]
|
test-esplora = ["esplora", "ureq", "electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
|
||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
serial_test = "0.4"
|
electrsd = { version= "0.8", features = ["trigger", "bitcoind_0_21_1"] }
|
||||||
bitcoind = "0.10.0"
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
@@ -87,6 +103,6 @@ required-features = ["compiler"]
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
|
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "all-keys", "verify"]
|
||||||
# defines the configuration attribute `docsrs`
|
# defines the configuration attribute `docsrs`
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -151,6 +151,25 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit testing
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration testing
|
||||||
|
|
||||||
|
Integration testing require testing features, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo test --features test-electrum
|
||||||
|
```
|
||||||
|
|
||||||
|
The other options are `test-esplora` or `test-rpc`.
|
||||||
|
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under either of
|
Licensed under either of
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk-macros"
|
name = "bdk-macros"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
|
|||||||
@@ -121,26 +121,3 @@ pub fn maybe_await(expr: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
quoted.into()
|
quoted.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Awaits if target_arch is "wasm32", uses `tokio::Runtime::block_on()` otherwise
|
|
||||||
///
|
|
||||||
/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build on non-wasm32 platforms.
|
|
||||||
#[proc_macro]
|
|
||||||
pub fn await_or_block(expr: TokenStream) -> TokenStream {
|
|
||||||
let expr: proc_macro2::TokenStream = expr.into();
|
|
||||||
let quoted = quote! {
|
|
||||||
{
|
|
||||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
|
|
||||||
{
|
|
||||||
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(#expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
|
|
||||||
{
|
|
||||||
#expr.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
quoted.into()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
//! )?;
|
//! )?;
|
||||||
//! # }
|
//! # }
|
||||||
//!
|
//!
|
||||||
//! # #[cfg(feature = "esplora")]
|
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
|
||||||
//! # {
|
//! # {
|
||||||
//! let esplora_blockchain = EsploraBlockchain::new("...", None);
|
//! let esplora_blockchain = EsploraBlockchain::new("...", 20);
|
||||||
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
|
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
|
||||||
//! "...",
|
//! "...",
|
||||||
//! None,
|
//! None,
|
||||||
@@ -60,6 +60,8 @@
|
|||||||
//! # use bdk::blockchain::*;
|
//! # use bdk::blockchain::*;
|
||||||
//! # use bdk::database::MemoryDatabase;
|
//! # use bdk::database::MemoryDatabase;
|
||||||
//! # use bdk::Wallet;
|
//! # use bdk::Wallet;
|
||||||
|
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
|
||||||
|
//! # {
|
||||||
//! let config = serde_json::from_str("...")?;
|
//! let config = serde_json::from_str("...")?;
|
||||||
//! let blockchain = AnyBlockchain::from_config(&config)?;
|
//! let blockchain = AnyBlockchain::from_config(&config)?;
|
||||||
//! let wallet = Wallet::new(
|
//! let wallet = Wallet::new(
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
//! MemoryDatabase::default(),
|
//! MemoryDatabase::default(),
|
||||||
//! blockchain,
|
//! blockchain,
|
||||||
//! )?;
|
//! )?;
|
||||||
|
//! # }
|
||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
@@ -94,6 +97,8 @@ macro_rules! impl_inner_method {
|
|||||||
AnyBlockchain::Esplora(inner) => inner.$name( $($args, )* ),
|
AnyBlockchain::Esplora(inner) => inner.$name( $($args, )* ),
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
AnyBlockchain::CompactFilters(inner) => inner.$name( $($args, )* ),
|
AnyBlockchain::CompactFilters(inner) => inner.$name( $($args, )* ),
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
AnyBlockchain::Rpc(inner) => inner.$name( $($args, )* ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +121,10 @@ pub enum AnyBlockchain {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||||
/// Compact filters client
|
/// Compact filters client
|
||||||
CompactFilters(compact_filters::CompactFiltersBlockchain),
|
CompactFilters(compact_filters::CompactFiltersBlockchain),
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||||
|
/// RPC client
|
||||||
|
Rpc(rpc::RpcBlockchain),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
@@ -126,31 +135,17 @@ impl Blockchain for AnyBlockchain {
|
|||||||
|
|
||||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(impl_inner_method!(
|
maybe_await!(impl_inner_method!(self, setup, database, progress_update))
|
||||||
self,
|
|
||||||
setup,
|
|
||||||
stop_gap,
|
|
||||||
database,
|
|
||||||
progress_update
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(impl_inner_method!(
|
maybe_await!(impl_inner_method!(self, sync, database, progress_update))
|
||||||
self,
|
|
||||||
sync,
|
|
||||||
stop_gap,
|
|
||||||
database,
|
|
||||||
progress_update
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
@@ -171,6 +166,7 @@ impl Blockchain for AnyBlockchain {
|
|||||||
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
|
||||||
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
|
||||||
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||||
|
impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
|
||||||
|
|
||||||
/// Type that can contain any of the blockchain configurations defined by the library
|
/// Type that can contain any of the blockchain configurations defined by the library
|
||||||
///
|
///
|
||||||
@@ -188,7 +184,8 @@ impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilt
|
|||||||
/// r#"{
|
/// r#"{
|
||||||
/// "type" : "electrum",
|
/// "type" : "electrum",
|
||||||
/// "url" : "ssl://electrum.blockstream.info:50002",
|
/// "url" : "ssl://electrum.blockstream.info:50002",
|
||||||
/// "retry": 2
|
/// "retry": 2,
|
||||||
|
/// "stop_gap": 20
|
||||||
/// }"#,
|
/// }"#,
|
||||||
/// )
|
/// )
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
@@ -198,7 +195,8 @@ impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilt
|
|||||||
/// url: "ssl://electrum.blockstream.info:50002".into(),
|
/// url: "ssl://electrum.blockstream.info:50002".into(),
|
||||||
/// retry: 2,
|
/// retry: 2,
|
||||||
/// socks5: None,
|
/// socks5: None,
|
||||||
/// timeout: None
|
/// timeout: None,
|
||||||
|
/// stop_gap: 20,
|
||||||
/// })
|
/// })
|
||||||
/// );
|
/// );
|
||||||
/// # }
|
/// # }
|
||||||
@@ -218,6 +216,10 @@ pub enum AnyBlockchainConfig {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||||
/// Compact filters client
|
/// Compact filters client
|
||||||
CompactFilters(compact_filters::CompactFiltersBlockchainConfig),
|
CompactFilters(compact_filters::CompactFiltersBlockchainConfig),
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||||
|
/// RPC client configuration
|
||||||
|
Rpc(rpc::RpcConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurableBlockchain for AnyBlockchain {
|
impl ConfigurableBlockchain for AnyBlockchain {
|
||||||
@@ -237,6 +239,10 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
|||||||
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
|
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
|
||||||
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
|
||||||
),
|
),
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
AnyBlockchainConfig::Rpc(inner) => {
|
||||||
|
AnyBlockchain::Rpc(rpc::RpcBlockchain::from_config(inner)?)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,3 +250,4 @@ impl ConfigurableBlockchain for AnyBlockchain {
|
|||||||
impl_from!(electrum::ElectrumBlockchainConfig, AnyBlockchainConfig, Electrum, #[cfg(feature = "electrum")]);
|
impl_from!(electrum::ElectrumBlockchainConfig, AnyBlockchainConfig, Electrum, #[cfg(feature = "electrum")]);
|
||||||
impl_from!(esplora::EsploraBlockchainConfig, AnyBlockchainConfig, Esplora, #[cfg(feature = "esplora")]);
|
impl_from!(esplora::EsploraBlockchainConfig, AnyBlockchainConfig, Esplora, #[cfg(feature = "esplora")]);
|
||||||
impl_from!(compact_filters::CompactFiltersBlockchainConfig, AnyBlockchainConfig, CompactFilters, #[cfg(feature = "compact_filters")]);
|
impl_from!(compact_filters::CompactFiltersBlockchainConfig, AnyBlockchainConfig, CompactFilters, #[cfg(feature = "compact_filters")]);
|
||||||
|
impl_from!(rpc::RpcConfig, AnyBlockchainConfig, Rpc, #[cfg(feature = "rpc")]);
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ impl CompactFiltersBlockchain {
|
|||||||
received: incoming,
|
received: incoming,
|
||||||
sent: outgoing,
|
sent: outgoing,
|
||||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||||
|
verified: height.is_some(),
|
||||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +229,6 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
_stop_gap: Option<usize>, // TODO: move to electrum and esplora only
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
|||||||
@@ -43,11 +43,17 @@ use crate::FeeRate;
|
|||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
|
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
|
||||||
pub struct ElectrumBlockchain(Client);
|
pub struct ElectrumBlockchain {
|
||||||
|
client: Client,
|
||||||
|
stop_gap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl std::convert::From<Client> for ElectrumBlockchain {
|
impl std::convert::From<Client> for ElectrumBlockchain {
|
||||||
fn from(client: Client) -> Self {
|
fn from(client: Client) -> Self {
|
||||||
ElectrumBlockchain(client)
|
ElectrumBlockchain {
|
||||||
|
client,
|
||||||
|
stop_gap: 20,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,34 +70,33 @@ impl Blockchain for ElectrumBlockchain {
|
|||||||
|
|
||||||
fn setup<D: BatchDatabase, P: Progress>(
|
fn setup<D: BatchDatabase, P: Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.0
|
self.client
|
||||||
.electrum_like_setup(stop_gap, database, progress_update)
|
.electrum_like_setup(self.stop_gap, database, progress_update)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
Ok(self.0.transaction_get(txid).map(Option::Some)?)
|
Ok(self.client.transaction_get(txid).map(Option::Some)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(self.0.transaction_broadcast(tx).map(|_| ())?)
|
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
// TODO: unsubscribe when added to the client, or is there a better call to use here?
|
||||||
|
|
||||||
Ok(self
|
Ok(self
|
||||||
.0
|
.client
|
||||||
.block_headers_subscribe()
|
.block_headers_subscribe()
|
||||||
.map(|data| data.height as u32)?)
|
.map(|data| data.height as u32)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
Ok(FeeRate::from_btc_per_kvb(
|
Ok(FeeRate::from_btc_per_kvb(
|
||||||
self.0.estimate_fee(target)? as f32
|
self.client.estimate_fee(target)? as f32
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +154,8 @@ pub struct ElectrumBlockchainConfig {
|
|||||||
pub retry: u8,
|
pub retry: u8,
|
||||||
/// Request timeout (seconds)
|
/// Request timeout (seconds)
|
||||||
pub timeout: Option<u8>,
|
pub timeout: Option<u8>,
|
||||||
|
/// Stop searching addresses for transactions after finding an unused gap of this length
|
||||||
|
pub stop_gap: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurableBlockchain for ElectrumBlockchain {
|
impl ConfigurableBlockchain for ElectrumBlockchain {
|
||||||
@@ -162,16 +169,17 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
|
|||||||
.socks5(socks5)?
|
.socks5(socks5)?
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Ok(ElectrumBlockchain(Client::from_config(
|
Ok(ElectrumBlockchain {
|
||||||
config.url.as_str(),
|
client: Client::from_config(config.url.as_str(), electrum_config)?,
|
||||||
electrum_config,
|
stop_gap: config.stop_gap,
|
||||||
)?))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-blockchains")]
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "test-electrum")]
|
||||||
crate::bdk_blockchain_tests! {
|
crate::bdk_blockchain_tests! {
|
||||||
fn test_instance() -> ElectrumBlockchain {
|
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
||||||
ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap())
|
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/blockchain/esplora/mod.rs
Normal file
143
src/blockchain/esplora/mod.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
//! Esplora
|
||||||
|
//!
|
||||||
|
//! This module defines a [`EsploraBlockchain`] struct that can query an Esplora
|
||||||
|
//! backend populate the wallet's [database](crate::database::Database) by:
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! # use bdk::blockchain::esplora::EsploraBlockchain;
|
||||||
|
//! let blockchain = EsploraBlockchain::new("https://blockstream.info/testnet/api", 20);
|
||||||
|
//! # Ok::<(), bdk::Error>(())
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Esplora blockchain can use either `ureq` or `reqwest` for the HTTP client
|
||||||
|
//! depending on your needs (blocking or async respectively).
|
||||||
|
//!
|
||||||
|
//! Please note, to configure the Esplora HTTP client correctly use one of:
|
||||||
|
//! Blocking: --features='esplora,ureq'
|
||||||
|
//! Async: --features='async-interface,esplora,reqwest' --no-default-features
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use bitcoin::consensus;
|
||||||
|
use bitcoin::{BlockHash, Txid};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "esplora",
|
||||||
|
feature = "reqwest",
|
||||||
|
any(feature = "async-interface", target_arch = "wasm32"),
|
||||||
|
))]
|
||||||
|
mod reqwest;
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "esplora",
|
||||||
|
feature = "reqwest",
|
||||||
|
any(feature = "async-interface", target_arch = "wasm32"),
|
||||||
|
))]
|
||||||
|
pub use self::reqwest::*;
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "esplora",
|
||||||
|
not(any(
|
||||||
|
feature = "async-interface",
|
||||||
|
feature = "reqwest",
|
||||||
|
target_arch = "wasm32"
|
||||||
|
)),
|
||||||
|
))]
|
||||||
|
mod ureq;
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "esplora",
|
||||||
|
not(any(
|
||||||
|
feature = "async-interface",
|
||||||
|
feature = "reqwest",
|
||||||
|
target_arch = "wasm32"
|
||||||
|
)),
|
||||||
|
))]
|
||||||
|
pub use self::ureq::*;
|
||||||
|
|
||||||
|
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
|
||||||
|
let fee_val = estimates
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::<usize>()?, v)))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| Error::Generic(e.to_string()))?
|
||||||
|
.into_iter()
|
||||||
|
.take_while(|(k, _)| k <= &target)
|
||||||
|
.map(|(_, v)| v)
|
||||||
|
.last()
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
|
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data type used when fetching transaction history from Esplora.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct EsploraGetHistory {
|
||||||
|
txid: Txid,
|
||||||
|
status: EsploraGetHistoryStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct EsploraGetHistoryStatus {
|
||||||
|
block_height: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that can happen during a sync with [`EsploraBlockchain`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EsploraError {
|
||||||
|
/// Error during ureq HTTP request
|
||||||
|
#[cfg(feature = "ureq")]
|
||||||
|
Ureq(::ureq::Error),
|
||||||
|
/// Transport error during the ureq HTTP call
|
||||||
|
#[cfg(feature = "ureq")]
|
||||||
|
UreqTransport(::ureq::Transport),
|
||||||
|
/// Error during reqwest HTTP request
|
||||||
|
#[cfg(feature = "reqwest")]
|
||||||
|
Reqwest(::reqwest::Error),
|
||||||
|
/// HTTP response error
|
||||||
|
HttpResponse(u16),
|
||||||
|
/// IO error during ureq response read
|
||||||
|
Io(io::Error),
|
||||||
|
/// No header found in ureq response
|
||||||
|
NoHeader,
|
||||||
|
/// Invalid number returned
|
||||||
|
Parsing(std::num::ParseIntError),
|
||||||
|
/// Invalid Bitcoin data returned
|
||||||
|
BitcoinEncoding(bitcoin::consensus::encode::Error),
|
||||||
|
/// Invalid Hex data returned
|
||||||
|
Hex(bitcoin::hashes::hex::Error),
|
||||||
|
|
||||||
|
/// Transaction not found
|
||||||
|
TransactionNotFound(Txid),
|
||||||
|
/// Header height not found
|
||||||
|
HeaderHeightNotFound(u32),
|
||||||
|
/// Header hash not found
|
||||||
|
HeaderHashNotFound(BlockHash),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EsploraError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{:?}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for EsploraError {}
|
||||||
|
|
||||||
|
#[cfg(feature = "ureq")]
|
||||||
|
impl_error!(::ureq::Error, Ureq, EsploraError);
|
||||||
|
#[cfg(feature = "ureq")]
|
||||||
|
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
|
||||||
|
#[cfg(feature = "reqwest")]
|
||||||
|
impl_error!(::reqwest::Error, Reqwest, EsploraError);
|
||||||
|
impl_error!(io::Error, Io, EsploraError);
|
||||||
|
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
|
||||||
|
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
|
||||||
|
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
|
||||||
@@ -9,38 +9,25 @@
|
|||||||
// You may not use this file except in accordance with one or both of these
|
// You may not use this file except in accordance with one or both of these
|
||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
//! Esplora
|
//! Esplora by way of `reqwest` HTTP client.
|
||||||
//!
|
|
||||||
//! This module defines a [`Blockchain`] struct that can query an Esplora backend
|
|
||||||
//! populate the wallet's [database](crate::database::Database) by
|
|
||||||
//!
|
|
||||||
//! ## Example
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! # use bdk::blockchain::esplora::EsploraBlockchain;
|
|
||||||
//! let blockchain = EsploraBlockchain::new("https://blockstream.info/testnet/api", None);
|
|
||||||
//! # Ok::<(), bdk::Error>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
|
use bitcoin::consensus::{deserialize, serialize};
|
||||||
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
|
use bitcoin::hashes::{sha256, Hash};
|
||||||
|
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
|
||||||
|
|
||||||
use reqwest::{Client, StatusCode};
|
use ::reqwest::{Client, StatusCode};
|
||||||
|
|
||||||
use bitcoin::consensus::{self, deserialize, serialize};
|
use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||||
use bitcoin::hashes::{sha256, Hash};
|
use crate::blockchain::*;
|
||||||
use bitcoin::{BlockHash, BlockHeader, Script, Transaction, Txid};
|
|
||||||
|
|
||||||
use self::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
|
||||||
use super::*;
|
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::wallet::utils::ChunksIterator;
|
use crate::wallet::utils::ChunksIterator;
|
||||||
@@ -62,22 +49,37 @@ struct UrlClient {
|
|||||||
/// ## Example
|
/// ## Example
|
||||||
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EsploraBlockchain(UrlClient);
|
pub struct EsploraBlockchain {
|
||||||
|
url_client: UrlClient,
|
||||||
|
stop_gap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||||
fn from(url_client: UrlClient) -> Self {
|
fn from(url_client: UrlClient) -> Self {
|
||||||
EsploraBlockchain(url_client)
|
EsploraBlockchain {
|
||||||
|
url_client,
|
||||||
|
stop_gap: 20,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EsploraBlockchain {
|
impl EsploraBlockchain {
|
||||||
/// Create a new instance of the client from a base URL
|
/// Create a new instance of the client from a base URL and `stop_gap`.
|
||||||
pub fn new(base_url: &str, concurrency: Option<u8>) -> Self {
|
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||||
EsploraBlockchain(UrlClient {
|
EsploraBlockchain {
|
||||||
url: base_url.to_string(),
|
url_client: UrlClient {
|
||||||
client: Client::new(),
|
url: base_url.to_string(),
|
||||||
concurrency: concurrency.unwrap_or(DEFAULT_CONCURRENT_REQUESTS),
|
client: Client::new(),
|
||||||
})
|
concurrency: DEFAULT_CONCURRENT_REQUESTS,
|
||||||
|
},
|
||||||
|
stop_gap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the concurrency to use when doing batch queries against the Esplora instance.
|
||||||
|
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
|
||||||
|
self.url_client.concurrency = concurrency;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,42 +97,29 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
|
|
||||||
fn setup<D: BatchDatabase, P: Progress>(
|
fn setup<D: BatchDatabase, P: Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(self
|
maybe_await!(self
|
||||||
.0
|
.url_client
|
||||||
.electrum_like_setup(stop_gap, database, progress_update))
|
.electrum_like_setup(self.stop_gap, database, progress_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
Ok(await_or_block!(self.0._get_tx(txid))?)
|
Ok(self.url_client._get_tx(txid).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(await_or_block!(self.0._broadcast(tx))?)
|
Ok(self.url_client._broadcast(tx).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(await_or_block!(self.0._get_height())?)
|
Ok(self.url_client._get_height().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
let estimates = await_or_block!(self.0._get_fee_estimates())?;
|
let estimates = self.url_client._get_fee_estimates().await?;
|
||||||
|
super::into_fee_rate(target, estimates)
|
||||||
let fee_val = estimates
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::<usize>()?, v)))
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.map_err(|e| Error::Generic(e.to_string()))?
|
|
||||||
.into_iter()
|
|
||||||
.take_while(|(k, _)| k <= &target)
|
|
||||||
.map(|(_, v)| v)
|
|
||||||
.last()
|
|
||||||
.unwrap_or(1.0);
|
|
||||||
|
|
||||||
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,74 +281,51 @@ impl ElectrumLikeSync for UrlClient {
|
|||||||
&self,
|
&self,
|
||||||
scripts: I,
|
scripts: I,
|
||||||
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||||
let future = async {
|
let mut results = vec![];
|
||||||
let mut results = vec![];
|
for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
|
||||||
for chunk in ChunksIterator::new(scripts.into_iter(), self.concurrency as usize) {
|
let mut futs = FuturesOrdered::new();
|
||||||
let mut futs = FuturesOrdered::new();
|
for script in chunk {
|
||||||
for script in chunk {
|
futs.push(self._script_get_history(script));
|
||||||
futs.push(self._script_get_history(&script));
|
|
||||||
}
|
|
||||||
let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?;
|
|
||||||
results.extend(partial_results);
|
|
||||||
}
|
}
|
||||||
Ok(stream::iter(results).collect().await)
|
let partial_results: Vec<Vec<ElsGetHistoryRes>> = futs.try_collect().await?;
|
||||||
};
|
results.extend(partial_results);
|
||||||
|
}
|
||||||
await_or_block!(future)
|
Ok(stream::iter(results).collect().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
|
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
|
||||||
&self,
|
&self,
|
||||||
txids: I,
|
txids: I,
|
||||||
) -> Result<Vec<Transaction>, Error> {
|
) -> Result<Vec<Transaction>, Error> {
|
||||||
let future = async {
|
let mut results = vec![];
|
||||||
let mut results = vec![];
|
for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) {
|
||||||
for chunk in ChunksIterator::new(txids.into_iter(), self.concurrency as usize) {
|
let mut futs = FuturesOrdered::new();
|
||||||
let mut futs = FuturesOrdered::new();
|
for txid in chunk {
|
||||||
for txid in chunk {
|
futs.push(self._get_tx_no_opt(txid));
|
||||||
futs.push(self._get_tx_no_opt(&txid));
|
|
||||||
}
|
|
||||||
let partial_results: Vec<Transaction> = futs.try_collect().await?;
|
|
||||||
results.extend(partial_results);
|
|
||||||
}
|
}
|
||||||
Ok(stream::iter(results).collect().await)
|
let partial_results: Vec<Transaction> = futs.try_collect().await?;
|
||||||
};
|
results.extend(partial_results);
|
||||||
|
}
|
||||||
await_or_block!(future)
|
Ok(stream::iter(results).collect().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn els_batch_block_header<I: IntoIterator<Item = u32>>(
|
fn els_batch_block_header<I: IntoIterator<Item = u32>>(
|
||||||
&self,
|
&self,
|
||||||
heights: I,
|
heights: I,
|
||||||
) -> Result<Vec<BlockHeader>, Error> {
|
) -> Result<Vec<BlockHeader>, Error> {
|
||||||
let future = async {
|
let mut results = vec![];
|
||||||
let mut results = vec![];
|
for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) {
|
||||||
for chunk in ChunksIterator::new(heights.into_iter(), self.concurrency as usize) {
|
let mut futs = FuturesOrdered::new();
|
||||||
let mut futs = FuturesOrdered::new();
|
for height in chunk {
|
||||||
for height in chunk {
|
futs.push(self._get_header(height));
|
||||||
futs.push(self._get_header(height));
|
|
||||||
}
|
|
||||||
let partial_results: Vec<BlockHeader> = futs.try_collect().await?;
|
|
||||||
results.extend(partial_results);
|
|
||||||
}
|
}
|
||||||
Ok(stream::iter(results).collect().await)
|
let partial_results: Vec<BlockHeader> = futs.try_collect().await?;
|
||||||
};
|
results.extend(partial_results);
|
||||||
|
}
|
||||||
await_or_block!(future)
|
Ok(stream::iter(results).collect().await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct EsploraGetHistoryStatus {
|
|
||||||
block_height: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct EsploraGetHistory {
|
|
||||||
txid: Txid,
|
|
||||||
status: EsploraGetHistoryStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for an [`EsploraBlockchain`]
|
/// Configuration for an [`EsploraBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
pub struct EsploraBlockchainConfig {
|
pub struct EsploraBlockchainConfig {
|
||||||
@@ -369,55 +335,26 @@ pub struct EsploraBlockchainConfig {
|
|||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
/// Number of parallel requests sent to the esplora service (default: 4)
|
/// Number of parallel requests sent to the esplora service (default: 4)
|
||||||
pub concurrency: Option<u8>,
|
pub concurrency: Option<u8>,
|
||||||
|
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||||
|
pub stop_gap: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurableBlockchain for EsploraBlockchain {
|
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||||
type Config = EsploraBlockchainConfig;
|
type Config = EsploraBlockchainConfig;
|
||||||
|
|
||||||
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||||
Ok(EsploraBlockchain::new(
|
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
|
||||||
config.base_url.as_str(),
|
if let Some(concurrency) = config.concurrency {
|
||||||
config.concurrency,
|
blockchain.url_client.concurrency = concurrency;
|
||||||
))
|
};
|
||||||
|
Ok(blockchain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can happen during a sync with [`EsploraBlockchain`]
|
#[cfg(test)]
|
||||||
#[derive(Debug)]
|
#[cfg(feature = "test-esplora")]
|
||||||
pub enum EsploraError {
|
|
||||||
/// Error with the HTTP call
|
|
||||||
Reqwest(reqwest::Error),
|
|
||||||
/// Invalid number returned
|
|
||||||
Parsing(std::num::ParseIntError),
|
|
||||||
/// Invalid Bitcoin data returned
|
|
||||||
BitcoinEncoding(bitcoin::consensus::encode::Error),
|
|
||||||
/// Invalid Hex data returned
|
|
||||||
Hex(bitcoin::hashes::hex::Error),
|
|
||||||
|
|
||||||
/// Transaction not found
|
|
||||||
TransactionNotFound(Txid),
|
|
||||||
/// Header height not found
|
|
||||||
HeaderHeightNotFound(u32),
|
|
||||||
/// Header hash not found
|
|
||||||
HeaderHashNotFound(BlockHash),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for EsploraError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for EsploraError {}
|
|
||||||
|
|
||||||
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! {
|
crate::bdk_blockchain_tests! {
|
||||||
fn test_instance() -> EsploraBlockchain {
|
fn test_instance(test_client: &TestClient) -> EsploraBlockchain {
|
||||||
EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None)
|
EsploraBlockchain::new(&format!("http://{}",test_client.electrsd.esplora_url.as_ref().unwrap()), None, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
379
src/blockchain/esplora/ureq.rs
Normal file
379
src/blockchain/esplora/ureq.rs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
//! Esplora by way of `ureq` HTTP client.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
|
use ureq::{Agent, Response};
|
||||||
|
|
||||||
|
use bitcoin::consensus::{deserialize, serialize};
|
||||||
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
|
use bitcoin::hashes::{sha256, Hash};
|
||||||
|
use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
||||||
|
|
||||||
|
use crate::blockchain::esplora::{EsploraError, EsploraGetHistory};
|
||||||
|
use crate::blockchain::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||||
|
use crate::blockchain::*;
|
||||||
|
use crate::database::BatchDatabase;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct UrlClient {
|
||||||
|
url: String,
|
||||||
|
agent: Agent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure that implements the logic to sync with Esplora
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EsploraBlockchain {
|
||||||
|
url_client: UrlClient,
|
||||||
|
stop_gap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<UrlClient> for EsploraBlockchain {
|
||||||
|
fn from(url_client: UrlClient) -> Self {
|
||||||
|
EsploraBlockchain {
|
||||||
|
url_client,
|
||||||
|
stop_gap: 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EsploraBlockchain {
|
||||||
|
/// Create a new instance of the client from a base URL and `stop_gap`.
|
||||||
|
pub fn new(base_url: &str, stop_gap: usize) -> Self {
|
||||||
|
EsploraBlockchain {
|
||||||
|
url_client: UrlClient {
|
||||||
|
url: base_url.to_string(),
|
||||||
|
agent: Agent::new(),
|
||||||
|
},
|
||||||
|
stop_gap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the inner `ureq` agent.
|
||||||
|
pub fn with_agent(mut self, agent: Agent) -> Self {
|
||||||
|
self.url_client.agent = agent;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blockchain for EsploraBlockchain {
|
||||||
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||||
|
vec![
|
||||||
|
Capability::FullHistory,
|
||||||
|
Capability::GetAnyTx,
|
||||||
|
Capability::AccurateFees,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup<D: BatchDatabase, P: Progress>(
|
||||||
|
&self,
|
||||||
|
database: &mut D,
|
||||||
|
progress_update: P,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.url_client
|
||||||
|
.electrum_like_setup(self.stop_gap, database, progress_update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
Ok(self.url_client._get_tx(txid)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
|
let _txid = self.url_client._broadcast(tx)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
Ok(self.url_client._get_height()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
|
let estimates = self.url_client._get_fee_estimates()?;
|
||||||
|
super::into_fee_rate(target, estimates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrlClient {
|
||||||
|
fn script_to_scripthash(script: &Script) -> String {
|
||||||
|
sha256::Hash::hash(script.as_bytes()).into_inner().to_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!("{}/tx/{}/raw", self.url, txid))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
|
||||||
|
Err(ureq::Error::Status(code, _)) => {
|
||||||
|
if is_status_not_found(code) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(EsploraError::HttpResponse(code))
|
||||||
|
}
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
|
||||||
|
match self._get_tx(txid) {
|
||||||
|
Ok(Some(tx)) => Ok(tx),
|
||||||
|
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!("{}/block-height/{}", self.url, block_height))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
let bytes = match resp {
|
||||||
|
Ok(resp) => Ok(into_bytes(resp)?),
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let hash = std::str::from_utf8(&bytes)
|
||||||
|
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!("{}/block/{}/header", self.url, hash))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.post(&format!("{}/tx", self.url))
|
||||||
|
.send_string(&serialize(transaction).to_hex());
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(_) => Ok(()), // We do not return the txid?
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_height(&self) -> Result<u32, EsploraError> {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!("{}/blocks/tip/height", self.url))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(resp) => Ok(resp.into_string()?.parse()?),
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _script_get_history(&self, script: &Script) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let scripthash = Self::script_to_scripthash(script);
|
||||||
|
|
||||||
|
// Add the unconfirmed transactions first
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!(
|
||||||
|
"{}/scripthash/{}/txs/mempool",
|
||||||
|
self.url, scripthash
|
||||||
|
))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
let v = match resp {
|
||||||
|
Ok(resp) => {
|
||||||
|
let v: Vec<EsploraGetHistory> = resp.into_json()?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
result.extend(v.into_iter().map(|x| ElsGetHistoryRes {
|
||||||
|
tx_hash: x.txid,
|
||||||
|
height: x.status.block_height.unwrap_or(0) as i32,
|
||||||
|
}));
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Found {} mempool txs for {} - {:?}",
|
||||||
|
result.len(),
|
||||||
|
scripthash,
|
||||||
|
script
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then go through all the pages of confirmed transactions
|
||||||
|
let mut last_txid = String::new();
|
||||||
|
loop {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!(
|
||||||
|
"{}/scripthash/{}/txs/chain/{}",
|
||||||
|
self.url, scripthash, last_txid
|
||||||
|
))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
let v = match resp {
|
||||||
|
Ok(resp) => {
|
||||||
|
let v: Vec<EsploraGetHistory> = resp.into_json()?;
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let len = v.len();
|
||||||
|
if let Some(elem) = v.last() {
|
||||||
|
last_txid = elem.txid.to_hex();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("... adding {} confirmed transactions", len);
|
||||||
|
|
||||||
|
result.extend(v.into_iter().map(|x| ElsGetHistoryRes {
|
||||||
|
tx_hash: x.txid,
|
||||||
|
height: x.status.block_height.unwrap_or(0) as i32,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if len < 25 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
|
||||||
|
let resp = self
|
||||||
|
.agent
|
||||||
|
.get(&format!("{}/fee-estimates", self.url,))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
let map = match resp {
|
||||||
|
Ok(resp) => {
|
||||||
|
let map: HashMap<String, f64> = resp.into_json()?;
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
|
||||||
|
Err(e) => Err(EsploraError::Ureq(e)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_status_not_found(status: u16) -> bool {
|
||||||
|
status == 404
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
|
||||||
|
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
|
||||||
|
|
||||||
|
let mut buf: Vec<u8> = vec![];
|
||||||
|
resp.into_reader()
|
||||||
|
.take((BYTES_LIMIT + 1) as u64)
|
||||||
|
.read_to_end(&mut buf)?;
|
||||||
|
if buf.len() > BYTES_LIMIT {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"response too big for into_bytes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElectrumLikeSync for UrlClient {
|
||||||
|
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
||||||
|
&self,
|
||||||
|
scripts: I,
|
||||||
|
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||||
|
let mut results = vec![];
|
||||||
|
for script in scripts.into_iter() {
|
||||||
|
let v = self._script_get_history(script)?;
|
||||||
|
results.push(v);
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid>>(
|
||||||
|
&self,
|
||||||
|
txids: I,
|
||||||
|
) -> Result<Vec<Transaction>, Error> {
|
||||||
|
let mut results = vec![];
|
||||||
|
for txid in txids.into_iter() {
|
||||||
|
let tx = self._get_tx_no_opt(txid)?;
|
||||||
|
results.push(tx);
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn els_batch_block_header<I: IntoIterator<Item = u32>>(
|
||||||
|
&self,
|
||||||
|
heights: I,
|
||||||
|
) -> Result<Vec<BlockHeader>, Error> {
|
||||||
|
let mut results = vec![];
|
||||||
|
for height in heights.into_iter() {
|
||||||
|
let header = self._get_header(height)?;
|
||||||
|
results.push(header);
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for an [`EsploraBlockchain`]
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
|
pub struct EsploraBlockchainConfig {
|
||||||
|
/// Base URL of the esplora service eg. `https://blockstream.info/api/`
|
||||||
|
pub base_url: String,
|
||||||
|
/// Socket read timeout.
|
||||||
|
pub timeout_read: u64,
|
||||||
|
/// Socket write timeout.
|
||||||
|
pub timeout_write: u64,
|
||||||
|
/// Stop searching addresses for transactions after finding an unused gap of this length.
|
||||||
|
pub stop_gap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigurableBlockchain for EsploraBlockchain {
|
||||||
|
type Config = EsploraBlockchainConfig;
|
||||||
|
|
||||||
|
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||||
|
let agent: Agent = ureq::AgentBuilder::new()
|
||||||
|
.timeout_read(Duration::from_secs(config.timeout_read))
|
||||||
|
.timeout_write(Duration::from_secs(config.timeout_write))
|
||||||
|
.build();
|
||||||
|
Ok(EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap).with_agent(agent))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,19 @@ use crate::FeeRate;
|
|||||||
#[cfg(any(feature = "electrum", feature = "esplora"))]
|
#[cfg(any(feature = "electrum", feature = "esplora"))]
|
||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
|
||||||
#[cfg(any(feature = "electrum", feature = "esplora", feature = "compact_filters"))]
|
#[cfg(any(
|
||||||
|
feature = "electrum",
|
||||||
|
feature = "esplora",
|
||||||
|
feature = "compact_filters",
|
||||||
|
feature = "rpc"
|
||||||
|
))]
|
||||||
pub mod any;
|
pub mod any;
|
||||||
#[cfg(any(feature = "electrum", feature = "esplora", feature = "compact_filters"))]
|
#[cfg(any(
|
||||||
|
feature = "electrum",
|
||||||
|
feature = "esplora",
|
||||||
|
feature = "compact_filters",
|
||||||
|
feature = "rpc"
|
||||||
|
))]
|
||||||
pub use any::{AnyBlockchain, AnyBlockchainConfig};
|
pub use any::{AnyBlockchain, AnyBlockchainConfig};
|
||||||
|
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
@@ -44,6 +54,7 @@ pub use self::electrum::ElectrumBlockchain;
|
|||||||
pub use self::electrum::ElectrumBlockchainConfig;
|
pub use self::electrum::ElectrumBlockchainConfig;
|
||||||
|
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
pub use self::rpc::RpcBlockchain;
|
pub use self::rpc::RpcBlockchain;
|
||||||
@@ -92,7 +103,6 @@ pub trait Blockchain {
|
|||||||
/// [`Blockchain::sync`] defaults to calling this internally if not overridden.
|
/// [`Blockchain::sync`] defaults to calling this internally if not overridden.
|
||||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
@@ -117,11 +127,10 @@ pub trait Blockchain {
|
|||||||
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
|
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
|
||||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(self.setup(stop_gap, database, progress_update))
|
maybe_await!(self.setup(database, progress_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a transaction from the blockchain given its txid
|
/// Fetch a transaction from the blockchain given its txid
|
||||||
@@ -217,20 +226,18 @@ impl<T: Blockchain> Blockchain for Arc<T> {
|
|||||||
|
|
||||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(self.deref().setup(stop_gap, database, progress_update))
|
maybe_await!(self.deref().setup(database, progress_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
maybe_await!(self.deref().sync(stop_gap, database, progress_update))
|
maybe_await!(self.deref().sync(database, progress_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
|||||||
@@ -13,13 +13,17 @@
|
|||||||
//!
|
//!
|
||||||
//! Backend that gets blockchain data from Bitcoin Core RPC
|
//! Backend that gets blockchain data from Bitcoin Core RPC
|
||||||
//!
|
//!
|
||||||
|
//! This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||||
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain};
|
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain, rpc::Auth};
|
||||||
//! let config = RpcConfig {
|
//! let config = RpcConfig {
|
||||||
//! url: "127.0.0.1:18332".to_string(),
|
//! url: "127.0.0.1:18332".to_string(),
|
||||||
//! auth: bitcoincore_rpc::Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
|
//! auth: Auth::Cookie {
|
||||||
|
//! file: "/home/user/.bitcoin/.cookie".into(),
|
||||||
|
//! },
|
||||||
//! network: bdk::bitcoin::Network::Testnet,
|
//! network: bdk::bitcoin::Network::Testnet,
|
||||||
//! wallet_name: "wallet_name".to_string(),
|
//! wallet_name: "wallet_name".to_string(),
|
||||||
//! skip_blocks: None,
|
//! skip_blocks: None,
|
||||||
@@ -39,10 +43,12 @@ use bitcoincore_rpc::json::{
|
|||||||
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
||||||
};
|
};
|
||||||
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||||
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
use bitcoincore_rpc::Auth as RpcAuth;
|
||||||
|
use bitcoincore_rpc::{Client, RpcApi};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
|
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
|
||||||
@@ -62,7 +68,7 @@ pub struct RpcBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RpcBlockchain configuration options
|
/// RpcBlockchain configuration options
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct RpcConfig {
|
pub struct RpcConfig {
|
||||||
/// The bitcoin node url
|
/// The bitcoin node url
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -76,6 +82,39 @@ pub struct RpcConfig {
|
|||||||
pub skip_blocks: Option<u32>,
|
pub skip_blocks: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This struct is equivalent to [bitcoincore_rpc::Auth] but it implements [serde::Serialize]
|
||||||
|
/// To be removed once upstream equivalent is implementing Serialize (json serialization format
|
||||||
|
/// should be the same) https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/181
|
||||||
|
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Auth {
|
||||||
|
/// None authentication
|
||||||
|
None,
|
||||||
|
/// Authentication with username and password, usually [Auth::Cookie] should be preferred
|
||||||
|
UserPass {
|
||||||
|
/// Username
|
||||||
|
username: String,
|
||||||
|
/// Password
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
|
/// Authentication with a cookie file
|
||||||
|
Cookie {
|
||||||
|
/// Cookie file
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Auth> for RpcAuth {
|
||||||
|
fn from(auth: Auth) -> Self {
|
||||||
|
match auth {
|
||||||
|
Auth::None => RpcAuth::None,
|
||||||
|
Auth::UserPass { username, password } => RpcAuth::UserPass(username, password),
|
||||||
|
Auth::Cookie { file } => RpcAuth::CookieFile(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RpcBlockchain {
|
impl RpcBlockchain {
|
||||||
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
||||||
let info = self.client.get_address_info(&self._storage_address)?;
|
let info = self.client.get_address_info(&self._storage_address)?;
|
||||||
@@ -104,7 +143,6 @@ impl Blockchain for RpcBlockchain {
|
|||||||
|
|
||||||
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
progress_update: P,
|
progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -148,12 +186,11 @@ impl Blockchain for RpcBlockchain {
|
|||||||
|
|
||||||
self.set_node_synced_height(current_height)?;
|
self.set_node_synced_height(current_height)?;
|
||||||
|
|
||||||
self.sync(stop_gap, database, progress_update)
|
self.sync(database, progress_update)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||||
&self,
|
&self,
|
||||||
_stop_gap: Option<usize>,
|
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
_progress_update: P,
|
_progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -233,6 +270,7 @@ impl Blockchain for RpcBlockchain {
|
|||||||
received,
|
received,
|
||||||
sent,
|
sent,
|
||||||
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
||||||
@@ -319,7 +357,7 @@ impl ConfigurableBlockchain for RpcBlockchain {
|
|||||||
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
||||||
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
||||||
|
|
||||||
let client = Client::new(wallet_url, config.auth.clone())?;
|
let client = Client::new(wallet_url, config.auth.clone().into())?;
|
||||||
let loaded_wallets = client.list_wallets()?;
|
let loaded_wallets = client.list_wallets()?;
|
||||||
if loaded_wallets.contains(&wallet_name) {
|
if loaded_wallets.contains(&wallet_name) {
|
||||||
debug!("wallet already loaded {:?}", wallet_name);
|
debug!("wallet already loaded {:?}", wallet_name);
|
||||||
@@ -419,27 +457,14 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
|||||||
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test-blockchains")]
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "test-rpc")]
|
||||||
crate::bdk_blockchain_tests! {
|
crate::bdk_blockchain_tests! {
|
||||||
|
|
||||||
fn test_instance() -> RpcBlockchain {
|
fn test_instance(test_client: &TestClient) -> 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 {
|
let config = RpcConfig {
|
||||||
url,
|
url: test_client.bitcoind.rpc_url(),
|
||||||
auth,
|
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
|
||||||
network: Network::Regtest,
|
network: Network::Regtest,
|
||||||
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
|
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
|
||||||
skip_blocks: None,
|
skip_blocks: None,
|
||||||
@@ -447,227 +472,3 @@ crate::bdk_blockchain_tests! {
|
|||||||
RpcBlockchain::from_config(&config).unwrap()
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub trait ElectrumLikeSync {
|
|||||||
|
|
||||||
fn electrum_like_setup<D: BatchDatabase, P: Progress>(
|
fn electrum_like_setup<D: BatchDatabase, P: Progress>(
|
||||||
&self,
|
&self,
|
||||||
stop_gap: Option<usize>,
|
stop_gap: usize,
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
_progress_update: P,
|
_progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -61,7 +61,6 @@ pub trait ElectrumLikeSync {
|
|||||||
let start = Instant::new();
|
let start = Instant::new();
|
||||||
debug!("start setup");
|
debug!("start setup");
|
||||||
|
|
||||||
let stop_gap = stop_gap.unwrap_or(20);
|
|
||||||
let chunk_size = stop_gap;
|
let chunk_size = stop_gap;
|
||||||
|
|
||||||
let mut history_txs_id = HashSet::new();
|
let mut history_txs_id = HashSet::new();
|
||||||
@@ -362,6 +361,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
|||||||
sent: outgoing,
|
sent: outgoing,
|
||||||
confirmation_time: ConfirmationTime::new(height, timestamp),
|
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
|
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
|
||||||
|
verified: height.is_some(),
|
||||||
};
|
};
|
||||||
updates.set_tx(&tx_details)?;
|
updates.set_tx(&tx_details)?;
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// returns utxo dependency as the inputs needed for the utxo to exist
|
/// returns utxo dependency as the inputs needed for the utxo to exist
|
||||||
/// `tx_raw_in_db` must contains utxo's generating txs or errors witt [crate::Error::TransactionNotFound]
|
/// `tx_raw_in_db` must contains utxo's generating txs or errors with [crate::Error::TransactionNotFound]
|
||||||
fn utxos_deps<D: BatchDatabase>(
|
fn utxos_deps<D: BatchDatabase>(
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
tx_raw_in_db: &HashMap<Txid, Transaction>,
|
tx_raw_in_db: &HashMap<Txid, Transaction>,
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ impl BatchDatabase for Tree {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use std::sync::{Arc, Condvar, Mutex, Once};
|
use std::sync::{Arc, Condvar, Mutex, Once};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
|||||||
@@ -485,6 +485,7 @@ macro_rules! populate_test_db {
|
|||||||
received: 0,
|
received: 0,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
confirmation_time,
|
confirmation_time,
|
||||||
|
verified: current_height.is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
db.set_tx(&tx_details).unwrap();
|
db.set_tx(&tx_details).unwrap();
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ pub mod test {
|
|||||||
timestamp: 123456,
|
timestamp: 123456,
|
||||||
height: 1000,
|
height: 1000,
|
||||||
}),
|
}),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
tree.set_tx(&tx_details).unwrap();
|
tree.set_tx(&tx_details).unwrap();
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ macro_rules! impl_node_opcode_two {
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_node_opcode_three {
|
macro_rules! impl_node_opcode_three {
|
||||||
( $terminal_variant:ident, $( $inner:tt )* ) => {
|
( $terminal_variant:ident, $( $inner:tt )* ) => ({
|
||||||
use $crate::descriptor::CheckMiniscript;
|
use $crate::descriptor::CheckMiniscript;
|
||||||
|
|
||||||
let inner = $crate::fragment_internal!( @t $( $inner )* );
|
let inner = $crate::fragment_internal!( @t $( $inner )* );
|
||||||
@@ -201,7 +201,7 @@ macro_rules! impl_node_opcode_three {
|
|||||||
|
|
||||||
Ok((minisc, a_keymap, networks))
|
Ok((minisc, a_keymap, networks))
|
||||||
})
|
})
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -790,6 +790,25 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fixed_threeop_descriptors() {
|
||||||
|
let redeem_key = bitcoin::PublicKey::from_str(
|
||||||
|
"03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let move_key = bitcoin::PublicKey::from_str(
|
||||||
|
"032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
check(
|
||||||
|
descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
&["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bip32_legacy_descriptors() {
|
fn test_bip32_legacy_descriptors() {
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
|
|||||||
29
src/error.rs
29
src/error.rs
@@ -24,10 +24,6 @@ pub enum Error {
|
|||||||
Generic(String),
|
Generic(String),
|
||||||
/// This error is thrown when trying to convert Bare and Public key script to address
|
/// This error is thrown when trying to convert Bare and Public key script to address
|
||||||
ScriptDoesntHaveAddressForm,
|
ScriptDoesntHaveAddressForm,
|
||||||
/// Found multiple outputs when `single_recipient` option has been specified
|
|
||||||
SingleRecipientMultipleOutputs,
|
|
||||||
/// `single_recipient` option is selected but neither `drain_wallet` nor `manually_selected_only` are
|
|
||||||
SingleRecipientNoInputs,
|
|
||||||
/// Cannot build a tx without recipients
|
/// Cannot build a tx without recipients
|
||||||
NoRecipients,
|
NoRecipients,
|
||||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||||
@@ -90,6 +86,10 @@ pub enum Error {
|
|||||||
/// found network, for example the network of the bitcoin node
|
/// found network, for example the network of the bitcoin node
|
||||||
found: Network,
|
found: Network,
|
||||||
},
|
},
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
/// Transaction verification error
|
||||||
|
Verification(crate::wallet::verify::VerifyError),
|
||||||
|
|
||||||
/// Progress value must be between `0.0` (included) and `100.0` (included)
|
/// Progress value must be between `0.0` (included) and `100.0` (included)
|
||||||
InvalidProgressValue(f32),
|
InvalidProgressValue(f32),
|
||||||
/// Progress update error (maybe the channel has been closed)
|
/// Progress update error (maybe the channel has been closed)
|
||||||
@@ -130,7 +130,7 @@ pub enum Error {
|
|||||||
Electrum(electrum_client::Error),
|
Electrum(electrum_client::Error),
|
||||||
#[cfg(feature = "esplora")]
|
#[cfg(feature = "esplora")]
|
||||||
/// Esplora client error
|
/// Esplora client error
|
||||||
Esplora(crate::blockchain::esplora::EsploraError),
|
Esplora(Box<crate::blockchain::esplora::EsploraError>),
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
/// Compact filters client error)
|
/// Compact filters client error)
|
||||||
CompactFilters(crate::blockchain::compact_filters::CompactFiltersError),
|
CompactFilters(crate::blockchain::compact_filters::CompactFiltersError),
|
||||||
@@ -190,8 +190,6 @@ impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
|
|||||||
|
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
impl_error!(electrum_client::Error, Electrum);
|
impl_error!(electrum_client::Error, Electrum);
|
||||||
#[cfg(feature = "esplora")]
|
|
||||||
impl_error!(crate::blockchain::esplora::EsploraError, Esplora);
|
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
impl_error!(sled::Error, Sled);
|
impl_error!(sled::Error, Sled);
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
@@ -206,3 +204,20 @@ impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
impl From<crate::wallet::verify::VerifyError> for Error {
|
||||||
|
fn from(other: crate::wallet::verify::VerifyError) -> Self {
|
||||||
|
match other {
|
||||||
|
crate::wallet::verify::VerifyError::Global(inner) => *inner,
|
||||||
|
err => Error::Verification(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "esplora")]
|
||||||
|
impl From<crate::blockchain::esplora::EsploraError> for Error {
|
||||||
|
fn from(other: crate::blockchain::esplora::EsploraError) -> Self {
|
||||||
|
Error::Esplora(Box::new(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
src/lib.rs
24
src/lib.rs
@@ -40,7 +40,7 @@
|
|||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! bdk = "0.8.0"
|
//! bdk = "0.10.0"
|
||||||
//! ```
|
//! ```
|
||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
feature = "electrum",
|
feature = "electrum",
|
||||||
@@ -205,11 +205,24 @@ extern crate serde;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "reqwest", feature = "ureq"))]
|
||||||
|
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
|
||||||
|
|
||||||
#[cfg(all(feature = "async-interface", feature = "electrum"))]
|
#[cfg(all(feature = "async-interface", feature = "electrum"))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"Features async-interface and electrum are mutually exclusive and cannot be enabled together"
|
"Features async-interface and electrum are mutually exclusive and cannot be enabled together"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async-interface", feature = "ureq"))]
|
||||||
|
compile_error!(
|
||||||
|
"Features async-interface and ureq are mutually exclusive and cannot be enabled together"
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async-interface", feature = "compact_filters"))]
|
||||||
|
compile_error!(
|
||||||
|
"Features async-interface and compact_filters are mutually exclusive and cannot be enabled together"
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "keys-bip39")]
|
#[cfg(feature = "keys-bip39")]
|
||||||
extern crate bip39;
|
extern crate bip39;
|
||||||
|
|
||||||
@@ -228,19 +241,10 @@ pub extern crate bitcoincore_rpc;
|
|||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
pub extern crate electrum_client;
|
pub extern crate electrum_client;
|
||||||
|
|
||||||
#[cfg(feature = "esplora")]
|
|
||||||
pub extern crate reqwest;
|
|
||||||
|
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
pub extern crate sled;
|
pub extern crate sled;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[cfg(test)]
|
|
||||||
#[macro_use]
|
|
||||||
pub extern crate serial_test;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub mod blockchain;
|
pub mod blockchain;
|
||||||
|
|||||||
@@ -6,38 +6,53 @@ use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
|||||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
use electrsd::bitcoind::BitcoinD;
|
||||||
|
use electrsd::{bitcoind, Conf, ElectrsD};
|
||||||
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct TestClient {
|
pub struct TestClient {
|
||||||
client: RpcClient,
|
pub bitcoind: BitcoinD,
|
||||||
electrum: ElectrumClient,
|
pub electrsd: ElectrsD,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestClient {
|
impl TestClient {
|
||||||
pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
|
pub fn new(bitcoind_exe: String, electrs_exe: String) -> Self {
|
||||||
let client = RpcClient::new(
|
debug!("launching {} and {}", &bitcoind_exe, &electrs_exe);
|
||||||
format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
|
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||||
get_auth(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
|
|
||||||
|
|
||||||
TestClient { client, electrum }
|
let http_enabled = cfg!(feature = "test-esplora");
|
||||||
|
|
||||||
|
let conf = Conf {
|
||||||
|
http_enabled,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &conf).unwrap();
|
||||||
|
|
||||||
|
let node_address = bitcoind.client.get_new_address(None, None).unwrap();
|
||||||
|
bitcoind
|
||||||
|
.client
|
||||||
|
.generate_to_address(101, &node_address)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut test_client = TestClient { bitcoind, electrsd };
|
||||||
|
TestClient::wait_for_block(&mut test_client, 101);
|
||||||
|
test_client
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
|
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
|
||||||
// wait for electrs to index the tx
|
// wait for electrs to index the tx
|
||||||
exponential_backoff_poll(|| {
|
exponential_backoff_poll(|| {
|
||||||
|
self.electrsd.trigger().unwrap();
|
||||||
trace!("wait_for_tx {}", txid);
|
trace!("wait_for_tx {}", txid);
|
||||||
|
|
||||||
self.electrum
|
self.electrsd
|
||||||
|
.client
|
||||||
.script_get_history(monitor_script)
|
.script_get_history(monitor_script)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -46,12 +61,13 @@ impl TestClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_block(&mut self, min_height: usize) {
|
fn wait_for_block(&mut self, min_height: usize) {
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let header = exponential_backoff_poll(|| {
|
let header = exponential_backoff_poll(|| {
|
||||||
self.electrum.ping().unwrap();
|
self.electrsd.trigger().unwrap();
|
||||||
self.electrum.block_headers_pop().unwrap()
|
self.electrsd.client.ping().unwrap();
|
||||||
|
self.electrsd.client.block_headers_pop().unwrap()
|
||||||
});
|
});
|
||||||
if header.height >= min_height {
|
if header.height >= min_height {
|
||||||
break;
|
break;
|
||||||
@@ -96,10 +112,13 @@ impl TestClient {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// broadcast through electrum so that it caches the tx immediately
|
// broadcast through electrum so that it caches the tx immediately
|
||||||
|
|
||||||
let txid = self
|
let txid = self
|
||||||
.electrum
|
.electrsd
|
||||||
|
.client
|
||||||
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
debug!("broadcasted to electrum {}", txid);
|
||||||
|
|
||||||
if let Some(num) = meta_tx.min_confirmations {
|
if let Some(num) = meta_tx.min_confirmations {
|
||||||
self.generate(num, None);
|
self.generate(num, None);
|
||||||
@@ -209,7 +228,7 @@ impl TestClient {
|
|||||||
let block_hex: String = serialize(&block).to_hex();
|
let block_hex: String = serialize(&block).to_hex();
|
||||||
debug!("generated block hex: {}", block_hex);
|
debug!("generated block hex: {}", block_hex);
|
||||||
|
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
let submit_result: serde_json::Value =
|
let submit_result: serde_json::Value =
|
||||||
self.call("submitblock", &[block_hex.into()]).unwrap();
|
self.call("submitblock", &[block_hex.into()]).unwrap();
|
||||||
@@ -237,7 +256,7 @@ impl TestClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate(&mut self, num_blocks: u64) {
|
pub fn invalidate(&mut self, num_blocks: u64) {
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
let best_hash = self.get_best_block_hash().unwrap();
|
let best_hash = self.get_best_block_hash().unwrap();
|
||||||
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
||||||
@@ -288,16 +307,25 @@ impl Deref for TestClient {
|
|||||||
type Target = RpcClient;
|
type Target = RpcClient;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.client
|
&self.bitcoind.client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestClient {
|
impl Default for TestClient {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let rpc_host_and_port =
|
let bitcoind_exe = env::var("BITCOIND_EXE")
|
||||||
env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
|
.ok()
|
||||||
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
|
.or(bitcoind::downloaded_exe_path())
|
||||||
Self::new(rpc_host_and_port, wallet)
|
.expect(
|
||||||
|
"you should provide env var BITCOIND_EXE or specifiy a bitcoind version feature",
|
||||||
|
);
|
||||||
|
let electrs_exe = env::var("ELECTRS_EXE")
|
||||||
|
.ok()
|
||||||
|
.or(electrsd::downloaded_exe_path())
|
||||||
|
.expect(
|
||||||
|
"you should provide env var ELECTRS_EXE or specifiy a electrsd version feature",
|
||||||
|
);
|
||||||
|
Self::new(bitcoind_exe, electrs_exe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,27 +345,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
/// 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
|
/// 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.
|
/// the setup required to run the tests yourself.
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! bdk_blockchain_tests {
|
macro_rules! bdk_blockchain_tests {
|
||||||
(
|
(
|
||||||
fn test_instance() -> $blockchain:ty $block:block) => {
|
fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod bdk_blockchain_tests {
|
mod bdk_blockchain_tests {
|
||||||
use $crate::bitcoin::Network;
|
use $crate::bitcoin::Network;
|
||||||
@@ -347,16 +361,17 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
use $crate::types::KeychainKind;
|
use $crate::types::KeychainKind;
|
||||||
use $crate::{Wallet, FeeRate};
|
use $crate::{Wallet, FeeRate};
|
||||||
use $crate::testutils;
|
use $crate::testutils;
|
||||||
use $crate::serial_test::serial;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn get_blockchain() -> $blockchain {
|
#[allow(unused_variables)]
|
||||||
|
fn get_blockchain(test_client: &TestClient) -> $blockchain {
|
||||||
|
$( let $test_client = test_client; )?
|
||||||
$block
|
$block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
|
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>), test_client: &TestClient) -> Wallet<$blockchain, MemoryDatabase> {
|
||||||
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
|
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain(test_client)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
|
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
|
||||||
@@ -367,7 +382,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let test_client = TestClient::default();
|
let test_client = TestClient::default();
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
|
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
|
||||||
#[cfg(feature = "test-rpc")]
|
#[cfg(feature = "test-rpc")]
|
||||||
@@ -377,7 +392,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_simple() {
|
fn test_sync_simple() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -400,7 +414,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_stop_gap_20() {
|
fn test_sync_stop_gap_20() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -418,7 +431,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_before_and_after_receive() {
|
fn test_sync_before_and_after_receive() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -436,7 +448,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_multiple_outputs_same_tx() {
|
fn test_sync_multiple_outputs_same_tx() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -458,7 +469,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_multi() {
|
fn test_sync_receive_multi() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -477,7 +487,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_address_reuse() {
|
fn test_sync_address_reuse() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -497,7 +506,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_rbf_replaced() {
|
fn test_sync_receive_rbf_replaced() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -536,7 +544,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
// doesn't work for some reason.
|
// doesn't work for some reason.
|
||||||
#[cfg(not(feature = "esplora"))]
|
#[cfg(not(feature = "esplora"))]
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_reorg_block() {
|
fn test_sync_reorg_block() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -567,7 +574,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_after_send() {
|
fn test_sync_after_send() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
println!("{}", descriptors.0);
|
println!("{}", descriptors.0);
|
||||||
@@ -588,7 +594,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let tx = psbt.extract_tx();
|
let tx = psbt.extract_tx();
|
||||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||||
wallet.broadcast(tx).unwrap();
|
wallet.broadcast(tx).unwrap();
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
||||||
|
|
||||||
@@ -597,7 +602,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_update_confirmation_time_after_generate() {
|
fn test_update_confirmation_time_after_generate() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
println!("{}", descriptors.0);
|
println!("{}", descriptors.0);
|
||||||
@@ -623,9 +627,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_outgoing_from_scratch() {
|
fn test_sync_outgoing_from_scratch() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -648,7 +650,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
@@ -667,7 +669,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_long_change_chain() {
|
fn test_sync_long_change_chain() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -698,7 +699,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
// empty wallet
|
// empty wallet
|
||||||
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||||
test_client.generate(1, Some(node_addr));
|
test_client.generate(1, Some(node_addr));
|
||||||
@@ -709,7 +710,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_basic() {
|
fn test_sync_bump_fee_basic() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -745,7 +745,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_remove_change() {
|
fn test_sync_bump_fee_remove_change() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -781,7 +780,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_add_input_simple() {
|
fn test_sync_bump_fee_add_input_simple() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -815,7 +813,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_add_input_no_change() {
|
fn test_sync_bump_fee_add_input_no_change() {
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
let node_addr = test_client.get_node_address(None);
|
let node_addr = test_client.get_node_address(None);
|
||||||
@@ -852,7 +849,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_coinbase() {
|
fn test_sync_receive_coinbase() {
|
||||||
let (wallet, _, mut test_client) = init_single_sig();
|
let (wallet, _, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
@@ -875,5 +871,10 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
( fn $fn_name:ident ($( $tt:tt )+) -> $blockchain:ty $block:block) => {
|
||||||
|
compile_error!(concat!("Invalid arguments `", stringify!($($tt)*), "` in the blockchain tests fn."));
|
||||||
|
compile_error!("Only the exact `&TestClient` type is supported, **without** any leading path items.");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
// licenses.
|
// licenses.
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[cfg(feature = "test-blockchains")]
|
#[cfg(feature = "test-blockchains")]
|
||||||
pub mod blockchain_tests;
|
pub mod blockchain_tests;
|
||||||
|
|
||||||
|
|||||||
57
src/types.rs
57
src/types.rs
@@ -10,6 +10,7 @@
|
|||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
|
use std::ops::Sub;
|
||||||
|
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
||||||
use bitcoin::{hash_types::Txid, util::psbt};
|
use bitcoin::{hash_types::Txid, util::psbt};
|
||||||
@@ -65,10 +66,31 @@ impl FeeRate {
|
|||||||
FeeRate(1.0)
|
FeeRate(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate fee rate from `fee` and weight units (`wu`).
|
||||||
|
pub fn from_wu(fee: u64, wu: usize) -> FeeRate {
|
||||||
|
Self::from_vb(fee, wu.vbytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate fee rate from `fee` and `vbytes`.
|
||||||
|
pub fn from_vb(fee: u64, vbytes: usize) -> FeeRate {
|
||||||
|
let rate = fee as f32 / vbytes as f32;
|
||||||
|
Self::from_sat_per_vb(rate)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the value as satoshi/vbyte
|
/// Return the value as satoshi/vbyte
|
||||||
pub fn as_sat_vb(&self) -> f32 {
|
pub fn as_sat_vb(&self) -> f32 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate absolute fee in Satoshis using size in weight units.
|
||||||
|
pub fn fee_wu(&self, wu: usize) -> u64 {
|
||||||
|
self.fee_vb(wu.vbytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||||
|
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||||
|
(self.as_sat_vb() * vbytes as f32).ceil() as u64
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for FeeRate {
|
impl std::default::Default for FeeRate {
|
||||||
@@ -77,6 +99,27 @@ impl std::default::Default for FeeRate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sub for FeeRate {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, other: FeeRate) -> Self::Output {
|
||||||
|
FeeRate(self.0 - other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait implemented by types that can be used to measure weight units.
|
||||||
|
pub trait Vbytes {
|
||||||
|
/// Convert weight units to virtual bytes.
|
||||||
|
fn vbytes(self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vbytes for usize {
|
||||||
|
fn vbytes(self) -> usize {
|
||||||
|
// ref: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
|
||||||
|
(self as f32 / 4.0).ceil() as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An unspent output owned by a [`Wallet`].
|
/// An unspent output owned by a [`Wallet`].
|
||||||
///
|
///
|
||||||
/// [`Wallet`]: crate::Wallet
|
/// [`Wallet`]: crate::Wallet
|
||||||
@@ -160,11 +203,23 @@ pub struct TransactionDetails {
|
|||||||
pub received: u64,
|
pub received: u64,
|
||||||
/// Sent value (sats)
|
/// Sent value (sats)
|
||||||
pub sent: u64,
|
pub sent: u64,
|
||||||
/// Fee value (sats) if available
|
/// Fee value (sats) if available.
|
||||||
|
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||||
|
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||||
|
/// funds while offline.
|
||||||
pub fee: Option<u64>,
|
pub fee: Option<u64>,
|
||||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||||
/// transaction, unconfirmed transaction contains `None`.
|
/// transaction, unconfirmed transaction contains `None`.
|
||||||
pub confirmation_time: Option<ConfirmationTime>,
|
pub confirmation_time: Option<ConfirmationTime>,
|
||||||
|
/// Whether the tx has been verified against the consensus rules
|
||||||
|
///
|
||||||
|
/// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to
|
||||||
|
/// ensure an unstrusted [`Blockchain`](crate::blockchain::Blockchain) backend can't trick the
|
||||||
|
/// wallet into using an invalid tx as an RBF template.
|
||||||
|
///
|
||||||
|
/// The check is only perfomed when the `verify` feature is enabled.
|
||||||
|
#[serde(default = "bool::default")] // default to `false` if not specified
|
||||||
|
pub verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block height and timestamp of the block containing the confirmed transaction
|
/// Block height and timestamp of the block containing the confirmed transaction
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//! # use std::str::FromStr;
|
//! # use std::str::FromStr;
|
||||||
//! # use bitcoin::*;
|
//! # use bitcoin::*;
|
||||||
//! # use bdk::wallet::coin_selection::*;
|
//! # use bdk::wallet::{self, coin_selection::*};
|
||||||
//! # use bdk::database::Database;
|
//! # use bdk::database::Database;
|
||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
|
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
//! optional_utxos: Vec<WeightedUtxo>,
|
//! optional_utxos: Vec<WeightedUtxo>,
|
||||||
//! fee_rate: FeeRate,
|
//! fee_rate: FeeRate,
|
||||||
//! amount_needed: u64,
|
//! amount_needed: u64,
|
||||||
//! fee_amount: f32,
|
//! fee_amount: u64,
|
||||||
//! ) -> Result<CoinSelectionResult, bdk::Error> {
|
//! ) -> Result<CoinSelectionResult, bdk::Error> {
|
||||||
//! let mut selected_amount = 0;
|
//! let mut selected_amount = 0;
|
||||||
//! let mut additional_weight = 0;
|
//! let mut additional_weight = 0;
|
||||||
@@ -57,9 +57,8 @@
|
|||||||
//! },
|
//! },
|
||||||
//! )
|
//! )
|
||||||
//! .collect::<Vec<_>>();
|
//! .collect::<Vec<_>>();
|
||||||
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
|
//! let additional_fees = fee_rate.fee_wu(additional_weight);
|
||||||
//! let amount_needed_with_fees =
|
//! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed;
|
||||||
//! (fee_amount + additional_fees).ceil() as u64 + amount_needed;
|
|
||||||
//! if amount_needed_with_fees > selected_amount {
|
//! if amount_needed_with_fees > selected_amount {
|
||||||
//! return Err(bdk::Error::InsufficientFunds {
|
//! return Err(bdk::Error::InsufficientFunds {
|
||||||
//! needed: amount_needed_with_fees,
|
//! needed: amount_needed_with_fees,
|
||||||
@@ -117,7 +116,7 @@ pub struct CoinSelectionResult {
|
|||||||
/// List of outputs selected for use as inputs
|
/// List of outputs selected for use as inputs
|
||||||
pub selected: Vec<Utxo>,
|
pub selected: Vec<Utxo>,
|
||||||
/// Total fee amount in satoshi
|
/// Total fee amount in satoshi
|
||||||
pub fee_amount: f32,
|
pub fee_amount: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoinSelectionResult {
|
impl CoinSelectionResult {
|
||||||
@@ -164,7 +163,7 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
|
|||||||
optional_utxos: Vec<WeightedUtxo>,
|
optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
fee_amount: f32,
|
fee_amount: u64,
|
||||||
) -> Result<CoinSelectionResult, Error>;
|
) -> Result<CoinSelectionResult, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +182,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
mut optional_utxos: Vec<WeightedUtxo>,
|
mut optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
mut fee_amount: f32,
|
mut fee_amount: u64,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
|
"amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
|
||||||
amount_needed,
|
amount_needed,
|
||||||
@@ -211,9 +208,9 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
.scan(
|
.scan(
|
||||||
(&mut selected_amount, &mut fee_amount),
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||||
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
|
if must_use || **selected_amount < amount_needed + **fee_amount {
|
||||||
**fee_amount +=
|
**fee_amount +=
|
||||||
calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||||
**selected_amount += weighted_utxo.utxo.txout().value;
|
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -230,7 +227,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let amount_needed_with_fees = amount_needed + (fee_amount.ceil() as u64);
|
let amount_needed_with_fees = amount_needed + fee_amount;
|
||||||
if selected_amount < amount_needed_with_fees {
|
if selected_amount < amount_needed_with_fees {
|
||||||
return Err(Error::InsufficientFunds {
|
return Err(Error::InsufficientFunds {
|
||||||
needed: amount_needed_with_fees,
|
needed: amount_needed_with_fees,
|
||||||
@@ -250,16 +247,15 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
struct OutputGroup {
|
struct OutputGroup {
|
||||||
weighted_utxo: WeightedUtxo,
|
weighted_utxo: WeightedUtxo,
|
||||||
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate
|
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate
|
||||||
fee: f32,
|
fee: u64,
|
||||||
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
|
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
|
||||||
effective_value: i64,
|
effective_value: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputGroup {
|
impl OutputGroup {
|
||||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||||
let fee = (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as f32 / 4.0
|
let fee = fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||||
* fee_rate.as_sat_vb();
|
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||||
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
|
|
||||||
OutputGroup {
|
OutputGroup {
|
||||||
weighted_utxo,
|
weighted_utxo,
|
||||||
fee,
|
fee,
|
||||||
@@ -302,7 +298,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
optional_utxos: Vec<WeightedUtxo>,
|
optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
fee_amount: f32,
|
fee_amount: u64,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
// Mapping every (UTXO, usize) to an output group
|
// Mapping every (UTXO, usize) to an output group
|
||||||
let required_utxos: Vec<OutputGroup> = required_utxos
|
let required_utxos: Vec<OutputGroup> = required_utxos
|
||||||
@@ -324,7 +320,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let actual_target = fee_amount.ceil() as u64 + amount_needed;
|
let actual_target = fee_amount + amount_needed;
|
||||||
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
|
let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb();
|
||||||
|
|
||||||
let expected = (curr_available_value + curr_value)
|
let expected = (curr_available_value + curr_value)
|
||||||
@@ -344,6 +340,14 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
.try_into()
|
.try_into()
|
||||||
.expect("Bitcoin amount to fit into i64");
|
.expect("Bitcoin amount to fit into i64");
|
||||||
|
|
||||||
|
if curr_value > actual_target {
|
||||||
|
return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
|
||||||
|
vec![],
|
||||||
|
required_utxos,
|
||||||
|
fee_amount,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(self
|
Ok(self
|
||||||
.bnb(
|
.bnb(
|
||||||
required_utxos.clone(),
|
required_utxos.clone(),
|
||||||
@@ -377,7 +381,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
mut curr_value: i64,
|
mut curr_value: i64,
|
||||||
mut curr_available_value: i64,
|
mut curr_available_value: i64,
|
||||||
actual_target: i64,
|
actual_target: i64,
|
||||||
fee_amount: f32,
|
fee_amount: u64,
|
||||||
cost_of_change: f32,
|
cost_of_change: f32,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
// current_selection[i] will contain true if we are using optional_utxos[i],
|
// current_selection[i] will contain true if we are using optional_utxos[i],
|
||||||
@@ -485,7 +489,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
mut optional_utxos: Vec<OutputGroup>,
|
mut optional_utxos: Vec<OutputGroup>,
|
||||||
curr_value: i64,
|
curr_value: i64,
|
||||||
actual_target: i64,
|
actual_target: i64,
|
||||||
fee_amount: f32,
|
fee_amount: u64,
|
||||||
) -> CoinSelectionResult {
|
) -> CoinSelectionResult {
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
optional_utxos.shuffle(&mut thread_rng());
|
optional_utxos.shuffle(&mut thread_rng());
|
||||||
@@ -514,10 +518,10 @@ impl BranchAndBoundCoinSelection {
|
|||||||
fn calculate_cs_result(
|
fn calculate_cs_result(
|
||||||
mut selected_utxos: Vec<OutputGroup>,
|
mut selected_utxos: Vec<OutputGroup>,
|
||||||
mut required_utxos: Vec<OutputGroup>,
|
mut required_utxos: Vec<OutputGroup>,
|
||||||
mut fee_amount: f32,
|
mut fee_amount: u64,
|
||||||
) -> CoinSelectionResult {
|
) -> CoinSelectionResult {
|
||||||
selected_utxos.append(&mut required_utxos);
|
selected_utxos.append(&mut required_utxos);
|
||||||
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
|
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<u64>();
|
||||||
let selected = selected_utxos
|
let selected = selected_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| u.weighted_utxo.utxo)
|
.map(|u| u.weighted_utxo.utxo)
|
||||||
@@ -539,6 +543,7 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::MemoryDatabase;
|
use crate::database::MemoryDatabase;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
use crate::wallet::Vbytes;
|
||||||
|
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
@@ -546,52 +551,33 @@ mod test {
|
|||||||
|
|
||||||
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
||||||
|
|
||||||
const FEE_AMOUNT: f32 = 50.0;
|
const FEE_AMOUNT: u64 = 50;
|
||||||
|
|
||||||
|
fn utxo(value: u64, index: u32) -> WeightedUtxo {
|
||||||
|
assert!(index < 10);
|
||||||
|
let outpoint = OutPoint::from_str(&format!(
|
||||||
|
"000000000000000000000000000000000000000000000000000000000000000{}:0",
|
||||||
|
index
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
WeightedUtxo {
|
||||||
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
|
outpoint,
|
||||||
|
txout: TxOut {
|
||||||
|
value,
|
||||||
|
script_pubkey: Script::new(),
|
||||||
|
},
|
||||||
|
keychain: KeychainKind::External,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_test_utxos() -> Vec<WeightedUtxo> {
|
fn get_test_utxos() -> Vec<WeightedUtxo> {
|
||||||
vec![
|
vec![
|
||||||
WeightedUtxo {
|
utxo(100_000, 0),
|
||||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
utxo(FEE_AMOUNT as u64 - 40, 1),
|
||||||
utxo: Utxo::Local(LocalUtxo {
|
utxo(200_000, 2),
|
||||||
outpoint: OutPoint::from_str(
|
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000:0",
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
txout: TxOut {
|
|
||||||
value: 100_000,
|
|
||||||
script_pubkey: Script::new(),
|
|
||||||
},
|
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
WeightedUtxo {
|
|
||||||
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
|
||||||
utxo: Utxo::Local(LocalUtxo {
|
|
||||||
outpoint: OutPoint::from_str(
|
|
||||||
"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 {
|
|
||||||
value: 200_000,
|
|
||||||
script_pubkey: Script::new(),
|
|
||||||
},
|
|
||||||
keychain: KeychainKind::Internal,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,13 +641,13 @@ mod test {
|
|||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
250_000,
|
250_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount(), 300_010);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 254)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -676,13 +662,13 @@ mod test {
|
|||||||
vec![],
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
20_000,
|
20_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount(), 300_010);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -697,13 +683,13 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
20_000,
|
20_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount(), 200_000);
|
assert_eq!(result.selected_amount(), 200_000);
|
||||||
assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 118);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -719,7 +705,7 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
500_000,
|
500_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -737,7 +723,7 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
250_000,
|
250_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -757,13 +743,13 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
250_000,
|
250_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount(), 300_000);
|
assert_eq!(result.selected_amount(), 300_000);
|
||||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -784,7 +770,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount(), 300_010);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -805,7 +791,38 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount(), 300010);
|
assert_eq!(result.selected_amount(), 300010);
|
||||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
assert_eq!(result.fee_amount, 254);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bnb_coin_selection_required_not_enough() {
|
||||||
|
let utxos = get_test_utxos();
|
||||||
|
let database = MemoryDatabase::default();
|
||||||
|
|
||||||
|
let required = vec![utxos[0].clone()];
|
||||||
|
let mut optional = utxos[1..].to_vec();
|
||||||
|
optional.push(utxo(500_000, 3));
|
||||||
|
|
||||||
|
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
|
||||||
|
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
|
||||||
|
assert_eq!(amount, 100_000);
|
||||||
|
let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
|
||||||
|
assert!(amount > 150_000);
|
||||||
|
|
||||||
|
let result = BranchAndBoundCoinSelection::default()
|
||||||
|
.coin_select(
|
||||||
|
&database,
|
||||||
|
required,
|
||||||
|
optional,
|
||||||
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
|
150_000,
|
||||||
|
FEE_AMOUNT,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.selected.len(), 3);
|
||||||
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
|
assert!((result.fee_amount as f32 - 254.0).abs() < f32::EPSILON);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -821,7 +838,7 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
500_000,
|
500_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -839,7 +856,7 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
250_000,
|
250_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -856,15 +873,15 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
99932, // first utxo's effective value
|
99932, // first utxo's effective value
|
||||||
0.0,
|
0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount(), 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
let input_size = (TXIN_BASE_WEIGHT as f32) / 4.0 + P2WPKH_WITNESS_SIZE as f32 / 4.0;
|
let input_size = (TXIN_BASE_WEIGHT + P2WPKH_WITNESS_SIZE).vbytes();
|
||||||
let epsilon = 0.5;
|
let epsilon = 0.5;
|
||||||
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
|
assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < epsilon);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -883,7 +900,7 @@ mod test {
|
|||||||
optional_utxos,
|
optional_utxos,
|
||||||
FeeRate::from_sat_per_vb(0.0),
|
FeeRate::from_sat_per_vb(0.0),
|
||||||
target_amount,
|
target_amount,
|
||||||
0.0,
|
0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.selected_amount(), target_amount);
|
assert_eq!(result.selected_amount(), target_amount);
|
||||||
@@ -910,7 +927,7 @@ mod test {
|
|||||||
0,
|
0,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
20_000,
|
20_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
cost_of_change,
|
cost_of_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -937,7 +954,7 @@ mod test {
|
|||||||
0,
|
0,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
20_000,
|
20_000,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
cost_of_change,
|
cost_of_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -949,7 +966,6 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let size_of_change = 31;
|
let size_of_change = 31;
|
||||||
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
|
let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb();
|
||||||
let fee_amount = 50.0;
|
|
||||||
|
|
||||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -971,12 +987,12 @@ mod test {
|
|||||||
curr_value,
|
curr_value,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
target_amount,
|
target_amount,
|
||||||
fee_amount,
|
FEE_AMOUNT,
|
||||||
cost_of_change,
|
cost_of_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
|
|
||||||
assert_eq!(result.selected_amount(), 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
|
assert_eq!(result.fee_amount, 186);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: bnb() function should be optimized, and this test should be done with more utxos
|
// TODO: bnb() function should be optimized, and this test should be done with more utxos
|
||||||
@@ -1008,7 +1024,7 @@ mod test {
|
|||||||
curr_value,
|
curr_value,
|
||||||
curr_available_value,
|
curr_available_value,
|
||||||
target_amount,
|
target_amount,
|
||||||
0.0,
|
0,
|
||||||
0.0,
|
0.0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1034,12 +1050,10 @@ mod test {
|
|||||||
utxos,
|
utxos,
|
||||||
0,
|
0,
|
||||||
target_amount as i64,
|
target_amount as i64,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(result.selected_amount() > target_amount);
|
assert!(result.selected_amount() > target_amount);
|
||||||
assert!(
|
assert_eq!(result.fee_amount, (50 + result.selected.len() * 68) as u64);
|
||||||
(result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ mod test {
|
|||||||
timestamp: 12345678,
|
timestamp: 12345678,
|
||||||
height: 5000,
|
height: 5000,
|
||||||
}),
|
}),
|
||||||
|
verified: true,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ pub mod signer;
|
|||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod tx_builder;
|
pub mod tx_builder;
|
||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
|
||||||
|
pub mod verify;
|
||||||
|
|
||||||
pub use utils::IsDust;
|
pub use utils::IsDust;
|
||||||
|
|
||||||
@@ -540,7 +543,7 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(FeeRate::from_sat_per_vb(0.0), *fee as f32)
|
(FeeRate::from_sat_per_vb(0.0), *fee)
|
||||||
}
|
}
|
||||||
FeePolicy::FeeRate(rate) => {
|
FeePolicy::FeeRate(rate) => {
|
||||||
if let Some(previous_fee) = params.bumping_fee {
|
if let Some(previous_fee) = params.bumping_fee {
|
||||||
@@ -551,7 +554,7 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(*rate, 0.0)
|
(*rate, 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -562,21 +565,6 @@ where
|
|||||||
output: vec![],
|
output: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let recipients = match ¶ms.single_recipient {
|
|
||||||
Some(recipient) => vec![(recipient, 0)],
|
|
||||||
None => params.recipients.iter().map(|(r, v)| (r, *v)).collect(),
|
|
||||||
};
|
|
||||||
if params.single_recipient.is_some()
|
|
||||||
&& !params.manually_selected_only
|
|
||||||
&& !params.drain_wallet
|
|
||||||
&& params.bumping_fee.is_none()
|
|
||||||
{
|
|
||||||
return Err(Error::SingleRecipientNoInputs);
|
|
||||||
}
|
|
||||||
if recipients.is_empty() {
|
|
||||||
return Err(Error::NoRecipients);
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.manually_selected_only && params.utxos.is_empty() {
|
if params.manually_selected_only && params.utxos.is_empty() {
|
||||||
return Err(Error::NoUtxosSelected);
|
return Err(Error::NoUtxosSelected);
|
||||||
}
|
}
|
||||||
@@ -585,15 +573,14 @@ where
|
|||||||
let mut outgoing: u64 = 0;
|
let mut outgoing: u64 = 0;
|
||||||
let mut received: u64 = 0;
|
let mut received: u64 = 0;
|
||||||
|
|
||||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
fee_amount += fee_rate.fee_wu(tx.get_weight());
|
||||||
fee_amount += calc_fee_bytes(tx.get_weight());
|
|
||||||
|
|
||||||
for (index, (script_pubkey, satoshi)) in recipients.into_iter().enumerate() {
|
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
|
||||||
let value = match params.single_recipient {
|
|
||||||
Some(_) => 0,
|
for (index, (script_pubkey, value)) in recipients.enumerate() {
|
||||||
None if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
|
if value.is_dust() {
|
||||||
None => satoshi,
|
return Err(Error::OutputBelowDustLimit(index));
|
||||||
};
|
}
|
||||||
|
|
||||||
if self.is_mine(script_pubkey)? {
|
if self.is_mine(script_pubkey)? {
|
||||||
received += value;
|
received += value;
|
||||||
@@ -603,7 +590,7 @@ where
|
|||||||
script_pubkey: script_pubkey.clone(),
|
script_pubkey: script_pubkey.clone(),
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
fee_amount += calc_fee_bytes(serialize(&new_out).len() * 4);
|
fee_amount += fee_rate.fee_vb(serialize(&new_out).len());
|
||||||
|
|
||||||
tx.output.push(new_out);
|
tx.output.push(new_out);
|
||||||
|
|
||||||
@@ -648,54 +635,46 @@ where
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// prepare the change output
|
// prepare the drain output
|
||||||
let change_output = match params.single_recipient {
|
let mut drain_output = {
|
||||||
Some(_) => None,
|
let script_pubkey = match params.drain_to {
|
||||||
None => {
|
Some(ref drain_recipient) => drain_recipient.clone(),
|
||||||
let change_script = self.get_change_address()?;
|
None => self.get_change_address()?,
|
||||||
let change_output = TxOut {
|
};
|
||||||
script_pubkey: change_script,
|
|
||||||
value: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// take the change into account for fees
|
TxOut {
|
||||||
fee_amount += calc_fee_bytes(serialize(&change_output).len() * 4);
|
script_pubkey,
|
||||||
Some(change_output)
|
value: 0,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut fee_amount = fee_amount.ceil() as u64;
|
|
||||||
let change_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
|
||||||
|
|
||||||
match change_output {
|
fee_amount += fee_rate.fee_vb(serialize(&drain_output).len());
|
||||||
None if change_val.is_dust() => {
|
|
||||||
// single recipient, but the only output would be below dust limit
|
|
||||||
// TODO: or OutputBelowDustLimit?
|
|
||||||
return Err(Error::InsufficientFunds {
|
|
||||||
needed: DUST_LIMIT_SATOSHI,
|
|
||||||
available: change_val,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(_) if change_val.is_dust() => {
|
|
||||||
// skip the change output because it's dust -- just include it in the fee.
|
|
||||||
fee_amount += change_val;
|
|
||||||
}
|
|
||||||
Some(mut change_output) => {
|
|
||||||
change_output.value = change_val;
|
|
||||||
received += change_val;
|
|
||||||
|
|
||||||
tx.output.push(change_output);
|
let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// there's only one output, send everything to it
|
|
||||||
tx.output[0].value = change_val;
|
|
||||||
|
|
||||||
// the single recipient is our address
|
if tx.output.is_empty() {
|
||||||
if self.is_mine(&tx.output[0].script_pubkey)? {
|
if params.drain_to.is_some() {
|
||||||
received = change_val;
|
if drain_val.is_dust() {
|
||||||
|
return Err(Error::InsufficientFunds {
|
||||||
|
needed: DUST_LIMIT_SATOSHI,
|
||||||
|
available: drain_val,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::NoRecipients);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if drain_val.is_dust() {
|
||||||
|
fee_amount += drain_val;
|
||||||
|
} else {
|
||||||
|
drain_output.value = drain_val;
|
||||||
|
if self.is_mine(&drain_output.script_pubkey)? {
|
||||||
|
received += drain_val;
|
||||||
|
}
|
||||||
|
tx.output.push(drain_output);
|
||||||
|
}
|
||||||
|
|
||||||
// sort input/outputs according to the chosen algorithm
|
// sort input/outputs according to the chosen algorithm
|
||||||
params.ordering.sort_tx(&mut tx);
|
params.ordering.sort_tx(&mut tx);
|
||||||
|
|
||||||
@@ -710,6 +689,7 @@ where
|
|||||||
received,
|
received,
|
||||||
sent,
|
sent,
|
||||||
fee: Some(fee_amount),
|
fee: Some(fee_amount),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((psbt, transaction_details))
|
Ok((psbt, transaction_details))
|
||||||
@@ -721,10 +701,6 @@ where
|
|||||||
/// *repalce by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`]
|
/// *repalce by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`]
|
||||||
/// pre-populated with the inputs and outputs of the original transaction.
|
/// pre-populated with the inputs and outputs of the original transaction.
|
||||||
///
|
///
|
||||||
/// **NOTE**: if the original transaction was made with [`TxBuilder::set_single_recipient`],
|
|
||||||
/// the [`TxBuilder::maintain_single_recipient`] flag should be enabled to correctly reduce the
|
|
||||||
/// only output's value in order to increase the fees.
|
|
||||||
///
|
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@@ -776,8 +752,10 @@ where
|
|||||||
return Err(Error::IrreplaceableTransaction);
|
return Err(Error::IrreplaceableTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
let vbytes = tx.get_weight() as f32 / 4.0;
|
let feerate = FeeRate::from_wu(
|
||||||
let feerate = details.fee.ok_or(Error::FeeRateUnavailable)? as f32 / vbytes;
|
details.fee.ok_or(Error::FeeRateUnavailable)?,
|
||||||
|
tx.get_weight(),
|
||||||
|
);
|
||||||
|
|
||||||
// remove the inputs from the tx and process them
|
// remove the inputs from the tx and process them
|
||||||
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
|
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
|
||||||
@@ -854,7 +832,7 @@ where
|
|||||||
utxos: original_utxos,
|
utxos: original_utxos,
|
||||||
bumping_fee: Some(tx_builder::PreviousFee {
|
bumping_fee: Some(tx_builder::PreviousFee {
|
||||||
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
|
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
|
||||||
rate: feerate,
|
rate: feerate.as_sat_vb(),
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1522,18 +1500,33 @@ where
|
|||||||
// TODO: what if i generate an address first and cache some addresses?
|
// TODO: what if i generate an address first and cache some addresses?
|
||||||
// TODO: we should sync if generating an address triggers a new batch to be stored
|
// TODO: we should sync if generating an address triggers a new batch to be stored
|
||||||
if run_setup {
|
if run_setup {
|
||||||
maybe_await!(self.client.setup(
|
maybe_await!(self
|
||||||
None,
|
.client
|
||||||
self.database.borrow_mut().deref_mut(),
|
.setup(self.database.borrow_mut().deref_mut(), progress_update,))?;
|
||||||
progress_update,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
maybe_await!(self.client.sync(
|
maybe_await!(self
|
||||||
None,
|
.client
|
||||||
self.database.borrow_mut().deref_mut(),
|
.sync(self.database.borrow_mut().deref_mut(), progress_update,))?;
|
||||||
progress_update,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
{
|
||||||
|
debug!("Verifying transactions...");
|
||||||
|
for mut tx in self.database.borrow().iter_txs(true)? {
|
||||||
|
if !tx.verified {
|
||||||
|
verify::verify_tx(
|
||||||
|
tx.transaction.as_ref().ok_or(Error::TransactionNotFound)?,
|
||||||
|
self.database.borrow().deref(),
|
||||||
|
&self.client,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tx.verified = true;
|
||||||
|
self.database.borrow_mut().set_tx(&tx)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a reference to the internal blockchain client
|
/// Return a reference to the internal blockchain client
|
||||||
@@ -1741,13 +1734,13 @@ pub(crate) mod test {
|
|||||||
dust_change = true;
|
dust_change = true;
|
||||||
)*
|
)*
|
||||||
|
|
||||||
let tx_fee_rate = $fees as f32 / (tx.get_weight() as f32 / 4.0);
|
let tx_fee_rate = FeeRate::from_wu($fees, tx.get_weight());
|
||||||
let fee_rate = $fee_rate.as_sat_vb();
|
let fee_rate = $fee_rate;
|
||||||
|
|
||||||
if !dust_change {
|
if !dust_change {
|
||||||
assert!((tx_fee_rate - fee_rate).abs() < 0.5, "Expected fee rate of {}, the tx has {}", fee_rate, tx_fee_rate);
|
assert!((tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||||
} else {
|
} else {
|
||||||
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {}, the tx has {}", fee_rate, tx_fee_rate);
|
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1973,13 +1966,11 @@ pub(crate) mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_tx_single_recipient_drain_wallet() {
|
fn test_create_tx_drain_wallet_and_drain_to() {
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, details) = builder.finish().unwrap();
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
|
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
|
||||||
@@ -1989,6 +1980,33 @@ pub(crate) mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
|
let drain_addr = wallet.get_address(New).unwrap();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder
|
||||||
|
.add_recipient(addr.script_pubkey(), 20_000)
|
||||||
|
.drain_to(drain_addr.script_pubkey())
|
||||||
|
.drain_wallet();
|
||||||
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
|
dbg!(&psbt);
|
||||||
|
let outputs = psbt.global.unsigned_tx.output;
|
||||||
|
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
let main_output = outputs
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.script_pubkey == addr.script_pubkey())
|
||||||
|
.unwrap();
|
||||||
|
let drain_output = outputs
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.script_pubkey == drain_addr.script_pubkey())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(main_output.value, 20_000,);
|
||||||
|
assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_tx_default_fee_rate() {
|
fn test_create_tx_default_fee_rate() {
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
@@ -2019,7 +2037,7 @@ pub(crate) mod test {
|
|||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.fee_absolute(100);
|
.fee_absolute(100);
|
||||||
let (psbt, details) = builder.finish().unwrap();
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
@@ -2038,7 +2056,7 @@ pub(crate) mod test {
|
|||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.fee_absolute(0);
|
.fee_absolute(0);
|
||||||
let (psbt, details) = builder.finish().unwrap();
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
@@ -2058,7 +2076,7 @@ pub(crate) mod test {
|
|||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.fee_absolute(60_000);
|
.fee_absolute(60_000);
|
||||||
let (_psbt, _details) = builder.finish().unwrap();
|
let (_psbt, _details) = builder.finish().unwrap();
|
||||||
@@ -2099,13 +2117,13 @@ pub(crate) mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "InsufficientFunds")]
|
#[should_panic(expected = "InsufficientFunds")]
|
||||||
fn test_create_tx_single_recipient_dust_amount() {
|
fn test_create_tx_drain_to_dust_amount() {
|
||||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
// very high fee rate, so that the only output would be below dust
|
// very high fee rate, so that the only output would be below dust
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb(453.0));
|
.fee_rate(FeeRate::from_sat_per_vb(453.0));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
@@ -2166,9 +2184,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
|
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
|
assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
|
||||||
@@ -2192,9 +2208,7 @@ pub(crate) mod test {
|
|||||||
|
|
||||||
let addr = testutils!(@external descriptors, 5);
|
let addr = testutils!(@external descriptors, 5);
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
|
assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
|
||||||
@@ -2215,9 +2229,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2240,9 +2252,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert_eq!(psbt.inputs[0].redeem_script, None);
|
assert_eq!(psbt.inputs[0].redeem_script, None);
|
||||||
@@ -2265,9 +2275,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
|
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let script = Script::from(
|
let script = Script::from(
|
||||||
@@ -2287,9 +2295,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
||||||
@@ -2303,7 +2309,7 @@ pub(crate) mod test {
|
|||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.only_witness_utxo()
|
.only_witness_utxo()
|
||||||
.drain_wallet();
|
.drain_wallet();
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
@@ -2318,9 +2324,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert!(psbt.inputs[0].witness_utxo.is_some());
|
assert!(psbt.inputs[0].witness_utxo.is_some());
|
||||||
@@ -2332,9 +2336,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (psbt, _) = builder.finish().unwrap();
|
let (psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
||||||
@@ -2968,7 +2970,7 @@ pub(crate) mod test {
|
|||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.enable_rbf();
|
.enable_rbf();
|
||||||
let (psbt, mut original_details) = builder.finish().unwrap();
|
let (psbt, mut original_details) = builder.finish().unwrap();
|
||||||
@@ -2992,7 +2994,7 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.fee_rate(FeeRate::from_sat_per_vb(2.5))
|
.fee_rate(FeeRate::from_sat_per_vb(2.5))
|
||||||
.maintain_single_recipient()
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (psbt, details) = builder.finish().unwrap();
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
|
|
||||||
@@ -3012,7 +3014,7 @@ pub(crate) mod test {
|
|||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.enable_rbf();
|
.enable_rbf();
|
||||||
let (psbt, mut original_details) = builder.finish().unwrap();
|
let (psbt, mut original_details) = builder.finish().unwrap();
|
||||||
@@ -3035,7 +3037,7 @@ pub(crate) mod test {
|
|||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.maintain_single_recipient()
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fee_absolute(300);
|
.fee_absolute(300);
|
||||||
let (psbt, details) = builder.finish().unwrap();
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
@@ -3066,7 +3068,7 @@ pub(crate) mod test {
|
|||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.add_utxo(outpoint)
|
.add_utxo(outpoint)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.manually_selected_only()
|
.manually_selected_only()
|
||||||
@@ -3095,7 +3097,7 @@ pub(crate) mod test {
|
|||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
.drain_wallet()
|
.drain_wallet()
|
||||||
.maintain_single_recipient()
|
.allow_shrinking(addr.script_pubkey())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
let (_, details) = builder.finish().unwrap();
|
let (_, details) = builder.finish().unwrap();
|
||||||
@@ -3110,7 +3112,7 @@ pub(crate) mod test {
|
|||||||
// them, and make sure that `bump_fee` doesn't try to add more. This fails because we've
|
// them, and make sure that `bump_fee` doesn't try to add more. This fails because we've
|
||||||
// told the wallet it's not allowed to add more inputs AND it can't reduce the value of the
|
// told the wallet it's not allowed to add more inputs AND it can't reduce the value of the
|
||||||
// existing output. In other words, bump_fee + manually_selected_only is always an error
|
// existing output. In other words, bump_fee + manually_selected_only is always an error
|
||||||
// unless you've also set "maintain_single_recipient".
|
// unless you've also set "allow_shrinking" OR there is a change output.
|
||||||
let incoming_txid = crate::populate_test_db!(
|
let incoming_txid = crate::populate_test_db!(
|
||||||
wallet.database.borrow_mut(),
|
wallet.database.borrow_mut(),
|
||||||
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
|
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
|
||||||
@@ -3123,7 +3125,7 @@ pub(crate) mod test {
|
|||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.add_utxo(outpoint)
|
.add_utxo(outpoint)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.manually_selected_only()
|
.manually_selected_only()
|
||||||
@@ -3289,11 +3291,11 @@ pub(crate) mod test {
|
|||||||
Some(100),
|
Some(100),
|
||||||
);
|
);
|
||||||
|
|
||||||
// initially make a tx without change by using `set_single_recipient`
|
// initially make a tx without change by using `drain_to`
|
||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.add_utxo(OutPoint {
|
.add_utxo(OutPoint {
|
||||||
txid: incoming_txid,
|
txid: incoming_txid,
|
||||||
vout: 0,
|
vout: 0,
|
||||||
@@ -3321,7 +3323,7 @@ pub(crate) mod test {
|
|||||||
.set_tx(&original_details)
|
.set_tx(&original_details)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// now bump the fees without using `maintain_single_recipient`. the wallet should add an
|
// now bump the fees without using `allow_shrinking`. the wallet should add an
|
||||||
// extra input and a change output, and leave the original output untouched
|
// extra input and a change output, and leave the original output untouched
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
|
||||||
@@ -3567,9 +3569,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
@@ -3584,9 +3584,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
@@ -3601,9 +3599,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
|
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
@@ -3618,9 +3614,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
|
let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
@@ -3636,9 +3630,7 @@ pub(crate) mod test {
|
|||||||
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
|
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
@@ -3653,9 +3645,7 @@ pub(crate) mod test {
|
|||||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||||
.set_single_recipient(addr.script_pubkey())
|
|
||||||
.drain_wallet();
|
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
psbt.inputs[0].bip32_derivation.clear();
|
psbt.inputs[0].bip32_derivation.clear();
|
||||||
@@ -3737,7 +3727,7 @@ pub(crate) mod test {
|
|||||||
let addr = wallet.get_address(New).unwrap();
|
let addr = wallet.get_address(New).unwrap();
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.set_single_recipient(addr.script_pubkey())
|
.drain_to(addr.script_pubkey())
|
||||||
.sighash(sighash)
|
.sighash(sighash)
|
||||||
.drain_wallet();
|
.drain_wallet();
|
||||||
let (mut psbt, _) = builder.finish().unwrap();
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ pub struct TxBuilder<'a, B, D, Cs, Ctx> {
|
|||||||
pub(crate) struct TxParams {
|
pub(crate) struct TxParams {
|
||||||
pub(crate) recipients: Vec<(Script, u64)>,
|
pub(crate) recipients: Vec<(Script, u64)>,
|
||||||
pub(crate) drain_wallet: bool,
|
pub(crate) drain_wallet: bool,
|
||||||
pub(crate) single_recipient: Option<Script>,
|
pub(crate) drain_to: Option<Script>,
|
||||||
pub(crate) fee_policy: Option<FeePolicy>,
|
pub(crate) fee_policy: Option<FeePolicy>,
|
||||||
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||||
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||||
@@ -560,49 +560,81 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a single recipient that will get all the selected funds minus the fee. No change will
|
/// Sets the address to *drain* excess coins to.
|
||||||
/// be created
|
|
||||||
///
|
///
|
||||||
/// This method overrides any recipient set with [`set_recipients`](Self::set_recipients) or
|
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||||
/// [`add_recipient`](Self::add_recipient).
|
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
|
||||||
|
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
|
||||||
|
/// coins are too small) it will not be included in the resulting transaction. The only
|
||||||
|
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||||
|
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||||
///
|
///
|
||||||
/// It can only be used in conjunction with [`drain_wallet`](Self::drain_wallet) to send the
|
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||||
/// entire content of the wallet (minus filters) to a single recipient or with a
|
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||||
/// list of manually selected UTXOs by enabling [`manually_selected_only`](Self::manually_selected_only)
|
|
||||||
/// and selecting them with or [`add_utxo`](Self::add_utxo).
|
|
||||||
///
|
///
|
||||||
/// When bumping the fees of a transaction made with this option, the user should remeber to
|
/// # Example
|
||||||
/// add [`maintain_single_recipient`](Self::maintain_single_recipient) to correctly update the
|
///
|
||||||
/// single output instead of adding one more for the change.
|
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||||
pub fn set_single_recipient(&mut self, recipient: Script) -> &mut Self {
|
/// single address.
|
||||||
self.params.single_recipient = Some(recipient);
|
///
|
||||||
self.params.recipients.clear();
|
/// ```
|
||||||
|
/// # use std::str::FromStr;
|
||||||
|
/// # use bitcoin::*;
|
||||||
|
/// # use bdk::*;
|
||||||
|
/// # use bdk::wallet::tx_builder::CreateTx;
|
||||||
|
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
|
/// # let wallet = doctest_wallet!();
|
||||||
|
/// let mut tx_builder = wallet.build_tx();
|
||||||
|
///
|
||||||
|
/// tx_builder
|
||||||
|
/// // Spend all outputs in this wallet.
|
||||||
|
/// .drain_wallet()
|
||||||
|
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||||
|
/// .drain_to(to_address.script_pubkey())
|
||||||
|
/// .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||||
|
/// .enable_rbf();
|
||||||
|
/// let (psbt, tx_details) = tx_builder.finish()?;
|
||||||
|
/// # Ok::<(), bdk::Error>(())
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||||
|
/// [`add_recipient`]: Self::add_recipient
|
||||||
|
/// [`drain_wallet`]: Self::drain_wallet
|
||||||
|
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
||||||
|
self.params.drain_to = Some(script_pubkey);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// methods supported only by bump_fee
|
// methods supported only by bump_fee
|
||||||
impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||||
/// Bump the fees of a transaction made with [`set_single_recipient`](Self::set_single_recipient)
|
/// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
|
||||||
|
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||||
|
/// will attempt to find a change output to shrink instead.
|
||||||
///
|
///
|
||||||
/// Unless extra inputs are specified with [`add_utxo`], this flag will make
|
/// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is
|
||||||
/// `bump_fee` reduce the value of the existing output, or fail if it would be consumed
|
/// preserved then it is currently not guaranteed to be in the same position as it was
|
||||||
/// entirely given the higher new fee rate.
|
/// originally.
|
||||||
///
|
///
|
||||||
/// If extra inputs are added and they are not entirely consumed in fees, a change output will not
|
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
|
||||||
/// be added; the existing output will simply grow in value.
|
/// transaction we are bumping.
|
||||||
///
|
pub fn allow_shrinking(&mut self, script_pubkey: Script) -> Result<&mut Self, Error> {
|
||||||
/// Fails if the transaction has more than one outputs.
|
match self
|
||||||
///
|
.params
|
||||||
/// [`add_utxo`]: Self::add_utxo
|
.recipients
|
||||||
pub fn maintain_single_recipient(&mut self) -> Result<&mut Self, Error> {
|
.iter()
|
||||||
let mut recipients = self.params.recipients.drain(..).collect::<Vec<_>>();
|
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
|
||||||
if recipients.len() != 1 {
|
{
|
||||||
return Err(Error::SingleRecipientMultipleOutputs);
|
Some(position) => {
|
||||||
|
self.params.recipients.remove(position);
|
||||||
|
self.params.drain_to = Some(script_pubkey);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
None => Err(Error::Generic(format!(
|
||||||
|
"{} was not in the original transaction",
|
||||||
|
script_pubkey
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
self.params.single_recipient = Some(recipients.pop().unwrap().0);
|
|
||||||
Ok(self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
185
src/wallet/verify.rs
Normal file
185
src/wallet/verify.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2021 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.
|
||||||
|
|
||||||
|
//! Verify transactions against the consensus rules
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use bitcoin::consensus::serialize;
|
||||||
|
use bitcoin::{OutPoint, Transaction, Txid};
|
||||||
|
|
||||||
|
use crate::blockchain::Blockchain;
|
||||||
|
use crate::database::Database;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
/// Verify a transaction against the consensus rules
|
||||||
|
///
|
||||||
|
/// This function uses [`bitcoinconsensus`] to verify transactions by fetching the required data
|
||||||
|
/// either from the [`Database`] or using the [`Blockchain`].
|
||||||
|
///
|
||||||
|
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
||||||
|
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
||||||
|
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
||||||
|
pub fn verify_tx<D: Database, B: Blockchain>(
|
||||||
|
tx: &Transaction,
|
||||||
|
database: &D,
|
||||||
|
blockchain: &B,
|
||||||
|
) -> Result<(), VerifyError> {
|
||||||
|
log::debug!("Verifying {}", tx.txid());
|
||||||
|
|
||||||
|
let serialized_tx = serialize(tx);
|
||||||
|
let mut tx_cache = HashMap::<_, Transaction>::new();
|
||||||
|
|
||||||
|
for (index, input) in tx.input.iter().enumerate() {
|
||||||
|
let prev_tx = if let Some(prev_tx) = tx_cache.get(&input.previous_output.txid) {
|
||||||
|
prev_tx.clone()
|
||||||
|
} else if let Some(prev_tx) = database.get_raw_tx(&input.previous_output.txid)? {
|
||||||
|
prev_tx
|
||||||
|
} else if let Some(prev_tx) = blockchain.get_tx(&input.previous_output.txid)? {
|
||||||
|
prev_tx
|
||||||
|
} else {
|
||||||
|
return Err(VerifyError::MissingInputTx(input.previous_output.txid));
|
||||||
|
};
|
||||||
|
|
||||||
|
let spent_output = prev_tx
|
||||||
|
.output
|
||||||
|
.get(input.previous_output.vout as usize)
|
||||||
|
.ok_or(VerifyError::InvalidInput(input.previous_output))?;
|
||||||
|
|
||||||
|
bitcoinconsensus::verify(
|
||||||
|
&spent_output.script_pubkey.to_bytes(),
|
||||||
|
spent_output.value,
|
||||||
|
&serialized_tx,
|
||||||
|
index,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Since we have a local cache we might as well cache stuff from the db, as it will very
|
||||||
|
// likely decrease latency compared to reading from disk or performing an SQL query.
|
||||||
|
tx_cache.insert(prev_tx.txid(), prev_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error during validation of a tx agains the consensus rules
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum VerifyError {
|
||||||
|
/// The transaction being spent is not available in the database or the blockchain client
|
||||||
|
MissingInputTx(Txid),
|
||||||
|
/// The transaction being spent doesn't have the requested output
|
||||||
|
InvalidInput(OutPoint),
|
||||||
|
|
||||||
|
/// Consensus error
|
||||||
|
Consensus(bitcoinconsensus::Error),
|
||||||
|
|
||||||
|
/// Generic error
|
||||||
|
///
|
||||||
|
/// It has to be wrapped in a `Box` since `Error` has a variant that contains this enum
|
||||||
|
Global(Box<Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for VerifyError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{:?}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VerifyError {}
|
||||||
|
|
||||||
|
impl From<Error> for VerifyError {
|
||||||
|
fn from(other: Error) -> Self {
|
||||||
|
VerifyError::Global(Box::new(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use bitcoin::consensus::encode::deserialize;
|
||||||
|
use bitcoin::hashes::hex::FromHex;
|
||||||
|
use bitcoin::{Transaction, Txid};
|
||||||
|
|
||||||
|
use crate::blockchain::{Blockchain, Capability, Progress};
|
||||||
|
use crate::database::{BatchDatabase, BatchOperations, MemoryDatabase};
|
||||||
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct DummyBlockchain;
|
||||||
|
|
||||||
|
impl Blockchain for DummyBlockchain {
|
||||||
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
|
&self,
|
||||||
|
_database: &mut D,
|
||||||
|
_progress_update: P,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn broadcast(&self, _tx: &Transaction) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
Ok(42)
|
||||||
|
}
|
||||||
|
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
|
||||||
|
Ok(FeeRate::default_min_relay_fee())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verify_fail_unsigned_tx() {
|
||||||
|
// https://blockstream.info/tx/95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f
|
||||||
|
let prev_tx: Transaction = deserialize(&Vec::<u8>::from_hex("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700").unwrap()).unwrap();
|
||||||
|
// https://blockstream.info/tx/aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d
|
||||||
|
let signed_tx: Transaction = deserialize(&Vec::<u8>::from_hex("02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700").unwrap()).unwrap();
|
||||||
|
|
||||||
|
let mut database = MemoryDatabase::new();
|
||||||
|
let blockchain = DummyBlockchain;
|
||||||
|
|
||||||
|
let mut unsigned_tx = signed_tx.clone();
|
||||||
|
for input in &mut unsigned_tx.input {
|
||||||
|
input.script_sig = Default::default();
|
||||||
|
input.witness = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = verify_tx(&signed_tx, &database, &blockchain);
|
||||||
|
assert!(result.is_err(), "Should fail with missing input tx");
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid()),
|
||||||
|
"Error should be a `MissingInputTx` error"
|
||||||
|
);
|
||||||
|
|
||||||
|
// insert the prev_tx
|
||||||
|
database.set_raw_tx(&prev_tx).unwrap();
|
||||||
|
|
||||||
|
let result = verify_tx(&unsigned_tx, &database, &blockchain);
|
||||||
|
assert!(result.is_err(), "Should fail since the TX is unsigned");
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(VerifyError::Consensus(_))),
|
||||||
|
"Error should be a `Consensus` error"
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = verify_tx(&signed_tx, &database, &blockchain);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Should work since the TX is correctly signed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user