Compare commits
331 Commits
release/0.
...
v0.10.0
| 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 | ||
|
|
476fa3fd7d | ||
|
|
2755b09e7b | ||
|
|
5e6286a493 | ||
|
|
67714adc80 | ||
|
|
9ff86ea37c | ||
|
|
ceeb3a40cf | ||
|
|
e3316aee4c | ||
|
|
c2567b61aa | ||
|
|
e1a77b87ab | ||
|
|
5bf758b03a | ||
|
|
0bbfa5f989 | ||
|
|
18254110c6 | ||
|
|
44217539e5 | ||
|
|
33b45ebe82 | ||
|
|
2faed425ed | ||
|
|
2cc05c07a5 | ||
|
|
fe371f9d92 | ||
|
|
12de13b95c | ||
|
|
9205295332 | ||
|
|
3b446c9e14 | ||
|
|
378167efca | ||
|
|
224be27aa8 | ||
|
|
4a23070cc8 | ||
|
|
ba2e3042cc | ||
|
|
f8117c0f9f | ||
|
|
1639984b56 | ||
|
|
ab54a17eb7 | ||
|
|
ae5aa06586 | ||
|
|
ab98283159 | ||
|
|
81851190f0 | ||
|
|
e1b037a921 | ||
|
|
9b7ed08891 | ||
|
|
dffb753ce3 | ||
|
|
0b969657cd | ||
|
|
bfef2e3cfe | ||
|
|
0ec064ef13 | ||
|
|
6b60914ca1 | ||
|
|
881ca8d1e3 | ||
|
|
5633475ce8 | ||
|
|
ea8488b2a7 | ||
|
|
d2a981efee | ||
|
|
4c92daf517 | ||
|
|
aba2a05d83 | ||
|
|
5b194c268d | ||
|
|
00bdf08f2a | ||
|
|
38b0470b14 | ||
|
|
d60c5003bf | ||
|
|
fcae5adabd | ||
|
|
9f04a9d82d | ||
|
|
465ef6e674 | ||
|
|
aaa9943a5f | ||
|
|
3897e29740 | ||
|
|
8f06e45872 | ||
|
|
766570abfd | ||
|
|
934ec366d9 | ||
|
|
d0733e9496 | ||
|
|
3c7a1f5918 | ||
|
|
85aadaccd2 | ||
|
|
fad0fe9f30 | ||
|
|
6546b77c08 | ||
|
|
e1066e955c | ||
|
|
7f06dc3330 | ||
|
|
de40351710 | ||
|
|
de811bea30 | ||
|
|
74cc80d127 | ||
|
|
009f68a06a | ||
|
|
47f26447da | ||
|
|
12641b9e8f | ||
|
|
aa3707b5b4 | ||
|
|
f6631e35b8 | ||
|
|
3608ff9f14 | ||
|
|
7fdb98e147 | ||
|
|
9aea90bd81 | ||
|
|
898dfe6cf1 | ||
|
|
7961ae7f8e | ||
|
|
8bf77c8f07 | ||
|
|
3c7bae9ce9 | ||
|
|
17bcd8ed7d | ||
|
|
b5e9589803 | ||
|
|
1d628d84b5 | ||
|
|
b84fd6ea5c | ||
|
|
8fe4222c33 | ||
|
|
e626f2e255 | ||
|
|
5a0c150ff9 | ||
|
|
00f07818f9 | ||
|
|
136a4bddb2 | ||
|
|
ff7b74ec27 | ||
|
|
8c00326990 | ||
|
|
afcd26032d | ||
|
|
8f422a1bf9 | ||
|
|
45983d2166 | ||
|
|
89cb4de7f6 | ||
|
|
7ca0e0e2bd | ||
|
|
2bddd9baed | ||
|
|
0135ba29c5 | ||
|
|
549cd24812 | ||
|
|
a841b5d635 | ||
|
|
16ceb6cb30 | ||
|
|
edfd7d454c | ||
|
|
1d874e50c2 | ||
|
|
98127cc5da | ||
|
|
e243107bb6 | ||
|
|
237a8d4e69 | ||
|
|
7f4042ba1b | ||
|
|
3ed44ce8cf | ||
|
|
8e7d8312a9 | ||
|
|
4da7488dc4 | ||
|
|
e37680af96 | ||
|
|
5f873ae500 | ||
|
|
2380634496 | ||
|
|
af98b8da06 | ||
|
|
b68ec050e2 | ||
|
|
ac7df09200 | ||
|
|
192965413c | ||
|
|
745be7bea8 | ||
|
|
b6007e05c1 | ||
|
|
f53654d9f4 | ||
|
|
e5ecc7f541 | ||
|
|
882a9c27cc | ||
|
|
1e6b8e12b2 | ||
|
|
b226658977 | ||
|
|
6d6776eb58 | ||
|
|
f1f844a5b6 | ||
|
|
a3e45358de | ||
|
|
07e79f6e8a | ||
|
|
d94b8f87a3 | ||
|
|
fdb895d26c | ||
|
|
7041e96737 | ||
|
|
199f716ebb | ||
|
|
b12e358c1d | ||
|
|
f786f0e624 | ||
|
|
71e0472dc9 | ||
|
|
f7944e871b | ||
|
|
2fea1761c1 | ||
|
|
fa27ae210f | ||
|
|
46fa41470e | ||
|
|
c456a252f8 | ||
|
|
d837a762fc | ||
|
|
e82dfa971e | ||
|
|
cc17ac8859 | ||
|
|
3798b4d115 | ||
|
|
2d0f6c4ec5 | ||
|
|
f3b475ff0e | ||
|
|
41ae202d02 | ||
|
|
fef6176275 | ||
|
|
8ebe7f0ea5 | ||
|
|
eb85390846 | ||
|
|
dc83db273a | ||
|
|
201bd6ee02 | ||
|
|
396ffb42f9 | ||
|
|
9cf62ce874 | ||
|
|
9c6b98d98b | ||
|
|
14ae64e09d | ||
|
|
48215675b0 | ||
|
|
37fa35b24a | ||
|
|
23ec9c3ba0 | ||
|
|
e33a6a12c1 | ||
|
|
12ae1c3479 | ||
|
|
fdde0e691e | ||
|
|
1cbd47b988 | ||
|
|
e0183ed5c7 | ||
|
|
dae900cc59 | ||
|
|
4c2042ab01 | ||
|
|
2f0ca206f3 | ||
|
|
ac7c1bd97b | ||
|
|
d9a102afa9 | ||
|
|
7c1dcd8a72 | ||
|
|
1fbfeabd77 | ||
|
|
9a918f285d | ||
|
|
a7183f34ef | ||
|
|
bda416df0a | ||
|
|
a838c2bacc | ||
|
|
d2a094aa4c | ||
|
|
bdb2a53597 | ||
|
|
97ad0f1b4f | ||
|
|
2b5e177ab2 | ||
|
|
bfe29c4ef6 | ||
|
|
e35601bb19 | ||
|
|
24df438607 | ||
|
|
cb3b8cf21b | ||
|
|
0e6add0cfb | ||
|
|
343e97da0e | ||
|
|
ba8ce7233d | ||
|
|
35184e6908 | ||
|
|
824b00c9e0 | ||
|
|
79cab93d49 | ||
|
|
2afc9faa08 | ||
|
|
0e99d02fbe | ||
|
|
3a0a1e6d4a | ||
|
|
2057c35468 | ||
|
|
5eaa3b0916 | ||
|
|
4ad0f54c30 | ||
|
|
eeff3b5049 | ||
|
|
5e352489a0 | ||
|
|
7ee262ef4b | ||
|
|
2759231f7b | ||
|
|
e3f893dbd1 | ||
|
|
3f5513a2d6 | ||
|
|
fcf5e971a6 | ||
|
|
cdf7b33104 | ||
|
|
7bbff79d4b | ||
|
|
3a2b8bdb85 | ||
|
|
7843732e17 | ||
|
|
fa5a5c8c05 | ||
|
|
6092c6e789 | ||
|
|
7fe5a30424 | ||
|
|
a82b2155e9 | ||
|
|
b61427c07b | ||
|
|
fa2610538f | ||
|
|
d0ffcdd009 | ||
|
|
1c6864aee8 | ||
|
|
d638da2f10 | ||
|
|
2f7513753c | ||
|
|
c90a1f70a6 | ||
|
|
04348d0090 | ||
|
|
eda23491c0 | ||
|
|
dccf09861c | ||
|
|
02b9eda6fa | ||
|
|
6611ef0e5f | ||
|
|
db5e663f05 | ||
|
|
c4f21799a6 | ||
|
|
fedd92c022 | ||
|
|
19eca4e2d1 | ||
|
|
023dabd9b2 | ||
|
|
b44d1f7a92 | ||
|
|
3d9d6fee07 | ||
|
|
4c36020e95 | ||
|
|
6d01c51c63 | ||
|
|
693fb24e02 | ||
|
|
6689384c8a | ||
|
|
35a61f5759 | ||
|
|
d0ffd5606a | ||
|
|
c2b1268675 | ||
|
|
ccbbad3e9e | ||
|
|
dbf8cf7674 | ||
|
|
eb96ac374b | ||
|
|
c431a60171 | ||
|
|
2e0ca4fe05 | ||
|
|
df32c849bb | ||
|
|
33426d4c3a | ||
|
|
03e6e8126d | ||
|
|
ff10aa5ceb | ||
|
|
21d382315a | ||
|
|
6fe3be0243 | ||
|
|
10fcba9439 | ||
|
|
890d6191a1 | ||
|
|
735db02850 | ||
|
|
7bf46c7d71 | ||
|
|
8319b32466 | ||
|
|
0faca43744 | ||
|
|
6f66de3d16 | ||
|
|
5fb7fdffe1 | ||
|
|
7553b905c4 | ||
|
|
f74f17e227 | ||
|
|
7566904926 | ||
|
|
03b7c1b46b | ||
|
|
4686ebb420 | ||
|
|
082db351c0 | ||
|
|
84db6ce453 | ||
|
|
5c82789e57 | ||
|
|
7bc8c3c380 | ||
|
|
813c1ddcd0 |
34
.github/workflows/code_coverage.yml
vendored
34
.github/workflows/code_coverage.yml
vendored
@@ -3,25 +3,35 @@ on: [push]
|
|||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tarpaulin-codecov:
|
|
||||||
name: Tarpaulin to codecov.io
|
Codecov:
|
||||||
|
name: Code Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: '0'
|
||||||
|
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||||
|
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Install rustup
|
||||||
|
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default nightly
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
||||||
|
|
||||||
|
- id: coverage
|
||||||
|
name: Generate coverage
|
||||||
|
uses: actions-rs/grcov@v0.1.5
|
||||||
|
|
||||||
- name: Install tarpaulin
|
- name: Upload coverage to Codecov
|
||||||
run: cargo install cargo-tarpaulin
|
uses: codecov/codecov-action@v1
|
||||||
- name: Tarpaulin
|
|
||||||
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
|
|
||||||
|
|
||||||
- name: Publish to codecov.io
|
|
||||||
uses: codecov/codecov-action@v1.0.15
|
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
file: ${{ steps.coverage.outputs.report }}
|
||||||
file: ./cobertura.xml
|
directory: ./coverage/reports/
|
||||||
|
|||||||
72
.github/workflows/cont_integration.yml
vendored
72
.github/workflows/cont_integration.yml
vendored
@@ -10,18 +10,22 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust:
|
rust:
|
||||||
- stable
|
- 1.53.0 # STABLE
|
||||||
- 1.45.0 # MSRV
|
- 1.46.0 # MSRV
|
||||||
features:
|
features:
|
||||||
- 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
|
||||||
|
- verify
|
||||||
|
- async-interface
|
||||||
|
- use-esplora-reqwest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -41,10 +45,12 @@ jobs:
|
|||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
- name: Add clippy
|
- name: Add clippy
|
||||||
run: rustup component add clippy
|
run: rustup component add clippy
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --features ${{ matrix.features }} --no-default-features
|
run: cargo build --features ${{ matrix.features }} --no-default-features
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --features ${{ matrix.features }} --no-default-features -- -D warnings
|
run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --features ${{ matrix.features }} --no-default-features
|
run: cargo test --features ${{ matrix.features }} --no-default-features
|
||||||
|
|
||||||
@@ -66,19 +72,21 @@ jobs:
|
|||||||
run: rustup default nightly
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
|
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
|
||||||
|
|
||||||
test-electrum:
|
test-blockchains:
|
||||||
name: Test electrum
|
name: Test ${{ matrix.blockchain.name }}
|
||||||
runs-on: ubuntu-16.04
|
runs-on: ubuntu-20.04
|
||||||
container: bitcoindevkit/electrs
|
strategy:
|
||||||
env:
|
fail-fast: false
|
||||||
MAGICAL_RPC_AUTH: USER_PASS
|
matrix:
|
||||||
MAGICAL_RPC_USER: admin
|
blockchain:
|
||||||
MAGICAL_RPC_PASS: passw
|
- name: electrum
|
||||||
MAGICAL_RPC_URL: 127.0.0.1:18443
|
- name: rpc
|
||||||
MAGICAL_ELECTRUM_URL: tcp://127.0.0.1:60401
|
- name: esplora
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -87,19 +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: Install rustup
|
- name: Setup rust toolchain
|
||||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
uses: actions-rs/toolchain@v1
|
||||||
- name: Set default toolchain
|
with:
|
||||||
run: $HOME/.cargo/bin/rustup default stable
|
toolchain: stable
|
||||||
- name: Set profile
|
override: true
|
||||||
run: $HOME/.cargo/bin/rustup set profile minimal
|
|
||||||
- name: Start core
|
|
||||||
run: ./ci/start-core.sh
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features
|
run: cargo test --features test-${{ matrix.blockchain.name }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||||
|
|
||||||
check-wasm:
|
check-wasm:
|
||||||
name: Check WASM
|
name: Check WASM
|
||||||
@@ -122,15 +129,18 @@ jobs:
|
|||||||
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
||||||
- run: sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-10 main" || exit 1
|
- run: sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-10 main" || exit 1
|
||||||
- run: sudo apt-get update || exit 1
|
- run: sudo apt-get update || exit 1
|
||||||
- run: sudo apt-get install -y clang-10 libc6-dev-i386 || exit 1
|
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default stable
|
run: rustup default 1.53.0 # STABLE
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
- name: Add target wasm32
|
- name: Add target wasm32
|
||||||
run: rustup target add wasm32-unknown-unknown
|
run: rustup target add wasm32-unknown-unknown
|
||||||
|
- name: Update toolchain
|
||||||
|
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
|
||||||
@@ -139,10 +149,12 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default stable
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
- name: Add clippy
|
- name: Add rustfmt
|
||||||
run: rustup component add rustfmt
|
run: rustup component add rustfmt
|
||||||
|
- name: Update toolchain
|
||||||
|
run: rustup update
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||||
|
|||||||
17
.github/workflows/nightly_docs.yml
vendored
17
.github/workflows/nightly_docs.yml
vendored
@@ -17,17 +17,14 @@ jobs:
|
|||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||||
- name: Install nightly toolchain
|
- name: Set default toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
run: rustup default nightly
|
||||||
with:
|
- name: Set profile
|
||||||
profile: minimal
|
run: rustup set profile minimal
|
||||||
toolchain: nightly
|
- name: Update toolchain
|
||||||
override: true
|
run: rustup update
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
uses: actions-rs/cargo@v1
|
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
|
||||||
with:
|
|
||||||
command: rustdoc
|
|
||||||
args: --verbose --features=compiler,electrum,esplora,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:
|
||||||
|
|||||||
141
CHANGELOG.md
141
CHANGELOG.md
@@ -6,6 +6,118 @@ 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
|
||||||
|
|
||||||
|
- 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]
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
|
||||||
|
#### Changed
|
||||||
|
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
|
||||||
|
|
||||||
|
## [v0.7.0] - [v0.6.0]
|
||||||
|
|
||||||
|
### Policy
|
||||||
|
#### Changed
|
||||||
|
Removed `fill_satisfaction` method in favor of enum parameter in `extract_policy` method
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
Timelocks are considered (optionally) in building the `satisfaction` field
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
|
||||||
|
- Changed `Wallet::{sign, finalize_psbt}` now take a `&mut psbt` rather than consuming it.
|
||||||
|
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
|
||||||
|
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
|
||||||
|
|
||||||
|
## [v0.6.0] - [v0.5.1]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
#### Changed
|
||||||
|
- New minimum supported rust version is 1.46.0
|
||||||
|
- Changed `AnyBlockchainConfig` to use serde tagged representation.
|
||||||
|
|
||||||
|
### Descriptor
|
||||||
|
#### Added
|
||||||
|
- Added ability to analyze a `PSBT` to check which and how many signatures are already available
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
#### Changed
|
||||||
|
- `get_new_address()` refactored to `get_address(AddressIndex::New)` to support different `get_address()` index selection strategies
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
- Added `get_address(AddressIndex::LastUnused)` which returns the last derived address if it has not been used or if used in a received transaction returns a new address
|
||||||
|
- Added `get_address(AddressIndex::Peek(u32))` which returns a derived address for a specified descriptor index but does not change the current index
|
||||||
|
- Added `get_address(AddressIndex::Reset(u32))` which returns a derived address for a specified descriptor index and resets current index to the given value
|
||||||
|
- Added `get_psbt_input` to create the corresponding psbt input for a local utxo.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
- Fixed `coin_select` calculation for UTXOs where `value < fee` that caused over-/underflow errors.
|
||||||
|
|
||||||
|
## [v0.5.1] - [v0.5.0]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
#### Changed
|
||||||
|
- Pin `hyper` to `=0.14.4` to make it compile on Rust 1.45
|
||||||
|
|
||||||
|
## [v0.5.0] - [v0.4.0]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
#### Changed
|
||||||
|
- Updated `electrum-client` to version `0.7`
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
#### Changed
|
||||||
|
- `FeeRate` constructors `from_sat_per_vb` and `default_min_relay_fee` are now `const` functions
|
||||||
|
|
||||||
|
## [v0.4.0] - [v0.3.0]
|
||||||
|
|
||||||
|
### Keys
|
||||||
|
#### Changed
|
||||||
|
- Renamed `DerivableKey::add_metadata()` to `DerivableKey::into_descriptor_key()`
|
||||||
|
- Renamed `ToDescriptorKey::to_descriptor_key()` to `IntoDescriptorKey::into_descriptor_key()`
|
||||||
|
#### Added
|
||||||
|
- Added an `ExtendedKey` type that is an enum of `bip32::ExtendedPubKey` and `bip32::ExtendedPrivKey`
|
||||||
|
- Added `DerivableKey::into_extended_key()` as the only method that needs to be implemented
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
#### Removed
|
||||||
|
- Removed the `parse_descriptor` example, since it wasn't demonstrating any bdk-specific API anymore.
|
||||||
|
#### Changed
|
||||||
|
- Updated `bitcoin` to `0.26`, `miniscript` to `5.1` and `electrum-client` to `0.6`
|
||||||
|
#### Added
|
||||||
|
- Added support for the `signet` network (issue #62)
|
||||||
|
- Added a function to get the version of BDK at runtime
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
#### Changed
|
||||||
|
- Removed the explicit `id` argument from `Wallet::add_signer()` since that's now part of `Signer` itself
|
||||||
|
- Renamed `ToWalletDescriptor::to_wallet_descriptor()` to `IntoWalletDescriptor::into_wallet_descriptor()`
|
||||||
|
|
||||||
|
### Policy
|
||||||
|
#### Changed
|
||||||
|
- Removed unneeded `Result<(), PolicyError>` return type for `Satisfaction::finalize()`
|
||||||
|
- Removed the `TooManyItemsSelected` policy error (see commit message for more details)
|
||||||
|
|
||||||
## [v0.3.0] - [v0.2.0]
|
## [v0.3.0] - [v0.2.0]
|
||||||
|
|
||||||
### Descriptor
|
### Descriptor
|
||||||
@@ -19,6 +131,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
#### Changed
|
#### Changed
|
||||||
- Remove `BlockchainMarker`, `OfflineClient` and `OfflineWallet` in favor of just using the unit
|
- Remove `BlockchainMarker`, `OfflineClient` and `OfflineWallet` in favor of just using the unit
|
||||||
type to mark for a missing client.
|
type to mark for a missing client.
|
||||||
|
- Upgrade `tokio` to `1.0`.
|
||||||
|
|
||||||
|
### Transaction Creation Overhaul
|
||||||
|
|
||||||
|
The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the
|
||||||
|
final transaction is created by calling `finish` on the builder.
|
||||||
|
|
||||||
|
- Removed `TxBuilder::utxos` in favor of `TxBuilder::add_utxos`
|
||||||
|
- Added `Wallet::build_tx` to replace `Wallet::create_tx`
|
||||||
|
- Added `Wallet::build_fee_bump` to replace `Wallet::bump_fee`
|
||||||
|
- Added `Wallet::get_utxo`
|
||||||
|
- Added `Wallet::get_descriptor_for_keychain`
|
||||||
|
|
||||||
|
### `add_foreign_utxo`
|
||||||
|
|
||||||
|
- Renamed `UTXO` to `LocalUtxo`
|
||||||
|
- Added `WeightedUtxo` to replace floating `(UTXO, usize)`.
|
||||||
|
- Added `Utxo` enum to incorporate both local utxos and foreign utxos
|
||||||
|
- Added `TxBuilder::add_foreign_utxo` which allows adding a utxo external to the wallet.
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
#### Changed
|
#### Changed
|
||||||
@@ -229,7 +360,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- 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.2.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
|
||||||
|
[v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0
|
||||||
|
[v0.5.0]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...v0.5.0
|
||||||
|
[v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
|
||||||
|
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
|
||||||
|
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
|
||||||
|
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0
|
||||||
|
[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
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Anyone is invited to contribute without regard to technical experience,
|
|||||||
cryptocurrencies demands a high-level of rigor, adversarial thinking, thorough
|
cryptocurrencies demands a high-level of rigor, adversarial thinking, thorough
|
||||||
testing and risk-minimization.
|
testing and risk-minimization.
|
||||||
Any bug may cost users real money. That being said, we deeply welcome people
|
Any bug may cost users real money. That being said, we deeply welcome people
|
||||||
contributing for the first time to an open source project or pick up Rust while
|
contributing for the first time to an open source project or picking up Rust while
|
||||||
contributing. Don't be shy, you'll learn.
|
contributing. Don't be shy, you'll learn.
|
||||||
|
|
||||||
Communications Channels
|
Communications Channels
|
||||||
@@ -46,7 +46,7 @@ Every new feature should be covered by functional tests where possible.
|
|||||||
When refactoring, structure your PR to make it easy to review and don't
|
When refactoring, structure your PR to make it easy to review and don't
|
||||||
hesitate to split it into multiple small, focused PRs.
|
hesitate to split it into multiple small, focused PRs.
|
||||||
|
|
||||||
The Minimal Supported Rust Version is 1.45 (enforced by our CI).
|
The Minimal Supported Rust Version is 1.46 (enforced by our CI).
|
||||||
|
|
||||||
Commits should cover both the issue fixed and the solution's rationale.
|
Commits should cover both the issue fixed and the solution's rationale.
|
||||||
These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind.
|
These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind.
|
||||||
|
|||||||
77
Cargo.toml
77
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.3.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"
|
||||||
@@ -9,33 +9,34 @@ documentation = "https://docs.rs/bdk"
|
|||||||
description = "A modern, lightweight, descriptor-based wallet library"
|
description = "A modern, lightweight, descriptor-based wallet library"
|
||||||
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
|
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bdk-macros = "0.2"
|
bdk-macros = "0.5"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
miniscript = "4.0"
|
miniscript = "5.1"
|
||||||
bitcoin = { version = "^0.25.2", features = ["use-serde"] }
|
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
rand = "^0.7"
|
rand = "^0.7"
|
||||||
|
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.5.0-beta.1", optional = true }
|
electrum-client = { version = "0.7", optional = true }
|
||||||
reqwest = { version = "0.10", 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 }
|
||||||
# pin cc version to 1.0.62 because 1.0.63 break rocksdb build
|
cc = { version = ">=1.0.64", optional = true }
|
||||||
cc = { version = "=1.0.62", 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 }
|
||||||
|
|
||||||
# Platform-specific dependencies
|
# Needed by bdk_blockchain_tests macro
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
bitcoincore-rpc = { version = "0.13", optional = true }
|
||||||
tokio = { version = "0.2", features = ["rt-core"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -45,33 +46,54 @@ 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"]
|
||||||
|
|
||||||
|
# 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
|
||||||
debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"]
|
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||||
test-electrum = ["electrum"]
|
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||||
|
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
|
||||||
|
test-esplora = ["esplora", "ureq", "electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
|
||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bdk-testutils = "0.2"
|
|
||||||
bdk-testutils-macros = "0.2"
|
|
||||||
serial_test = "0.4"
|
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
base64 = "^0.11"
|
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
|
electrsd = { version= "0.8", features = ["trigger", "bitcoind_0_21_1"] }
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "parse_descriptor"
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
|
[[example]]
|
||||||
|
name = "compact_filters_balance"
|
||||||
|
required-features = ["compact_filters"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "miniscriptc"
|
name = "miniscriptc"
|
||||||
@@ -79,11 +101,8 @@ path = "examples/compiler.rs"
|
|||||||
required-features = ["compiler"]
|
required-features = ["compiler"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros", "testutils", "testutils-macros"]
|
members = ["macros"]
|
||||||
|
|
||||||
# Generate docs with nightly to add the "features required" badge
|
|
||||||
# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
|
|
||||||
[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"]
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
# Development Cycle
|
# Development Cycle
|
||||||
|
|
||||||
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular
|
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
||||||
cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
|
||||||
|
|
||||||
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers
|
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
||||||
to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
|
||||||
|
|
||||||
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
||||||
|
|
||||||
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus
|
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
||||||
on ensuring the ones we've added are working properly.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
master: - - - - * - - - * - - - - - - * - - - * ...
|
master: - - - - * - - - * - - - - - - * - - - * ...
|
||||||
@@ -19,30 +16,31 @@ release/0.x.0: * - - # | |
|
|||||||
release/0.y.0: * - - #
|
release/0.y.0: * - - #
|
||||||
```
|
```
|
||||||
|
|
||||||
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature
|
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
|
||||||
freeze window.
|
|
||||||
|
|
||||||
## Making the Release
|
## Making the Release
|
||||||
|
|
||||||
What follows are notes and procedures that maintaners can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
|
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
|
||||||
|
|
||||||
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
|
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
|
||||||
|
|
||||||
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
|
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
|
||||||
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
|
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
|
||||||
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
|
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
|
||||||
4. During the one week of feature freeze run additional tests on the release branch
|
4. During the one week of feature freeze run additional tests on the release branch.
|
||||||
5. If a bug is found:
|
5. If a bug is found:
|
||||||
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
|
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
|
||||||
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
|
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
|
||||||
6. Update the changelog with the new release version
|
6. Update the changelog with the new release version.
|
||||||
7. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
|
7. Update `src/lib.rs` with the new version (line ~59)
|
||||||
8. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
|
||||||
9. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
|
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||||
10. Publish **all** the updated crates to crates.io.
|
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
|
||||||
11. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
11. Publish **all** the updated crates to crates.io.
|
||||||
12. Merge the release branch back into `master`.
|
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
||||||
13. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
13. Merge the release branch back into `master`.
|
||||||
14. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
14. If the `master` branch contains any unreleased changes to the `bdk-macros`, `bdk-testutils`, or `bdk-testutils-macros` crates, change the `bdk` Cargo.toml `[dev-dependencies]` to point to the local path (ie. `bdk-testutils-macros = { path = "./testutils-macros"}`)
|
||||||
15. Announce the release on Twitter, Discord and Telegram.
|
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
||||||
16. Celebrate :tada:
|
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
||||||
|
17. Announce the release on Twitter, Discord and Telegram.
|
||||||
|
18. Celebrate :tada:
|
||||||
|
|||||||
29
LICENSE
29
LICENSE
@@ -1,21 +1,14 @@
|
|||||||
MIT License
|
This software is licensed under [Apache 2.0](LICENSE-APACHE) or
|
||||||
|
[MIT](LICENSE-MIT), at your option.
|
||||||
|
|
||||||
Copyright (c) 2020 Magical Bitcoin
|
Some files retain their own copyright notice, however, for full authorship
|
||||||
|
information, see version control history.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Except as otherwise noted in individual files, all files in this repository are
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
in the Software without restriction, including without limitation the rights
|
http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
http://opensource.org/licenses/MIT>, at your option.
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
You may not use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
copies or substantial portions of the Software.
|
sell copies of this software or any files in this repository except in
|
||||||
|
accordance with one or both of these licenses.
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
201
LICENSE-APACHE
Normal file
201
LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
16
LICENSE-MIT
Normal file
16
LICENSE-MIT
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
69
README.md
69
README.md
@@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT Licensed" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
|
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
||||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||||
<a href="https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html"><img alt="Rustc Version 1.45+" src="https://img.shields.io/badge/rustc-1.45%2B-lightgrey.svg"/></a>
|
<a href="https://blog.rust-lang.org/2020/08/27/Rust-1.46.0.html"><img alt="Rustc Version 1.46+" src="https://img.shields.io/badge/rustc-1.46%2B-lightgrey.svg"/></a>
|
||||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bdk::{Wallet, database::MemoryDatabase};
|
use bdk::{Wallet, database::MemoryDatabase};
|
||||||
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
fn main() -> Result<(), bdk::Error> {
|
fn main() -> Result<(), bdk::Error> {
|
||||||
let wallet = Wallet::new_offline(
|
let wallet = Wallet::new_offline(
|
||||||
@@ -76,9 +77,9 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
MemoryDatabase::default(),
|
MemoryDatabase::default(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("Address #0: {}", wallet.get_new_address()?);
|
println!("Address #0: {}", wallet.get_address(New)?);
|
||||||
println!("Address #1: {}", wallet.get_new_address()?);
|
println!("Address #1: {}", wallet.get_address(New)?);
|
||||||
println!("Address #2: {}", wallet.get_new_address()?);
|
println!("Address #2: {}", wallet.get_address(New)?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -87,11 +88,12 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
### Create a transaction
|
### Create a transaction
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use bdk::{FeeRate, TxBuilder, Wallet};
|
use bdk::{FeeRate, Wallet};
|
||||||
use bdk::database::MemoryDatabase;
|
use bdk::database::MemoryDatabase;
|
||||||
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||||
|
|
||||||
use bdk::electrum_client::Client;
|
use bdk::electrum_client::Client;
|
||||||
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
use bitcoin::consensus::serialize;
|
use bitcoin::consensus::serialize;
|
||||||
|
|
||||||
@@ -107,13 +109,16 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
|
|
||||||
wallet.sync(noop_progress(), None)?;
|
wallet.sync(noop_progress(), None)?;
|
||||||
|
|
||||||
let send_to = wallet.get_new_address()?;
|
let send_to = wallet.get_address(New)?;
|
||||||
let (psbt, details) = wallet.create_tx(
|
let (psbt, details) = {
|
||||||
TxBuilder::with_recipients(vec![(send_to.script_pubkey(), 50_000)])
|
let mut builder = wallet.build_tx();
|
||||||
|
builder
|
||||||
|
.add_recipient(send_to.script_pubkey(), 50_000)
|
||||||
.enable_rbf()
|
.enable_rbf()
|
||||||
.do_not_spend_change()
|
.do_not_spend_change()
|
||||||
.fee_rate(FeeRate::from_sat_per_vb(5.0))
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
)?;
|
builder.finish()?
|
||||||
|
};
|
||||||
|
|
||||||
println!("Transaction details: {:#?}", details);
|
println!("Transaction details: {:#?}", details);
|
||||||
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
||||||
@@ -125,7 +130,7 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
### Sign a transaction
|
### Sign a transaction
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use bdk::{Wallet, database::MemoryDatabase};
|
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||||
|
|
||||||
use bitcoin::consensus::deserialize;
|
use bitcoin::consensus::deserialize;
|
||||||
|
|
||||||
@@ -138,10 +143,46 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let psbt = "...";
|
let psbt = "...";
|
||||||
let psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||||
|
|
||||||
let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
|
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* Apache License, Version 2.0
|
||||||
|
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license
|
||||||
|
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||||
|
dual licensed as above, without any additional terms or conditions.
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
echo "Starting bitcoin node."
|
echo "Starting bitcoin node."
|
||||||
/root/bitcoind -regtest -server -daemon -fallbackfee=0.0002 -rpcuser=admin -rpcpassword=passw -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0
|
mkdir $GITHUB_WORKSPACE/.bitcoin
|
||||||
|
/root/bitcoind -regtest -server -daemon -datadir=$GITHUB_WORKSPACE/.bitcoin -fallbackfee=0.0002 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
|
||||||
|
|
||||||
echo "Waiting for bitcoin node."
|
echo "Waiting for bitcoin node."
|
||||||
until /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw getblockchaininfo; do
|
until /root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin getblockchaininfo; do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin createwallet $BDK_RPC_WALLET
|
||||||
echo "Generating 150 bitcoin blocks."
|
echo "Generating 150 bitcoin blocks."
|
||||||
ADDR=$(/root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw getnewaddress)
|
ADDR=$(/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin -rpcwallet=$BDK_RPC_WALLET getnewaddress)
|
||||||
/root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw generatetoaddress 150 $ADDR
|
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin generatetoaddress 150 $ADDR
|
||||||
|
|
||||||
echo "Starting electrs node."
|
|
||||||
nohup /root/electrs --network regtest --jsonrpc-import &
|
|
||||||
sleep 5
|
|
||||||
|
|||||||
13
codecov.yaml
Normal file
13
codecov.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 1%
|
||||||
|
base: auto
|
||||||
|
informational: false
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 100%
|
||||||
|
base: auto
|
||||||
@@ -1,46 +1,35 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bdk::bitcoin;
|
use bdk::bitcoin;
|
||||||
use bdk::database::MemoryDatabase;
|
use bdk::database::MemoryDatabase;
|
||||||
use bdk::descriptor::HDKeyPaths;
|
use bdk::descriptor::HdKeyPaths;
|
||||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
||||||
use bdk::KeychainKind;
|
use bdk::KeychainKind;
|
||||||
use bdk::Wallet;
|
use bdk::Wallet;
|
||||||
|
|
||||||
|
use bdk::wallet::AddressIndex::New;
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use bitcoin::util::bip32::Fingerprint;
|
use bitcoin::util::bip32::Fingerprint;
|
||||||
use bitcoin::{Network, Script};
|
use bitcoin::{Network, Script};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct DummyValidator;
|
struct DummyValidator;
|
||||||
impl AddressValidator for DummyValidator {
|
impl AddressValidator for DummyValidator {
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
hd_keypaths: &HDKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
script: &Script,
|
script: &Script,
|
||||||
) -> Result<(), AddressValidatorError> {
|
) -> Result<(), AddressValidatorError> {
|
||||||
let (_, path) = hd_keypaths
|
let (_, path) = hd_keypaths
|
||||||
@@ -64,9 +53,9 @@ fn main() -> Result<(), bdk::Error> {
|
|||||||
|
|
||||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
wallet.add_address_validator(Arc::new(DummyValidator));
|
||||||
|
|
||||||
wallet.get_new_address()?;
|
wallet.get_address(New)?;
|
||||||
wallet.get_new_address()?;
|
wallet.get_address(New)?;
|
||||||
wallet.get_new_address()?;
|
wallet.get_address(New)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
43
examples/compact_filters_balance.rs
Normal file
43
examples/compact_filters_balance.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
use bdk::blockchain::compact_filters::*;
|
||||||
|
use bdk::blockchain::noop_progress;
|
||||||
|
use bdk::database::MemoryDatabase;
|
||||||
|
use bdk::*;
|
||||||
|
use bitcoin::*;
|
||||||
|
use blockchain::compact_filters::CompactFiltersBlockchain;
|
||||||
|
use blockchain::compact_filters::CompactFiltersError;
|
||||||
|
use log::info;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// This will return wallet balance using compact filters
|
||||||
|
/// Requires a synced local bitcoin node 0.21 running on testnet with blockfilterindex=1 and peerblockfilters=1
|
||||||
|
fn main() -> Result<(), CompactFiltersError> {
|
||||||
|
env_logger::init();
|
||||||
|
info!("start");
|
||||||
|
|
||||||
|
let num_threads = 4;
|
||||||
|
let mempool = Arc::new(Mempool::default());
|
||||||
|
let peers = (0..num_threads)
|
||||||
|
.map(|_| Peer::connect("localhost:18333", Arc::clone(&mempool), Network::Testnet))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
let blockchain = CompactFiltersBlockchain::new(peers, "./wallet-filters", Some(500_000))?;
|
||||||
|
info!("done {:?}", blockchain);
|
||||||
|
let descriptor = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
|
||||||
|
|
||||||
|
let database = MemoryDatabase::default();
|
||||||
|
let wallet =
|
||||||
|
Arc::new(Wallet::new(descriptor, None, Network::Testnet, database, blockchain).unwrap());
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
info!("balance: {}", wallet.get_balance()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
extern crate bdk;
|
extern crate bdk;
|
||||||
extern crate bitcoin;
|
extern crate bitcoin;
|
||||||
@@ -29,6 +16,7 @@ extern crate log;
|
|||||||
extern crate miniscript;
|
extern crate miniscript;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -40,9 +28,10 @@ use miniscript::policy::Concrete;
|
|||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
use bdk::database::memory::MemoryDatabase;
|
use bdk::database::memory::MemoryDatabase;
|
||||||
|
use bdk::wallet::AddressIndex::New;
|
||||||
use bdk::{KeychainKind, Wallet};
|
use bdk::{KeychainKind, Wallet};
|
||||||
|
|
||||||
fn main() {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
env_logger::init_from_env(
|
env_logger::init_from_env(
|
||||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||||
);
|
);
|
||||||
@@ -74,19 +63,19 @@ fn main() {
|
|||||||
.help("Sets the network")
|
.help("Sets the network")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.default_value("testnet")
|
.default_value("testnet")
|
||||||
.possible_values(&["testnet", "regtest"]),
|
.possible_values(&["testnet", "regtest", "bitcoin", "signet"]),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let policy_str = matches.value_of("POLICY").unwrap();
|
let policy_str = matches.value_of("POLICY").unwrap();
|
||||||
info!("Compiling policy: {}", policy_str);
|
info!("Compiling policy: {}", policy_str);
|
||||||
|
|
||||||
let policy = Concrete::<String>::from_str(&policy_str).unwrap();
|
let policy = Concrete::<String>::from_str(&policy_str)?;
|
||||||
|
|
||||||
let descriptor = match matches.value_of("TYPE").unwrap() {
|
let descriptor = match matches.value_of("TYPE").unwrap() {
|
||||||
"sh" => Descriptor::Sh(policy.compile().unwrap()),
|
"sh" => Descriptor::new_sh(policy.compile()?)?,
|
||||||
"wsh" => Descriptor::Wsh(policy.compile().unwrap()),
|
"wsh" => Descriptor::new_wsh(policy.compile()?)?,
|
||||||
"sh-wsh" => Descriptor::ShWsh(policy.compile().unwrap()),
|
"sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?)?,
|
||||||
_ => panic!("Invalid type"),
|
_ => panic!("Invalid type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,19 +83,23 @@ fn main() {
|
|||||||
|
|
||||||
let database = MemoryDatabase::new();
|
let database = MemoryDatabase::new();
|
||||||
|
|
||||||
let network = match matches.value_of("network") {
|
let network = matches
|
||||||
Some("regtest") => Network::Regtest,
|
.value_of("network")
|
||||||
Some("testnet") | _ => Network::Testnet,
|
.map(|n| Network::from_str(n))
|
||||||
};
|
.transpose()
|
||||||
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database).unwrap();
|
.unwrap()
|
||||||
|
.unwrap_or(Network::Testnet);
|
||||||
|
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database)?;
|
||||||
|
|
||||||
info!("... First address: {}", wallet.get_new_address().unwrap());
|
info!("... First address: {}", wallet.get_address(New)?);
|
||||||
|
|
||||||
if matches.is_present("parsed_policy") {
|
if matches.is_present("parsed_policy") {
|
||||||
let spending_policy = wallet.policies(KeychainKind::External).unwrap();
|
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||||
info!(
|
info!(
|
||||||
"... Spending policy:\n{}",
|
"... Spending policy:\n{}",
|
||||||
serde_json::to_string_pretty(&spending_policy).unwrap()
|
serde_json::to_string_pretty(&spending_policy)?
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
// Magical Bitcoin Library
|
|
||||||
// Written in 2020 by
|
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
extern crate bdk;
|
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
|
||||||
use bdk::bitcoin::util::bip32::ChildNumber;
|
|
||||||
use bdk::bitcoin::*;
|
|
||||||
use bdk::descriptor::*;
|
|
||||||
use bdk::miniscript::DescriptorPublicKeyCtx;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
|
|
||||||
let desc = "wsh(or_d(\
|
|
||||||
multi(\
|
|
||||||
2,[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/*\
|
|
||||||
),\
|
|
||||||
and_v(vc:pk_h(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy),older(1000))\
|
|
||||||
))";
|
|
||||||
|
|
||||||
let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(desc).unwrap();
|
|
||||||
println!("{:?}", extended_desc);
|
|
||||||
|
|
||||||
let deriv_ctx = DescriptorPublicKeyCtx::new(&secp, ChildNumber::from_normal_idx(42).unwrap());
|
|
||||||
|
|
||||||
let signers = Arc::new(key_map.into());
|
|
||||||
let policy = extended_desc.extract_policy(&signers, &secp).unwrap();
|
|
||||||
println!("policy: {}", serde_json::to_string(&policy).unwrap());
|
|
||||||
|
|
||||||
let addr = extended_desc.address(Network::Testnet, deriv_ctx).unwrap();
|
|
||||||
println!("{}", addr);
|
|
||||||
|
|
||||||
let script = extended_desc.witness_script(deriv_ctx);
|
|
||||||
println!("{:?}", script);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk-macros"
|
name = "bdk-macros"
|
||||||
version = "0.2.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"
|
||||||
@@ -8,7 +8,7 @@ repository = "https://github.com/bitcoindevkit/bdk"
|
|||||||
documentation = "https://docs.rs/bdk-macros"
|
documentation = "https://docs.rs/bdk-macros"
|
||||||
description = "Supporting macros for `bdk`"
|
description = "Supporting macros for `bdk`"
|
||||||
keywords = ["bdk"]
|
keywords = ["bdk"]
|
||||||
license = "MIT"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate quote;
|
extern crate quote;
|
||||||
@@ -134,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::Runtime::new().unwrap().block_on(#expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
|
|
||||||
{
|
|
||||||
#expr.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
quoted.into()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Runtime-checked blockchain types
|
//! Runtime-checked blockchain types
|
||||||
//!
|
//!
|
||||||
@@ -50,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,
|
||||||
@@ -73,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(
|
||||||
@@ -82,6 +71,7 @@
|
|||||||
//! MemoryDatabase::default(),
|
//! MemoryDatabase::default(),
|
||||||
//! blockchain,
|
//! blockchain,
|
||||||
//! )?;
|
//! )?;
|
||||||
|
//! # }
|
||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
@@ -107,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, )* ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,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]
|
||||||
@@ -139,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> {
|
||||||
@@ -184,13 +166,43 @@ 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
|
||||||
///
|
///
|
||||||
/// This allows storing a single configuration that can be loaded into an [`AnyBlockchain`]
|
/// This allows storing a single configuration that can be loaded into an [`AnyBlockchain`]
|
||||||
/// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime
|
/// instance. Wallets that plan to offer users the ability to switch blockchain backend at runtime
|
||||||
/// will find this particularly useful.
|
/// will find this particularly useful.
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
///
|
||||||
|
/// This type can be serialized from a JSON object like:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # #[cfg(feature = "electrum")]
|
||||||
|
/// # {
|
||||||
|
/// use bdk::blockchain::{electrum::ElectrumBlockchainConfig, AnyBlockchainConfig};
|
||||||
|
/// let config: AnyBlockchainConfig = serde_json::from_str(
|
||||||
|
/// r#"{
|
||||||
|
/// "type" : "electrum",
|
||||||
|
/// "url" : "ssl://electrum.blockstream.info:50002",
|
||||||
|
/// "retry": 2,
|
||||||
|
/// "stop_gap": 20
|
||||||
|
/// }"#,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(
|
||||||
|
/// config,
|
||||||
|
/// AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
|
||||||
|
/// url: "ssl://electrum.blockstream.info:50002".into(),
|
||||||
|
/// retry: 2,
|
||||||
|
/// socks5: None,
|
||||||
|
/// timeout: None,
|
||||||
|
/// stop_gap: 20,
|
||||||
|
/// })
|
||||||
|
/// );
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum AnyBlockchainConfig {
|
pub enum AnyBlockchainConfig {
|
||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
|
||||||
@@ -204,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 {
|
||||||
@@ -223,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)?)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,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")]);
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Compact Filters
|
//! Compact Filters
|
||||||
//!
|
//!
|
||||||
@@ -83,8 +70,8 @@ mod sync;
|
|||||||
use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::types::{KeychainKind, TransactionDetails, UTXO};
|
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||||
use crate::FeeRate;
|
use crate::{ConfirmationTime, FeeRate};
|
||||||
|
|
||||||
use peer::*;
|
use peer::*;
|
||||||
use store::*;
|
use store::*;
|
||||||
@@ -159,7 +146,7 @@ impl CompactFiltersBlockchain {
|
|||||||
database: &mut D,
|
database: &mut D,
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
height: Option<u32>,
|
height: Option<u32>,
|
||||||
timestamp: u64,
|
timestamp: Option<u64>,
|
||||||
internal_max_deriv: &mut Option<u32>,
|
internal_max_deriv: &mut Option<u32>,
|
||||||
external_max_deriv: &mut Option<u32>,
|
external_max_deriv: &mut Option<u32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -194,7 +181,7 @@ impl CompactFiltersBlockchain {
|
|||||||
database.get_path_from_script_pubkey(&output.script_pubkey)?
|
database.get_path_from_script_pubkey(&output.script_pubkey)?
|
||||||
{
|
{
|
||||||
debug!("{} output #{} is mine, adding utxo", tx.txid(), i);
|
debug!("{} output #{} is mine, adding utxo", tx.txid(), i);
|
||||||
updates.set_utxo(&UTXO {
|
updates.set_utxo(&LocalUtxo {
|
||||||
outpoint: OutPoint::new(tx.txid(), i as u32),
|
outpoint: OutPoint::new(tx.txid(), i as u32),
|
||||||
txout: output.clone(),
|
txout: output.clone(),
|
||||||
keychain,
|
keychain,
|
||||||
@@ -219,9 +206,9 @@ impl CompactFiltersBlockchain {
|
|||||||
transaction: Some(tx.clone()),
|
transaction: Some(tx.clone()),
|
||||||
received: incoming,
|
received: incoming,
|
||||||
sent: outgoing,
|
sent: outgoing,
|
||||||
height,
|
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||||
timestamp,
|
verified: height.is_some(),
|
||||||
fees: inputs_sum.saturating_sub(outputs_sum),
|
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Saving tx {}", tx.txid);
|
info!("Saving tx {}", tx.txid);
|
||||||
@@ -239,9 +226,9 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
vec![Capability::FullHistory].into_iter().collect()
|
vec![Capability::FullHistory].into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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> {
|
||||||
@@ -249,7 +236,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
|
|
||||||
let skip_blocks = self.skip_blocks.unwrap_or(0);
|
let skip_blocks = self.skip_blocks.unwrap_or(0);
|
||||||
|
|
||||||
let cf_sync = Arc::new(CFSync::new(Arc::clone(&self.headers), skip_blocks, 0x00)?);
|
let cf_sync = Arc::new(CfSync::new(Arc::clone(&self.headers), skip_blocks, 0x00)?);
|
||||||
|
|
||||||
let initial_height = self.headers.get_height()?;
|
let initial_height = self.headers.get_height()?;
|
||||||
let total_bundles = (first_peer.get_version().start_height as usize)
|
let total_bundles = (first_peer.get_version().start_height as usize)
|
||||||
@@ -376,14 +363,19 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
);
|
);
|
||||||
let mut updates = database.begin_batch();
|
let mut updates = database.begin_batch();
|
||||||
for details in database.iter_txs(false)? {
|
for details in database.iter_txs(false)? {
|
||||||
match details.height {
|
match details.confirmation_time {
|
||||||
Some(height) if (height as usize) < last_synced_block => continue,
|
Some(c) if (c.height as usize) < last_synced_block => continue,
|
||||||
_ => updates.del_tx(&details.txid, false)?,
|
_ => updates.del_tx(&details.txid, false)?,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
database.commit_batch(updates)?;
|
database.commit_batch(updates)?;
|
||||||
|
|
||||||
first_peer.ask_for_mempool()?;
|
match first_peer.ask_for_mempool() {
|
||||||
|
Err(CompactFiltersError::PeerBloomDisabled) => {
|
||||||
|
log::warn!("Peer has BLOOM disabled, we can't ask for the mempool")
|
||||||
|
}
|
||||||
|
e => e?,
|
||||||
|
};
|
||||||
|
|
||||||
let mut internal_max_deriv = None;
|
let mut internal_max_deriv = None;
|
||||||
let mut external_max_deriv = None;
|
let mut external_max_deriv = None;
|
||||||
@@ -394,7 +386,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
database,
|
database,
|
||||||
tx,
|
tx,
|
||||||
Some(height as u32),
|
Some(height as u32),
|
||||||
0,
|
None,
|
||||||
&mut internal_max_deriv,
|
&mut internal_max_deriv,
|
||||||
&mut external_max_deriv,
|
&mut external_max_deriv,
|
||||||
)?;
|
)?;
|
||||||
@@ -405,7 +397,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
database,
|
database,
|
||||||
tx,
|
tx,
|
||||||
None,
|
None,
|
||||||
0,
|
None,
|
||||||
&mut internal_max_deriv,
|
&mut internal_max_deriv,
|
||||||
&mut external_max_deriv,
|
&mut external_max_deriv,
|
||||||
)?;
|
)?;
|
||||||
@@ -463,7 +455,7 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Data to connect to a Bitcoin P2P peer
|
/// Data to connect to a Bitcoin P2P peer
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
pub struct BitcoinPeerConfig {
|
pub struct BitcoinPeerConfig {
|
||||||
/// Peer address such as 127.0.0.1:18333
|
/// Peer address such as 127.0.0.1:18333
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@@ -474,7 +466,7 @@ pub struct BitcoinPeerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for a [`CompactFiltersBlockchain`]
|
/// Configuration for a [`CompactFiltersBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
pub struct CompactFiltersBlockchainConfig {
|
pub struct CompactFiltersBlockchainConfig {
|
||||||
/// List of peers to try to connect to for asking headers and filters
|
/// List of peers to try to connect to for asking headers and filters
|
||||||
pub peers: Vec<BitcoinPeerConfig>,
|
pub peers: Vec<BitcoinPeerConfig>,
|
||||||
@@ -537,16 +529,18 @@ pub enum CompactFiltersError {
|
|||||||
NotConnected,
|
NotConnected,
|
||||||
/// A peer took too long to reply to one of our messages
|
/// A peer took too long to reply to one of our messages
|
||||||
Timeout,
|
Timeout,
|
||||||
|
/// The peer doesn't advertise the [`BLOOM`](bitcoin::network::constants::ServiceFlags::BLOOM) service flag
|
||||||
|
PeerBloomDisabled,
|
||||||
|
|
||||||
/// No peers have been specified
|
/// No peers have been specified
|
||||||
NoPeers,
|
NoPeers,
|
||||||
|
|
||||||
/// Internal database error
|
/// Internal database error
|
||||||
DB(rocksdb::Error),
|
Db(rocksdb::Error),
|
||||||
/// Internal I/O error
|
/// Internal I/O error
|
||||||
IO(std::io::Error),
|
Io(std::io::Error),
|
||||||
/// Invalid BIP158 filter
|
/// Invalid BIP158 filter
|
||||||
BIP158(bitcoin::util::bip158::Error),
|
Bip158(bitcoin::util::bip158::Error),
|
||||||
/// Internal system time error
|
/// Internal system time error
|
||||||
Time(std::time::SystemTimeError),
|
Time(std::time::SystemTimeError),
|
||||||
|
|
||||||
@@ -562,9 +556,9 @@ impl fmt::Display for CompactFiltersError {
|
|||||||
|
|
||||||
impl std::error::Error for CompactFiltersError {}
|
impl std::error::Error for CompactFiltersError {}
|
||||||
|
|
||||||
impl_error!(rocksdb::Error, DB, CompactFiltersError);
|
impl_error!(rocksdb::Error, Db, CompactFiltersError);
|
||||||
impl_error!(std::io::Error, IO, CompactFiltersError);
|
impl_error!(std::io::Error, Io, CompactFiltersError);
|
||||||
impl_error!(bitcoin::util::bip158::Error, BIP158, CompactFiltersError);
|
impl_error!(bitcoin::util::bip158::Error, Bip158, CompactFiltersError);
|
||||||
impl_error!(std::time::SystemTimeError, Time, CompactFiltersError);
|
impl_error!(std::time::SystemTimeError, Time, CompactFiltersError);
|
||||||
|
|
||||||
impl From<crate::error::Error> for CompactFiltersError {
|
impl From<crate::error::Error> for CompactFiltersError {
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{TcpStream, ToSocketAddrs};
|
use std::net::{TcpStream, ToSocketAddrs};
|
||||||
@@ -34,7 +21,6 @@ use rand::{thread_rng, Rng};
|
|||||||
|
|
||||||
use bitcoin::consensus::Encodable;
|
use bitcoin::consensus::Encodable;
|
||||||
use bitcoin::hash_types::BlockHash;
|
use bitcoin::hash_types::BlockHash;
|
||||||
use bitcoin::hashes::Hash;
|
|
||||||
use bitcoin::network::constants::ServiceFlags;
|
use bitcoin::network::constants::ServiceFlags;
|
||||||
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
|
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
|
||||||
use bitcoin::network::message_blockdata::*;
|
use bitcoin::network::message_blockdata::*;
|
||||||
@@ -42,7 +28,7 @@ use bitcoin::network::message_filter::*;
|
|||||||
use bitcoin::network::message_network::VersionMessage;
|
use bitcoin::network::message_network::VersionMessage;
|
||||||
use bitcoin::network::stream_reader::StreamReader;
|
use bitcoin::network::stream_reader::StreamReader;
|
||||||
use bitcoin::network::Address;
|
use bitcoin::network::Address;
|
||||||
use bitcoin::{Block, Network, Transaction, Txid};
|
use bitcoin::{Block, Network, Transaction, Txid, Wtxid};
|
||||||
|
|
||||||
use super::CompactFiltersError;
|
use super::CompactFiltersError;
|
||||||
|
|
||||||
@@ -55,37 +41,71 @@ pub(crate) const TIMEOUT_SECS: u64 = 30;
|
|||||||
/// It is normally shared between [`Peer`]s with the use of [`Arc`], so that transactions are not
|
/// It is normally shared between [`Peer`]s with the use of [`Arc`], so that transactions are not
|
||||||
/// duplicated in memory.
|
/// duplicated in memory.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Mempool {
|
pub struct Mempool(RwLock<InnerMempool>);
|
||||||
txs: RwLock<HashMap<Txid, Transaction>>,
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct InnerMempool {
|
||||||
|
txs: HashMap<Txid, Transaction>,
|
||||||
|
wtxids: HashMap<Wtxid, Txid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum TxIdentifier {
|
||||||
|
Wtxid(Wtxid),
|
||||||
|
Txid(Txid),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mempool {
|
impl Mempool {
|
||||||
|
/// Create a new empty mempool
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a transaction to the mempool
|
/// Add a transaction to the mempool
|
||||||
///
|
///
|
||||||
/// Note that this doesn't propagate the transaction to other
|
/// Note that this doesn't propagate the transaction to other
|
||||||
/// peers. To do that, [`broadcast`](crate::blockchain::Blockchain::broadcast) should be used.
|
/// peers. To do that, [`broadcast`](crate::blockchain::Blockchain::broadcast) should be used.
|
||||||
pub fn add_tx(&self, tx: Transaction) {
|
pub fn add_tx(&self, tx: Transaction) {
|
||||||
self.txs.write().unwrap().insert(tx.txid(), tx);
|
let mut guard = self.0.write().unwrap();
|
||||||
|
|
||||||
|
guard.wtxids.insert(tx.wtxid(), tx.txid());
|
||||||
|
guard.txs.insert(tx.txid(), tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
/// Look-up a transaction in the mempool given an [`Inventory`] request
|
||||||
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
|
||||||
let txid = match inventory {
|
let identifer = match inventory {
|
||||||
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None,
|
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None,
|
||||||
Inventory::Transaction(txid) => *txid,
|
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
|
||||||
Inventory::WitnessTransaction(wtxid) => Txid::from_inner(wtxid.into_inner()),
|
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
|
||||||
|
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),
|
||||||
|
Inventory::Unknown { inv_type, hash } => {
|
||||||
|
log::warn!(
|
||||||
|
"Unknown inventory request type `{}`, hash `{:?}`",
|
||||||
|
inv_type,
|
||||||
|
hash
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.txs.read().unwrap().get(&txid).cloned()
|
|
||||||
|
let txid = match identifer {
|
||||||
|
TxIdentifier::Txid(txid) => Some(txid),
|
||||||
|
TxIdentifier::Wtxid(wtxid) => self.0.read().unwrap().wtxids.get(&wtxid).cloned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
txid.map(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
|
||||||
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether or not the mempool contains a transaction with a given txid
|
/// Return whether or not the mempool contains a transaction with a given txid
|
||||||
pub fn has_tx(&self, txid: &Txid) -> bool {
|
pub fn has_tx(&self, txid: &Txid) -> bool {
|
||||||
self.txs.read().unwrap().contains_key(txid)
|
self.0.read().unwrap().txs.contains_key(txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the list of transactions contained in the mempool
|
/// Return the list of transactions contained in the mempool
|
||||||
pub fn iter_txs(&self) -> Vec<Transaction> {
|
pub fn iter_txs(&self) -> Vec<Transaction> {
|
||||||
self.txs.read().unwrap().values().cloned().collect()
|
self.0.read().unwrap().txs.values().cloned().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,14 +210,14 @@ impl Peer {
|
|||||||
)),
|
)),
|
||||||
)?;
|
)?;
|
||||||
let version = if let NetworkMessage::Version(version) =
|
let version = if let NetworkMessage::Version(version) =
|
||||||
Self::_recv(&responses, "version", None)?.unwrap()
|
Self::_recv(&responses, "version", None).unwrap()
|
||||||
{
|
{
|
||||||
version
|
version
|
||||||
} else {
|
} else {
|
||||||
return Err(CompactFiltersError::InvalidResponse);
|
return Err(CompactFiltersError::InvalidResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
if let NetworkMessage::Verack = Self::_recv(&responses, "verack", None)?.unwrap() {
|
if let NetworkMessage::Verack = Self::_recv(&responses, "verack", None).unwrap() {
|
||||||
Self::_send(&mut locked_writer, network.magic(), NetworkMessage::Verack)?;
|
Self::_send(&mut locked_writer, network.magic(), NetworkMessage::Verack)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(CompactFiltersError::InvalidResponse);
|
return Err(CompactFiltersError::InvalidResponse);
|
||||||
@@ -207,12 +227,12 @@ impl Peer {
|
|||||||
|
|
||||||
Ok(Peer {
|
Ok(Peer {
|
||||||
writer,
|
writer,
|
||||||
reader_thread,
|
|
||||||
responses,
|
responses,
|
||||||
|
reader_thread,
|
||||||
connected,
|
connected,
|
||||||
mempool,
|
mempool,
|
||||||
network,
|
|
||||||
version,
|
version,
|
||||||
|
network,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +258,7 @@ impl Peer {
|
|||||||
responses: &Arc<RwLock<ResponsesMap>>,
|
responses: &Arc<RwLock<ResponsesMap>>,
|
||||||
wait_for: &'static str,
|
wait_for: &'static str,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> Result<Option<NetworkMessage>, CompactFiltersError> {
|
) -> Option<NetworkMessage> {
|
||||||
let message_resp = {
|
let message_resp = {
|
||||||
let mut lock = responses.write().unwrap();
|
let mut lock = responses.write().unwrap();
|
||||||
let message_resp = lock.entry(wait_for).or_default();
|
let message_resp = lock.entry(wait_for).or_default();
|
||||||
@@ -254,15 +274,14 @@ impl Peer {
|
|||||||
Some(t) => {
|
Some(t) => {
|
||||||
let result = cvar.wait_timeout(messages, t).unwrap();
|
let result = cvar.wait_timeout(messages, t).unwrap();
|
||||||
if result.1.timed_out() {
|
if result.1.timed_out() {
|
||||||
return Ok(None);
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages = result.0;
|
messages = result.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(messages.pop())
|
messages.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`VersionMessage`] sent by the peer
|
/// Return the [`VersionMessage`] sent by the peer
|
||||||
@@ -382,7 +401,7 @@ impl Peer {
|
|||||||
wait_for: &'static str,
|
wait_for: &'static str,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> Result<Option<NetworkMessage>, CompactFiltersError> {
|
) -> Result<Option<NetworkMessage>, CompactFiltersError> {
|
||||||
Self::_recv(&self.responses, wait_for, timeout)
|
Ok(Self::_recv(&self.responses, wait_for, timeout))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,6 +527,10 @@ impl InvPeer for Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ask_for_mempool(&self) -> Result<(), CompactFiltersError> {
|
fn ask_for_mempool(&self) -> Result<(), CompactFiltersError> {
|
||||||
|
if !self.version.services.has(ServiceFlags::BLOOM) {
|
||||||
|
return Err(CompactFiltersError::PeerBloomDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
self.send(NetworkMessage::MemPool)?;
|
self.send(NetworkMessage::MemPool)?;
|
||||||
let inv = match self.recv("inv", Some(Duration::from_secs(5)))? {
|
let inv = match self.recv("inv", Some(Duration::from_secs(5)))? {
|
||||||
None => return Ok(()), // empty mempool
|
None => return Ok(()), // empty mempool
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -36,9 +23,9 @@ use rand::{thread_rng, Rng};
|
|||||||
use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch, DB};
|
use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch, DB};
|
||||||
|
|
||||||
use bitcoin::consensus::{deserialize, encode::VarInt, serialize, Decodable, Encodable};
|
use bitcoin::consensus::{deserialize, encode::VarInt, serialize, Decodable, Encodable};
|
||||||
use bitcoin::hash_types::FilterHash;
|
use bitcoin::hash_types::{FilterHash, FilterHeader};
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use bitcoin::hashes::{sha256d, Hash};
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::util::bip158::BlockFilter;
|
use bitcoin::util::bip158::BlockFilter;
|
||||||
use bitcoin::util::uint::Uint256;
|
use bitcoin::util::uint::Uint256;
|
||||||
use bitcoin::Block;
|
use bitcoin::Block;
|
||||||
@@ -46,12 +33,15 @@ use bitcoin::BlockHash;
|
|||||||
use bitcoin::BlockHeader;
|
use bitcoin::BlockHeader;
|
||||||
use bitcoin::Network;
|
use bitcoin::Network;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
use super::CompactFiltersError;
|
use super::CompactFiltersError;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref MAINNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A29AB5F49FFFF001D1DAC2B7C0101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
static ref MAINNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A29AB5F49FFFF001D1DAC2B7C0101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||||
static ref TESTNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF001D1AA4AE180101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
static ref TESTNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF001D1AA4AE180101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||||
static ref REGTEST_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF7F20020000000101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
static ref REGTEST_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF7F20020000000101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||||
|
static ref SIGNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A008F4D5FAE77031E8AD222030101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait StoreType: Default + fmt::Debug {}
|
pub trait StoreType: Default + fmt::Debug {}
|
||||||
@@ -118,45 +108,19 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize(data: &[u8]) -> Result<Self, CompactFiltersError> {
|
fn deserialize(data: &[u8]) -> Result<Self, CompactFiltersError> {
|
||||||
Ok(deserialize(data).map_err(|_| CompactFiltersError::DataCorruption)?)
|
deserialize(data).map_err(|_| CompactFiltersError::DataCorruption)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encodable for FilterHeader {
|
|
||||||
fn consensus_encode<W: Write>(
|
|
||||||
&self,
|
|
||||||
mut e: W,
|
|
||||||
) -> Result<usize, bitcoin::consensus::encode::Error> {
|
|
||||||
let mut written = self.prev_header_hash.consensus_encode(&mut e)?;
|
|
||||||
written += self.filter_hash.consensus_encode(&mut e)?;
|
|
||||||
Ok(written)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Decodable for FilterHeader {
|
|
||||||
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
|
|
||||||
let prev_header_hash = FilterHeaderHash::consensus_decode(&mut d)?;
|
|
||||||
let filter_hash = FilterHash::consensus_decode(&mut d)?;
|
|
||||||
|
|
||||||
Ok(FilterHeader {
|
|
||||||
prev_header_hash,
|
|
||||||
filter_hash,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encodable for BundleStatus {
|
impl Encodable for BundleStatus {
|
||||||
fn consensus_encode<W: Write>(
|
fn consensus_encode<W: Write>(&self, mut e: W) -> Result<usize, std::io::Error> {
|
||||||
&self,
|
|
||||||
mut e: W,
|
|
||||||
) -> Result<usize, bitcoin::consensus::encode::Error> {
|
|
||||||
let mut written = 0;
|
let mut written = 0;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
BundleStatus::Init => {
|
BundleStatus::Init => {
|
||||||
written += 0x00u8.consensus_encode(&mut e)?;
|
written += 0x00u8.consensus_encode(&mut e)?;
|
||||||
}
|
}
|
||||||
BundleStatus::CFHeaders { cf_headers } => {
|
BundleStatus::CfHeaders { cf_headers } => {
|
||||||
written += 0x01u8.consensus_encode(&mut e)?;
|
written += 0x01u8.consensus_encode(&mut e)?;
|
||||||
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
|
||||||
for header in cf_headers {
|
for header in cf_headers {
|
||||||
@@ -207,7 +171,7 @@ impl Decodable for BundleStatus {
|
|||||||
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
|
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BundleStatus::CFHeaders { cf_headers })
|
Ok(BundleStatus::CfHeaders { cf_headers })
|
||||||
}
|
}
|
||||||
0x02 => {
|
0x02 => {
|
||||||
let num = VarInt::consensus_decode(&mut d)?;
|
let num = VarInt::consensus_decode(&mut d)?;
|
||||||
@@ -264,6 +228,7 @@ impl ChainStore<Full> {
|
|||||||
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
||||||
Network::Testnet => TESTNET_GENESIS.deref(),
|
Network::Testnet => TESTNET_GENESIS.deref(),
|
||||||
Network::Regtest => REGTEST_GENESIS.deref(),
|
Network::Regtest => REGTEST_GENESIS.deref(),
|
||||||
|
Network::Signet => SIGNET_GENESIS.deref(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cf_name = "default".to_string();
|
let cf_name = "default".to_string();
|
||||||
@@ -460,15 +425,14 @@ impl ChainStore<Full> {
|
|||||||
|
|
||||||
let key = StoreEntry::BlockHeaderIndex(Some(*block_hash)).get_key();
|
let key = StoreEntry::BlockHeaderIndex(Some(*block_hash)).get_key();
|
||||||
let data = read_store.get_pinned_cf(cf_handle, key)?;
|
let data = read_store.get_pinned_cf(cf_handle, key)?;
|
||||||
Ok(data
|
data.map(|data| {
|
||||||
.map(|data| {
|
Ok::<_, CompactFiltersError>(usize::from_be_bytes(
|
||||||
Ok::<_, CompactFiltersError>(usize::from_be_bytes(
|
data.as_ref()
|
||||||
data.as_ref()
|
.try_into()
|
||||||
.try_into()
|
.map_err(|_| CompactFiltersError::DataCorruption)?,
|
||||||
.map_err(|_| CompactFiltersError::DataCorruption)?,
|
))
|
||||||
))
|
})
|
||||||
})
|
.transpose()
|
||||||
.transpose()?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_block_hash(&self, height: usize) -> Result<Option<BlockHash>, CompactFiltersError> {
|
pub fn get_block_hash(&self, height: usize) -> Result<Option<BlockHash>, CompactFiltersError> {
|
||||||
@@ -477,13 +441,12 @@ impl ChainStore<Full> {
|
|||||||
|
|
||||||
let key = StoreEntry::BlockHeader(Some(height)).get_key();
|
let key = StoreEntry::BlockHeader(Some(height)).get_key();
|
||||||
let data = read_store.get_pinned_cf(cf_handle, key)?;
|
let data = read_store.get_pinned_cf(cf_handle, key)?;
|
||||||
Ok(data
|
data.map(|data| {
|
||||||
.map(|data| {
|
let (header, _): (BlockHeader, Uint256) =
|
||||||
let (header, _): (BlockHeader, Uint256) =
|
deserialize(&data).map_err(|_| CompactFiltersError::DataCorruption)?;
|
||||||
deserialize(&data).map_err(|_| CompactFiltersError::DataCorruption)?;
|
Ok::<_, CompactFiltersError>(header.block_hash())
|
||||||
Ok::<_, CompactFiltersError>(header.block_hash())
|
})
|
||||||
})
|
.transpose()
|
||||||
.transpose()?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_full_block(&self, block: &Block, height: usize) -> Result<(), CompactFiltersError> {
|
pub fn save_full_block(&self, block: &Block, height: usize) -> Result<(), CompactFiltersError> {
|
||||||
@@ -499,10 +462,10 @@ impl ChainStore<Full> {
|
|||||||
let key = StoreEntry::Block(Some(height)).get_key();
|
let key = StoreEntry::Block(Some(height)).get_key();
|
||||||
let opt_block = read_store.get_pinned(key)?;
|
let opt_block = read_store.get_pinned(key)?;
|
||||||
|
|
||||||
Ok(opt_block
|
opt_block
|
||||||
.map(|data| deserialize(&data))
|
.map(|data| deserialize(&data))
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|_| CompactFiltersError::DataCorruption)?)
|
.map_err(|_| CompactFiltersError::DataCorruption)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_blocks_until(&self, height: usize) -> Result<(), CompactFiltersError> {
|
pub fn delete_blocks_until(&self, height: usize) -> Result<(), CompactFiltersError> {
|
||||||
@@ -589,14 +552,14 @@ impl<T: StoreType> ChainStore<T> {
|
|||||||
let prefix = StoreEntry::BlockHeader(None).get_key();
|
let prefix = StoreEntry::BlockHeader(None).get_key();
|
||||||
let iterator = read_store.prefix_iterator_cf(cf_handle, prefix);
|
let iterator = read_store.prefix_iterator_cf(cf_handle, prefix);
|
||||||
|
|
||||||
Ok(iterator
|
iterator
|
||||||
.last()
|
.last()
|
||||||
.map(|(_, v)| -> Result<_, CompactFiltersError> {
|
.map(|(_, v)| -> Result<_, CompactFiltersError> {
|
||||||
let (header, _): (BlockHeader, Uint256) = SerializeDb::deserialize(&v)?;
|
let (header, _): (BlockHeader, Uint256) = SerializeDb::deserialize(&v)?;
|
||||||
|
|
||||||
Ok(header.block_hash())
|
Ok(header.block_hash())
|
||||||
})
|
})
|
||||||
.transpose()?)
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply(
|
pub fn apply(
|
||||||
@@ -658,44 +621,28 @@ impl<T: StoreType> fmt::Debug for ChainStore<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FilterHeaderHash = FilterHash;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FilterHeader {
|
|
||||||
prev_header_hash: FilterHeaderHash,
|
|
||||||
filter_hash: FilterHash,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FilterHeader {
|
|
||||||
fn header_hash(&self) -> FilterHeaderHash {
|
|
||||||
let mut hash_data = self.filter_hash.into_inner().to_vec();
|
|
||||||
hash_data.extend_from_slice(&self.prev_header_hash);
|
|
||||||
sha256d::Hash::hash(&hash_data).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum BundleStatus {
|
pub enum BundleStatus {
|
||||||
Init,
|
Init,
|
||||||
CFHeaders { cf_headers: Vec<FilterHeader> },
|
CfHeaders { cf_headers: Vec<FilterHeader> },
|
||||||
CFilters { cf_filters: Vec<Vec<u8>> },
|
CFilters { cf_filters: Vec<Vec<u8>> },
|
||||||
Processed { cf_filters: Vec<Vec<u8>> },
|
Processed { cf_filters: Vec<Vec<u8>> },
|
||||||
Tip { cf_filters: Vec<Vec<u8>> },
|
Tip { cf_filters: Vec<Vec<u8>> },
|
||||||
Pruned,
|
Pruned,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CFStore {
|
pub struct CfStore {
|
||||||
store: Arc<RwLock<DB>>,
|
store: Arc<RwLock<DB>>,
|
||||||
filter_type: u8,
|
filter_type: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
type BundleEntry = (BundleStatus, FilterHeaderHash);
|
type BundleEntry = (BundleStatus, FilterHeader);
|
||||||
|
|
||||||
impl CFStore {
|
impl CfStore {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
headers_store: &ChainStore<Full>,
|
headers_store: &ChainStore<Full>,
|
||||||
filter_type: u8,
|
filter_type: u8,
|
||||||
) -> Result<Self, CompactFiltersError> {
|
) -> Result<Self, CompactFiltersError> {
|
||||||
let cf_store = CFStore {
|
let cf_store = CfStore {
|
||||||
store: Arc::clone(&headers_store.store),
|
store: Arc::clone(&headers_store.store),
|
||||||
filter_type,
|
filter_type,
|
||||||
};
|
};
|
||||||
@@ -704,6 +651,7 @@ impl CFStore {
|
|||||||
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
Network::Bitcoin => MAINNET_GENESIS.deref(),
|
||||||
Network::Testnet => TESTNET_GENESIS.deref(),
|
Network::Testnet => TESTNET_GENESIS.deref(),
|
||||||
Network::Regtest => REGTEST_GENESIS.deref(),
|
Network::Regtest => REGTEST_GENESIS.deref(),
|
||||||
|
Network::Signet => SIGNET_GENESIS.deref(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = BlockFilter::new_script_filter(genesis, |utxo| {
|
let filter = BlockFilter::new_script_filter(genesis, |utxo| {
|
||||||
@@ -717,7 +665,11 @@ impl CFStore {
|
|||||||
if read_store.get_pinned(&first_key)?.is_none() {
|
if read_store.get_pinned(&first_key)?.is_none() {
|
||||||
read_store.put(
|
read_store.put(
|
||||||
&first_key,
|
&first_key,
|
||||||
(BundleStatus::Init, filter.filter_id(&FilterHash::default())).serialize(),
|
(
|
||||||
|
BundleStatus::Init,
|
||||||
|
filter.filter_header(&FilterHeader::from_hash(Default::default())),
|
||||||
|
)
|
||||||
|
.serialize(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -743,7 +695,7 @@ impl CFStore {
|
|||||||
.collect::<Result<_, _>>()
|
.collect::<Result<_, _>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_checkpoints(&self) -> Result<Vec<FilterHash>, CompactFiltersError> {
|
pub fn get_checkpoints(&self) -> Result<Vec<FilterHeader>, CompactFiltersError> {
|
||||||
let read_store = self.store.read().unwrap();
|
let read_store = self.store.read().unwrap();
|
||||||
|
|
||||||
let prefix = StoreEntry::CFilterTable((self.filter_type, None)).get_key();
|
let prefix = StoreEntry::CFilterTable((self.filter_type, None)).get_key();
|
||||||
@@ -751,16 +703,16 @@ impl CFStore {
|
|||||||
|
|
||||||
// FIXME: we have to filter manually because rocksdb sometimes returns stuff that doesn't
|
// FIXME: we have to filter manually because rocksdb sometimes returns stuff that doesn't
|
||||||
// have the right prefix
|
// have the right prefix
|
||||||
Ok(iterator
|
iterator
|
||||||
.filter(|(k, _)| k.starts_with(&prefix))
|
.filter(|(k, _)| k.starts_with(&prefix))
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.map(|(_, data)| Ok::<_, CompactFiltersError>(BundleEntry::deserialize(&data)?.1))
|
.map(|(_, data)| Ok::<_, CompactFiltersError>(BundleEntry::deserialize(&data)?.1))
|
||||||
.collect::<Result<_, _>>()?)
|
.collect::<Result<_, _>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_checkpoints(
|
pub fn replace_checkpoints(
|
||||||
&self,
|
&self,
|
||||||
checkpoints: Vec<FilterHash>,
|
checkpoints: Vec<FilterHeader>,
|
||||||
) -> Result<(), CompactFiltersError> {
|
) -> Result<(), CompactFiltersError> {
|
||||||
let current_checkpoints = self.get_checkpoints()?;
|
let current_checkpoints = self.get_checkpoints()?;
|
||||||
|
|
||||||
@@ -802,20 +754,16 @@ impl CFStore {
|
|||||||
pub fn advance_to_cf_headers(
|
pub fn advance_to_cf_headers(
|
||||||
&self,
|
&self,
|
||||||
bundle: usize,
|
bundle: usize,
|
||||||
checkpoint_hash: FilterHeaderHash,
|
checkpoint: FilterHeader,
|
||||||
filter_headers: Vec<FilterHash>,
|
filter_hashes: Vec<FilterHash>,
|
||||||
) -> Result<BundleStatus, CompactFiltersError> {
|
) -> Result<BundleStatus, CompactFiltersError> {
|
||||||
let mut last_hash = checkpoint_hash;
|
let cf_headers: Vec<FilterHeader> = filter_hashes
|
||||||
let cf_headers = filter_headers
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|filter_hash| {
|
.scan(checkpoint, |prev_header, filter_hash| {
|
||||||
let filter_header = FilterHeader {
|
let filter_header = filter_hash.filter_header(&prev_header);
|
||||||
prev_header_hash: last_hash,
|
*prev_header = filter_header;
|
||||||
filter_hash,
|
|
||||||
};
|
|
||||||
last_hash = filter_header.header_hash();
|
|
||||||
|
|
||||||
filter_header
|
Some(filter_header)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -828,13 +776,13 @@ impl CFStore {
|
|||||||
.transpose()?
|
.transpose()?
|
||||||
{
|
{
|
||||||
// check connection with the next bundle if present
|
// check connection with the next bundle if present
|
||||||
if last_hash != next_checkpoint {
|
if cf_headers.iter().last() != Some(&next_checkpoint) {
|
||||||
return Err(CompactFiltersError::InvalidFilterHeader);
|
return Err(CompactFiltersError::InvalidFilterHeader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
||||||
let value = (BundleStatus::CFHeaders { cf_headers }, checkpoint_hash);
|
let value = (BundleStatus::CfHeaders { cf_headers }, checkpoint);
|
||||||
|
|
||||||
read_store.put(key, value.serialize())?;
|
read_store.put(key, value.serialize())?;
|
||||||
|
|
||||||
@@ -844,24 +792,26 @@ impl CFStore {
|
|||||||
pub fn advance_to_cf_filters(
|
pub fn advance_to_cf_filters(
|
||||||
&self,
|
&self,
|
||||||
bundle: usize,
|
bundle: usize,
|
||||||
checkpoint_hash: FilterHeaderHash,
|
checkpoint: FilterHeader,
|
||||||
headers: Vec<FilterHeader>,
|
headers: Vec<FilterHeader>,
|
||||||
filters: Vec<(usize, Vec<u8>)>,
|
filters: Vec<(usize, Vec<u8>)>,
|
||||||
) -> Result<BundleStatus, CompactFiltersError> {
|
) -> Result<BundleStatus, CompactFiltersError> {
|
||||||
let cf_filters = filters
|
let cf_filters = filters
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(headers.iter())
|
.zip(headers.into_iter())
|
||||||
.map(|((_, filter_content), header)| {
|
.scan(checkpoint, |prev_header, ((_, filter_content), header)| {
|
||||||
if header.filter_hash != sha256d::Hash::hash(&filter_content).into() {
|
let filter = BlockFilter::new(&filter_content);
|
||||||
return Err(CompactFiltersError::InvalidFilter);
|
if header != filter.filter_header(&prev_header) {
|
||||||
|
return Some(Err(CompactFiltersError::InvalidFilter));
|
||||||
}
|
}
|
||||||
|
*prev_header = header;
|
||||||
|
|
||||||
Ok::<_, CompactFiltersError>(filter_content)
|
Some(Ok::<_, CompactFiltersError>(filter_content))
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
||||||
let value = (BundleStatus::CFilters { cf_filters }, checkpoint_hash);
|
let value = (BundleStatus::CFilters { cf_filters }, checkpoint);
|
||||||
|
|
||||||
let read_store = self.store.read().unwrap();
|
let read_store = self.store.read().unwrap();
|
||||||
read_store.put(key, value.serialize())?;
|
read_store.put(key, value.serialize())?;
|
||||||
@@ -872,10 +822,10 @@ impl CFStore {
|
|||||||
pub fn prune_filters(
|
pub fn prune_filters(
|
||||||
&self,
|
&self,
|
||||||
bundle: usize,
|
bundle: usize,
|
||||||
checkpoint_hash: FilterHeaderHash,
|
checkpoint: FilterHeader,
|
||||||
) -> Result<BundleStatus, CompactFiltersError> {
|
) -> Result<BundleStatus, CompactFiltersError> {
|
||||||
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
||||||
let value = (BundleStatus::Pruned, checkpoint_hash);
|
let value = (BundleStatus::Pruned, checkpoint);
|
||||||
|
|
||||||
let read_store = self.store.read().unwrap();
|
let read_store = self.store.read().unwrap();
|
||||||
read_store.put(key, value.serialize())?;
|
read_store.put(key, value.serialize())?;
|
||||||
@@ -887,10 +837,10 @@ impl CFStore {
|
|||||||
&self,
|
&self,
|
||||||
bundle: usize,
|
bundle: usize,
|
||||||
cf_filters: Vec<Vec<u8>>,
|
cf_filters: Vec<Vec<u8>>,
|
||||||
checkpoint_hash: FilterHeaderHash,
|
checkpoint: FilterHeader,
|
||||||
) -> Result<BundleStatus, CompactFiltersError> {
|
) -> Result<BundleStatus, CompactFiltersError> {
|
||||||
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
let key = StoreEntry::CFilterTable((self.filter_type, Some(bundle))).get_key();
|
||||||
let value = (BundleStatus::Tip { cf_filters }, checkpoint_hash);
|
let value = (BundleStatus::Tip { cf_filters }, checkpoint);
|
||||||
|
|
||||||
let read_store = self.store.read().unwrap();
|
let read_store = self.store.read().unwrap();
|
||||||
read_store.put(key, value.serialize())?;
|
read_store.put(key, value.serialize())?;
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bitcoin::hash_types::{BlockHash, FilterHash};
|
use bitcoin::hash_types::{BlockHash, FilterHeader};
|
||||||
use bitcoin::network::message::NetworkMessage;
|
use bitcoin::network::message::NetworkMessage;
|
||||||
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
use bitcoin::network::message_blockdata::GetHeadersMessage;
|
||||||
use bitcoin::util::bip158::BlockFilter;
|
use bitcoin::util::bip158::BlockFilter;
|
||||||
@@ -38,22 +25,22 @@ use crate::error::Error;
|
|||||||
|
|
||||||
pub(crate) const BURIED_CONFIRMATIONS: usize = 100;
|
pub(crate) const BURIED_CONFIRMATIONS: usize = 100;
|
||||||
|
|
||||||
pub struct CFSync {
|
pub struct CfSync {
|
||||||
headers_store: Arc<ChainStore<Full>>,
|
headers_store: Arc<ChainStore<Full>>,
|
||||||
cf_store: Arc<CFStore>,
|
cf_store: Arc<CfStore>,
|
||||||
skip_blocks: usize,
|
skip_blocks: usize,
|
||||||
bundles: Mutex<VecDeque<(BundleStatus, FilterHash, usize)>>,
|
bundles: Mutex<VecDeque<(BundleStatus, FilterHeader, usize)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CFSync {
|
impl CfSync {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
headers_store: Arc<ChainStore<Full>>,
|
headers_store: Arc<ChainStore<Full>>,
|
||||||
skip_blocks: usize,
|
skip_blocks: usize,
|
||||||
filter_type: u8,
|
filter_type: u8,
|
||||||
) -> Result<Self, CompactFiltersError> {
|
) -> Result<Self, CompactFiltersError> {
|
||||||
let cf_store = Arc::new(CFStore::new(&headers_store, filter_type)?);
|
let cf_store = Arc::new(CfStore::new(&headers_store, filter_type)?);
|
||||||
|
|
||||||
Ok(CFSync {
|
Ok(CfSync {
|
||||||
headers_store,
|
headers_store,
|
||||||
cf_store,
|
cf_store,
|
||||||
skip_blocks,
|
skip_blocks,
|
||||||
@@ -148,7 +135,7 @@ impl CFSync {
|
|||||||
|
|
||||||
let resp = peer.get_cf_headers(0x00, start_height as u32, stop_hash)?;
|
let resp = peer.get_cf_headers(0x00, start_height as u32, stop_hash)?;
|
||||||
|
|
||||||
assert!(resp.previous_filter == checkpoint);
|
assert!(resp.previous_filter_header == checkpoint);
|
||||||
status =
|
status =
|
||||||
self.cf_store
|
self.cf_store
|
||||||
.advance_to_cf_headers(index, checkpoint, resp.filter_hashes)?;
|
.advance_to_cf_headers(index, checkpoint, resp.filter_hashes)?;
|
||||||
@@ -164,7 +151,7 @@ impl CFSync {
|
|||||||
checkpoint,
|
checkpoint,
|
||||||
headers_resp.filter_hashes,
|
headers_resp.filter_hashes,
|
||||||
)? {
|
)? {
|
||||||
BundleStatus::CFHeaders { cf_headers } => cf_headers,
|
BundleStatus::CfHeaders { cf_headers } => cf_headers,
|
||||||
_ => return Err(CompactFiltersError::InvalidResponse),
|
_ => return Err(CompactFiltersError::InvalidResponse),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,7 +171,7 @@ impl CFSync {
|
|||||||
.cf_store
|
.cf_store
|
||||||
.advance_to_cf_filters(index, checkpoint, cf_headers, filters)?;
|
.advance_to_cf_filters(index, checkpoint, cf_headers, filters)?;
|
||||||
}
|
}
|
||||||
if let BundleStatus::CFHeaders { cf_headers } = status {
|
if let BundleStatus::CfHeaders { cf_headers } = status {
|
||||||
log::trace!("status: CFHeaders");
|
log::trace!("status: CFHeaders");
|
||||||
|
|
||||||
peer.get_cf_filters(
|
peer.get_cf_filters(
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Electrum
|
//! Electrum
|
||||||
//!
|
//!
|
||||||
@@ -46,7 +33,7 @@ use bitcoin::{BlockHeader, Script, Transaction, Txid};
|
|||||||
|
|
||||||
use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config};
|
use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config};
|
||||||
|
|
||||||
use self::utils::{ELSGetHistoryRes, ElectrumLikeSync};
|
use self::utils::{ElectrumLikeSync, ElsGetHistoryRes};
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
@@ -56,18 +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,
|
||||||
#[cfg(test)]
|
stop_gap: usize,
|
||||||
#[cfg(feature = "test-electrum")]
|
|
||||||
#[bdk_blockchain_tests(crate)]
|
|
||||||
fn local_electrs() -> ElectrumBlockchain {
|
|
||||||
ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<Client> for ElectrumBlockchain {
|
impl std::convert::From<Client> for ElectrumBlockchain {
|
||||||
fn from(client: Client) -> Self {
|
fn from(client: Client) -> Self {
|
||||||
ElectrumBlockchain(client)
|
ElectrumBlockchain {
|
||||||
|
client,
|
||||||
|
stop_gap: 20,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,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
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +105,7 @@ impl ElectrumLikeSync for Client {
|
|||||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
||||||
&self,
|
&self,
|
||||||
scripts: I,
|
scripts: I,
|
||||||
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error> {
|
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error> {
|
||||||
self.batch_script_get_history(scripts)
|
self.batch_script_get_history(scripts)
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
v.into_iter()
|
v.into_iter()
|
||||||
@@ -129,7 +114,7 @@ impl ElectrumLikeSync for Client {
|
|||||||
.map(
|
.map(
|
||||||
|electrum_client::GetHistoryRes {
|
|electrum_client::GetHistoryRes {
|
||||||
height, tx_hash, ..
|
height, tx_hash, ..
|
||||||
}| ELSGetHistoryRes {
|
}| ElsGetHistoryRes {
|
||||||
height,
|
height,
|
||||||
tx_hash,
|
tx_hash,
|
||||||
},
|
},
|
||||||
@@ -157,7 +142,7 @@ impl ElectrumLikeSync for Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for an [`ElectrumBlockchain`]
|
/// Configuration for an [`ElectrumBlockchain`]
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
pub struct ElectrumBlockchainConfig {
|
pub struct ElectrumBlockchainConfig {
|
||||||
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
|
||||||
///
|
///
|
||||||
@@ -169,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 {
|
||||||
@@ -182,9 +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(test)]
|
||||||
|
#[cfg(feature = "test-electrum")]
|
||||||
|
crate::bdk_blockchain_tests! {
|
||||||
|
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
|
||||||
|
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
@@ -1,59 +1,33 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! 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::{ELSGetHistoryRes, ElectrumLikeSync};
|
|
||||||
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;
|
||||||
@@ -75,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +199,7 @@ impl UrlClient {
|
|||||||
async fn _script_get_history(
|
async fn _script_get_history(
|
||||||
&self,
|
&self,
|
||||||
script: &Script,
|
script: &Script,
|
||||||
) -> Result<Vec<ELSGetHistoryRes>, EsploraError> {
|
) -> Result<Vec<ElsGetHistoryRes>, EsploraError> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let scripthash = Self::script_to_scripthash(script);
|
let scripthash = Self::script_to_scripthash(script);
|
||||||
|
|
||||||
@@ -240,7 +216,7 @@ impl UrlClient {
|
|||||||
.json::<Vec<EsploraGetHistory>>()
|
.json::<Vec<EsploraGetHistory>>()
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| ELSGetHistoryRes {
|
.map(|x| ElsGetHistoryRes {
|
||||||
tx_hash: x.txid,
|
tx_hash: x.txid,
|
||||||
height: x.status.block_height.unwrap_or(0) as i32,
|
height: x.status.block_height.unwrap_or(0) as i32,
|
||||||
}),
|
}),
|
||||||
@@ -274,7 +250,7 @@ impl UrlClient {
|
|||||||
|
|
||||||
debug!("... adding {} confirmed transactions", len);
|
debug!("... adding {} confirmed transactions", len);
|
||||||
|
|
||||||
result.extend(response.into_iter().map(|x| ELSGetHistoryRes {
|
result.extend(response.into_iter().map(|x| ElsGetHistoryRes {
|
||||||
tx_hash: x.txid,
|
tx_hash: x.txid,
|
||||||
height: x.status.block_height.unwrap_or(0) as i32,
|
height: x.status.block_height.unwrap_or(0) as i32,
|
||||||
}));
|
}));
|
||||||
@@ -304,77 +280,54 @@ impl ElectrumLikeSync for UrlClient {
|
|||||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script>>(
|
||||||
&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)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
|
||||||
pub struct EsploraBlockchainConfig {
|
pub struct EsploraBlockchainConfig {
|
||||||
/// Base URL of the esplora service
|
/// Base URL of the esplora service
|
||||||
///
|
///
|
||||||
@@ -382,48 +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 {
|
crate::bdk_blockchain_tests! {
|
||||||
/// Error with the HTTP call
|
fn test_instance(test_client: &TestClient) -> EsploraBlockchain {
|
||||||
Reqwest(reqwest::Error),
|
EsploraBlockchain::new(&format!("http://{}",test_client.electrsd.esplora_url.as_ref().unwrap()), None, 20)
|
||||||
/// 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);
|
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Blockchain backends
|
//! Blockchain backends
|
||||||
//!
|
//!
|
||||||
@@ -43,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")]
|
||||||
@@ -56,6 +53,14 @@ pub use self::electrum::ElectrumBlockchain;
|
|||||||
#[cfg(feature = "electrum")]
|
#[cfg(feature = "electrum")]
|
||||||
pub use self::electrum::ElectrumBlockchainConfig;
|
pub use self::electrum::ElectrumBlockchainConfig;
|
||||||
|
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
|
||||||
|
pub mod rpc;
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
pub use self::rpc::RpcBlockchain;
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
pub use self::rpc::RpcConfig;
|
||||||
|
|
||||||
#[cfg(feature = "esplora")]
|
#[cfg(feature = "esplora")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
|
||||||
pub mod esplora;
|
pub mod esplora;
|
||||||
@@ -65,6 +70,7 @@ pub use self::esplora::EsploraBlockchain;
|
|||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
|
||||||
pub mod compact_filters;
|
pub mod compact_filters;
|
||||||
|
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
pub use self::compact_filters::CompactFiltersBlockchain;
|
pub use self::compact_filters::CompactFiltersBlockchain;
|
||||||
|
|
||||||
@@ -97,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>;
|
||||||
@@ -122,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
|
||||||
@@ -179,7 +183,7 @@ impl Progress for Sender<ProgressData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Type that implements [`Progress`] and drops every update received
|
/// Type that implements [`Progress`] and drops every update received
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct NoopProgress;
|
pub struct NoopProgress;
|
||||||
|
|
||||||
/// Create a new instance of [`NoopProgress`]
|
/// Create a new instance of [`NoopProgress`]
|
||||||
@@ -194,7 +198,7 @@ impl Progress for NoopProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Type that implements [`Progress`] and logs at level `INFO` every update received
|
/// Type that implements [`Progress`] and logs at level `INFO` every update received
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct LogProgress;
|
pub struct LogProgress;
|
||||||
|
|
||||||
/// Create a nwe instance of [`LogProgress`]
|
/// Create a nwe instance of [`LogProgress`]
|
||||||
@@ -222,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> {
|
||||||
|
|||||||
474
src/blockchain/rpc.rs
Normal file
474
src/blockchain/rpc.rs
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2021 by Riccardo Casatta <riccardo@casatta.it>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
//! Rpc Blockchain
|
||||||
|
//!
|
||||||
|
//! Backend that gets blockchain data from Bitcoin Core RPC
|
||||||
|
//!
|
||||||
|
//! This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain, rpc::Auth};
|
||||||
|
//! let config = RpcConfig {
|
||||||
|
//! url: "127.0.0.1:18332".to_string(),
|
||||||
|
//! auth: Auth::Cookie {
|
||||||
|
//! file: "/home/user/.bitcoin/.cookie".into(),
|
||||||
|
//! },
|
||||||
|
//! network: bdk::bitcoin::Network::Testnet,
|
||||||
|
//! wallet_name: "wallet_name".to_string(),
|
||||||
|
//! skip_blocks: None,
|
||||||
|
//! };
|
||||||
|
//! let blockchain = RpcBlockchain::from_config(&config);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::bitcoin::consensus::deserialize;
|
||||||
|
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
|
||||||
|
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
|
||||||
|
use crate::database::{BatchDatabase, DatabaseUtils};
|
||||||
|
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
|
||||||
|
use crate::wallet::utils::SecpCtx;
|
||||||
|
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||||
|
use bitcoincore_rpc::json::{
|
||||||
|
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
|
||||||
|
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
||||||
|
};
|
||||||
|
use bitcoincore_rpc::jsonrpc::serde_json::Value;
|
||||||
|
use bitcoincore_rpc::Auth as RpcAuth;
|
||||||
|
use bitcoincore_rpc::{Client, RpcApi};
|
||||||
|
use log::debug;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RpcBlockchain {
|
||||||
|
/// Rpc client to the node, includes the wallet name
|
||||||
|
client: Client,
|
||||||
|
/// Network used
|
||||||
|
network: Network,
|
||||||
|
/// Blockchain capabilities, cached here at startup
|
||||||
|
capabilities: HashSet<Capability>,
|
||||||
|
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
||||||
|
skip_blocks: Option<u32>,
|
||||||
|
|
||||||
|
/// This is a fixed Address used as a hack key to store information on the node
|
||||||
|
_storage_address: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RpcBlockchain configuration options
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
pub struct RpcConfig {
|
||||||
|
/// The bitcoin node url
|
||||||
|
pub url: String,
|
||||||
|
/// The bitcoin node authentication mechanism
|
||||||
|
pub auth: Auth,
|
||||||
|
/// The network we are using (it will be checked the bitcoin node network matches this)
|
||||||
|
pub network: Network,
|
||||||
|
/// The wallet name in the bitcoin node, consider using [wallet_name_from_descriptor] for this
|
||||||
|
pub wallet_name: String,
|
||||||
|
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
||||||
|
pub skip_blocks: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
||||||
|
let info = self.client.get_address_info(&self._storage_address)?;
|
||||||
|
if let Some(GetAddressInfoResultLabel::Simple(label)) = info.labels.first() {
|
||||||
|
Ok(label
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap_or_else(|_| self.skip_blocks.unwrap_or(0)))
|
||||||
|
} else {
|
||||||
|
Ok(self.skip_blocks.unwrap_or(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the synced height in the core node by using a label of a fixed address so that
|
||||||
|
/// another client with the same descriptor doesn't rescan the blockchain
|
||||||
|
fn set_node_synced_height(&self, height: u32) -> Result<(), Error> {
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.set_label(&self._storage_address, &height.to_string())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blockchain for RpcBlockchain {
|
||||||
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||||
|
self.capabilities.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
|
&self,
|
||||||
|
database: &mut D,
|
||||||
|
progress_update: P,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
|
||||||
|
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
|
||||||
|
debug!(
|
||||||
|
"importing {} script_pubkeys (some maybe already imported)",
|
||||||
|
scripts_pubkeys.len()
|
||||||
|
);
|
||||||
|
let requests: Vec<_> = scripts_pubkeys
|
||||||
|
.iter()
|
||||||
|
.map(|s| ImportMultiRequest {
|
||||||
|
timestamp: ImportMultiRescanSince::Timestamp(0),
|
||||||
|
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(&s)),
|
||||||
|
watchonly: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let options = ImportMultiOptions {
|
||||||
|
rescan: Some(false),
|
||||||
|
};
|
||||||
|
// Note we use import_multi because as of bitcoin core 0.21.0 many descriptors are not supported
|
||||||
|
// https://bitcoindevkit.org/descriptors/#compatibility-matrix
|
||||||
|
//TODO maybe convenient using import_descriptor for compatible descriptor and import_multi as fallback
|
||||||
|
self.client.import_multi(&requests, Some(&options))?;
|
||||||
|
|
||||||
|
let current_height = self.get_height()?;
|
||||||
|
|
||||||
|
// min because block invalidate may cause height to go down
|
||||||
|
let node_synced = self.get_node_synced_height()?.min(current_height);
|
||||||
|
|
||||||
|
//TODO call rescan in chunks (updating node_synced_height) so that in case of
|
||||||
|
// interruption work can be partially recovered
|
||||||
|
debug!(
|
||||||
|
"rescan_blockchain from:{} to:{}",
|
||||||
|
node_synced, current_height
|
||||||
|
);
|
||||||
|
self.client
|
||||||
|
.rescan_blockchain(Some(node_synced as usize), Some(current_height as usize))?;
|
||||||
|
progress_update.update(1.0, None)?;
|
||||||
|
|
||||||
|
self.set_node_synced_height(current_height)?;
|
||||||
|
|
||||||
|
self.sync(database, progress_update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync<D: BatchDatabase, P: 'static + Progress>(
|
||||||
|
&self,
|
||||||
|
db: &mut D,
|
||||||
|
_progress_update: P,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut indexes = HashMap::new();
|
||||||
|
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
|
||||||
|
indexes.insert(*keykind, db.get_last_index(*keykind)?.unwrap_or(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut known_txs: HashMap<_, _> = db
|
||||||
|
.iter_txs(true)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|tx| (tx.txid, tx))
|
||||||
|
.collect();
|
||||||
|
let known_utxos: HashSet<_> = db.iter_utxos()?.into_iter().collect();
|
||||||
|
|
||||||
|
//TODO list_since_blocks would be more efficient
|
||||||
|
let current_utxo = self
|
||||||
|
.client
|
||||||
|
.list_unspent(Some(0), None, None, Some(true), None)?;
|
||||||
|
debug!("current_utxo len {}", current_utxo.len());
|
||||||
|
|
||||||
|
//TODO supported up to 1_000 txs, should use since_blocks or do paging
|
||||||
|
let list_txs = self
|
||||||
|
.client
|
||||||
|
.list_transactions(None, Some(1_000), None, Some(true))?;
|
||||||
|
let mut list_txs_ids = HashSet::new();
|
||||||
|
|
||||||
|
for tx_result in list_txs.iter().filter(|t| {
|
||||||
|
// list_txs returns all conflicting tx we want to
|
||||||
|
// filter out replaced tx => unconfirmed and not in the mempool
|
||||||
|
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
|
||||||
|
}) {
|
||||||
|
let txid = tx_result.info.txid;
|
||||||
|
list_txs_ids.insert(txid);
|
||||||
|
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
|
||||||
|
let confirmation_time =
|
||||||
|
ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
|
||||||
|
if confirmation_time != known_tx.confirmation_time {
|
||||||
|
// reorg may change tx height
|
||||||
|
debug!(
|
||||||
|
"updating tx({}) confirmation time to: {:?}",
|
||||||
|
txid, confirmation_time
|
||||||
|
);
|
||||||
|
known_tx.confirmation_time = confirmation_time;
|
||||||
|
db.set_tx(&known_tx)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//TODO check there is already the raw tx in db?
|
||||||
|
let tx_result = self.client.get_transaction(&txid, Some(true))?;
|
||||||
|
let tx: Transaction = deserialize(&tx_result.hex)?;
|
||||||
|
let mut received = 0u64;
|
||||||
|
let mut sent = 0u64;
|
||||||
|
for output in tx.output.iter() {
|
||||||
|
if let Ok(Some((kind, index))) =
|
||||||
|
db.get_path_from_script_pubkey(&output.script_pubkey)
|
||||||
|
{
|
||||||
|
if index > *indexes.get(&kind).unwrap() {
|
||||||
|
indexes.insert(kind, index);
|
||||||
|
}
|
||||||
|
received += output.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for input in tx.input.iter() {
|
||||||
|
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
|
||||||
|
sent += previous_output.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let td = TransactionDetails {
|
||||||
|
transaction: Some(tx),
|
||||||
|
txid: tx_result.info.txid,
|
||||||
|
confirmation_time: ConfirmationTime::new(
|
||||||
|
tx_result.info.blockheight,
|
||||||
|
tx_result.info.blocktime,
|
||||||
|
),
|
||||||
|
received,
|
||||||
|
sent,
|
||||||
|
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||||
|
verified: true,
|
||||||
|
};
|
||||||
|
debug!(
|
||||||
|
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
||||||
|
td.txid, tx_result.fee, td.fee
|
||||||
|
);
|
||||||
|
db.set_tx(&td)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for known_txid in known_txs.keys() {
|
||||||
|
if !list_txs_ids.contains(known_txid) {
|
||||||
|
debug!("removing tx: {}", known_txid);
|
||||||
|
db.del_tx(known_txid, false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_utxos: HashSet<_> = current_utxo
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| {
|
||||||
|
Ok(LocalUtxo {
|
||||||
|
outpoint: OutPoint::new(u.txid, u.vout),
|
||||||
|
keychain: db
|
||||||
|
.get_path_from_script_pubkey(&u.script_pub_key)?
|
||||||
|
.ok_or(Error::TransactionNotFound)?
|
||||||
|
.0,
|
||||||
|
txout: TxOut {
|
||||||
|
value: u.amount.as_sat(),
|
||||||
|
script_pubkey: u.script_pub_key,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
|
||||||
|
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
||||||
|
for s in spent {
|
||||||
|
debug!("removing utxo: {:?}", s);
|
||||||
|
db.del_utxo(&s.outpoint)?;
|
||||||
|
}
|
||||||
|
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
|
||||||
|
for s in received {
|
||||||
|
debug!("adding utxo: {:?}", s);
|
||||||
|
db.set_utxo(s)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (keykind, index) in indexes {
|
||||||
|
debug!("{:?} max {}", keykind, index);
|
||||||
|
db.set_last_index(keykind, index)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
Ok(Some(self.client.get_raw_transaction(txid, None)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
|
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
||||||
|
let sat_per_kb = self
|
||||||
|
.client
|
||||||
|
.estimate_smart_fee(target as u16, None)?
|
||||||
|
.fee_rate
|
||||||
|
.ok_or(Error::FeeRateUnavailable)?
|
||||||
|
.as_sat() as f64;
|
||||||
|
|
||||||
|
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigurableBlockchain for RpcBlockchain {
|
||||||
|
type Config = RpcConfig;
|
||||||
|
|
||||||
|
/// Returns RpcBlockchain backend creating an RPC client to a specific wallet named as the descriptor's checksum
|
||||||
|
/// if it's the first time it creates the wallet in the node and upon return is granted the wallet is loaded
|
||||||
|
fn from_config(config: &Self::Config) -> Result<Self, Error> {
|
||||||
|
let wallet_name = config.wallet_name.clone();
|
||||||
|
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
||||||
|
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
||||||
|
|
||||||
|
let client = Client::new(wallet_url, config.auth.clone().into())?;
|
||||||
|
let loaded_wallets = client.list_wallets()?;
|
||||||
|
if loaded_wallets.contains(&wallet_name) {
|
||||||
|
debug!("wallet already loaded {:?}", wallet_name);
|
||||||
|
} else {
|
||||||
|
let existing_wallets = list_wallet_dir(&client)?;
|
||||||
|
if existing_wallets.contains(&wallet_name) {
|
||||||
|
client.load_wallet(&wallet_name)?;
|
||||||
|
debug!("wallet loaded {:?}", wallet_name);
|
||||||
|
} else {
|
||||||
|
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
|
||||||
|
debug!("wallet created {:?}", wallet_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let blockchain_info = client.get_blockchain_info()?;
|
||||||
|
let network = match blockchain_info.chain.as_str() {
|
||||||
|
"main" => Network::Bitcoin,
|
||||||
|
"test" => Network::Testnet,
|
||||||
|
"regtest" => Network::Regtest,
|
||||||
|
"signet" => Network::Signet,
|
||||||
|
_ => return Err(Error::Generic("Invalid network".to_string())),
|
||||||
|
};
|
||||||
|
if network != config.network {
|
||||||
|
return Err(Error::InvalidNetwork {
|
||||||
|
requested: config.network,
|
||||||
|
found: network,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
|
||||||
|
let rpc_version = client.version()?;
|
||||||
|
if rpc_version >= 210_000 {
|
||||||
|
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
|
||||||
|
if info.contains_key("txindex") {
|
||||||
|
capabilities.insert(Capability::GetAnyTx);
|
||||||
|
capabilities.insert(Capability::AccurateFees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is just a fixed address used only to store a label containing the synced height in the node
|
||||||
|
let mut storage_address =
|
||||||
|
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
|
||||||
|
storage_address.network = network;
|
||||||
|
|
||||||
|
Ok(RpcBlockchain {
|
||||||
|
client,
|
||||||
|
network,
|
||||||
|
capabilities,
|
||||||
|
_storage_address: storage_address,
|
||||||
|
skip_blocks: config.skip_blocks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministically generate a unique name given the descriptors defining the wallet
|
||||||
|
pub fn wallet_name_from_descriptor<T>(
|
||||||
|
descriptor: T,
|
||||||
|
change_descriptor: Option<T>,
|
||||||
|
network: Network,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
) -> Result<String, Error>
|
||||||
|
where
|
||||||
|
T: IntoWalletDescriptor,
|
||||||
|
{
|
||||||
|
//TODO check descriptors contains only public keys
|
||||||
|
let descriptor = descriptor
|
||||||
|
.into_wallet_descriptor(&secp, network)?
|
||||||
|
.0
|
||||||
|
.to_string();
|
||||||
|
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
|
||||||
|
if let Some(change_descriptor) = change_descriptor {
|
||||||
|
let change_descriptor = change_descriptor
|
||||||
|
.into_wallet_descriptor(&secp, network)?
|
||||||
|
.0
|
||||||
|
.to_string();
|
||||||
|
wallet_name.push_str(
|
||||||
|
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wallet_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return the wallets available in default wallet directory
|
||||||
|
//TODO use bitcoincore_rpc method when PR #179 lands
|
||||||
|
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Name {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CallResult {
|
||||||
|
wallets: Vec<Name>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: CallResult = client.call("listwalletdir", &[])?;
|
||||||
|
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "test-rpc")]
|
||||||
|
crate::bdk_blockchain_tests! {
|
||||||
|
|
||||||
|
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
||||||
|
let config = RpcConfig {
|
||||||
|
url: test_client.bitcoind.rpc_url(),
|
||||||
|
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
|
||||||
|
network: Network::Regtest,
|
||||||
|
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
|
||||||
|
skip_blocks: None,
|
||||||
|
};
|
||||||
|
RpcBlockchain::from_config(&config).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
@@ -34,12 +21,12 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid};
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::types::{KeychainKind, TransactionDetails, UTXO};
|
use crate::types::{ConfirmationTime, KeychainKind, LocalUtxo, TransactionDetails};
|
||||||
use crate::wallet::time::Instant;
|
use crate::wallet::time::Instant;
|
||||||
use crate::wallet::utils::ChunksIterator;
|
use crate::wallet::utils::ChunksIterator;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ELSGetHistoryRes {
|
pub struct ElsGetHistoryRes {
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
pub tx_hash: Txid,
|
pub tx_hash: Txid,
|
||||||
}
|
}
|
||||||
@@ -50,7 +37,7 @@ pub trait ElectrumLikeSync {
|
|||||||
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
fn els_batch_script_get_history<'s, I: IntoIterator<Item = &'s Script> + Clone>(
|
||||||
&self,
|
&self,
|
||||||
scripts: I,
|
scripts: I,
|
||||||
) -> Result<Vec<Vec<ELSGetHistoryRes>>, Error>;
|
) -> Result<Vec<Vec<ElsGetHistoryRes>>, Error>;
|
||||||
|
|
||||||
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid> + Clone>(
|
fn els_batch_transaction_get<'s, I: IntoIterator<Item = &'s Txid> + Clone>(
|
||||||
&self,
|
&self,
|
||||||
@@ -66,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> {
|
||||||
@@ -74,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();
|
||||||
@@ -90,7 +76,7 @@ pub trait ElectrumLikeSync {
|
|||||||
|
|
||||||
for (i, chunk) in ChunksIterator::new(script_iter, stop_gap).enumerate() {
|
for (i, chunk) in ChunksIterator::new(script_iter, stop_gap).enumerate() {
|
||||||
// TODO if i == last, should create another chunk of addresses in db
|
// TODO if i == last, should create another chunk of addresses in db
|
||||||
let call_result: Vec<Vec<ELSGetHistoryRes>> =
|
let call_result: Vec<Vec<ElsGetHistoryRes>> =
|
||||||
maybe_await!(self.els_batch_script_get_history(chunk.iter()))?;
|
maybe_await!(self.els_batch_script_get_history(chunk.iter()))?;
|
||||||
let max_index = call_result
|
let max_index = call_result
|
||||||
.iter()
|
.iter()
|
||||||
@@ -100,7 +86,7 @@ pub trait ElectrumLikeSync {
|
|||||||
if let Some(max) = max_index {
|
if let Some(max) = max_index {
|
||||||
max_indexes.insert(keychain, max + (i * chunk_size) as u32);
|
max_indexes.insert(keychain, max + (i * chunk_size) as u32);
|
||||||
}
|
}
|
||||||
let flattened: Vec<ELSGetHistoryRes> = call_result.into_iter().flatten().collect();
|
let flattened: Vec<ElsGetHistoryRes> = call_result.into_iter().flatten().collect();
|
||||||
debug!("#{} of {:?} results:{}", i, keychain, flattened.len());
|
debug!("#{} of {:?} results:{}", i, keychain, flattened.len());
|
||||||
if flattened.is_empty() {
|
if flattened.is_empty() {
|
||||||
// Didn't find anything in the last `stop_gap` script_pubkeys, breaking
|
// Didn't find anything in the last `stop_gap` script_pubkeys, breaking
|
||||||
@@ -160,18 +146,19 @@ pub trait ElectrumLikeSync {
|
|||||||
// save any tx details not in db but in history_txs_id or with different height/timestamp
|
// save any tx details not in db but in history_txs_id or with different height/timestamp
|
||||||
for txid in history_txs_id.iter() {
|
for txid in history_txs_id.iter() {
|
||||||
let height = txid_height.get(txid).cloned().flatten();
|
let height = txid_height.get(txid).cloned().flatten();
|
||||||
let timestamp = *new_timestamps.get(txid).unwrap_or(&0u64);
|
let timestamp = new_timestamps.get(txid).cloned();
|
||||||
if let Some(tx_details) = txs_details_in_db.get(txid) {
|
if let Some(tx_details) = txs_details_in_db.get(txid) {
|
||||||
// check if height matches, otherwise updates it
|
// check if tx height matches, otherwise updates it. timestamp is not in the if clause
|
||||||
if tx_details.height != height {
|
// because we are not asking headers for confirmed tx we know about
|
||||||
|
if tx_details.confirmation_time.as_ref().map(|c| c.height) != height {
|
||||||
|
let confirmation_time = ConfirmationTime::new(height, timestamp);
|
||||||
let mut new_tx_details = tx_details.clone();
|
let mut new_tx_details = tx_details.clone();
|
||||||
new_tx_details.height = height;
|
new_tx_details.confirmation_time = confirmation_time;
|
||||||
new_tx_details.timestamp = timestamp;
|
|
||||||
batch.set_tx(&new_tx_details)?;
|
batch.set_tx(&new_tx_details)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
save_transaction_details_and_utxos(
|
save_transaction_details_and_utxos(
|
||||||
&txid,
|
txid,
|
||||||
db,
|
db,
|
||||||
timestamp,
|
timestamp,
|
||||||
height,
|
height,
|
||||||
@@ -184,7 +171,7 @@ pub trait ElectrumLikeSync {
|
|||||||
// remove any tx details in db but not in history_txs_id
|
// remove any tx details in db but not in history_txs_id
|
||||||
for txid in txs_details_in_db.keys() {
|
for txid in txs_details_in_db.keys() {
|
||||||
if !history_txs_id.contains(txid) {
|
if !history_txs_id.contains(txid) {
|
||||||
batch.del_tx(&txid, false)?;
|
batch.del_tx(txid, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,9 +238,13 @@ pub trait ElectrumLikeSync {
|
|||||||
chunk_size: usize,
|
chunk_size: usize,
|
||||||
) -> Result<HashMap<Txid, u64>, Error> {
|
) -> Result<HashMap<Txid, u64>, Error> {
|
||||||
let mut txid_timestamp = HashMap::new();
|
let mut txid_timestamp = HashMap::new();
|
||||||
|
let txid_in_db_with_conf: HashSet<_> = txs_details_in_db
|
||||||
|
.values()
|
||||||
|
.filter_map(|details| details.confirmation_time.as_ref().map(|_| details.txid))
|
||||||
|
.collect();
|
||||||
let needed_txid_height: HashMap<&Txid, u32> = txid_height
|
let needed_txid_height: HashMap<&Txid, u32> = txid_height
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(t, _)| txs_details_in_db.get(*t).is_none())
|
.filter(|(t, _)| !txid_in_db_with_conf.contains(*t))
|
||||||
.filter_map(|(t, o)| o.map(|h| (t, h)))
|
.filter_map(|(t, o)| o.map(|h| (t, h)))
|
||||||
.collect();
|
.collect();
|
||||||
let needed_heights: HashSet<u32> = needed_txid_height.values().cloned().collect();
|
let needed_heights: HashSet<u32> = needed_txid_height.values().cloned().collect();
|
||||||
@@ -305,7 +296,7 @@ pub trait ElectrumLikeSync {
|
|||||||
fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
||||||
txid: &Txid,
|
txid: &Txid,
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
timestamp: u64,
|
timestamp: Option<u64>,
|
||||||
height: Option<u32>,
|
height: Option<u32>,
|
||||||
updates: &mut dyn BatchOperations,
|
updates: &mut dyn BatchOperations,
|
||||||
utxo_deps: &HashMap<OutPoint, OutPoint>,
|
utxo_deps: &HashMap<OutPoint, OutPoint>,
|
||||||
@@ -342,7 +333,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
|||||||
|
|
||||||
// removes conflicting UTXO if any (generated from same inputs, like for example RBF)
|
// removes conflicting UTXO if any (generated from same inputs, like for example RBF)
|
||||||
if let Some(outpoint) = utxo_deps.get(&input.previous_output) {
|
if let Some(outpoint) = utxo_deps.get(&input.previous_output) {
|
||||||
updates.del_utxo(&outpoint)?;
|
updates.del_utxo(outpoint)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +344,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
|||||||
// this output is ours, we have a path to derive it
|
// this output is ours, we have a path to derive it
|
||||||
if let Some((keychain, _child)) = db.get_path_from_script_pubkey(&output.script_pubkey)? {
|
if let Some((keychain, _child)) = db.get_path_from_script_pubkey(&output.script_pubkey)? {
|
||||||
debug!("{} output #{} is mine, adding utxo", txid, i);
|
debug!("{} output #{} is mine, adding utxo", txid, i);
|
||||||
updates.set_utxo(&UTXO {
|
updates.set_utxo(&LocalUtxo {
|
||||||
outpoint: OutPoint::new(tx.txid(), i as u32),
|
outpoint: OutPoint::new(tx.txid(), i as u32),
|
||||||
txout: output.clone(),
|
txout: output.clone(),
|
||||||
keychain,
|
keychain,
|
||||||
@@ -368,9 +359,9 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
|
|||||||
transaction: Some(tx),
|
transaction: Some(tx),
|
||||||
received: incoming,
|
received: incoming,
|
||||||
sent: outgoing,
|
sent: outgoing,
|
||||||
height,
|
confirmation_time: ConfirmationTime::new(height, timestamp),
|
||||||
timestamp,
|
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
|
||||||
fees: 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)?;
|
||||||
|
|
||||||
@@ -378,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>,
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Runtime-checked database types
|
//! Runtime-checked database types
|
||||||
//!
|
//!
|
||||||
@@ -133,7 +120,7 @@ impl BatchOperations for AnyDatabase {
|
|||||||
child
|
child
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> {
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, set_utxo, utxo)
|
impl_inner_method!(AnyDatabase, self, set_utxo, utxo)
|
||||||
}
|
}
|
||||||
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> {
|
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> {
|
||||||
@@ -165,7 +152,7 @@ impl BatchOperations for AnyDatabase {
|
|||||||
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, del_path_from_script_pubkey, script)
|
impl_inner_method!(AnyDatabase, self, del_path_from_script_pubkey, script)
|
||||||
}
|
}
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, del_utxo, outpoint)
|
impl_inner_method!(AnyDatabase, self, del_utxo, outpoint)
|
||||||
}
|
}
|
||||||
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
@@ -201,7 +188,7 @@ impl Database for AnyDatabase {
|
|||||||
fn iter_script_pubkeys(&self, keychain: Option<KeychainKind>) -> Result<Vec<Script>, Error> {
|
fn iter_script_pubkeys(&self, keychain: Option<KeychainKind>) -> Result<Vec<Script>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, iter_script_pubkeys, keychain)
|
impl_inner_method!(AnyDatabase, self, iter_script_pubkeys, keychain)
|
||||||
}
|
}
|
||||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error> {
|
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, iter_utxos)
|
impl_inner_method!(AnyDatabase, self, iter_utxos)
|
||||||
}
|
}
|
||||||
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error> {
|
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error> {
|
||||||
@@ -230,7 +217,7 @@ impl Database for AnyDatabase {
|
|||||||
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, get_path_from_script_pubkey, script)
|
impl_inner_method!(AnyDatabase, self, get_path_from_script_pubkey, script)
|
||||||
}
|
}
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
impl_inner_method!(AnyDatabase, self, get_utxo, outpoint)
|
impl_inner_method!(AnyDatabase, self, get_utxo, outpoint)
|
||||||
}
|
}
|
||||||
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
@@ -257,7 +244,7 @@ impl BatchOperations for AnyBatch {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
impl_inner_method!(AnyBatch, self, set_script_pubkey, script, keychain, child)
|
impl_inner_method!(AnyBatch, self, set_script_pubkey, script, keychain, child)
|
||||||
}
|
}
|
||||||
fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> {
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||||
impl_inner_method!(AnyBatch, self, set_utxo, utxo)
|
impl_inner_method!(AnyBatch, self, set_utxo, utxo)
|
||||||
}
|
}
|
||||||
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> {
|
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> {
|
||||||
@@ -283,7 +270,7 @@ impl BatchOperations for AnyBatch {
|
|||||||
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
) -> Result<Option<(KeychainKind, u32)>, Error> {
|
||||||
impl_inner_method!(AnyBatch, self, del_path_from_script_pubkey, script)
|
impl_inner_method!(AnyBatch, self, del_path_from_script_pubkey, script)
|
||||||
}
|
}
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
impl_inner_method!(AnyBatch, self, del_utxo, outpoint)
|
impl_inner_method!(AnyBatch, self, del_utxo, outpoint)
|
||||||
}
|
}
|
||||||
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
@@ -312,24 +299,17 @@ impl BatchDatabase for AnyDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> {
|
fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> {
|
||||||
// TODO: refactor once `move_ref_pattern` is stable
|
|
||||||
#[allow(irrefutable_let_patterns)]
|
|
||||||
match self {
|
match self {
|
||||||
AnyDatabase::Memory(db) => {
|
AnyDatabase::Memory(db) => match batch {
|
||||||
if let AnyBatch::Memory(batch) = batch {
|
AnyBatch::Memory(batch) => db.commit_batch(batch),
|
||||||
db.commit_batch(batch)
|
#[cfg(feature = "key-value-db")]
|
||||||
} else {
|
_ => unimplemented!("Sled batch shouldn't be used with Memory db."),
|
||||||
unimplemented!()
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
AnyDatabase::Sled(db) => {
|
AnyDatabase::Sled(db) => match batch {
|
||||||
if let AnyBatch::Sled(batch) = batch {
|
AnyBatch::Sled(batch) => db.commit_batch(batch),
|
||||||
db.commit_batch(batch)
|
_ => unimplemented!("Memory batch shouldn't be used with Sled db."),
|
||||||
} else {
|
},
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
@@ -51,8 +38,8 @@ macro_rules! impl_batch_operations {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> {
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||||
let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||||
let value = json!({
|
let value = json!({
|
||||||
"t": utxo.txout,
|
"t": utxo.txout,
|
||||||
"i": utxo.keychain,
|
"i": utxo.keychain,
|
||||||
@@ -120,8 +107,8 @@ macro_rules! impl_batch_operations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||||
let res = self.remove(key);
|
let res = self.remove(key);
|
||||||
let res = $process_delete!(res);
|
let res = $process_delete!(res);
|
||||||
|
|
||||||
@@ -132,7 +119,7 @@ macro_rules! impl_batch_operations {
|
|||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
|
|
||||||
Ok(Some(UTXO { outpoint: outpoint.clone(), txout, keychain }))
|
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,8 +221,8 @@ impl Database for Tree {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error> {
|
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(None).as_map_key();
|
let key = MapKey::Utxo(None).as_map_key();
|
||||||
self.scan_prefix(key)
|
self.scan_prefix(key)
|
||||||
.map(|x| -> Result<_, Error> {
|
.map(|x| -> Result<_, Error> {
|
||||||
let (k, v) = x?;
|
let (k, v) = x?;
|
||||||
@@ -245,7 +232,7 @@ impl Database for Tree {
|
|||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
|
|
||||||
Ok(UTXO {
|
Ok(LocalUtxo {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
@@ -305,15 +292,15 @@ impl Database for Tree {
|
|||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||||
self.get(key)?
|
self.get(key)?
|
||||||
.map(|b| -> Result<_, Error> {
|
.map(|b| -> Result<_, Error> {
|
||||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
|
|
||||||
Ok(UTXO {
|
Ok(LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
@@ -333,7 +320,7 @@ impl Database for Tree {
|
|||||||
.map(|b| -> Result<_, Error> {
|
.map(|b| -> Result<_, Error> {
|
||||||
let mut txdetails: TransactionDetails = serde_json::from_slice(&b)?;
|
let mut txdetails: TransactionDetails = serde_json::from_slice(&b)?;
|
||||||
if include_raw {
|
if include_raw {
|
||||||
txdetails.transaction = self.get_raw_tx(&txid)?;
|
txdetails.transaction = self.get_raw_tx(txid)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(txdetails)
|
Ok(txdetails)
|
||||||
@@ -396,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};
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! In-memory ephemeral database
|
//! In-memory ephemeral database
|
||||||
//!
|
//!
|
||||||
@@ -49,7 +36,7 @@ use crate::types::*;
|
|||||||
pub(crate) enum MapKey<'a> {
|
pub(crate) enum MapKey<'a> {
|
||||||
Path((Option<KeychainKind>, Option<u32>)),
|
Path((Option<KeychainKind>, Option<u32>)),
|
||||||
Script(Option<&'a Script>),
|
Script(Option<&'a Script>),
|
||||||
UTXO(Option<&'a OutPoint>),
|
Utxo(Option<&'a OutPoint>),
|
||||||
RawTx(Option<&'a Txid>),
|
RawTx(Option<&'a Txid>),
|
||||||
Transaction(Option<&'a Txid>),
|
Transaction(Option<&'a Txid>),
|
||||||
LastIndex(KeychainKind),
|
LastIndex(KeychainKind),
|
||||||
@@ -67,7 +54,7 @@ impl MapKey<'_> {
|
|||||||
v
|
v
|
||||||
}
|
}
|
||||||
MapKey::Script(_) => b"s".to_vec(),
|
MapKey::Script(_) => b"s".to_vec(),
|
||||||
MapKey::UTXO(_) => b"u".to_vec(),
|
MapKey::Utxo(_) => b"u".to_vec(),
|
||||||
MapKey::RawTx(_) => b"r".to_vec(),
|
MapKey::RawTx(_) => b"r".to_vec(),
|
||||||
MapKey::Transaction(_) => b"t".to_vec(),
|
MapKey::Transaction(_) => b"t".to_vec(),
|
||||||
MapKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
MapKey::LastIndex(st) => [b"c", st.as_ref()].concat(),
|
||||||
@@ -79,7 +66,7 @@ impl MapKey<'_> {
|
|||||||
match self {
|
match self {
|
||||||
MapKey::Path((_, Some(child))) => child.to_be_bytes().to_vec(),
|
MapKey::Path((_, Some(child))) => child.to_be_bytes().to_vec(),
|
||||||
MapKey::Script(Some(s)) => serialize(*s),
|
MapKey::Script(Some(s)) => serialize(*s),
|
||||||
MapKey::UTXO(Some(s)) => serialize(*s),
|
MapKey::Utxo(Some(s)) => serialize(*s),
|
||||||
MapKey::RawTx(Some(s)) => serialize(*s),
|
MapKey::RawTx(Some(s)) => serialize(*s),
|
||||||
MapKey::Transaction(Some(s)) => serialize(*s),
|
MapKey::Transaction(Some(s)) => serialize(*s),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
@@ -157,8 +144,8 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> {
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||||
let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||||
self.map
|
self.map
|
||||||
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
||||||
|
|
||||||
@@ -223,8 +210,8 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||||
let res = self.map.remove(&key);
|
let res = self.map.remove(&key);
|
||||||
self.deleted_keys.push(key);
|
self.deleted_keys.push(key);
|
||||||
|
|
||||||
@@ -232,7 +219,7 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||||
Ok(Some(UTXO {
|
Ok(Some(LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
@@ -316,14 +303,14 @@ impl Database for MemoryDatabase {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error> {
|
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(None).as_map_key();
|
let key = MapKey::Utxo(None).as_map_key();
|
||||||
self.map
|
self.map
|
||||||
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let outpoint = deserialize(&k[1..]).unwrap();
|
let outpoint = deserialize(&k[1..]).unwrap();
|
||||||
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
|
||||||
Ok(UTXO {
|
Ok(LocalUtxo {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
@@ -382,11 +369,11 @@ impl Database for MemoryDatabase {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
let key = MapKey::UTXO(Some(outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||||
Ok(self.map.get(&key).map(|b| {
|
Ok(self.map.get(&key).map(|b| {
|
||||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||||
UTXO {
|
LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
@@ -407,7 +394,7 @@ impl Database for MemoryDatabase {
|
|||||||
Ok(self.map.get(&key).map(|b| {
|
Ok(self.map.get(&key).map(|b| {
|
||||||
let mut txdetails: TransactionDetails = b.downcast_ref().cloned().unwrap();
|
let mut txdetails: TransactionDetails = b.downcast_ref().cloned().unwrap();
|
||||||
if include_raw {
|
if include_raw {
|
||||||
txdetails.transaction = self.get_raw_tx(&txid).unwrap();
|
txdetails.transaction = self.get_raw_tx(txid).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
txdetails
|
txdetails
|
||||||
@@ -442,8 +429,8 @@ impl BatchDatabase for MemoryDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn commit_batch(&mut self, mut batch: Self::Batch) -> Result<(), Error> {
|
fn commit_batch(&mut self, mut batch: Self::Batch) -> Result<(), Error> {
|
||||||
for key in batch.deleted_keys {
|
for key in batch.deleted_keys.iter() {
|
||||||
self.map.remove(&key);
|
self.map.remove(key);
|
||||||
}
|
}
|
||||||
self.map.append(&mut batch.map);
|
self.map.append(&mut batch.map);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -458,16 +445,17 @@ impl ConfigurableDatabase for MemoryDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[macro_export]
|
||||||
impl MemoryDatabase {
|
#[doc(hidden)]
|
||||||
// Artificially insert a tx in the database, as if we had found it with a `sync`
|
/// Artificially insert a tx in the database, as if we had found it with a `sync`. This is a hidden
|
||||||
pub fn received_tx(
|
/// macro and not a `[cfg(test)]` function so it can be called within the context of doctests which
|
||||||
&mut self,
|
/// don't have `test` set.
|
||||||
tx_meta: testutils::TestIncomingTx,
|
macro_rules! populate_test_db {
|
||||||
current_height: Option<u32>,
|
($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{
|
||||||
) -> bitcoin::Txid {
|
use $crate::database::BatchOperations;
|
||||||
use std::str::FromStr;
|
let mut db = $db;
|
||||||
|
let tx_meta = $tx_meta;
|
||||||
|
let current_height: Option<u32> = $current_height;
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: 0,
|
||||||
@@ -485,23 +473,24 @@ impl MemoryDatabase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
let height = tx_meta
|
let confirmation_time = tx_meta.min_confirmations.map(|conf| ConfirmationTime {
|
||||||
.min_confirmations
|
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||||
.map(|conf| current_height.unwrap().checked_sub(conf as u32).unwrap());
|
timestamp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
let tx_details = TransactionDetails {
|
let tx_details = TransactionDetails {
|
||||||
transaction: Some(tx.clone()),
|
transaction: Some(tx.clone()),
|
||||||
txid,
|
txid,
|
||||||
timestamp: 0,
|
fee: Some(0),
|
||||||
height,
|
|
||||||
received: 0,
|
received: 0,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
fees: 0,
|
confirmation_time,
|
||||||
|
verified: current_height.is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set_tx(&tx_details).unwrap();
|
db.set_tx(&tx_details).unwrap();
|
||||||
for (vout, out) in tx.output.iter().enumerate() {
|
for (vout, out) in tx.output.iter().enumerate() {
|
||||||
self.set_utxo(&UTXO {
|
db.set_utxo(&LocalUtxo {
|
||||||
txout: out.clone(),
|
txout: out.clone(),
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
txid,
|
txid,
|
||||||
@@ -513,7 +502,37 @@ impl MemoryDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txid
|
txid
|
||||||
}
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Macro for getting a wallet for use in a doctest
|
||||||
|
macro_rules! doctest_wallet {
|
||||||
|
() => {{
|
||||||
|
use $crate::bitcoin::Network;
|
||||||
|
use $crate::database::MemoryDatabase;
|
||||||
|
use $crate::testutils;
|
||||||
|
let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
|
||||||
|
let descriptors = testutils!(@descriptors (descriptor) (descriptor));
|
||||||
|
|
||||||
|
let mut db = MemoryDatabase::new();
|
||||||
|
let txid = populate_test_db!(
|
||||||
|
&mut db,
|
||||||
|
testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 500_000 ) (@confirmations 1)
|
||||||
|
},
|
||||||
|
Some(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
$crate::Wallet::new_offline(
|
||||||
|
&descriptors.0,
|
||||||
|
descriptors.1.as_ref(),
|
||||||
|
Network::Regtest,
|
||||||
|
db
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Database types
|
//! Database types
|
||||||
//!
|
//!
|
||||||
@@ -64,8 +51,8 @@ pub trait BatchOperations {
|
|||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
child: u32,
|
child: u32,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
/// Store a [`UTXO`]
|
/// Store a [`LocalUtxo`]
|
||||||
fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error>;
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error>;
|
||||||
/// Store a raw transaction
|
/// Store a raw transaction
|
||||||
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error>;
|
fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error>;
|
||||||
/// Store the metadata of a transaction
|
/// Store the metadata of a transaction
|
||||||
@@ -85,8 +72,8 @@ pub trait BatchOperations {
|
|||||||
&mut self,
|
&mut self,
|
||||||
script: &Script,
|
script: &Script,
|
||||||
) -> Result<Option<(KeychainKind, u32)>, Error>;
|
) -> Result<Option<(KeychainKind, u32)>, Error>;
|
||||||
/// Delete a [`UTXO`] given its [`OutPoint`]
|
/// Delete a [`LocalUtxo`] given its [`OutPoint`]
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error>;
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error>;
|
||||||
/// Delete a raw transaction given its [`Txid`]
|
/// Delete a raw transaction given its [`Txid`]
|
||||||
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
||||||
/// Delete the metadata of a transaction and optionally the raw transaction itself
|
/// Delete the metadata of a transaction and optionally the raw transaction itself
|
||||||
@@ -116,8 +103,8 @@ pub trait Database: BatchOperations {
|
|||||||
|
|
||||||
/// Return the list of script_pubkeys
|
/// Return the list of script_pubkeys
|
||||||
fn iter_script_pubkeys(&self, keychain: Option<KeychainKind>) -> Result<Vec<Script>, Error>;
|
fn iter_script_pubkeys(&self, keychain: Option<KeychainKind>) -> Result<Vec<Script>, Error>;
|
||||||
/// Return the list of [`UTXO`]s
|
/// Return the list of [`LocalUtxo`]s
|
||||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error>;
|
fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error>;
|
||||||
/// Return the list of raw transactions
|
/// Return the list of raw transactions
|
||||||
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
|
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
|
||||||
/// Return the list of transactions metadata
|
/// Return the list of transactions metadata
|
||||||
@@ -134,8 +121,8 @@ pub trait Database: BatchOperations {
|
|||||||
&self,
|
&self,
|
||||||
script: &Script,
|
script: &Script,
|
||||||
) -> Result<Option<(KeychainKind, u32)>, Error>;
|
) -> Result<Option<(KeychainKind, u32)>, Error>;
|
||||||
/// Fetch a [`UTXO`] given its [`OutPoint`]
|
/// Fetch a [`LocalUtxo`] given its [`OutPoint`]
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<UTXO>, Error>;
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error>;
|
||||||
/// Fetch a raw transaction given its [`Txid`]
|
/// Fetch a raw transaction given its [`Txid`]
|
||||||
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
||||||
/// Fetch the transaction metadata and optionally also the raw transaction
|
/// Fetch the transaction metadata and optionally also the raw transaction
|
||||||
@@ -177,14 +164,14 @@ pub(crate) trait DatabaseUtils: Database {
|
|||||||
.map(|o| o.is_some())
|
.map(|o| o.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_raw_tx_or<F>(&self, txid: &Txid, f: F) -> Result<Option<Transaction>, Error>
|
fn get_raw_tx_or<D>(&self, txid: &Txid, default: D) -> Result<Option<Transaction>, Error>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<Option<Transaction>, Error>,
|
D: FnOnce() -> Result<Option<Transaction>, Error>,
|
||||||
{
|
{
|
||||||
self.get_tx(txid, true)?
|
self.get_tx(txid, true)?
|
||||||
.map(|t| t.transaction)
|
.map(|t| t.transaction)
|
||||||
.flatten()
|
.flatten()
|
||||||
.map_or_else(f, |t| Ok(Some(t)))
|
.map_or_else(default, |t| Ok(Some(t)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {
|
fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {
|
||||||
@@ -227,7 +214,7 @@ pub mod test {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.get_path_from_script_pubkey(&script).unwrap(),
|
tree.get_path_from_script_pubkey(&script).unwrap(),
|
||||||
Some((keychain, path.clone()))
|
Some((keychain, path))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +243,7 @@ pub mod test {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.get_path_from_script_pubkey(&script).unwrap(),
|
tree.get_path_from_script_pubkey(&script).unwrap(),
|
||||||
Some((keychain, path.clone()))
|
Some((keychain, path))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +285,7 @@ pub mod test {
|
|||||||
value: 133742,
|
value: 133742,
|
||||||
script_pubkey: script,
|
script_pubkey: script,
|
||||||
};
|
};
|
||||||
let utxo = UTXO {
|
let utxo = LocalUtxo {
|
||||||
txout,
|
txout,
|
||||||
outpoint,
|
outpoint,
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
@@ -327,11 +314,14 @@ pub mod test {
|
|||||||
let mut tx_details = TransactionDetails {
|
let mut tx_details = TransactionDetails {
|
||||||
transaction: Some(tx),
|
transaction: Some(tx),
|
||||||
txid,
|
txid,
|
||||||
timestamp: 123456,
|
|
||||||
received: 1337,
|
received: 1337,
|
||||||
sent: 420420,
|
sent: 420420,
|
||||||
fees: 140,
|
fee: Some(140),
|
||||||
height: Some(1000),
|
confirmation_time: Some(ConfirmationTime {
|
||||||
|
timestamp: 123456,
|
||||||
|
height: 1000,
|
||||||
|
}),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
tree.set_tx(&tx_details).unwrap();
|
tree.set_tx(&tx_details).unwrap();
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Descriptor checksum
|
//! Descriptor checksum
|
||||||
//!
|
//!
|
||||||
|
|||||||
150
src/descriptor/derived.rs
Normal file
150
src/descriptor/derived.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
|
||||||
|
//! Derived descriptor keys
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use bitcoin::hashes::hash160;
|
||||||
|
use bitcoin::PublicKey;
|
||||||
|
|
||||||
|
pub use miniscript::{
|
||||||
|
descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript,
|
||||||
|
ScriptContext, Segwitv0,
|
||||||
|
};
|
||||||
|
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
|
||||||
|
|
||||||
|
use crate::wallet::utils::SecpCtx;
|
||||||
|
|
||||||
|
/// Extended [`DescriptorPublicKey`] that has been derived
|
||||||
|
///
|
||||||
|
/// Derived keys are guaranteed to never contain wildcards of any kind
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx);
|
||||||
|
|
||||||
|
impl<'s> DerivedDescriptorKey<'s> {
|
||||||
|
/// Construct a new derived key
|
||||||
|
///
|
||||||
|
/// Panics if the key is wildcard
|
||||||
|
pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> {
|
||||||
|
if let DescriptorPublicKey::XPub(xpub) = &key {
|
||||||
|
assert!(xpub.wildcard == Wildcard::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
DerivedDescriptorKey(key, secp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Deref for DerivedDescriptorKey<'s> {
|
||||||
|
type Target = DescriptorPublicKey;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> PartialEq for DerivedDescriptorKey<'s> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Eq for DerivedDescriptorKey<'s> {}
|
||||||
|
|
||||||
|
impl<'s> PartialOrd for DerivedDescriptorKey<'s> {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.0.partial_cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Ord for DerivedDescriptorKey<'s> {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.0.cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> fmt::Display for DerivedDescriptorKey<'s> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Hash for DerivedDescriptorKey<'s> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.0.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
|
||||||
|
type Hash = Self;
|
||||||
|
|
||||||
|
fn to_pubkeyhash(&self) -> Self::Hash {
|
||||||
|
DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_uncompressed(&self) -> bool {
|
||||||
|
self.0.is_uncompressed()
|
||||||
|
}
|
||||||
|
fn serialized_len(&self) -> usize {
|
||||||
|
self.0.serialized_len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
||||||
|
fn to_public_key(&self) -> PublicKey {
|
||||||
|
match &self.0 {
|
||||||
|
DescriptorPublicKey::SinglePub(ref spub) => spub.key.to_public_key(),
|
||||||
|
DescriptorPublicKey::XPub(ref xpub) => {
|
||||||
|
xpub.xkey
|
||||||
|
.derive_pub(self.1, &xpub.derivation_path)
|
||||||
|
.expect("Shouldn't fail, only normal derivations")
|
||||||
|
.public_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash {
|
||||||
|
hash.to_public_key().to_pubkeyhash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait AsDerived {
|
||||||
|
// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
|
||||||
|
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
|
||||||
|
-> Descriptor<DerivedDescriptorKey<'s>>;
|
||||||
|
|
||||||
|
// Transform the keys into `DerivedDescriptorKey`.
|
||||||
|
//
|
||||||
|
// Panics if the descriptor is not "fixed", i.e. if it's derivable
|
||||||
|
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsDerived for Descriptor<DescriptorPublicKey> {
|
||||||
|
fn as_derived<'s>(
|
||||||
|
&self,
|
||||||
|
index: u32,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Descriptor<DerivedDescriptorKey<'s>> {
|
||||||
|
self.derive(index).translate_pk_infallible(
|
||||||
|
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
||||||
|
|key| DerivedDescriptorKey::new(key.clone(), secp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>> {
|
||||||
|
assert!(!self.is_deriveable());
|
||||||
|
|
||||||
|
self.as_derived(0, secp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Descriptors DSL
|
//! Descriptors DSL
|
||||||
|
|
||||||
@@ -28,48 +15,61 @@
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_top_level_sh {
|
macro_rules! impl_top_level_sh {
|
||||||
// disallow `sortedmulti` in `bare()`
|
// disallow `sortedmulti` in `bare()`
|
||||||
( Bare, Bare, sortedmulti $( $inner:tt )* ) => {
|
( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => {
|
||||||
compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands");
|
compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands");
|
||||||
};
|
};
|
||||||
( Bare, Bare, sortedmulti_vec $( $inner:tt )* ) => {
|
( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => {
|
||||||
compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands");
|
compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands");
|
||||||
};
|
};
|
||||||
|
|
||||||
( $descriptor_variant:ident, $sortedmulti_variant:ident, sortedmulti $( $inner:tt )* ) => {
|
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{
|
||||||
$crate::impl_sortedmulti!(sortedmulti $( $inner )*)
|
use std::marker::PhantomData;
|
||||||
.and_then(|(inner, key_map, valid_networks)| Ok(($crate::miniscript::Descriptor::$sortedmulti_variant(inner), key_map, valid_networks)))
|
|
||||||
};
|
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
|
||||||
( $descriptor_variant:ident, $sortedmulti_variant:ident, sortedmulti_vec $( $inner:tt )* ) => {
|
use $crate::miniscript::$ctx;
|
||||||
$crate::impl_sortedmulti!(sortedmulti_vec $( $inner )*)
|
|
||||||
.and_then(|(inner, key_map, valid_networks)| Ok(($crate::miniscript::Descriptor::$sortedmulti_variant(inner), key_map, valid_networks)))
|
let build_desc = |k, pks| {
|
||||||
};
|
Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
|
||||||
|
};
|
||||||
|
|
||||||
|
$crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*)
|
||||||
|
}};
|
||||||
|
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
|
||||||
|
use $crate::miniscript::$ctx;
|
||||||
|
|
||||||
|
let build_desc = |k, pks| {
|
||||||
|
Ok((Descriptor::<DescriptorPublicKey>::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>))
|
||||||
|
};
|
||||||
|
|
||||||
|
$crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*)
|
||||||
|
}};
|
||||||
|
|
||||||
|
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{
|
||||||
|
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
( $descriptor_variant:ident, $sortedmulti_variant:ident, $( $minisc:tt )* ) => {
|
|
||||||
$crate::fragment!($( $minisc )*)
|
$crate::fragment!($( $minisc )*)
|
||||||
.map(|(minisc, keymap, networks)|($crate::miniscript::Descriptor::<$crate::miniscript::descriptor::DescriptorPublicKey>::$descriptor_variant(minisc), keymap, networks))
|
.and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks)))
|
||||||
};
|
.and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::<DescriptorPublicKey>::$inner_struct(inner), key_map, valid_networks)))
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_top_level_pk {
|
macro_rules! impl_top_level_pk {
|
||||||
( $descriptor_variant:ident, $ctx:ty, $key:expr ) => {{
|
( $inner_type:ident, $ctx:ty, $key:expr ) => {{
|
||||||
|
use $crate::miniscript::descriptor::$inner_type;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use $crate::keys::{DescriptorKey, ToDescriptorKey};
|
use $crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
$key.to_descriptor_key()
|
$key.into_descriptor_key()
|
||||||
.and_then(|key: DescriptorKey<$ctx>| key.extract(&secp))
|
.and_then(|key: DescriptorKey<$ctx>| key.extract(&secp))
|
||||||
.map_err($crate::descriptor::DescriptorError::Key)
|
.map_err($crate::descriptor::DescriptorError::Key)
|
||||||
.map(|(pk, key_map, valid_networks)| {
|
.map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks))
|
||||||
(
|
|
||||||
$crate::miniscript::Descriptor::<
|
|
||||||
$crate::miniscript::descriptor::DescriptorPublicKey,
|
|
||||||
>::$descriptor_variant(pk),
|
|
||||||
key_map,
|
|
||||||
valid_networks,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,28 +201,29 @@ macro_rules! impl_node_opcode_three {
|
|||||||
|
|
||||||
Ok((minisc, a_keymap, networks))
|
Ok((minisc, a_keymap, networks))
|
||||||
})
|
})
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_sortedmulti {
|
macro_rules! impl_sortedmulti {
|
||||||
( sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({
|
( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
$crate::keys::make_sortedmulti_inner($thresh, $keys, &secp)
|
$crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp)
|
||||||
});
|
});
|
||||||
( sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||||
use $crate::keys::ToDescriptorKey;
|
use $crate::keys::IntoDescriptorKey;
|
||||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
let mut keys = vec![];
|
let keys = vec![
|
||||||
$(
|
$(
|
||||||
keys.push($key.to_descriptor_key());
|
$key.into_descriptor_key(),
|
||||||
)*
|
)*
|
||||||
|
];
|
||||||
|
|
||||||
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
||||||
.map_err($crate::descriptor::DescriptorError::Key)
|
.map_err($crate::descriptor::DescriptorError::Key)
|
||||||
.and_then(|keys| $crate::keys::make_sortedmulti_inner($thresh, keys, &secp))
|
.and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp))
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -313,11 +314,11 @@ macro_rules! apply_modifier {
|
|||||||
/// broken up to `s:d:v:older(144)`.
|
/// broken up to `s:d:v:older(144)`.
|
||||||
///
|
///
|
||||||
/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements
|
/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements
|
||||||
/// [`ToDescriptorKey`]. This means that keys can also be written inline as strings, but in that
|
/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that
|
||||||
/// case they must be wrapped in quotes, which is another difference compared to the standard
|
/// case they must be wrapped in quotes, which is another difference compared to the standard
|
||||||
/// descriptor syntax.
|
/// descriptor syntax.
|
||||||
///
|
///
|
||||||
/// [`ToDescriptorKey`]: crate::keys::ToDescriptorKey
|
/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
@@ -399,34 +400,46 @@ macro_rules! apply_modifier {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! descriptor {
|
macro_rules! descriptor {
|
||||||
( bare ( $( $minisc:tt )* ) ) => ({
|
( bare ( $( $minisc:tt )* ) ) => ({
|
||||||
$crate::impl_top_level_sh!(Bare, Bare, $( $minisc )*)
|
$crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*)
|
||||||
});
|
});
|
||||||
( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({
|
( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({
|
||||||
$crate::descriptor!(shwsh ($( $minisc )*))
|
$crate::descriptor!(shwsh ($( $minisc )*))
|
||||||
});
|
});
|
||||||
( shwsh ( $( $minisc:tt )* ) ) => ({
|
( shwsh ( $( $minisc:tt )* ) ) => ({
|
||||||
$crate::impl_top_level_sh!(ShWsh, ShWshSortedMulti, $( $minisc )*)
|
$crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*)
|
||||||
});
|
});
|
||||||
( pk ( $key:expr ) ) => ({
|
( pk ( $key:expr ) ) => ({
|
||||||
$crate::impl_top_level_pk!(Pk, $crate::miniscript::Legacy, $key)
|
// `pk()` is actually implemented as `bare(pk())`
|
||||||
|
$crate::descriptor!( bare ( pk ( $key ) ) )
|
||||||
});
|
});
|
||||||
( pkh ( $key:expr ) ) => ({
|
( pkh ( $key:expr ) ) => ({
|
||||||
$crate::impl_top_level_pk!(Pkh,$crate::miniscript::Legacy, $key)
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
|
$crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key)
|
||||||
|
.map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Pkh(a), b, c))
|
||||||
});
|
});
|
||||||
( wpkh ( $key:expr ) ) => ({
|
( wpkh ( $key:expr ) ) => ({
|
||||||
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||||
|
|
||||||
$crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
|
$crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
|
||||||
|
.and_then(|(a, b, c)| Ok((a?, b, c)))
|
||||||
|
.map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Wpkh(a), b, c))
|
||||||
});
|
});
|
||||||
( sh ( wpkh ( $key:expr ) ) ) => ({
|
( sh ( wpkh ( $key:expr ) ) ) => ({
|
||||||
$crate::descriptor!(shwpkh ( $key ))
|
$crate::descriptor!(shwpkh ( $key ))
|
||||||
});
|
});
|
||||||
( shwpkh ( $key:expr ) ) => ({
|
( shwpkh ( $key:expr ) ) => ({
|
||||||
$crate::impl_top_level_pk!(ShWpkh, $crate::miniscript::Segwitv0, $key)
|
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh};
|
||||||
|
|
||||||
|
$crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
|
||||||
|
.and_then(|(a, b, c)| Ok((a?, b, c)))
|
||||||
|
.and_then(|(a, b, c)| Ok((Descriptor::<DescriptorPublicKey>::Sh(Sh::new_wpkh(a.into_inner())?), b, c)))
|
||||||
});
|
});
|
||||||
( sh ( $( $minisc:tt )* ) ) => ({
|
( sh ( $( $minisc:tt )* ) ) => ({
|
||||||
$crate::impl_top_level_sh!(Sh, ShSortedMulti, $( $minisc )*)
|
$crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*)
|
||||||
});
|
});
|
||||||
( wsh ( $( $minisc:tt )* ) ) => ({
|
( wsh ( $( $minisc:tt )* ) ) => ({
|
||||||
$crate::impl_top_level_sh!(Wsh, WshSortedMulti, $( $minisc )*)
|
$crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,9 +535,7 @@ macro_rules! fragment_internal {
|
|||||||
( @t , $( $tail:tt )* ) => ({
|
( @t , $( $tail:tt )* ) => ({
|
||||||
$crate::fragment_internal!( @t $( $tail )* )
|
$crate::fragment_internal!( @t $( $tail )* )
|
||||||
});
|
});
|
||||||
( @t ) => ({
|
( @t ) => ({});
|
||||||
()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback to calling `fragment!()`
|
// Fallback to calling `fragment!()`
|
||||||
( $( $tokens:tt )* ) => ({
|
( $( $tokens:tt )* ) => ({
|
||||||
@@ -628,13 +639,14 @@ macro_rules! fragment {
|
|||||||
$crate::keys::make_multi($thresh, $keys)
|
$crate::keys::make_multi($thresh, $keys)
|
||||||
});
|
});
|
||||||
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
|
||||||
use $crate::keys::ToDescriptorKey;
|
use $crate::keys::IntoDescriptorKey;
|
||||||
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
|
||||||
|
|
||||||
let mut keys = vec![];
|
let keys = vec![
|
||||||
$(
|
$(
|
||||||
keys.push($key.to_descriptor_key());
|
$key.into_descriptor_key(),
|
||||||
)*
|
)*
|
||||||
|
];
|
||||||
|
|
||||||
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
||||||
.map_err($crate::descriptor::DescriptorError::Key)
|
.map_err($crate::descriptor::DescriptorError::Key)
|
||||||
@@ -654,18 +666,19 @@ macro_rules! fragment {
|
|||||||
mod test {
|
mod test {
|
||||||
use bitcoin::hashes::hex::ToHex;
|
use bitcoin::hashes::hex::ToHex;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorPublicKeyCtx, KeyMap};
|
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||||
use miniscript::{Descriptor, Legacy, Segwitv0};
|
use miniscript::{Descriptor, Legacy, Segwitv0};
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||||
use crate::keys::{DescriptorKey, ToDescriptorKey, ValidNetworks};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
||||||
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Testnet};
|
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::util::bip32::ChildNumber;
|
|
||||||
use bitcoin::PrivateKey;
|
use bitcoin::PrivateKey;
|
||||||
|
|
||||||
|
use crate::descriptor::derived::AsDerived;
|
||||||
|
|
||||||
// test the descriptor!() macro
|
// test the descriptor!() macro
|
||||||
|
|
||||||
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
|
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
|
||||||
@@ -676,30 +689,29 @@ mod test {
|
|||||||
expected: &[&str],
|
expected: &[&str],
|
||||||
) {
|
) {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let deriv_ctx = DescriptorPublicKeyCtx::new(&secp, ChildNumber::Normal { index: 0 });
|
|
||||||
|
|
||||||
let (desc, _key_map, _networks) = desc.unwrap();
|
let (desc, _key_map, _networks) = desc.unwrap();
|
||||||
assert_eq!(desc.is_witness(), is_witness);
|
assert_eq!(desc.is_witness(), is_witness);
|
||||||
assert_eq!(desc.is_fixed(), is_fixed);
|
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||||
for i in 0..expected.len() {
|
for i in 0..expected.len() {
|
||||||
let index = i as u32;
|
let index = i as u32;
|
||||||
let child_desc = if desc.is_fixed() {
|
let child_desc = if !desc.is_deriveable() {
|
||||||
desc.clone()
|
desc.as_derived_fixed(&secp)
|
||||||
} else {
|
} else {
|
||||||
desc.derive(ChildNumber::from_normal_idx(index).unwrap())
|
desc.as_derived(index, &secp)
|
||||||
};
|
};
|
||||||
let address = child_desc.address(Regtest, deriv_ctx);
|
let address = child_desc.address(Regtest);
|
||||||
if let Some(address) = address {
|
if let Ok(address) = address {
|
||||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||||
} else {
|
} else {
|
||||||
let script = child_desc.script_pubkey(deriv_ctx);
|
let script = child_desc.script_pubkey();
|
||||||
assert_eq!(script.to_hex().as_str(), *expected.get(i).unwrap());
|
assert_eq!(script.to_hex().as_str(), *expected.get(i).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
||||||
// - mixing up key types that implement ToDescriptorKey in multi() or thresh()
|
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
||||||
|
|
||||||
// expected script for pk and bare manually created
|
// expected script for pk and bare manually created
|
||||||
// expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses`
|
// expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses`
|
||||||
@@ -778,12 +790,31 @@ 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();
|
||||||
|
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(pk(desc_key)),
|
descriptor!(pk(desc_key)),
|
||||||
false,
|
false,
|
||||||
@@ -795,7 +826,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(pkh(desc_key)),
|
descriptor!(pkh(desc_key)),
|
||||||
false,
|
false,
|
||||||
@@ -808,8 +839,8 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||||
let desc_key1 = (xprv, path).to_descriptor_key().unwrap();
|
let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
|
||||||
let desc_key2 = (xprv, path2).to_descriptor_key().unwrap();
|
let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
check(
|
check(
|
||||||
descriptor!(sh(multi(1, desc_key1, desc_key2))),
|
descriptor!(sh(multi(1, desc_key1, desc_key2))),
|
||||||
@@ -828,7 +859,7 @@ mod test {
|
|||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
|
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(wpkh(desc_key)),
|
descriptor!(wpkh(desc_key)),
|
||||||
true,
|
true,
|
||||||
@@ -840,7 +871,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(sh(wpkh(desc_key))),
|
descriptor!(sh(wpkh(desc_key))),
|
||||||
true,
|
true,
|
||||||
@@ -853,8 +884,8 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||||
let desc_key1 = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let desc_key2 = (xprv, path2.clone()).to_descriptor_key().unwrap();
|
let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(wsh(multi(1, desc_key1, desc_key2))),
|
descriptor!(wsh(multi(1, desc_key1, desc_key2))),
|
||||||
true,
|
true,
|
||||||
@@ -866,8 +897,8 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let desc_key1 = (xprv, path).to_descriptor_key().unwrap();
|
let desc_key1 = (xprv, path).into_descriptor_key().unwrap();
|
||||||
let desc_key2 = (xprv, path2).to_descriptor_key().unwrap();
|
let desc_key2 = (xprv, path2).into_descriptor_key().unwrap();
|
||||||
check(
|
check(
|
||||||
descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))),
|
descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))),
|
||||||
true,
|
true,
|
||||||
@@ -943,14 +974,17 @@ mod test {
|
|||||||
fn test_valid_networks() {
|
fn test_valid_networks() {
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
||||||
assert_eq!(valid_networks, [Testnet, Regtest].iter().cloned().collect());
|
assert_eq!(
|
||||||
|
valid_networks,
|
||||||
|
[Testnet, Regtest, Signet].iter().cloned().collect()
|
||||||
|
);
|
||||||
|
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||||
let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap();
|
let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap();
|
||||||
assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect());
|
assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect());
|
||||||
@@ -963,26 +997,23 @@ mod test {
|
|||||||
|
|
||||||
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key1 = (xprv1, path1.clone()).to_descriptor_key().unwrap();
|
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||||
let desc_key2 = (xprv2, path2.clone()).to_descriptor_key().unwrap();
|
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||||
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||||
let desc_key3 = (xprv3, path3.clone()).to_descriptor_key().unwrap();
|
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let (_desc, key_map, _valid_networks) =
|
let (_desc, key_map, _valid_networks) =
|
||||||
descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap();
|
descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap();
|
||||||
assert_eq!(key_map.len(), 3);
|
assert_eq!(key_map.len(), 3);
|
||||||
|
|
||||||
let desc_key1: DescriptorKey<Segwitv0> =
|
let desc_key1: DescriptorKey<Segwitv0> = (xprv1, path1).into_descriptor_key().unwrap();
|
||||||
(xprv1, path1.clone()).to_descriptor_key().unwrap();
|
let desc_key2: DescriptorKey<Segwitv0> = (xprv2, path2).into_descriptor_key().unwrap();
|
||||||
let desc_key2: DescriptorKey<Segwitv0> =
|
let desc_key3: DescriptorKey<Segwitv0> = (xprv3, path3).into_descriptor_key().unwrap();
|
||||||
(xprv2, path2.clone()).to_descriptor_key().unwrap();
|
|
||||||
let desc_key3: DescriptorKey<Segwitv0> =
|
|
||||||
(xprv3, path3.clone()).to_descriptor_key().unwrap();
|
|
||||||
|
|
||||||
let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap();
|
let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap();
|
||||||
let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap();
|
let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap();
|
||||||
@@ -992,19 +1023,19 @@ mod test {
|
|||||||
assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*");
|
assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*");
|
||||||
}
|
}
|
||||||
|
|
||||||
// - verify the ScriptContext is correctly validated (i.e. passing a type that only impl ToDescriptorKey<Segwitv0> to a pkh() descriptor should throw a compilation error
|
// - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey<Segwitv0> to a pkh() descriptor should throw a compilation error
|
||||||
#[test]
|
#[test]
|
||||||
fn test_script_context_validation() {
|
fn test_script_context_validation() {
|
||||||
// this compiles
|
// this compiles
|
||||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
let desc_key: DescriptorKey<Legacy> = (xprv, path.clone()).to_descriptor_key().unwrap();
|
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
||||||
assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)");
|
assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2");
|
||||||
|
|
||||||
// as expected this does not compile due to invalid context
|
// as expected this does not compile due to invalid context
|
||||||
//let desc_key:DescriptorKey<Segwitv0> = (xprv, path.clone()).to_descriptor_key().unwrap();
|
//let desc_key:DescriptorKey<Segwitv0> = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
//let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
//let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,17 +1046,16 @@ mod test {
|
|||||||
let (descriptor, _, _) =
|
let (descriptor, _, _) =
|
||||||
descriptor!(wsh(thresh(2,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
descriptor!(wsh(thresh(2,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
||||||
|
|
||||||
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))")
|
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#cfdcqs3s")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: uncomment once https://github.com/rust-bitcoin/rust-miniscript/pull/221 is released
|
#[test]
|
||||||
//
|
#[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
|
||||||
// #[test]
|
fn test_dsl_miniscript_checks() {
|
||||||
// #[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
|
let mut uncompressed_pk =
|
||||||
// fn test_dsl_miniscript_checks() {
|
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
||||||
// let mut uncompressed_pk = PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
uncompressed_pk.compressed = false;
|
||||||
// uncompressed_pk.compressed = false;
|
|
||||||
|
|
||||||
// descriptor!(wsh(v:pk(uncompressed_pk))).unwrap();
|
descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Descriptor errors
|
//! Descriptor errors
|
||||||
|
|
||||||
@@ -28,9 +15,13 @@
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Invalid HD Key path, such as having a wildcard but a length != 1
|
/// Invalid HD Key path, such as having a wildcard but a length != 1
|
||||||
InvalidHDKeyPath,
|
InvalidHdKeyPath,
|
||||||
/// The provided descriptor doesn't match its checksum
|
/// The provided descriptor doesn't match its checksum
|
||||||
InvalidDescriptorChecksum,
|
InvalidDescriptorChecksum,
|
||||||
|
/// The descriptor contains hardened derivation steps on public extended keys
|
||||||
|
HardenedDerivationXpub,
|
||||||
|
/// The descriptor contains multiple keys with the same BIP32 fingerprint
|
||||||
|
DuplicatedKeys,
|
||||||
|
|
||||||
/// Error thrown while working with [`keys`](crate::keys)
|
/// Error thrown while working with [`keys`](crate::keys)
|
||||||
Key(crate::keys::KeyError),
|
Key(crate::keys::KeyError),
|
||||||
@@ -41,11 +32,11 @@ pub enum Error {
|
|||||||
InvalidDescriptorCharacter(char),
|
InvalidDescriptorCharacter(char),
|
||||||
|
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
BIP32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
/// Error during base58 decoding
|
/// Error during base58 decoding
|
||||||
Base58(bitcoin::util::base58::Error),
|
Base58(bitcoin::util::base58::Error),
|
||||||
/// Key-related error
|
/// Key-related error
|
||||||
PK(bitcoin::util::key::Error),
|
Pk(bitcoin::util::key::Error),
|
||||||
/// Miniscript error
|
/// Miniscript error
|
||||||
Miniscript(miniscript::Error),
|
Miniscript(miniscript::Error),
|
||||||
/// Hex decoding error
|
/// Hex decoding error
|
||||||
@@ -56,7 +47,7 @@ impl From<crate::keys::KeyError> for Error {
|
|||||||
fn from(key_error: crate::keys::KeyError) -> Error {
|
fn from(key_error: crate::keys::KeyError) -> Error {
|
||||||
match key_error {
|
match key_error {
|
||||||
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
||||||
crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner),
|
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
|
||||||
e => Error::Key(e),
|
e => Error::Key(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,9 +61,9 @@ impl std::fmt::Display for Error {
|
|||||||
|
|
||||||
impl std::error::Error for Error {}
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
impl_error!(bitcoin::util::bip32::Error, BIP32);
|
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||||
impl_error!(bitcoin::util::base58::Error, Base58);
|
impl_error!(bitcoin::util::base58::Error, Base58);
|
||||||
impl_error!(bitcoin::util::key::Error, PK);
|
impl_error!(bitcoin::util::key::Error, Pk);
|
||||||
impl_error!(miniscript::Error, Miniscript);
|
impl_error!(miniscript::Error, Miniscript);
|
||||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||||
impl_error!(crate::descriptor::policy::PolicyError, Policy);
|
impl_error!(crate::descriptor::policy::PolicyError, Policy);
|
||||||
|
|||||||
@@ -1,47 +1,36 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Descriptors
|
//! Descriptors
|
||||||
//!
|
//!
|
||||||
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
||||||
//! from [`miniscript`].
|
//! from [`miniscript`].
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::util::bip32::{
|
||||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, KeySource,
|
||||||
|
};
|
||||||
use bitcoin::util::psbt;
|
use bitcoin::util::psbt;
|
||||||
use bitcoin::{Network, PublicKey, Script, TxOut};
|
use bitcoin::{Network, PublicKey, Script, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorXKey, InnerXKey};
|
use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey, Wildcard};
|
||||||
pub use miniscript::{
|
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||||
descriptor::KeyMap, Descriptor, Legacy, Miniscript, MiniscriptKey, ScriptContext, Segwitv0,
|
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||||
Terminal, ToPublicKey,
|
|
||||||
};
|
use crate::descriptor::policy::BuildSatisfaction;
|
||||||
|
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
|
pub(crate) mod derived;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod dsl;
|
pub mod dsl;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -49,35 +38,42 @@ pub mod policy;
|
|||||||
pub mod template;
|
pub mod template;
|
||||||
|
|
||||||
pub use self::checksum::get_checksum;
|
pub use self::checksum::get_checksum;
|
||||||
|
use self::derived::AsDerived;
|
||||||
|
pub use self::derived::DerivedDescriptorKey;
|
||||||
pub use self::error::Error as DescriptorError;
|
pub use self::error::Error as DescriptorError;
|
||||||
pub use self::policy::Policy;
|
pub use self::policy::Policy;
|
||||||
use self::template::DescriptorTemplateOut;
|
use self::template::DescriptorTemplateOut;
|
||||||
use crate::keys::{KeyError, ToDescriptorKey};
|
use crate::keys::{IntoDescriptorKey, KeyError};
|
||||||
use crate::wallet::signer::SignersContainer;
|
use crate::wallet::signer::SignersContainer;
|
||||||
use crate::wallet::utils::{descriptor_to_pk_ctx, SecpCtx};
|
use crate::wallet::utils::SecpCtx;
|
||||||
|
|
||||||
/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`]
|
/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`]
|
||||||
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
||||||
|
|
||||||
|
/// Alias for a [`Descriptor`] that contains extended **derived** keys
|
||||||
|
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
||||||
|
|
||||||
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
||||||
/// [`psbt::Output`]
|
/// [`psbt::Output`]
|
||||||
///
|
///
|
||||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||||
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||||
pub type HDKeyPaths = BTreeMap<PublicKey, KeySource>;
|
pub type HdKeyPaths = BTreeMap<PublicKey, KeySource>;
|
||||||
|
|
||||||
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
||||||
pub trait ToWalletDescriptor {
|
pub trait IntoWalletDescriptor {
|
||||||
/// Convert to wallet descriptor
|
/// Convert to wallet descriptor
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>;
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToWalletDescriptor for &str {
|
impl IntoWalletDescriptor for &str {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
let descriptor = if self.contains('#') {
|
let descriptor = if self.contains('#') {
|
||||||
@@ -95,46 +91,48 @@ impl ToWalletDescriptor for &str {
|
|||||||
self
|
self
|
||||||
};
|
};
|
||||||
|
|
||||||
ExtendedDescriptor::parse_descriptor(descriptor)?.to_wallet_descriptor(network)
|
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
|
||||||
|
.into_wallet_descriptor(secp, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToWalletDescriptor for &String {
|
impl IntoWalletDescriptor for &String {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
self.as_str().to_wallet_descriptor(network)
|
self.as_str().into_wallet_descriptor(secp, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToWalletDescriptor for ExtendedDescriptor {
|
impl IntoWalletDescriptor for ExtendedDescriptor {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
(self, KeyMap::default()).to_wallet_descriptor(network)
|
(self, KeyMap::default()).into_wallet_descriptor(secp, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
use crate::keys::DescriptorKey;
|
use crate::keys::DescriptorKey;
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
|
|
||||||
let check_key = |pk: &DescriptorPublicKey| {
|
let check_key = |pk: &DescriptorPublicKey| {
|
||||||
let (pk, _, networks) = if self.0.is_witness() {
|
let (pk, _, networks) = if self.0.is_witness() {
|
||||||
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||||
pk.clone().to_descriptor_key()?;
|
pk.clone().into_descriptor_key()?;
|
||||||
desciptor_key.extract(&secp)?
|
desciptor_key.extract(secp)?
|
||||||
} else {
|
} else {
|
||||||
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
||||||
pk.clone().to_descriptor_key()?;
|
pk.clone().into_descriptor_key()?;
|
||||||
desciptor_key.extract(&secp)?
|
desciptor_key.extract(secp)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if networks.contains(&network) {
|
if networks.contains(&network) {
|
||||||
@@ -151,9 +149,10 @@ impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToWalletDescriptor for DescriptorTemplateOut {
|
impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
_secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
let valid_networks = &self.2;
|
let valid_networks = &self.2;
|
||||||
@@ -188,6 +187,54 @@ impl ToWalletDescriptor for DescriptorTemplateOut {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
|
||||||
|
/// descriptor
|
||||||
|
pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||||
|
inner: T,
|
||||||
|
secp: &SecpCtx,
|
||||||
|
network: Network,
|
||||||
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
|
let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
|
||||||
|
|
||||||
|
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
|
||||||
|
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
|
||||||
|
if let DescriptorPublicKey::XPub(DescriptorXKey {
|
||||||
|
derivation_path,
|
||||||
|
wildcard,
|
||||||
|
..
|
||||||
|
}) = k.as_key()
|
||||||
|
{
|
||||||
|
return *wildcard == Wildcard::Hardened
|
||||||
|
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
});
|
||||||
|
if descriptor_contains_hardened_steps {
|
||||||
|
return Err(DescriptorError::HardenedDerivationXpub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there are no duplicated keys
|
||||||
|
let mut found_keys = HashSet::new();
|
||||||
|
let descriptor_contains_duplicated_keys = descriptor.for_any_key(|k| {
|
||||||
|
if let DescriptorPublicKey::XPub(xkey) = k.as_key() {
|
||||||
|
let fingerprint = xkey.root_fingerprint(secp);
|
||||||
|
if found_keys.contains(&fingerprint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
found_keys.insert(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
});
|
||||||
|
if descriptor_contains_duplicated_keys {
|
||||||
|
return Err(DescriptorError::DuplicatedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((descriptor, keymap))
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Used internally mainly by the `descriptor!()` and `fragment!()` macros
|
/// Used internally mainly by the `descriptor!()` and `fragment!()` macros
|
||||||
pub trait CheckMiniscript<Ctx: miniscript::ScriptContext> {
|
pub trait CheckMiniscript<Ctx: miniscript::ScriptContext> {
|
||||||
@@ -210,6 +257,7 @@ pub trait ExtractPolicy {
|
|||||||
fn extract_policy(
|
fn extract_policy(
|
||||||
&self,
|
&self,
|
||||||
signers: &SignersContainer,
|
signers: &SignersContainer,
|
||||||
|
psbt: BuildSatisfaction,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<Option<Policy>, DescriptorError>;
|
) -> Result<Option<Policy>, DescriptorError>;
|
||||||
}
|
}
|
||||||
@@ -219,7 +267,12 @@ pub(crate) trait XKeyUtils {
|
|||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: InnerXKey> XKeyUtils for DescriptorXKey<K> {
|
// FIXME: `InnerXKey` was made private in rust-miniscript, so we have to implement this manually on
|
||||||
|
// both `ExtendedPubKey` and `ExtendedPrivKey`.
|
||||||
|
//
|
||||||
|
// Revert back to using the trait once https://github.com/rust-bitcoin/rust-miniscript/pull/230 is
|
||||||
|
// released
|
||||||
|
impl XKeyUtils for DescriptorXKey<ExtendedPubKey> {
|
||||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||||
let full_path = match self.origin {
|
let full_path = match self.origin {
|
||||||
Some((_, ref path)) => path
|
Some((_, ref path)) => path
|
||||||
@@ -230,7 +283,36 @@ impl<K: InnerXKey> XKeyUtils for DescriptorXKey<K> {
|
|||||||
None => self.derivation_path.clone(),
|
None => self.derivation_path.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.is_wildcard {
|
if self.wildcard != Wildcard::None {
|
||||||
|
full_path
|
||||||
|
.into_iter()
|
||||||
|
.chain(append.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
full_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_fingerprint(&self, _: &SecpCtx) -> Fingerprint {
|
||||||
|
match self.origin {
|
||||||
|
Some((fingerprint, _)) => fingerprint,
|
||||||
|
None => self.xkey.fingerprint(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
|
||||||
|
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||||
|
let full_path = match self.origin {
|
||||||
|
Some((_, ref path)) => path
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.derivation_path.into_iter())
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
None => self.derivation_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.wildcard != Wildcard::None {
|
||||||
full_path
|
full_path
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(append.iter())
|
.chain(append.iter())
|
||||||
@@ -244,195 +326,111 @@ impl<K: InnerXKey> XKeyUtils for DescriptorXKey<K> {
|
|||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||||
match self.origin {
|
match self.origin {
|
||||||
Some((fingerprint, _)) => fingerprint,
|
Some((fingerprint, _)) => fingerprint,
|
||||||
None => self.xkey.xkey_fingerprint(secp),
|
None => self.xkey.fingerprint(secp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DescriptorMeta: Sized {
|
pub(crate) trait DerivedDescriptorMeta {
|
||||||
|
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait DescriptorMeta {
|
||||||
fn is_witness(&self) -> bool;
|
fn is_witness(&self) -> bool;
|
||||||
fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError>;
|
|
||||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
||||||
fn is_fixed(&self) -> bool;
|
fn derive_from_hd_keypaths<'s>(
|
||||||
fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option<Self>;
|
&self,
|
||||||
fn derive_from_psbt_input(
|
hd_keypaths: &HdKeyPaths,
|
||||||
|
secp: &'s SecpCtx,
|
||||||
|
) -> Option<DerivedDescriptor<'s>>;
|
||||||
|
fn derive_from_psbt_input<'s>(
|
||||||
&self,
|
&self,
|
||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
utxo: Option<TxOut>,
|
utxo: Option<TxOut>,
|
||||||
secp: &SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<Self>;
|
) -> Option<DerivedDescriptor<'s>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait DescriptorScripts {
|
pub(crate) trait DescriptorScripts {
|
||||||
fn psbt_redeem_script(&self, secp: &SecpCtx) -> Option<Script>;
|
fn psbt_redeem_script(&self) -> Option<Script>;
|
||||||
fn psbt_witness_script(&self, secp: &SecpCtx) -> Option<Script>;
|
fn psbt_witness_script(&self) -> Option<Script>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescriptorScripts for Descriptor<DescriptorPublicKey> {
|
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
||||||
fn psbt_redeem_script(&self, secp: &SecpCtx) -> Option<Script> {
|
fn psbt_redeem_script(&self) -> Option<Script> {
|
||||||
let deriv_ctx = descriptor_to_pk_ctx(secp);
|
match self.desc_type() {
|
||||||
|
DescriptorType::ShWpkh => Some(self.explicit_script()),
|
||||||
match self {
|
DescriptorType::ShWsh => Some(self.explicit_script().to_v0_p2wsh()),
|
||||||
Descriptor::ShWpkh(_) => Some(self.witness_script(deriv_ctx)),
|
DescriptorType::Sh => Some(self.explicit_script()),
|
||||||
Descriptor::ShWsh(ref script) => Some(script.encode(deriv_ctx).to_v0_p2wsh()),
|
DescriptorType::Bare => Some(self.explicit_script()),
|
||||||
Descriptor::Sh(ref script) => Some(script.encode(deriv_ctx)),
|
DescriptorType::ShSortedMulti => Some(self.explicit_script()),
|
||||||
Descriptor::Bare(ref script) => Some(script.encode(deriv_ctx)),
|
|
||||||
Descriptor::ShSortedMulti(ref keys) => Some(keys.encode(deriv_ctx)),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn psbt_witness_script(&self, secp: &SecpCtx) -> Option<Script> {
|
fn psbt_witness_script(&self) -> Option<Script> {
|
||||||
let deriv_ctx = descriptor_to_pk_ctx(secp);
|
match self.desc_type() {
|
||||||
|
DescriptorType::Wsh => Some(self.explicit_script()),
|
||||||
match self {
|
DescriptorType::ShWsh => Some(self.explicit_script()),
|
||||||
Descriptor::Wsh(ref script) => Some(script.encode(deriv_ctx)),
|
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
||||||
Descriptor::ShWsh(ref script) => Some(script.encode(deriv_ctx)),
|
Some(self.explicit_script())
|
||||||
Descriptor::WshSortedMulti(ref keys) | Descriptor::ShWshSortedMulti(ref keys) => {
|
|
||||||
Some(keys.encode(deriv_ctx))
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
|
impl DescriptorMeta for ExtendedDescriptor {
|
||||||
fn is_witness(&self) -> bool {
|
fn is_witness(&self) -> bool {
|
||||||
match self {
|
matches!(
|
||||||
Descriptor::Bare(_)
|
self.desc_type(),
|
||||||
| Descriptor::Pk(_)
|
DescriptorType::Wpkh
|
||||||
| Descriptor::Pkh(_)
|
| DescriptorType::ShWpkh
|
||||||
| Descriptor::Sh(_)
|
| DescriptorType::Wsh
|
||||||
| Descriptor::ShSortedMulti(_) => false,
|
| DescriptorType::ShWsh
|
||||||
Descriptor::Wpkh(_)
|
| DescriptorType::ShWshSortedMulti
|
||||||
| Descriptor::ShWpkh(_)
|
| DescriptorType::WshSortedMulti
|
||||||
| Descriptor::Wsh(_)
|
)
|
||||||
| Descriptor::ShWsh(_)
|
|
||||||
| Descriptor::ShWshSortedMulti(_)
|
|
||||||
| Descriptor::WshSortedMulti(_) => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, DescriptorError> {
|
|
||||||
let translate_key = |key: &DescriptorPublicKey,
|
|
||||||
index: u32,
|
|
||||||
paths: &mut HDKeyPaths|
|
|
||||||
-> Result<DummyKey, DescriptorError> {
|
|
||||||
match key {
|
|
||||||
DescriptorPublicKey::SinglePub(_) => {}
|
|
||||||
DescriptorPublicKey::XPub(xpub) => {
|
|
||||||
let derive_path = if xpub.is_wildcard {
|
|
||||||
xpub.derivation_path
|
|
||||||
.into_iter()
|
|
||||||
.chain([ChildNumber::from_normal_idx(index)?].iter())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
xpub.derivation_path.clone()
|
|
||||||
};
|
|
||||||
let derived_pubkey = xpub
|
|
||||||
.xkey
|
|
||||||
.derive_pub(&Secp256k1::verification_only(), &derive_path)?;
|
|
||||||
|
|
||||||
paths.insert(
|
|
||||||
derived_pubkey.public_key,
|
|
||||||
(
|
|
||||||
xpub.root_fingerprint(secp),
|
|
||||||
xpub.full_path(&[ChildNumber::from_normal_idx(index)?]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DummyKey::default())
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut answer_pk = BTreeMap::new();
|
|
||||||
let mut answer_pkh = BTreeMap::new();
|
|
||||||
|
|
||||||
self.translate_pk(
|
|
||||||
|pk| translate_key(pk, index, &mut answer_pk),
|
|
||||||
|pkh| translate_key(pkh, index, &mut answer_pkh),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
answer_pk.append(&mut answer_pkh);
|
|
||||||
|
|
||||||
Ok(answer_pk)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
||||||
let get_key = |key: &DescriptorPublicKey,
|
let mut answer = Vec::new();
|
||||||
keys: &mut Vec<DescriptorXKey<ExtendedPubKey>>|
|
|
||||||
-> Result<DummyKey, DescriptorError> {
|
self.for_each_key(|pk| {
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key {
|
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
|
||||||
keys.push(xpub.clone())
|
answer.push(xpub.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(DummyKey::default())
|
true
|
||||||
};
|
});
|
||||||
|
|
||||||
let mut answer_pk = Vec::new();
|
Ok(answer)
|
||||||
let mut answer_pkh = Vec::new();
|
|
||||||
|
|
||||||
self.translate_pk(
|
|
||||||
|pk| get_key(pk, &mut answer_pk),
|
|
||||||
|pkh| get_key(pkh, &mut answer_pkh),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
answer_pk.append(&mut answer_pkh);
|
|
||||||
|
|
||||||
Ok(answer_pk)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_fixed(&self) -> bool {
|
fn derive_from_hd_keypaths<'s>(
|
||||||
fn check_key(
|
&self,
|
||||||
key: &DescriptorPublicKey,
|
hd_keypaths: &HdKeyPaths,
|
||||||
flag: &mut bool,
|
secp: &'s SecpCtx,
|
||||||
) -> Result<DummyKey, DescriptorError> {
|
) -> Option<DerivedDescriptor<'s>> {
|
||||||
match key {
|
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
|
||||||
DescriptorPublicKey::SinglePub(_) => {}
|
|
||||||
DescriptorPublicKey::XPub(xpub) => {
|
|
||||||
if xpub.is_wildcard {
|
|
||||||
*flag = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DummyKey::default())
|
let mut path_found = None;
|
||||||
}
|
self.for_each_key(|key| {
|
||||||
|
if path_found.is_some() {
|
||||||
let mut found_wildcard_pk = false;
|
|
||||||
let mut found_wildcard_pkh = false;
|
|
||||||
|
|
||||||
self.translate_pk(
|
|
||||||
|pk| check_key(pk, &mut found_wildcard_pk),
|
|
||||||
|pkh| check_key(pkh, &mut found_wildcard_pkh),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
!found_wildcard_pk && !found_wildcard_pkh
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option<Self> {
|
|
||||||
let try_key = |key: &DescriptorPublicKey,
|
|
||||||
index: &HashMap<Fingerprint, DerivationPath>,
|
|
||||||
found_path: &mut Option<ChildNumber>|
|
|
||||||
-> Result<DummyKey, DescriptorError> {
|
|
||||||
if found_path.is_some() {
|
|
||||||
// already found a matching path, we are done
|
// already found a matching path, we are done
|
||||||
return Ok(DummyKey::default());
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let DescriptorPublicKey::XPub(xpub) = key {
|
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||||
// Check if the key matches one entry in our `index`. If it does, `matches()` will
|
// Check if the key matches one entry in our `index`. If it does, `matches()` will
|
||||||
// return the "prefix" that matched, so we remove that prefix from the full path
|
// return the "prefix" that matched, so we remove that prefix from the full path
|
||||||
// found in `index` and save it in `derive_path`. We expect this to be a derivation
|
// found in `index` and save it in `derive_path`. We expect this to be a derivation
|
||||||
// path of length 1 if the key `is_wildcard` and an empty path otherwise.
|
// path of length 1 if the key is `wildcard` and an empty path otherwise.
|
||||||
let root_fingerprint = xpub.root_fingerprint(secp);
|
let root_fingerprint = xpub.root_fingerprint(secp);
|
||||||
let derivation_path: Option<Vec<ChildNumber>> = index
|
let derivation_path: Option<Vec<ChildNumber>> = index
|
||||||
.get_key_value(&root_fingerprint)
|
.get_key_value(&root_fingerprint)
|
||||||
.and_then(|(fingerprint, path)| {
|
.and_then(|(fingerprint, path)| {
|
||||||
xpub.matches(&(*fingerprint, path.clone()), secp)
|
xpub.matches(&(**fingerprint, (*path).clone()), secp)
|
||||||
})
|
})
|
||||||
.map(|prefix| {
|
.map(|prefix| {
|
||||||
index
|
index
|
||||||
@@ -445,128 +443,90 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
match derivation_path {
|
match derivation_path {
|
||||||
Some(path) if xpub.is_wildcard && path.len() == 1 => {
|
Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
|
||||||
*found_path = Some(path[0])
|
// Ignore hardened wildcards
|
||||||
|
if let ChildNumber::Normal { index } = path[0] {
|
||||||
|
path_found = Some(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(path) if !xpub.is_wildcard && path.is_empty() => {
|
Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
|
||||||
*found_path = Some(ChildNumber::Normal { index: 0 })
|
path_found = Some(0)
|
||||||
}
|
}
|
||||||
Some(_) => return Err(DescriptorError::InvalidHDKeyPath),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(DummyKey::default())
|
true
|
||||||
};
|
});
|
||||||
|
|
||||||
let index: HashMap<_, _> = hd_keypaths.values().cloned().collect();
|
path_found.map(|path| self.as_derived(path, secp))
|
||||||
|
|
||||||
let mut found_path_pk = None;
|
|
||||||
let mut found_path_pkh = None;
|
|
||||||
|
|
||||||
if self
|
|
||||||
.translate_pk(
|
|
||||||
|pk| try_key(pk, &index, &mut found_path_pk),
|
|
||||||
|pkh| try_key(pkh, &index, &mut found_path_pkh),
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have found a path for both `found_path_pk` and `found_path_pkh` but they are
|
|
||||||
// different we consider this an error and return None. we only return a path either if
|
|
||||||
// they are equal or if only one of them is Some(_)
|
|
||||||
let merged_path = match (found_path_pk, found_path_pkh) {
|
|
||||||
(Some(a), Some(b)) if a != b => return None,
|
|
||||||
(a, b) => a.or(b),
|
|
||||||
};
|
|
||||||
|
|
||||||
merged_path.map(|path| self.derive(path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_from_psbt_input(
|
fn derive_from_psbt_input<'s>(
|
||||||
&self,
|
&self,
|
||||||
psbt_input: &psbt::Input,
|
psbt_input: &psbt::Input,
|
||||||
utxo: Option<TxOut>,
|
utxo: Option<TxOut>,
|
||||||
secp: &SecpCtx,
|
secp: &'s SecpCtx,
|
||||||
) -> Option<Self> {
|
) -> Option<DerivedDescriptor<'s>> {
|
||||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.hd_keypaths, secp) {
|
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||||
return Some(derived);
|
return Some(derived);
|
||||||
} else if !self.is_fixed() {
|
}
|
||||||
// If the descriptor is not fixed we can't brute-force the derivation address, so just
|
if self.is_deriveable() {
|
||||||
// exit here
|
// We can't try to bruteforce the derivation index, exit here
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deriv_ctx = descriptor_to_pk_ctx(secp);
|
let descriptor = self.as_derived_fixed(secp);
|
||||||
match self {
|
match descriptor.desc_type() {
|
||||||
Descriptor::Pk(_)
|
// TODO: add pk() here
|
||||||
| Descriptor::Pkh(_)
|
DescriptorType::Pkh | DescriptorType::Wpkh | DescriptorType::ShWpkh
|
||||||
| Descriptor::Wpkh(_)
|
|
||||||
| Descriptor::ShWpkh(_)
|
|
||||||
if utxo.is_some()
|
if utxo.is_some()
|
||||||
&& self.script_pubkey(deriv_ctx) == utxo.as_ref().unwrap().script_pubkey =>
|
&& descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
|
||||||
{
|
{
|
||||||
Some(self.clone())
|
Some(descriptor)
|
||||||
}
|
}
|
||||||
Descriptor::Bare(ms)
|
DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
|
||||||
if psbt_input.redeem_script.is_some()
|
if psbt_input.redeem_script.is_some()
|
||||||
&& &ms.encode(deriv_ctx) == psbt_input.redeem_script.as_ref().unwrap() =>
|
&& &descriptor.explicit_script()
|
||||||
|
== psbt_input.redeem_script.as_ref().unwrap() =>
|
||||||
{
|
{
|
||||||
Some(self.clone())
|
Some(descriptor)
|
||||||
}
|
}
|
||||||
Descriptor::Sh(ms)
|
DescriptorType::Wsh
|
||||||
if psbt_input.redeem_script.is_some()
|
| DescriptorType::ShWsh
|
||||||
&& &ms.encode(deriv_ctx) == psbt_input.redeem_script.as_ref().unwrap() =>
|
| DescriptorType::ShWshSortedMulti
|
||||||
{
|
| DescriptorType::WshSortedMulti
|
||||||
Some(self.clone())
|
|
||||||
}
|
|
||||||
Descriptor::Wsh(ms) | Descriptor::ShWsh(ms)
|
|
||||||
if psbt_input.witness_script.is_some()
|
if psbt_input.witness_script.is_some()
|
||||||
&& &ms.encode(deriv_ctx) == psbt_input.witness_script.as_ref().unwrap() =>
|
&& &descriptor.explicit_script()
|
||||||
|
== psbt_input.witness_script.as_ref().unwrap() =>
|
||||||
{
|
{
|
||||||
Some(self.clone())
|
Some(descriptor)
|
||||||
}
|
|
||||||
Descriptor::ShSortedMulti(keys)
|
|
||||||
if psbt_input.redeem_script.is_some()
|
|
||||||
&& &keys.encode(deriv_ctx) == psbt_input.redeem_script.as_ref().unwrap() =>
|
|
||||||
{
|
|
||||||
Some(self.clone())
|
|
||||||
}
|
|
||||||
Descriptor::WshSortedMulti(keys) | Descriptor::ShWshSortedMulti(keys)
|
|
||||||
if psbt_input.witness_script.is_some()
|
|
||||||
&& &keys.encode(deriv_ctx) == psbt_input.witness_script.as_ref().unwrap() =>
|
|
||||||
{
|
|
||||||
Some(self.clone())
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
|
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
||||||
struct DummyKey();
|
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError> {
|
||||||
|
let mut answer = BTreeMap::new();
|
||||||
|
self.for_each_key(|key| {
|
||||||
|
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||||
|
let derived_pubkey = xpub
|
||||||
|
.xkey
|
||||||
|
.derive_pub(secp, &xpub.derivation_path)
|
||||||
|
.expect("Derivation can't fail");
|
||||||
|
|
||||||
impl fmt::Display for DummyKey {
|
answer.insert(
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
derived_pubkey.public_key,
|
||||||
write!(f, "DummyKey")
|
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for DummyKey {
|
true
|
||||||
type Err = ();
|
});
|
||||||
|
|
||||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
Ok(answer)
|
||||||
Ok(DummyKey::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl miniscript::MiniscriptKey for DummyKey {
|
|
||||||
type Hash = DummyKey;
|
|
||||||
|
|
||||||
fn to_pubkeyhash(&self) -> DummyKey {
|
|
||||||
DummyKey::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +540,7 @@ mod test {
|
|||||||
use bitcoin::util::{bip32, psbt};
|
use bitcoin::util::{bip32, psbt};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::psbt::PSBTUtils;
|
use crate::psbt::PsbtUtils;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_from_psbt_input_wpkh_wif() {
|
fn test_derive_from_psbt_input_wpkh_wif() {
|
||||||
@@ -692,118 +652,153 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_wallet_descriptor_fixup_networks() {
|
fn test_to_wallet_descriptor_fixup_networks() {
|
||||||
use crate::keys::{any_network, ToDescriptorKey};
|
use crate::keys::{any_network, IntoDescriptorKey};
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
|
|
||||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||||
// we are using an "xpub"
|
// we are using an "xpub"
|
||||||
let key = (xpub, path).to_descriptor_key().unwrap();
|
let key = (xpub, path).into_descriptor_key().unwrap();
|
||||||
// override it with any. this happens in some key conversions, like bip39
|
// override it with any. this happens in some key conversions, like bip39
|
||||||
let key = key.override_valid_networks(any_network());
|
let key = key.override_valid_networks(any_network());
|
||||||
|
|
||||||
// make a descriptor out of it
|
// make a descriptor out of it
|
||||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||||
// this should conver the key that supports "any_network" to the right network (testnet)
|
// this should conver the key that supports "any_network" to the right network (testnet)
|
||||||
let (wallet_desc, _) = desc.to_wallet_descriptor(Network::Testnet).unwrap();
|
let (wallet_desc, _) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
|
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
|
||||||
}
|
}
|
||||||
|
|
||||||
// test ToWalletDescriptor trait from &str with and without checksum appended
|
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
||||||
#[test]
|
#[test]
|
||||||
fn test_descriptor_from_str_with_checksum() {
|
fn test_descriptor_from_str_with_checksum() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"
|
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
desc.err(),
|
desc.err(),
|
||||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||||
));
|
));
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
desc.err(),
|
desc.err(),
|
||||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// test ToWalletDescriptor trait from &str with keys from right and wrong network
|
// test IntoWalletDescriptor trait from &str with keys from right and wrong network
|
||||||
#[test]
|
#[test]
|
||||||
fn test_descriptor_from_str_with_keys_network() {
|
fn test_descriptor_from_str_with_keys_network() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Regtest);
|
.into_wallet_descriptor(&secp, Network::Regtest);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Regtest);
|
.into_wallet_descriptor(&secp, Network::Regtest);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
|
let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
|
||||||
.to_wallet_descriptor(Network::Testnet);
|
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
|
let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))"
|
||||||
.to_wallet_descriptor(Network::Bitcoin);
|
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||||
assert!(desc.is_ok());
|
assert!(desc.is_ok());
|
||||||
|
|
||||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Bitcoin);
|
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
desc.err(),
|
desc.err(),
|
||||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||||
));
|
));
|
||||||
|
|
||||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||||
.to_wallet_descriptor(Network::Bitcoin);
|
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
desc.err(),
|
desc.err(),
|
||||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// test ToWalletDescriptor trait from the output of the descriptor!() macro
|
// test IntoWalletDescriptor trait from the output of the descriptor!() macro
|
||||||
#[test]
|
#[test]
|
||||||
fn test_descriptor_from_str_from_output_of_macro() {
|
fn test_descriptor_from_str_from_output_of_macro() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
||||||
let key = (tpub, path).to_descriptor_key().unwrap();
|
let key = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
// make a descriptor out of it
|
// make a descriptor out of it
|
||||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||||
|
|
||||||
let (wallet_desc, _) = desc.to_wallet_descriptor(Network::Testnet).unwrap();
|
let (wallet_desc, _) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
let wallet_desc_str = wallet_desc.to_string();
|
let wallet_desc_str = wallet_desc.to_string();
|
||||||
assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)");
|
assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw");
|
||||||
|
|
||||||
let (wallet_desc2, _) = wallet_desc_str
|
let (wallet_desc2, _) = wallet_desc_str
|
||||||
.to_wallet_descriptor(Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(wallet_desc, wallet_desc2)
|
assert_eq!(wallet_desc, wallet_desc2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_wallet_descriptor_checked() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
|
||||||
|
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
DescriptorError::HardenedDerivationXpub
|
||||||
|
));
|
||||||
|
|
||||||
|
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||||
|
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
DescriptorError::DuplicatedKeys
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Descriptor templates
|
//! Descriptor templates
|
||||||
//!
|
//!
|
||||||
@@ -32,9 +19,10 @@ use bitcoin::Network;
|
|||||||
|
|
||||||
use miniscript::{Legacy, Segwitv0};
|
use miniscript::{Legacy, Segwitv0};
|
||||||
|
|
||||||
use super::{ExtendedDescriptor, KeyMap, ToWalletDescriptor};
|
use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap};
|
||||||
use crate::descriptor::DescriptorError;
|
use crate::descriptor::DescriptorError;
|
||||||
use crate::keys::{DerivableKey, ToDescriptorKey, ValidNetworks};
|
use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks};
|
||||||
|
use crate::wallet::utils::SecpCtx;
|
||||||
use crate::{descriptor, KeychainKind};
|
use crate::{descriptor, KeychainKind};
|
||||||
|
|
||||||
/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others
|
/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others
|
||||||
@@ -42,20 +30,20 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
|||||||
|
|
||||||
/// Trait for descriptor templates that can be built into a full descriptor
|
/// Trait for descriptor templates that can be built into a full descriptor
|
||||||
///
|
///
|
||||||
/// Since [`ToWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be
|
/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be
|
||||||
/// passed directly to the [`Wallet`](crate::Wallet) constructor.
|
/// passed directly to the [`Wallet`](crate::Wallet) constructor.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use bdk::descriptor::error::Error as DescriptorError;
|
/// use bdk::descriptor::error::Error as DescriptorError;
|
||||||
/// use bdk::keys::{KeyError, ToDescriptorKey};
|
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||||
/// use bdk::miniscript::Legacy;
|
/// use bdk::miniscript::Legacy;
|
||||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||||
///
|
///
|
||||||
/// struct MyP2PKH<K: ToDescriptorKey<Legacy>>(K);
|
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
||||||
///
|
///
|
||||||
/// impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
||||||
/// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
/// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
||||||
/// }
|
/// }
|
||||||
@@ -68,12 +56,13 @@ pub trait DescriptorTemplate {
|
|||||||
|
|
||||||
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
|
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
|
||||||
/// [`build`](DescriptorTemplate::build) method
|
/// [`build`](DescriptorTemplate::build) method
|
||||||
impl<T: DescriptorTemplate> ToWalletDescriptor for T {
|
impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||||
fn to_wallet_descriptor(
|
fn into_wallet_descriptor(
|
||||||
self,
|
self,
|
||||||
|
secp: &SecpCtx,
|
||||||
network: Network,
|
network: Network,
|
||||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||||
Ok(self.build()?.to_wallet_descriptor(network)?)
|
self.build()?.into_wallet_descriptor(secp, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,28 +74,29 @@ impl<T: DescriptorTemplate> ToWalletDescriptor for T {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet};
|
/// # use bdk::{Wallet};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::P2PKH;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::P2Pkh;
|
||||||
///
|
///
|
||||||
/// let key =
|
/// let key =
|
||||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// P2PKH(key),
|
/// P2Pkh(key),
|
||||||
/// None,
|
/// None,
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default(),
|
/// MemoryDatabase::default(),
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// wallet.get_new_address()?.to_string(),
|
/// wallet.get_address(New)?.to_string(),
|
||||||
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
||||||
/// );
|
/// );
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct P2PKH<K: ToDescriptorKey<Legacy>>(pub K);
|
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||||
|
|
||||||
impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
|
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(descriptor!(pkh(self.0))?)
|
descriptor!(pkh(self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,29 +108,30 @@ impl<K: ToDescriptorKey<Legacy>> DescriptorTemplate for P2PKH<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet};
|
/// # use bdk::{Wallet};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::P2WPKH_P2SH;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::P2Wpkh_P2Sh;
|
||||||
///
|
///
|
||||||
/// let key =
|
/// let key =
|
||||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// P2WPKH_P2SH(key),
|
/// P2Wpkh_P2Sh(key),
|
||||||
/// None,
|
/// None,
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default(),
|
/// MemoryDatabase::default(),
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// wallet.get_new_address()?.to_string(),
|
/// wallet.get_address(New)?.to_string(),
|
||||||
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
||||||
/// );
|
/// );
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub struct P2WPKH_P2SH<K: ToDescriptorKey<Segwitv0>>(pub K);
|
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||||
|
|
||||||
impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
|
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(descriptor!(sh(wpkh(self.0)))?)
|
descriptor!(sh(wpkh(self.0)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,28 +143,29 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH_P2SH<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet};
|
/// # use bdk::{Wallet};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::P2WPKH;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::P2Wpkh;
|
||||||
///
|
///
|
||||||
/// let key =
|
/// let key =
|
||||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// P2WPKH(key),
|
/// P2Wpkh(key),
|
||||||
/// None,
|
/// None,
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default(),
|
/// MemoryDatabase::default(),
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// wallet.get_new_address()?.to_string(),
|
/// wallet.get_address(New)?.to_string(),
|
||||||
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
||||||
/// );
|
/// );
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct P2WPKH<K: ToDescriptorKey<Segwitv0>>(pub K);
|
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||||
|
|
||||||
impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(descriptor!(wpkh(self.0))?)
|
descriptor!(wpkh(self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +173,7 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
|||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
/// See [`BIP44Public`] for a template that can work with a `xpub`/`tpub`.
|
/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
@@ -190,25 +182,26 @@ impl<K: ToDescriptorKey<Segwitv0>> DescriptorTemplate for P2WPKH<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP44;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip44;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP44(key.clone(), KeychainKind::External),
|
/// Bip44(key.clone(), KeychainKind::External),
|
||||||
/// Some(BIP44(key, KeychainKind::Internal)),
|
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2PKH(legacy::make_bipxx_private(44, self.0, self.1)?).build()?)
|
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +211,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
|||||||
///
|
///
|
||||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||||
///
|
///
|
||||||
/// See [`BIP44`] for a template that does the full derivation, but requires private data
|
/// See [`Bip44`] for a template that does the full derivation, but requires private data
|
||||||
/// for the key.
|
/// for the key.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
@@ -228,26 +221,27 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP44Public;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip44Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP44Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
/// Some(BIP44Public(key, fingerprint, KeychainKind::Internal)),
|
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2PKH(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()?)
|
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +249,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
|||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
/// See [`BIP49Public`] for a template that can work with a `xpub`/`tpub`.
|
/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
@@ -264,25 +258,26 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for BIP44Public<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP49;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip49;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP49(key.clone(), KeychainKind::External),
|
/// Bip49(key.clone(), KeychainKind::External),
|
||||||
/// Some(BIP49(key, KeychainKind::Internal)),
|
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()?)
|
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +287,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
|||||||
///
|
///
|
||||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||||
///
|
///
|
||||||
/// See [`BIP49`] for a template that does the full derivation, but requires private data
|
/// See [`Bip49`] for a template that does the full derivation, but requires private data
|
||||||
/// for the key.
|
/// for the key.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
@@ -302,26 +297,27 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP49Public;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip49Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP49Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
/// Some(BIP49Public(key, fingerprint, KeychainKind::Internal)),
|
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2WPKH_P2SH(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()?)
|
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +325,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
|||||||
///
|
///
|
||||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||||
///
|
///
|
||||||
/// See [`BIP84Public`] for a template that can work with a `xpub`/`tpub`.
|
/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
@@ -338,25 +334,26 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP49Public<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP84;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip84;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP84(key.clone(), KeychainKind::External),
|
/// Bip84(key.clone(), KeychainKind::External),
|
||||||
/// Some(BIP84(key, KeychainKind::Internal)),
|
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2WPKH(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()?)
|
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +363,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
|||||||
///
|
///
|
||||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||||
///
|
///
|
||||||
/// See [`BIP84`] for a template that does the full derivation, but requires private data
|
/// See [`Bip84`] for a template that does the full derivation, but requires private data
|
||||||
/// for the key.
|
/// for the key.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
@@ -376,26 +373,27 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84<K> {
|
|||||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
/// # use bdk::{Wallet, KeychainKind};
|
||||||
/// # use bdk::database::MemoryDatabase;
|
/// # use bdk::database::MemoryDatabase;
|
||||||
/// use bdk::template::BIP84Public;
|
/// # use bdk::wallet::AddressIndex::New;
|
||||||
|
/// use bdk::template::Bip84Public;
|
||||||
///
|
///
|
||||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||||
/// let wallet = Wallet::new_offline(
|
/// let wallet = Wallet::new_offline(
|
||||||
/// BIP84Public(key.clone(), fingerprint, KeychainKind::External),
|
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||||
/// Some(BIP84Public(key, fingerprint, KeychainKind::Internal)),
|
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
|
||||||
/// Network::Testnet,
|
/// Network::Testnet,
|
||||||
/// MemoryDatabase::default()
|
/// MemoryDatabase::default()
|
||||||
/// )?;
|
/// )?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(wallet.get_new_address()?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)");
|
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub struct BIP84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||||
|
|
||||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for BIP84Public<K> {
|
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||||
Ok(P2WPKH(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()?)
|
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +406,7 @@ macro_rules! expand_make_bipxx {
|
|||||||
bip: u32,
|
bip: u32,
|
||||||
key: K,
|
key: K,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
) -> Result<impl ToDescriptorKey<$ctx>, DescriptorError> {
|
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||||
let mut derivation_path = Vec::with_capacity(4);
|
let mut derivation_path = Vec::with_capacity(4);
|
||||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
||||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||||
@@ -432,17 +430,17 @@ macro_rules! expand_make_bipxx {
|
|||||||
key: K,
|
key: K,
|
||||||
parent_fingerprint: bip32::Fingerprint,
|
parent_fingerprint: bip32::Fingerprint,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
) -> Result<impl ToDescriptorKey<$ctx>, DescriptorError> {
|
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||||
let derivation_path: bip32::DerivationPath = match keychain {
|
let derivation_path: bip32::DerivationPath = match keychain {
|
||||||
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
|
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
|
||||||
KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(),
|
KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut source_path = Vec::with_capacity(3);
|
let source_path = bip32::DerivationPath::from(vec![
|
||||||
source_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
bip32::ChildNumber::from_hardened_idx(bip)?,
|
||||||
source_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||||
source_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||||
let source_path: bip32::DerivationPath = source_path.into();
|
]);
|
||||||
|
|
||||||
Ok((key, (parent_fingerprint, source_path), derivation_path))
|
Ok((key, (parent_fingerprint, source_path), derivation_path))
|
||||||
}
|
}
|
||||||
@@ -457,14 +455,15 @@ expand_make_bipxx!(segwit_v0, Segwitv0);
|
|||||||
mod test {
|
mod test {
|
||||||
// test existing descriptor templates, make sure they are expanded to the right descriptors
|
// test existing descriptor templates, make sure they are expanded to the right descriptors
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::descriptor::derived::AsDerived;
|
||||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||||
use crate::keys::ValidNetworks;
|
use crate::keys::ValidNetworks;
|
||||||
use bitcoin::hashes::core::str::FromStr;
|
|
||||||
use bitcoin::network::constants::Network::Regtest;
|
use bitcoin::network::constants::Network::Regtest;
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
use bitcoin::util::bip32::ChildNumber;
|
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorPublicKeyCtx, KeyMap};
|
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
|
|
||||||
// verify template descriptor generates expected address(es)
|
// verify template descriptor generates expected address(es)
|
||||||
@@ -475,20 +474,18 @@ mod test {
|
|||||||
expected: &[&str],
|
expected: &[&str],
|
||||||
) {
|
) {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let deriv_ctx =
|
|
||||||
DescriptorPublicKeyCtx::new(&secp, ChildNumber::from_normal_idx(0).unwrap());
|
|
||||||
|
|
||||||
let (desc, _key_map, _networks) = desc.unwrap();
|
let (desc, _key_map, _networks) = desc.unwrap();
|
||||||
assert_eq!(desc.is_witness(), is_witness);
|
assert_eq!(desc.is_witness(), is_witness);
|
||||||
assert_eq!(desc.is_fixed(), is_fixed);
|
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||||
for i in 0..expected.len() {
|
for i in 0..expected.len() {
|
||||||
let index = i as u32;
|
let index = i as u32;
|
||||||
let child_desc = if desc.is_fixed() {
|
let child_desc = if !desc.is_deriveable() {
|
||||||
desc.clone()
|
desc.as_derived_fixed(&secp)
|
||||||
} else {
|
} else {
|
||||||
desc.derive(ChildNumber::from_normal_idx(index).unwrap())
|
desc.as_derived(index, &secp)
|
||||||
};
|
};
|
||||||
let address = child_desc.address(Regtest, deriv_ctx).unwrap();
|
let address = child_desc.address(Regtest).unwrap();
|
||||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +497,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2PKH(prvkey).build(),
|
P2Pkh(prvkey).build(),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||||
@@ -511,7 +508,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2PKH(pubkey).build(),
|
P2Pkh(pubkey).build(),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||||
@@ -525,7 +522,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2WPKH_P2SH(prvkey).build(),
|
P2Wpkh_P2Sh(prvkey).build(),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||||
@@ -536,7 +533,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2WPKH_P2SH(pubkey).build(),
|
P2Wpkh_P2Sh(pubkey).build(),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||||
@@ -550,7 +547,7 @@ mod test {
|
|||||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2WPKH(prvkey).build(),
|
P2Wpkh(prvkey).build(),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||||
@@ -561,7 +558,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
check(
|
check(
|
||||||
P2WPKH(pubkey).build(),
|
P2Wpkh(pubkey).build(),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||||
@@ -573,7 +570,7 @@ mod test {
|
|||||||
fn test_bip44_template() {
|
fn test_bip44_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP44(prvkey, KeychainKind::External).build(),
|
Bip44(prvkey, KeychainKind::External).build(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -583,7 +580,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP44(prvkey, KeychainKind::Internal).build(),
|
Bip44(prvkey, KeychainKind::Internal).build(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -600,7 +597,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -610,7 +607,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -626,7 +623,7 @@ mod test {
|
|||||||
fn test_bip49_template() {
|
fn test_bip49_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP49(prvkey, KeychainKind::External).build(),
|
Bip49(prvkey, KeychainKind::External).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -636,7 +633,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP49(prvkey, KeychainKind::Internal).build(),
|
Bip49(prvkey, KeychainKind::Internal).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -653,7 +650,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -663,7 +660,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -679,7 +676,7 @@ mod test {
|
|||||||
fn test_bip84_template() {
|
fn test_bip84_template() {
|
||||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP84(prvkey, KeychainKind::External).build(),
|
Bip84(prvkey, KeychainKind::External).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -689,7 +686,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP84(prvkey, KeychainKind::Internal).build(),
|
Bip84(prvkey, KeychainKind::Internal).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -706,7 +703,7 @@ mod test {
|
|||||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||||
check(
|
check(
|
||||||
BIP84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
@@ -716,7 +713,7 @@ mod test {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
BIP84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
&[
|
&[
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
#[doc(include = "../README.md")]
|
#[doc(include = "../README.md")]
|
||||||
#[cfg(doctest)]
|
#[cfg(doctest)]
|
||||||
pub struct ReadmeDoctests;
|
pub struct ReadmeDoctests;
|
||||||
|
|||||||
91
src/error.rs
91
src/error.rs
@@ -1,29 +1,17 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::bitcoin::Network;
|
||||||
use crate::{descriptor, wallet, wallet::address_validator};
|
use crate::{descriptor, wallet, wallet::address_validator};
|
||||||
use bitcoin::OutPoint;
|
use bitcoin::OutPoint;
|
||||||
|
|
||||||
@@ -36,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
|
||||||
@@ -60,7 +44,7 @@ pub enum Error {
|
|||||||
/// the desired outputs plus fee, if there is not such combination this error is thrown
|
/// the desired outputs plus fee, if there is not such combination this error is thrown
|
||||||
BnBNoExactMatch,
|
BnBNoExactMatch,
|
||||||
/// Happens when trying to spend an UTXO that is not in the internal database
|
/// Happens when trying to spend an UTXO that is not in the internal database
|
||||||
UnknownUTXO,
|
UnknownUtxo,
|
||||||
/// Thrown when a tx is not found in the internal database
|
/// Thrown when a tx is not found in the internal database
|
||||||
TransactionNotFound,
|
TransactionNotFound,
|
||||||
/// Happens when trying to bump a transaction that is already confirmed
|
/// Happens when trying to bump a transaction that is already confirmed
|
||||||
@@ -77,6 +61,8 @@ pub enum Error {
|
|||||||
/// Required fee absolute value (satoshi)
|
/// Required fee absolute value (satoshi)
|
||||||
required: u64,
|
required: u64,
|
||||||
},
|
},
|
||||||
|
/// Node doesn't have data to estimate a fee rate
|
||||||
|
FeeRateUnavailable,
|
||||||
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
|
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
|
||||||
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
|
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
|
||||||
/// explicit origin provided
|
/// explicit origin provided
|
||||||
@@ -93,6 +79,16 @@ pub enum Error {
|
|||||||
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
||||||
/// Signing error
|
/// Signing error
|
||||||
Signer(crate::wallet::signer::SignerError),
|
Signer(crate::wallet::signer::SignerError),
|
||||||
|
/// Invalid network
|
||||||
|
InvalidNetwork {
|
||||||
|
/// requested network, for example what is given as bdk-cli option
|
||||||
|
requested: Network,
|
||||||
|
/// found network, for example the network of the bitcoin node
|
||||||
|
found: Network,
|
||||||
|
},
|
||||||
|
#[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),
|
||||||
@@ -110,15 +106,17 @@ pub enum Error {
|
|||||||
/// Miniscript error
|
/// Miniscript error
|
||||||
Miniscript(miniscript::Error),
|
Miniscript(miniscript::Error),
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
BIP32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
/// An ECDSA error
|
/// An ECDSA error
|
||||||
Secp256k1(bitcoin::secp256k1::Error),
|
Secp256k1(bitcoin::secp256k1::Error),
|
||||||
/// Error serializing or deserializing JSON data
|
/// Error serializing or deserializing JSON data
|
||||||
JSON(serde_json::Error),
|
Json(serde_json::Error),
|
||||||
/// Hex decoding error
|
/// Hex decoding error
|
||||||
Hex(bitcoin::hashes::hex::Error),
|
Hex(bitcoin::hashes::hex::Error),
|
||||||
/// Partially signed bitcoin transaction error
|
/// Partially signed bitcoin transaction error
|
||||||
PSBT(bitcoin::util::psbt::Error),
|
Psbt(bitcoin::util::psbt::Error),
|
||||||
|
/// Partially signed bitcoin transaction parseerror
|
||||||
|
PsbtParse(bitcoin::util::psbt::PsbtParseError),
|
||||||
|
|
||||||
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
|
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
|
||||||
//MissingInputUTXO(usize),
|
//MissingInputUTXO(usize),
|
||||||
@@ -132,13 +130,16 @@ 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),
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
/// Sled database error
|
/// Sled database error
|
||||||
Sled(sled::Error),
|
Sled(sled::Error),
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
/// Rpc client error
|
||||||
|
Rpc(bitcoincore_rpc::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@@ -171,7 +172,7 @@ impl From<crate::keys::KeyError> for Error {
|
|||||||
fn from(key_error: crate::keys::KeyError) -> Error {
|
fn from(key_error: crate::keys::KeyError) -> Error {
|
||||||
match key_error {
|
match key_error {
|
||||||
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
||||||
crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner),
|
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
|
||||||
crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
|
crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
|
||||||
e => Error::Key(e),
|
e => Error::Key(e),
|
||||||
}
|
}
|
||||||
@@ -180,18 +181,19 @@ impl From<crate::keys::KeyError> for Error {
|
|||||||
|
|
||||||
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
||||||
impl_error!(miniscript::Error, Miniscript);
|
impl_error!(miniscript::Error, Miniscript);
|
||||||
impl_error!(bitcoin::util::bip32::Error, BIP32);
|
impl_error!(bitcoin::util::bip32::Error, Bip32);
|
||||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||||
impl_error!(serde_json::Error, JSON);
|
impl_error!(serde_json::Error, Json);
|
||||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||||
impl_error!(bitcoin::util::psbt::Error, PSBT);
|
impl_error!(bitcoin::util::psbt::Error, Psbt);
|
||||||
|
impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
|
||||||
|
|
||||||
#[cfg(feature = "electrum")]
|
#[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")]
|
||||||
|
impl_error!(bitcoincore_rpc::Error, Rpc);
|
||||||
|
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
||||||
@@ -202,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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! BIP-0039
|
//! BIP-0039
|
||||||
|
|
||||||
@@ -32,51 +19,81 @@ use bitcoin::Network;
|
|||||||
|
|
||||||
use miniscript::ScriptContext;
|
use miniscript::ScriptContext;
|
||||||
|
|
||||||
use bip39::{Language, Mnemonic, MnemonicType, Seed};
|
pub use bip39::{Language, Mnemonic, MnemonicType, Seed};
|
||||||
|
|
||||||
use super::{any_network, DerivableKey, DescriptorKey, GeneratableKey, GeneratedKey, KeyError};
|
use super::{
|
||||||
|
any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn set_valid_on_any_network<Ctx: ScriptContext>(
|
||||||
|
descriptor_key: DescriptorKey<Ctx>,
|
||||||
|
) -> DescriptorKey<Ctx> {
|
||||||
|
// We have to pick one network to build the xprv, but since the bip39 standard doesn't
|
||||||
|
// encode the network, the xprv we create is actually valid everywhere. So we override the
|
||||||
|
// valid networks with `any_network()`.
|
||||||
|
descriptor_key.override_valid_networks(any_network())
|
||||||
|
}
|
||||||
|
|
||||||
/// Type for a BIP39 mnemonic with an optional passphrase
|
/// Type for a BIP39 mnemonic with an optional passphrase
|
||||||
pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||||
fn add_metadata(
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self.as_bytes())?.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_descriptor_key(
|
||||||
self,
|
self,
|
||||||
source: Option<bip32::KeySource>,
|
source: Option<bip32::KeySource>,
|
||||||
derivation_path: bip32::DerivationPath,
|
derivation_path: bip32::DerivationPath,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self.as_bytes())?;
|
let descriptor_key = self
|
||||||
let descriptor_key = xprv.add_metadata(source, derivation_path)?;
|
.into_extended_key()?
|
||||||
|
.into_descriptor_key(source, derivation_path)?;
|
||||||
|
|
||||||
// here we must choose one network to build the xpub, but since the bip39 standard doesn't
|
Ok(set_valid_on_any_network(descriptor_key))
|
||||||
// encode the network, the xpub we create is actually valid everywhere. so we override the
|
|
||||||
// valid networks with `any_network()`.
|
|
||||||
Ok(descriptor_key.override_valid_networks(any_network()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
|
||||||
fn add_metadata(
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
let (mnemonic, passphrase) = self;
|
||||||
|
let seed = Seed::new(&mnemonic, passphrase.as_deref().unwrap_or(""));
|
||||||
|
|
||||||
|
seed.into_extended_key()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_descriptor_key(
|
||||||
self,
|
self,
|
||||||
source: Option<bip32::KeySource>,
|
source: Option<bip32::KeySource>,
|
||||||
derivation_path: bip32::DerivationPath,
|
derivation_path: bip32::DerivationPath,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let (mnemonic, passphrase) = self;
|
let descriptor_key = self
|
||||||
let seed = Seed::new(&mnemonic, passphrase.as_deref().unwrap_or(""));
|
.into_extended_key()?
|
||||||
seed.add_metadata(source, derivation_path)
|
.into_descriptor_key(source, derivation_path)?;
|
||||||
|
|
||||||
|
Ok(set_valid_on_any_network(descriptor_key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
||||||
fn add_metadata(
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
(self, None).into_extended_key()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_descriptor_key(
|
||||||
self,
|
self,
|
||||||
source: Option<bip32::KeySource>,
|
source: Option<bip32::KeySource>,
|
||||||
derivation_path: bip32::DerivationPath,
|
derivation_path: bip32::DerivationPath,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
(self, None).add_metadata(source, derivation_path)
|
let descriptor_key = self
|
||||||
|
.into_extended_key()?
|
||||||
|
.into_descriptor_key(source, derivation_path)?;
|
||||||
|
|
||||||
|
Ok(set_valid_on_any_network(descriptor_key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +134,9 @@ mod test {
|
|||||||
|
|
||||||
let key = (mnemonic, path);
|
let key = (mnemonic, path);
|
||||||
let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
|
let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
|
||||||
assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)");
|
assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv");
|
||||||
assert_eq!(keys.len(), 1);
|
assert_eq!(keys.len(), 1);
|
||||||
assert_eq!(networks.len(), 3);
|
assert_eq!(networks.len(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -131,9 +148,9 @@ mod test {
|
|||||||
|
|
||||||
let key = ((mnemonic, Some("passphrase".into())), path);
|
let key = ((mnemonic, Some("passphrase".into())), path);
|
||||||
let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
|
let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap();
|
||||||
assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)");
|
assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m");
|
||||||
assert_eq!(keys.len(), 1);
|
assert_eq!(keys.len(), 1);
|
||||||
assert_eq!(networks.len(), 3);
|
assert_eq!(networks.len(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
425
src/keys/mod.rs
425
src/keys/mod.rs
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Key formats
|
//! Key formats
|
||||||
|
|
||||||
@@ -30,16 +17,16 @@ use std::marker::PhantomData;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitcoin::secp256k1;
|
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
||||||
|
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::{Network, PrivateKey, PublicKey};
|
use bitcoin::{Network, PrivateKey, PublicKey};
|
||||||
|
|
||||||
|
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||||
pub use miniscript::descriptor::{
|
pub use miniscript::descriptor::{
|
||||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub,
|
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
||||||
SortedMultiVec,
|
SortedMultiVec,
|
||||||
};
|
};
|
||||||
use miniscript::descriptor::{DescriptorXKey, KeyMap};
|
|
||||||
pub use miniscript::ScriptContext;
|
pub use miniscript::ScriptContext;
|
||||||
use miniscript::{Miniscript, Terminal};
|
use miniscript::{Miniscript, Terminal};
|
||||||
|
|
||||||
@@ -55,9 +42,14 @@ pub type ValidNetworks = HashSet<Network>;
|
|||||||
|
|
||||||
/// Create a set containing mainnet, testnet and regtest
|
/// Create a set containing mainnet, testnet and regtest
|
||||||
pub fn any_network() -> ValidNetworks {
|
pub fn any_network() -> ValidNetworks {
|
||||||
vec![Network::Bitcoin, Network::Testnet, Network::Regtest]
|
vec![
|
||||||
.into_iter()
|
Network::Bitcoin,
|
||||||
.collect()
|
Network::Testnet,
|
||||||
|
Network::Regtest,
|
||||||
|
Network::Signet,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
/// Create a set only containing mainnet
|
/// Create a set only containing mainnet
|
||||||
pub fn mainnet_network() -> ValidNetworks {
|
pub fn mainnet_network() -> ValidNetworks {
|
||||||
@@ -65,7 +57,7 @@ pub fn mainnet_network() -> ValidNetworks {
|
|||||||
}
|
}
|
||||||
/// Create a set containing testnet and regtest
|
/// Create a set containing testnet and regtest
|
||||||
pub fn test_networks() -> ValidNetworks {
|
pub fn test_networks() -> ValidNetworks {
|
||||||
vec![Network::Testnet, Network::Regtest]
|
vec![Network::Testnet, Network::Regtest, Network::Signet]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -200,15 +192,15 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use bdk::bitcoin::PublicKey;
|
/// use bdk::bitcoin::PublicKey;
|
||||||
///
|
///
|
||||||
/// use bdk::keys::{DescriptorKey, KeyError, ScriptContext, ToDescriptorKey};
|
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
|
||||||
///
|
///
|
||||||
/// pub struct MyKeyType {
|
/// pub struct MyKeyType {
|
||||||
/// pubkey: PublicKey,
|
/// pubkey: PublicKey,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for MyKeyType {
|
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||||
/// fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
/// self.pubkey.to_descriptor_key()
|
/// self.pubkey.into_descriptor_key()
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -219,16 +211,16 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// use bdk::bitcoin::PublicKey;
|
/// use bdk::bitcoin::PublicKey;
|
||||||
///
|
///
|
||||||
/// use bdk::keys::{
|
/// use bdk::keys::{
|
||||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub, KeyError,
|
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||||
/// ScriptContext, ToDescriptorKey,
|
/// IntoDescriptorKey, KeyError, ScriptContext,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// pub struct MyKeyType {
|
/// pub struct MyKeyType {
|
||||||
/// pubkey: PublicKey,
|
/// pubkey: PublicKey,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for MyKeyType {
|
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||||
/// fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
/// Ok(DescriptorKey::from_public(
|
/// Ok(DescriptorKey::from_public(
|
||||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
/// origin: None,
|
/// origin: None,
|
||||||
@@ -245,17 +237,17 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use bdk::bitcoin::PublicKey;
|
/// use bdk::bitcoin::PublicKey;
|
||||||
///
|
///
|
||||||
/// use bdk::keys::{DescriptorKey, ExtScriptContext, KeyError, ScriptContext, ToDescriptorKey};
|
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
|
||||||
///
|
///
|
||||||
/// pub struct MyKeyType {
|
/// pub struct MyKeyType {
|
||||||
/// is_legacy: bool,
|
/// is_legacy: bool,
|
||||||
/// pubkey: PublicKey,
|
/// pubkey: PublicKey,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// impl<Ctx: ScriptContext + 'static> ToDescriptorKey<Ctx> for MyKeyType {
|
/// impl<Ctx: ScriptContext + 'static> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||||
/// fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
/// if Ctx::is_legacy() == self.is_legacy {
|
/// if Ctx::is_legacy() == self.is_legacy {
|
||||||
/// self.pubkey.to_descriptor_key()
|
/// self.pubkey.into_descriptor_key()
|
||||||
/// } else {
|
/// } else {
|
||||||
/// Err(KeyError::InvalidScriptContext)
|
/// Err(KeyError::InvalidScriptContext)
|
||||||
/// }
|
/// }
|
||||||
@@ -274,15 +266,15 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
/// use bdk::bitcoin::PublicKey;
|
/// use bdk::bitcoin::PublicKey;
|
||||||
/// use std::str::FromStr;
|
/// use std::str::FromStr;
|
||||||
///
|
///
|
||||||
/// use bdk::keys::{DescriptorKey, KeyError, ToDescriptorKey};
|
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||||
///
|
///
|
||||||
/// pub struct MySegwitOnlyKeyType {
|
/// pub struct MySegwitOnlyKeyType {
|
||||||
/// pubkey: PublicKey,
|
/// pubkey: PublicKey,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// impl ToDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
/// impl IntoDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
|
||||||
/// fn to_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
|
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
|
||||||
/// self.pubkey.to_descriptor_key()
|
/// self.pubkey.into_descriptor_key()
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@@ -294,61 +286,234 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
|||||||
///
|
///
|
||||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub trait ToDescriptorKey<Ctx: ScriptContext>: Sized {
|
pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
|
||||||
/// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`]
|
/// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`]
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError>;
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum for extended keys that can be either `xprv` or `xpub`
|
||||||
|
///
|
||||||
|
/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey)
|
||||||
|
/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait.
|
||||||
|
///
|
||||||
|
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
|
||||||
|
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
|
||||||
|
/// A private extended key, aka an `xprv`
|
||||||
|
Private((bip32::ExtendedPrivKey, PhantomData<Ctx>)),
|
||||||
|
/// A public extended key, aka an `xpub`
|
||||||
|
Public((bip32::ExtendedPubKey, PhantomData<Ctx>)),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||||
|
/// Return whether or not the key contains the private data
|
||||||
|
pub fn has_secret(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ExtendedKey::Private(_) => true,
|
||||||
|
ExtendedKey::Public(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the
|
||||||
|
/// given [`Network`], if the key contains the private data
|
||||||
|
pub fn into_xprv(self, network: Network) -> Option<bip32::ExtendedPrivKey> {
|
||||||
|
match self {
|
||||||
|
ExtendedKey::Private((mut xprv, _)) => {
|
||||||
|
xprv.network = network;
|
||||||
|
Some(xprv)
|
||||||
|
}
|
||||||
|
ExtendedKey::Public(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the
|
||||||
|
/// given [`Network`]
|
||||||
|
pub fn into_xpub<C: Signing>(
|
||||||
|
self,
|
||||||
|
network: bitcoin::Network,
|
||||||
|
secp: &Secp256k1<C>,
|
||||||
|
) -> bip32::ExtendedPubKey {
|
||||||
|
let mut xpub = match self {
|
||||||
|
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_private(secp, &xprv),
|
||||||
|
ExtendedKey::Public((xpub, _)) => xpub,
|
||||||
|
};
|
||||||
|
|
||||||
|
xpub.network = network;
|
||||||
|
xpub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext> From<bip32::ExtendedPubKey> for ExtendedKey<Ctx> {
|
||||||
|
fn from(xpub: bip32::ExtendedPubKey) -> Self {
|
||||||
|
ExtendedKey::Public((xpub, PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||||
|
fn from(xprv: bip32::ExtendedPrivKey) -> Self {
|
||||||
|
ExtendedKey::Private((xprv, PhantomData))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for keys that can be derived.
|
/// Trait for keys that can be derived.
|
||||||
///
|
///
|
||||||
/// When extra metadata are provided, a [`DerivableKey`] can be transofrmed into a
|
/// When extra metadata are provided, a [`DerivableKey`] can be transofrmed into a
|
||||||
/// [`DescriptorKey`]: the trait [`ToDescriptorKey`] is automatically implemented
|
/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented
|
||||||
/// for `(DerivableKey, DerivationPath)` and
|
/// for `(DerivableKey, DerivationPath)` and
|
||||||
/// `(DerivableKey, KeySource, DerivationPath)` tuples.
|
/// `(DerivableKey, KeySource, DerivationPath)` tuples.
|
||||||
///
|
///
|
||||||
/// For key types that don't encode any indication about the path to use (like bip39), it's
|
/// For key types that don't encode any indication about the path to use (like bip39), it's
|
||||||
/// generally recommended to implemented this trait instead of [`ToDescriptorKey`]. The same
|
/// generally recommended to implemented this trait instead of [`IntoDescriptorKey`]. The same
|
||||||
/// rules regarding script context and valid networks apply.
|
/// rules regarding script context and valid networks apply.
|
||||||
///
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// Key types that can be directly converted into an [`ExtendedPrivKey`] or
|
||||||
|
/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use bdk::bitcoin;
|
||||||
|
/// use bdk::bitcoin::util::bip32;
|
||||||
|
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||||
|
///
|
||||||
|
/// struct MyCustomKeyType {
|
||||||
|
/// key_data: bitcoin::PrivateKey,
|
||||||
|
/// chain_code: Vec<u8>,
|
||||||
|
/// network: bitcoin::Network,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||||
|
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
/// let xprv = bip32::ExtendedPrivKey {
|
||||||
|
/// network: self.network,
|
||||||
|
/// depth: 0,
|
||||||
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
|
/// private_key: self.key_data,
|
||||||
|
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||||
|
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// xprv.into_extended_key()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra
|
||||||
|
/// steps to override the set of valid networks, otherwise only the network specified in the
|
||||||
|
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use bdk::bitcoin;
|
||||||
|
/// use bdk::bitcoin::util::bip32;
|
||||||
|
/// use bdk::keys::{
|
||||||
|
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// struct MyCustomKeyType {
|
||||||
|
/// key_data: bitcoin::PrivateKey,
|
||||||
|
/// chain_code: Vec<u8>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||||
|
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
/// let xprv = bip32::ExtendedPrivKey {
|
||||||
|
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
||||||
|
/// depth: 0,
|
||||||
|
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||||
|
/// private_key: self.key_data,
|
||||||
|
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||||
|
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// xprv.into_extended_key()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn into_descriptor_key(
|
||||||
|
/// self,
|
||||||
|
/// source: Option<bip32::KeySource>,
|
||||||
|
/// derivation_path: bip32::DerivationPath,
|
||||||
|
/// ) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
|
/// let descriptor_key = self
|
||||||
|
/// .into_extended_key()?
|
||||||
|
/// .into_descriptor_key(source, derivation_path)?;
|
||||||
|
///
|
||||||
|
/// // Override the set of valid networks here
|
||||||
|
/// Ok(descriptor_key.override_valid_networks(any_network()))
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// [`DerivationPath`]: (bip32::DerivationPath)
|
/// [`DerivationPath`]: (bip32::DerivationPath)
|
||||||
pub trait DerivableKey<Ctx: ScriptContext> {
|
/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey)
|
||||||
/// Add a extra metadata, consume `self` and turn it into a [`DescriptorKey`]
|
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
|
||||||
fn add_metadata(
|
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||||
self,
|
/// Consume `self` and turn it into an [`ExtendedKey`]
|
||||||
origin: Option<bip32::KeySource>,
|
///
|
||||||
derivation_path: bip32::DerivationPath,
|
/// This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError>;
|
/// like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
|
||||||
}
|
#[cfg_attr(
|
||||||
|
feature = "keys-bip39",
|
||||||
|
doc = r##"
|
||||||
|
```rust
|
||||||
|
use bdk::bitcoin::Network;
|
||||||
|
use bdk::keys::{DerivableKey, ExtendedKey};
|
||||||
|
use bdk::keys::bip39::{Mnemonic, Language};
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
|
# fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
fn add_metadata(
|
let xkey: ExtendedKey =
|
||||||
|
Mnemonic::from_phrase(
|
||||||
|
"jelly crash boy whisper mouse ecology tuna soccer memory million news short",
|
||||||
|
Language::English
|
||||||
|
)?
|
||||||
|
.into_extended_key()?;
|
||||||
|
let xprv = xkey.into_xprv(Network::Bitcoin).unwrap();
|
||||||
|
# Ok(()) }
|
||||||
|
```
|
||||||
|
"##
|
||||||
|
)]
|
||||||
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError>;
|
||||||
|
|
||||||
|
/// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as
|
||||||
|
/// key origin and derivation path
|
||||||
|
fn into_descriptor_key(
|
||||||
self,
|
self,
|
||||||
origin: Option<bip32::KeySource>,
|
origin: Option<bip32::KeySource>,
|
||||||
derivation_path: bip32::DerivationPath,
|
derivation_path: bip32::DerivationPath,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorPublicKey::XPub(DescriptorXKey {
|
match self.into_extended_key()? {
|
||||||
origin,
|
ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey {
|
||||||
xkey: self,
|
origin,
|
||||||
derivation_path,
|
xkey: xprv,
|
||||||
is_wildcard: true,
|
derivation_path,
|
||||||
})
|
wildcard: Wildcard::Unhardened,
|
||||||
.to_descriptor_key()
|
})
|
||||||
|
.into_descriptor_key(),
|
||||||
|
ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey {
|
||||||
|
origin,
|
||||||
|
xkey: xpub,
|
||||||
|
derivation_path,
|
||||||
|
wildcard: Wildcard::Unhardened,
|
||||||
|
})
|
||||||
|
.into_descriptor_key(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identity conversion
|
||||||
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
|
||||||
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
|
||||||
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
Ok(self.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||||
fn add_metadata(
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
self,
|
Ok(self.into())
|
||||||
origin: Option<bip32::KeySource>,
|
|
||||||
derivation_path: bip32::DerivationPath,
|
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
|
||||||
DescriptorSecretKey::XPrv(DescriptorXKey {
|
|
||||||
origin,
|
|
||||||
xkey: self,
|
|
||||||
derivation_path,
|
|
||||||
is_wildcard: true,
|
|
||||||
})
|
|
||||||
.to_descriptor_key()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,37 +554,41 @@ where
|
|||||||
Ctx: ScriptContext,
|
Ctx: ScriptContext,
|
||||||
K: DerivableKey<Ctx>,
|
K: DerivableKey<Ctx>,
|
||||||
{
|
{
|
||||||
fn add_metadata(
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
self.key.into_extended_key()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_descriptor_key(
|
||||||
self,
|
self,
|
||||||
origin: Option<bip32::KeySource>,
|
origin: Option<bip32::KeySource>,
|
||||||
derivation_path: bip32::DerivationPath,
|
derivation_path: bip32::DerivationPath,
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let descriptor_key = self.key.add_metadata(origin, derivation_path)?;
|
let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?;
|
||||||
Ok(descriptor_key.override_valid_networks(self.valid_networks))
|
Ok(descriptor_key.override_valid_networks(self.valid_networks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make generated keys directly usable in descriptors, and make sure they get assigned the right
|
// Make generated keys directly usable in descriptors, and make sure they get assigned the right
|
||||||
// `valid_networks`.
|
// `valid_networks`.
|
||||||
impl<Ctx, K> ToDescriptorKey<Ctx> for GeneratedKey<K, Ctx>
|
impl<Ctx, K> IntoDescriptorKey<Ctx> for GeneratedKey<K, Ctx>
|
||||||
where
|
where
|
||||||
Ctx: ScriptContext,
|
Ctx: ScriptContext,
|
||||||
K: ToDescriptorKey<Ctx>,
|
K: IntoDescriptorKey<Ctx>,
|
||||||
{
|
{
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let desc_key = self.key.to_descriptor_key()?;
|
let desc_key = self.key.into_descriptor_key()?;
|
||||||
Ok(desc_key.override_valid_networks(self.valid_networks))
|
Ok(desc_key.override_valid_networks(self.valid_networks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for keys that can be generated
|
/// Trait for keys that can be generated
|
||||||
///
|
///
|
||||||
/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`ToDescriptorKey`] apply.
|
/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply.
|
||||||
///
|
///
|
||||||
/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self`
|
/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self`
|
||||||
/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for
|
/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for
|
||||||
/// [`ToDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also
|
/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also
|
||||||
/// [`ToDescriptorKey`].
|
/// [`IntoDescriptorKey`].
|
||||||
pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
|
pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
|
||||||
/// Type specifying the amount of entropy required e.g. `[u8;32]`
|
/// Type specifying the amount of entropy required e.g. `[u8;32]`
|
||||||
type Entropy: AsMut<[u8]> + Default;
|
type Entropy: AsMut<[u8]> + Default;
|
||||||
@@ -529,27 +698,29 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> ToDescriptorKey<Ctx> for (T, bip32::DerivationPath) {
|
impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
for (T, bip32::DerivationPath)
|
||||||
self.0.add_metadata(None, self.1)
|
{
|
||||||
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
|
self.0.into_descriptor_key(None, self.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> ToDescriptorKey<Ctx>
|
impl<Ctx: ScriptContext, T: DerivableKey<Ctx>> IntoDescriptorKey<Ctx>
|
||||||
for (T, bip32::KeySource, bip32::DerivationPath)
|
for (T, bip32::KeySource, bip32::DerivationPath)
|
||||||
{
|
{
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
self.0.add_metadata(Some(self.1), self.2)
|
self.0.into_descriptor_key(Some(self.1), self.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_multi_keys<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||||
pks: Vec<Pk>,
|
pks: Vec<Pk>,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(Vec<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError> {
|
) -> Result<(Vec<DescriptorPublicKey>, KeyMap, ValidNetworks), KeyError> {
|
||||||
let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks
|
let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|key| Ok::<_, KeyError>(key.to_descriptor_key()?.extract(secp)?))
|
.map(|key| key.into_descriptor_key()?.extract(secp))
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(a, b, c)| (a, (b, c)))
|
.map(|(a, b, c)| (a, (b, c)))
|
||||||
@@ -570,11 +741,11 @@ fn expand_multi_keys<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
|||||||
|
|
||||||
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
|
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn make_pk<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||||
descriptor_key: Pk,
|
descriptor_key: Pk,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
|
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
|
||||||
let (key, key_map, valid_networks) = descriptor_key.to_descriptor_key()?.extract(secp)?;
|
let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?;
|
||||||
let minisc = Miniscript::from_ast(Terminal::PkK(key))?;
|
let minisc = Miniscript::from_ast(Terminal::PkK(key))?;
|
||||||
|
|
||||||
minisc.check_minsicript()?;
|
minisc.check_minsicript()?;
|
||||||
@@ -584,7 +755,7 @@ pub fn make_pk<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
|||||||
|
|
||||||
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
// Used internally by `bdk::fragment!` to build `multi()` fragments
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn make_multi<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
pub fn make_multi<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||||
thresh: usize,
|
thresh: usize,
|
||||||
pks: Vec<Pk>,
|
pks: Vec<Pk>,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
@@ -599,35 +770,35 @@ pub fn make_multi<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
|||||||
|
|
||||||
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
|
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn make_sortedmulti_inner<Pk: ToDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
pub fn make_sortedmulti<Pk, Ctx, F>(
|
||||||
thresh: usize,
|
thresh: usize,
|
||||||
pks: Vec<Pk>,
|
pks: Vec<Pk>,
|
||||||
|
build_desc: F,
|
||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<
|
) -> Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>
|
||||||
(
|
where
|
||||||
SortedMultiVec<DescriptorPublicKey, Ctx>,
|
Pk: IntoDescriptorKey<Ctx>,
|
||||||
KeyMap,
|
Ctx: ScriptContext,
|
||||||
ValidNetworks,
|
F: Fn(
|
||||||
),
|
usize,
|
||||||
DescriptorError,
|
Vec<DescriptorPublicKey>,
|
||||||
> {
|
) -> Result<(Descriptor<DescriptorPublicKey>, PhantomData<Ctx>), DescriptorError>,
|
||||||
|
{
|
||||||
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
|
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
|
||||||
let minisc = SortedMultiVec::new(thresh, pks)?;
|
let descriptor = build_desc(thresh, pks)?.0;
|
||||||
|
|
||||||
// TODO: should we apply the checks here as well?
|
Ok((descriptor, key_map, valid_networks))
|
||||||
|
|
||||||
Ok((minisc, key_map, valid_networks))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The "identity" conversion is used internally by some `bdk::fragment`s
|
/// The "identity" conversion is used internally by some `bdk::fragment`s
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for DescriptorPublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let networks = match self {
|
let networks = match self {
|
||||||
DescriptorPublicKey::SinglePub(_) => any_network(),
|
DescriptorPublicKey::SinglePub(_) => any_network(),
|
||||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||||
@@ -642,18 +813,18 @@ impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for DescriptorPublicKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for PublicKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||||
key: self,
|
key: self,
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
.to_descriptor_key()
|
.into_descriptor_key()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for DescriptorSecretKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
let networks = match &self {
|
let networks = match &self {
|
||||||
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
||||||
mainnet_network()
|
mainnet_network()
|
||||||
@@ -670,21 +841,21 @@ impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for DescriptorSecretKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for &'_ str {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorSecretKey::from_str(self)
|
DescriptorSecretKey::from_str(self)
|
||||||
.map_err(|e| KeyError::Message(e.to_string()))?
|
.map_err(|e| KeyError::Message(e.to_string()))?
|
||||||
.to_descriptor_key()
|
.into_descriptor_key()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Ctx: ScriptContext> ToDescriptorKey<Ctx> for PrivateKey {
|
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||||
fn to_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||||
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||||
key: self,
|
key: self,
|
||||||
origin: None,
|
origin: None,
|
||||||
})
|
})
|
||||||
.to_descriptor_key()
|
.into_descriptor_key()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,13 +873,13 @@ pub enum KeyError {
|
|||||||
Message(String),
|
Message(String),
|
||||||
|
|
||||||
/// BIP32 error
|
/// BIP32 error
|
||||||
BIP32(bitcoin::util::bip32::Error),
|
Bip32(bitcoin::util::bip32::Error),
|
||||||
/// Miniscript error
|
/// Miniscript error
|
||||||
Miniscript(miniscript::Error),
|
Miniscript(miniscript::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_error!(miniscript::Error, Miniscript, KeyError);
|
impl_error!(miniscript::Error, Miniscript, KeyError);
|
||||||
impl_error!(bitcoin::util::bip32::Error, BIP32, KeyError);
|
impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError);
|
||||||
|
|
||||||
impl std::fmt::Display for KeyError {
|
impl std::fmt::Display for KeyError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
|||||||
255
src/lib.rs
255
src/lib.rs
@@ -1,35 +1,19 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
// rustdoc will warn if there are missing docs
|
// rustdoc will warn if there are missing docs
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
// only enables the `doc_cfg` feature when
|
// only enables the `doc_cfg` feature when
|
||||||
// the `docsrs` configuration attribute is defined
|
// the `docsrs` configuration attribute is defined
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
// only enables the nightly `external_doc` feature when
|
|
||||||
// `test-md-docs` is enabled
|
|
||||||
#![cfg_attr(feature = "test-md-docs", feature(external_doc))]
|
|
||||||
|
|
||||||
//! A modern, lightweight, descriptor-based wallet library written in Rust.
|
//! A modern, lightweight, descriptor-based wallet library written in Rust.
|
||||||
//!
|
//!
|
||||||
@@ -56,36 +40,39 @@
|
|||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! bdk = "0.2.0"
|
//! bdk = "0.10.0"
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Sync the balance of a descriptor
|
|
||||||
//!
|
|
||||||
//! ### Example
|
|
||||||
//! ```ignore
|
|
||||||
//! use bdk::Wallet;
|
|
||||||
//! use bdk::database::MemoryDatabase;
|
|
||||||
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
|
||||||
//!
|
|
||||||
//! use bdk::electrum_client::Client;
|
|
||||||
//!
|
|
||||||
//! fn main() -> Result<(), bdk::Error> {
|
|
||||||
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
|
||||||
//! let wallet = Wallet::new(
|
|
||||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
|
||||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
|
||||||
//! bitcoin::Network::Testnet,
|
|
||||||
//! MemoryDatabase::default(),
|
|
||||||
//! ElectrumBlockchain::from(client)
|
|
||||||
//! )?;
|
|
||||||
//!
|
|
||||||
//! wallet.sync(noop_progress(), None)?;
|
|
||||||
//!
|
|
||||||
//! println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
|
||||||
//!
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! ```
|
//! ```
|
||||||
|
#![cfg_attr(
|
||||||
|
feature = "electrum",
|
||||||
|
doc = r##"
|
||||||
|
## Sync the balance of a descriptor
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```no_run
|
||||||
|
use bdk::Wallet;
|
||||||
|
use bdk::database::MemoryDatabase;
|
||||||
|
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||||
|
use bdk::electrum_client::Client;
|
||||||
|
|
||||||
|
fn main() -> Result<(), bdk::Error> {
|
||||||
|
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||||
|
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||||
|
bitcoin::Network::Testnet,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
ElectrumBlockchain::from(client)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None)?;
|
||||||
|
|
||||||
|
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"##
|
||||||
|
)]
|
||||||
//!
|
//!
|
||||||
//! ## Generate a few addresses
|
//! ## Generate a few addresses
|
||||||
//!
|
//!
|
||||||
@@ -93,72 +80,80 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//! use bdk::{Wallet};
|
//! use bdk::{Wallet};
|
||||||
//! use bdk::database::MemoryDatabase;
|
//! use bdk::database::MemoryDatabase;
|
||||||
|
//! use bdk::wallet::AddressIndex::New;
|
||||||
//!
|
//!
|
||||||
//! fn main() -> Result<(), bdk::Error> {
|
//! fn main() -> Result<(), bdk::Error> {
|
||||||
//! let wallet = Wallet::new_offline(
|
//! let wallet = Wallet::new_offline(
|
||||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||||
//! bitcoin::Network::Testnet,
|
//! bitcoin::Network::Testnet,
|
||||||
//! MemoryDatabase::default(),
|
//! MemoryDatabase::default(),
|
||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! println!("Address #0: {}", wallet.get_new_address()?);
|
//! println!("Address #0: {}", wallet.get_address(New)?);
|
||||||
//! println!("Address #1: {}", wallet.get_new_address()?);
|
//! println!("Address #1: {}", wallet.get_address(New)?);
|
||||||
//! println!("Address #2: {}", wallet.get_new_address()?);
|
//! println!("Address #2: {}", wallet.get_address(New)?);
|
||||||
//!
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Create a transaction
|
|
||||||
//!
|
|
||||||
//! ### Example
|
|
||||||
//! ```ignore
|
|
||||||
//! use base64::decode;
|
|
||||||
//! use bdk::{FeeRate, TxBuilder, Wallet};
|
|
||||||
//! use bdk::database::MemoryDatabase;
|
|
||||||
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
|
||||||
//!
|
|
||||||
//! use bdk::electrum_client::Client;
|
|
||||||
//!
|
|
||||||
//! use bitcoin::consensus::serialize;
|
|
||||||
//!
|
|
||||||
//! fn main() -> Result<(), bdk::Error> {
|
|
||||||
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
|
||||||
//! let wallet = Wallet::new(
|
|
||||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
|
||||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
|
||||||
//! bitcoin::Network::Testnet,
|
|
||||||
//! MemoryDatabase::default(),
|
|
||||||
//! ElectrumBlockchain::from(client)
|
|
||||||
//! )?;
|
|
||||||
//!
|
|
||||||
//! wallet.sync(noop_progress(), None)?;
|
|
||||||
//!
|
|
||||||
//! let send_to = wallet.get_new_address()?;
|
|
||||||
//! let (psbt, details) = wallet.create_tx(
|
|
||||||
//! TxBuilder::with_recipients(vec![(send_to.script_pubkey(), 50_000)])
|
|
||||||
//! .enable_rbf()
|
|
||||||
//! .do_not_spend_change()
|
|
||||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
|
||||||
//! )?;
|
|
||||||
//!
|
|
||||||
//! println!("Transaction details: {:#?}", details);
|
|
||||||
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
|
||||||
//!
|
//!
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
#![cfg_attr(
|
||||||
|
feature = "electrum",
|
||||||
|
doc = r##"
|
||||||
|
## Create a transaction
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```no_run
|
||||||
|
use bdk::{FeeRate, Wallet};
|
||||||
|
use bdk::database::MemoryDatabase;
|
||||||
|
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
|
||||||
|
use bdk::electrum_client::Client;
|
||||||
|
|
||||||
|
use bitcoin::consensus::serialize;
|
||||||
|
use bdk::wallet::AddressIndex::New;
|
||||||
|
|
||||||
|
fn main() -> Result<(), bdk::Error> {
|
||||||
|
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||||
|
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||||
|
bitcoin::Network::Testnet,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
ElectrumBlockchain::from(client)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None)?;
|
||||||
|
|
||||||
|
let send_to = wallet.get_address(New)?;
|
||||||
|
let (psbt, details) = {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder
|
||||||
|
.add_recipient(send_to.script_pubkey(), 50_000)
|
||||||
|
.enable_rbf()
|
||||||
|
.do_not_spend_change()
|
||||||
|
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
|
builder.finish()?
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Transaction details: {:#?}", details);
|
||||||
|
println!("Unsigned PSBT: {}", &psbt);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"##
|
||||||
|
)]
|
||||||
//!
|
//!
|
||||||
//! ## Sign a transaction
|
//! ## Sign a transaction
|
||||||
//!
|
//!
|
||||||
//! ### Example
|
//! ### Example
|
||||||
//! ```ignore
|
//! ```no_run
|
||||||
//! use base64::decode;
|
//! use std::str::FromStr;
|
||||||
//! use bdk::{Wallet};
|
|
||||||
//! use bdk::database::MemoryDatabase;
|
|
||||||
//!
|
//!
|
||||||
//! use bitcoin::consensus::deserialize;
|
//! use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||||
|
//!
|
||||||
|
//! use bdk::{Wallet, SignOptions};
|
||||||
|
//! use bdk::database::MemoryDatabase;
|
||||||
//!
|
//!
|
||||||
//! fn main() -> Result<(), bdk::Error> {
|
//! fn main() -> Result<(), bdk::Error> {
|
||||||
//! let wallet = Wallet::new_offline(
|
//! let wallet = Wallet::new_offline(
|
||||||
@@ -169,9 +164,9 @@
|
|||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! let psbt = "...";
|
//! let psbt = "...";
|
||||||
//! let psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
//! let mut psbt = Psbt::from_str(psbt)?;
|
||||||
//!
|
//!
|
||||||
//! let (signed_psbt, finalized) = wallet.sign(psbt, None)?;
|
//! let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||||
//!
|
//!
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
@@ -210,6 +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"))]
|
||||||
|
compile_error!(
|
||||||
|
"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;
|
||||||
|
|
||||||
@@ -220,31 +233,18 @@ extern crate async_trait;
|
|||||||
extern crate bdk_macros;
|
extern crate bdk_macros;
|
||||||
|
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
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)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate testutils;
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[cfg(test)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate testutils_macros;
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[cfg(test)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serial_test;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub mod blockchain;
|
pub mod blockchain;
|
||||||
@@ -258,10 +258,23 @@ pub(crate) mod types;
|
|||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|
||||||
pub use descriptor::template;
|
pub use descriptor::template;
|
||||||
pub use descriptor::HDKeyPaths;
|
pub use descriptor::HdKeyPaths;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub use wallet::address_validator;
|
pub use wallet::address_validator;
|
||||||
pub use wallet::signer;
|
pub use wallet::signer;
|
||||||
|
pub use wallet::signer::SignOptions;
|
||||||
pub use wallet::tx_builder::TxBuilder;
|
pub use wallet::tx_builder::TxBuilder;
|
||||||
pub use wallet::Wallet;
|
pub use wallet::Wallet;
|
||||||
|
|
||||||
|
/// Get the version of BDK at runtime
|
||||||
|
pub fn version() -> &'static str {
|
||||||
|
env!("CARGO_PKG_VERSION", "unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should consider putting this under a feature flag but we need the macro in doctets so we need
|
||||||
|
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
|
||||||
|
//
|
||||||
|
// Stuff in here is too rough to document atm
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod testutils;
|
||||||
|
|||||||
118
src/psbt/mod.rs
118
src/psbt/mod.rs
@@ -1,35 +1,23 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
|
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
|
||||||
use bitcoin::TxOut;
|
use bitcoin::TxOut;
|
||||||
|
|
||||||
pub trait PSBTUtils {
|
pub trait PsbtUtils {
|
||||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PSBTUtils for PSBT {
|
impl PsbtUtils for Psbt {
|
||||||
|
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||||
let tx = &self.global.unsigned_tx;
|
let tx = &self.global.unsigned_tx;
|
||||||
|
|
||||||
@@ -50,3 +38,85 @@ impl PSBTUtils for PSBT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::bitcoin::TxIn;
|
||||||
|
use crate::psbt::Psbt;
|
||||||
|
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||||
|
use crate::wallet::AddressIndex;
|
||||||
|
use crate::SignOptions;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// from bip 174
|
||||||
|
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||||
|
fn test_psbt_malformed_psbt_input_legacy() {
|
||||||
|
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||||
|
let options = SignOptions {
|
||||||
|
trust_witness_utxo: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||||
|
fn test_psbt_malformed_psbt_input_segwit() {
|
||||||
|
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
psbt.inputs.push(psbt_bip.inputs[1].clone());
|
||||||
|
let options = SignOptions {
|
||||||
|
trust_witness_utxo: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||||
|
fn test_psbt_malformed_tx_input() {
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
psbt.global.unsigned_tx.input.push(TxIn::default());
|
||||||
|
let options = SignOptions {
|
||||||
|
trust_witness_utxo: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_psbt_sign_with_finalized() {
|
||||||
|
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let send_to = wallet.get_address(AddressIndex::New).unwrap();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||||
|
let (mut psbt, _) = builder.finish().unwrap();
|
||||||
|
|
||||||
|
// add a finalized input
|
||||||
|
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||||
|
psbt.global
|
||||||
|
.unsigned_tx
|
||||||
|
.input
|
||||||
|
.push(psbt_bip.global.unsigned_tx.input[0].clone());
|
||||||
|
|
||||||
|
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
880
src/testutils/blockchain_tests.rs
Normal file
880
src/testutils/blockchain_tests.rs
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
use crate::testutils::TestIncomingTx;
|
||||||
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
|
use bitcoin::hashes::hex::{FromHex, ToHex};
|
||||||
|
use bitcoin::hashes::sha256d;
|
||||||
|
use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
||||||
|
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
||||||
|
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
||||||
|
use core::str::FromStr;
|
||||||
|
use electrsd::bitcoind::BitcoinD;
|
||||||
|
use electrsd::{bitcoind, Conf, ElectrsD};
|
||||||
|
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub struct TestClient {
|
||||||
|
pub bitcoind: BitcoinD,
|
||||||
|
pub electrsd: ElectrsD,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestClient {
|
||||||
|
pub fn new(bitcoind_exe: String, electrs_exe: String) -> Self {
|
||||||
|
debug!("launching {} and {}", &bitcoind_exe, &electrs_exe);
|
||||||
|
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// wait for electrs to index the tx
|
||||||
|
exponential_backoff_poll(|| {
|
||||||
|
self.electrsd.trigger().unwrap();
|
||||||
|
trace!("wait_for_tx {}", txid);
|
||||||
|
|
||||||
|
self.electrsd
|
||||||
|
.client
|
||||||
|
.script_get_history(monitor_script)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.tx_hash == txid)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_block(&mut self, min_height: usize) {
|
||||||
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let header = exponential_backoff_poll(|| {
|
||||||
|
self.electrsd.trigger().unwrap();
|
||||||
|
self.electrsd.client.ping().unwrap();
|
||||||
|
self.electrsd.client.block_headers_pop().unwrap()
|
||||||
|
});
|
||||||
|
if header.height >= min_height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
|
||||||
|
assert!(
|
||||||
|
!meta_tx.output.is_empty(),
|
||||||
|
"can't create a transaction with no outputs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
let mut required_balance = 0;
|
||||||
|
for out in &meta_tx.output {
|
||||||
|
required_balance += out.value;
|
||||||
|
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
||||||
|
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: core can't create a tx with two outputs to the same address
|
||||||
|
let tx = self
|
||||||
|
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
|
||||||
|
.unwrap();
|
||||||
|
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
||||||
|
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
||||||
|
|
||||||
|
if let Some(true) = meta_tx.replaceable {
|
||||||
|
// for some reason core doesn't set this field right
|
||||||
|
for input in &mut tx.input {
|
||||||
|
input.sequence = 0xFFFFFFFD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = self
|
||||||
|
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// broadcast through electrum so that it caches the tx immediately
|
||||||
|
|
||||||
|
let txid = self
|
||||||
|
.electrsd
|
||||||
|
.client
|
||||||
|
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
debug!("broadcasted to electrum {}", txid);
|
||||||
|
|
||||||
|
if let Some(num) = meta_tx.min_confirmations {
|
||||||
|
self.generate(num, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
|
||||||
|
.unwrap()
|
||||||
|
.script_pubkey();
|
||||||
|
self.wait_for_tx(txid, &monitor_script);
|
||||||
|
|
||||||
|
debug!("Sent tx: {}", txid);
|
||||||
|
|
||||||
|
txid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
|
||||||
|
let tx = self.get_raw_transaction_info(txid, None).unwrap();
|
||||||
|
assert!(
|
||||||
|
tx.confirmations.is_none(),
|
||||||
|
"Can't bump tx {} because it's already confirmed",
|
||||||
|
txid
|
||||||
|
);
|
||||||
|
|
||||||
|
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
|
||||||
|
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
|
||||||
|
|
||||||
|
let monitor_script =
|
||||||
|
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
|
||||||
|
self.wait_for_tx(new_txid, &monitor_script);
|
||||||
|
|
||||||
|
debug!("Bumped {}, new txid {}", txid, new_txid);
|
||||||
|
|
||||||
|
new_txid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
|
||||||
|
use bitcoin::blockdata::block::{Block, BlockHeader};
|
||||||
|
use bitcoin::blockdata::script::Builder;
|
||||||
|
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
||||||
|
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
||||||
|
|
||||||
|
let block_template: serde_json::Value = self
|
||||||
|
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
||||||
|
.unwrap();
|
||||||
|
trace!("getblocktemplate: {:#?}", block_template);
|
||||||
|
|
||||||
|
let header = BlockHeader {
|
||||||
|
version: block_template["version"].as_i64().unwrap() as i32,
|
||||||
|
prev_blockhash: BlockHash::from_hex(
|
||||||
|
block_template["previousblockhash"].as_str().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
merkle_root: TxMerkleNode::default(),
|
||||||
|
time: block_template["curtime"].as_u64().unwrap() as u32,
|
||||||
|
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
||||||
|
nonce: 0,
|
||||||
|
};
|
||||||
|
debug!("header: {:#?}", header);
|
||||||
|
|
||||||
|
let height = block_template["height"].as_u64().unwrap() as i64;
|
||||||
|
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
|
||||||
|
// burn block subsidy and fees, not a big deal
|
||||||
|
let mut coinbase_tx = Transaction {
|
||||||
|
version: 1,
|
||||||
|
lock_time: 0,
|
||||||
|
input: vec![TxIn {
|
||||||
|
previous_output: OutPoint::null(),
|
||||||
|
script_sig: Builder::new().push_int(height).into_script(),
|
||||||
|
sequence: 0xFFFFFFFF,
|
||||||
|
witness: vec![witness_reserved_value],
|
||||||
|
}],
|
||||||
|
output: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut txdata = vec![coinbase_tx.clone()];
|
||||||
|
txdata.extend_from_slice(&txs);
|
||||||
|
|
||||||
|
let mut block = Block { header, txdata };
|
||||||
|
|
||||||
|
let witness_root = block.witness_root();
|
||||||
|
let witness_commitment =
|
||||||
|
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
|
||||||
|
|
||||||
|
// now update and replace the coinbase tx
|
||||||
|
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
||||||
|
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
||||||
|
|
||||||
|
coinbase_tx.output.push(TxOut {
|
||||||
|
value: 0,
|
||||||
|
script_pubkey: coinbase_witness_commitment_script.into(),
|
||||||
|
});
|
||||||
|
block.txdata[0] = coinbase_tx;
|
||||||
|
|
||||||
|
// set merkle root
|
||||||
|
let merkle_root = block.merkle_root();
|
||||||
|
block.header.merkle_root = merkle_root;
|
||||||
|
|
||||||
|
assert!(block.check_merkle_root());
|
||||||
|
assert!(block.check_witness_commitment());
|
||||||
|
|
||||||
|
// now do PoW :)
|
||||||
|
let target = block.header.target();
|
||||||
|
while block.header.validate_pow(&target).is_err() {
|
||||||
|
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
|
||||||
|
}
|
||||||
|
|
||||||
|
let block_hex: String = serialize(&block).to_hex();
|
||||||
|
debug!("generated block hex: {}", block_hex);
|
||||||
|
|
||||||
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
|
let submit_result: serde_json::Value =
|
||||||
|
self.call("submitblock", &[block_hex.into()]).unwrap();
|
||||||
|
debug!("submitblock: {:?}", submit_result);
|
||||||
|
assert!(
|
||||||
|
submit_result.is_null(),
|
||||||
|
"submitblock error: {:?}",
|
||||||
|
submit_result.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
self.wait_for_block(height as usize);
|
||||||
|
|
||||||
|
block.header.block_hash().to_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
|
||||||
|
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
|
||||||
|
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
|
||||||
|
let best_hash = hashes.last().unwrap();
|
||||||
|
let height = self.get_block_info(best_hash).unwrap().height;
|
||||||
|
|
||||||
|
self.wait_for_block(height);
|
||||||
|
|
||||||
|
debug!("Generated blocks to new height {}", height);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate(&mut self, num_blocks: u64) {
|
||||||
|
self.electrsd.client.block_headers_subscribe().unwrap();
|
||||||
|
|
||||||
|
let best_hash = self.get_best_block_hash().unwrap();
|
||||||
|
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
||||||
|
|
||||||
|
let mut to_invalidate = best_hash;
|
||||||
|
for i in 1..=num_blocks {
|
||||||
|
trace!(
|
||||||
|
"Invalidating block {}/{} ({})",
|
||||||
|
i,
|
||||||
|
num_blocks,
|
||||||
|
to_invalidate
|
||||||
|
);
|
||||||
|
|
||||||
|
self.invalidate_block(&to_invalidate).unwrap();
|
||||||
|
to_invalidate = self.get_best_block_hash().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wait_for_block(initial_height - num_blocks as usize);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Invalidated {} blocks to new height of {}",
|
||||||
|
num_blocks,
|
||||||
|
initial_height - num_blocks as usize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reorg(&mut self, num_blocks: u64) {
|
||||||
|
self.invalidate(num_blocks);
|
||||||
|
self.generate(num_blocks, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
|
||||||
|
Address::from_str(
|
||||||
|
&self
|
||||||
|
.get_new_address(None, address_type)
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_electrum_url() -> String {
|
||||||
|
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TestClient {
|
||||||
|
type Target = RpcClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.bitcoind.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
let bitcoind_exe = env::var("BITCOIND_EXE")
|
||||||
|
.ok()
|
||||||
|
.or(bitcoind::downloaded_exe_path())
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
|
||||||
|
where
|
||||||
|
F: FnMut() -> Option<T>,
|
||||||
|
{
|
||||||
|
let mut delay = Duration::from_millis(64);
|
||||||
|
loop {
|
||||||
|
match poll() {
|
||||||
|
Some(data) => break data,
|
||||||
|
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
|
||||||
|
/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
|
||||||
|
/// the setup required to run the tests yourself.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! bdk_blockchain_tests {
|
||||||
|
(
|
||||||
|
fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => {
|
||||||
|
#[cfg(test)]
|
||||||
|
mod bdk_blockchain_tests {
|
||||||
|
use $crate::bitcoin::Network;
|
||||||
|
use $crate::testutils::blockchain_tests::TestClient;
|
||||||
|
use $crate::blockchain::noop_progress;
|
||||||
|
use $crate::database::MemoryDatabase;
|
||||||
|
use $crate::types::KeychainKind;
|
||||||
|
use $crate::{Wallet, FeeRate};
|
||||||
|
use $crate::testutils;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn get_blockchain(test_client: &TestClient) -> $blockchain {
|
||||||
|
$( let $test_client = test_client; )?
|
||||||
|
$block
|
||||||
|
}
|
||||||
|
|
||||||
|
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(test_client)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
|
let descriptors = testutils! {
|
||||||
|
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
||||||
|
};
|
||||||
|
|
||||||
|
let test_client = TestClient::default();
|
||||||
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
|
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
|
||||||
|
#[cfg(feature = "test-rpc")]
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
(wallet, descriptors, test_client)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_simple() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let tx = testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
};
|
||||||
|
println!("{:?}", tx);
|
||||||
|
let txid = test_client.receive(tx);
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||||
|
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
|
||||||
|
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||||
|
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_stop_gap_20() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 5) => 50_000 )
|
||||||
|
});
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 25) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_before_and_after_receive() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_multiple_outputs_same_tx() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let txid = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||||
|
assert_eq!(list_tx_item.received, 105_000, "incorrect received");
|
||||||
|
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||||
|
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_receive_multi() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 5) => 25_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_address_reuse() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 25_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_receive_rbf_replaced() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let txid = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||||
|
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
|
||||||
|
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
|
||||||
|
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
|
||||||
|
|
||||||
|
let new_txid = test_client.bump_fee(&txid);
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, new_txid, "incorrect txid after bump");
|
||||||
|
assert_eq!(list_tx_item.received, 50_000, "incorrect received after bump");
|
||||||
|
assert_eq!(list_tx_item.sent, 0, "incorrect sent after bump");
|
||||||
|
assert_eq!(list_tx_item.confirmation_time, None, "incorrect height after bump");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
|
||||||
|
// doesn't work for some reason.
|
||||||
|
#[cfg(not(feature = "esplora"))]
|
||||||
|
#[test]
|
||||||
|
fn test_sync_reorg_block() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let txid = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
|
||||||
|
assert!(list_tx_item.confirmation_time.is_some(), "incorrect confirmation_time");
|
||||||
|
|
||||||
|
// Invalidate 1 block
|
||||||
|
test_client.invalidate(1);
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
|
||||||
|
|
||||||
|
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||||
|
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
|
||||||
|
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time after invalidate");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_after_send() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
println!("{}", descriptors.0);
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
let tx = psbt.extract_tx();
|
||||||
|
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||||
|
wallet.broadcast(tx).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
||||||
|
|
||||||
|
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||||
|
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_confirmation_time_after_generate() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
println!("{}", descriptors.0);
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
let received_txid = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
|
let details = tx_map.get(&received_txid).unwrap();
|
||||||
|
assert!(details.confirmation_time.is_none());
|
||||||
|
|
||||||
|
test_client.generate(1, Some(node_addr));
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
|
let details = tx_map.get(&received_txid).unwrap();
|
||||||
|
assert!(details.confirmation_time.is_some());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_outgoing_from_scratch() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
let received_txid = test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
||||||
|
|
||||||
|
// empty wallet
|
||||||
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
|
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||||
|
test_client.generate(1, Some(node_addr));
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||||
|
|
||||||
|
let received = tx_map.get(&received_txid).unwrap();
|
||||||
|
assert_eq!(received.received, 50_000, "incorrect received from receiver");
|
||||||
|
assert_eq!(received.sent, 0, "incorrect sent from receiver");
|
||||||
|
|
||||||
|
let sent = tx_map.get(&sent_txid).unwrap();
|
||||||
|
assert_eq!(sent.received, details.received, "incorrect received from sender");
|
||||||
|
assert_eq!(sent.sent, details.sent, "incorrect sent from sender");
|
||||||
|
assert_eq!(sent.fee.unwrap_or(0), details.fee.unwrap_or(0), "incorrect fees from sender");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_long_change_chain() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 )
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut total_sent = 0;
|
||||||
|
for _ in 0..5 {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey(), 5_000);
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
|
||||||
|
total_sent += 5_000 + details.fee.unwrap_or(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
|
||||||
|
|
||||||
|
// empty wallet
|
||||||
|
|
||||||
|
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
|
||||||
|
|
||||||
|
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
|
||||||
|
test_client.generate(1, Some(node_addr));
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_bump_fee_basic() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
|
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||||
|
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
|
||||||
|
|
||||||
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_bump_fee_remove_change() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
|
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
|
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
||||||
|
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
||||||
|
|
||||||
|
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_bump_fee_add_input_simple() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
|
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
|
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
|
||||||
|
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_bump_fee_add_input_no_change() {
|
||||||
|
let (wallet, descriptors, mut test_client) = init_single_sig();
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
|
||||||
|
test_client.receive(testutils! {
|
||||||
|
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||||
|
let (mut psbt, details) = builder.finish().unwrap();
|
||||||
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||||
|
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||||
|
|
||||||
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
|
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
|
||||||
|
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||||
|
println!("{:#?}", new_details);
|
||||||
|
|
||||||
|
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||||
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
|
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
||||||
|
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_receive_coinbase() {
|
||||||
|
let (wallet, _, mut test_client) = init_single_sig();
|
||||||
|
|
||||||
|
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
|
||||||
|
|
||||||
|
test_client.generate(1, Some(wallet_addr));
|
||||||
|
|
||||||
|
#[cfg(feature = "rpc")]
|
||||||
|
{
|
||||||
|
// rpc consider coinbase only when mature (100 blocks)
|
||||||
|
let node_addr = test_client.get_node_address(None);
|
||||||
|
test_client.generate(100, Some(node_addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
wallet.sync(noop_progress(), None).unwrap();
|
||||||
|
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
( 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.");
|
||||||
|
};
|
||||||
|
}
|
||||||
231
src/testutils/mod.rs
Normal file
231
src/testutils/mod.rs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
// Bitcoin Dev Kit
|
||||||
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
|
//
|
||||||
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
|
// You may not use this file except in accordance with one or both of these
|
||||||
|
// licenses.
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "test-blockchains")]
|
||||||
|
pub mod blockchain_tests;
|
||||||
|
|
||||||
|
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||||
|
use bitcoin::{Address, PublicKey};
|
||||||
|
|
||||||
|
use miniscript::descriptor::DescriptorPublicKey;
|
||||||
|
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TestIncomingOutput {
|
||||||
|
pub value: u64,
|
||||||
|
pub to_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestIncomingOutput {
|
||||||
|
pub fn new(value: u64, to_address: Address) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
to_address: to_address.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TestIncomingTx {
|
||||||
|
pub output: Vec<TestIncomingOutput>,
|
||||||
|
pub min_confirmations: Option<u64>,
|
||||||
|
pub locktime: Option<i64>,
|
||||||
|
pub replaceable: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestIncomingTx {
|
||||||
|
pub fn new(
|
||||||
|
output: Vec<TestIncomingOutput>,
|
||||||
|
min_confirmations: Option<u64>,
|
||||||
|
locktime: Option<i64>,
|
||||||
|
replaceable: Option<bool>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
output,
|
||||||
|
min_confirmations,
|
||||||
|
locktime,
|
||||||
|
replaceable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
||||||
|
self.output.push(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub trait TranslateDescriptor {
|
||||||
|
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
|
||||||
|
fn derive_translated<C: Verification>(
|
||||||
|
&self,
|
||||||
|
secp: &Secp256k1<C>,
|
||||||
|
index: u32,
|
||||||
|
) -> Descriptor<PublicKey>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
|
||||||
|
fn derive_translated<C: Verification>(
|
||||||
|
&self,
|
||||||
|
secp: &Secp256k1<C>,
|
||||||
|
index: u32,
|
||||||
|
) -> Descriptor<PublicKey> {
|
||||||
|
let translate = |key: &DescriptorPublicKey| -> PublicKey {
|
||||||
|
match key {
|
||||||
|
DescriptorPublicKey::XPub(xpub) => {
|
||||||
|
xpub.xkey
|
||||||
|
.derive_pub(secp, &xpub.derivation_path)
|
||||||
|
.expect("hardened derivation steps")
|
||||||
|
.public_key
|
||||||
|
}
|
||||||
|
DescriptorPublicKey::SinglePub(key) => key.key,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.derive(index)
|
||||||
|
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! testutils {
|
||||||
|
( @external $descriptors:expr, $child:expr ) => ({
|
||||||
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
|
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||||
|
|
||||||
|
use $crate::testutils::TranslateDescriptor;
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
||||||
|
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||||
|
});
|
||||||
|
( @internal $descriptors:expr, $child:expr ) => ({
|
||||||
|
use bitcoin::secp256k1::Secp256k1;
|
||||||
|
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
|
||||||
|
|
||||||
|
use $crate::testutils::TranslateDescriptor;
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
||||||
|
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
|
||||||
|
});
|
||||||
|
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||||
|
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||||
|
|
||||||
|
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
|
||||||
|
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
|
||||||
|
|
||||||
|
let locktime = None::<i64>$(.or(Some($locktime)))?;
|
||||||
|
|
||||||
|
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
|
||||||
|
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
|
||||||
|
|
||||||
|
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
|
||||||
|
});
|
||||||
|
|
||||||
|
( @literal $key:expr ) => ({
|
||||||
|
let key = $key.to_string();
|
||||||
|
(key, None::<String>, None::<String>)
|
||||||
|
});
|
||||||
|
( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
let mut seed = [0u8; 32];
|
||||||
|
rand::thread_rng().fill(&mut seed[..]);
|
||||||
|
|
||||||
|
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
|
||||||
|
bitcoin::Network::Testnet,
|
||||||
|
&seed,
|
||||||
|
);
|
||||||
|
|
||||||
|
let external_path = None::<String>$(.or(Some($external_path.to_string())))?;
|
||||||
|
let internal_path = None::<String>$(.or(Some($internal_path.to_string())))?;
|
||||||
|
|
||||||
|
(key.unwrap().to_string(), external_path, internal_path)
|
||||||
|
});
|
||||||
|
( @generate_wif ) => ({
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
|
||||||
|
rand::thread_rng().fill(&mut key[..]);
|
||||||
|
|
||||||
|
(bitcoin::PrivateKey {
|
||||||
|
compressed: true,
|
||||||
|
network: bitcoin::Network::Testnet,
|
||||||
|
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
|
||||||
|
}.to_string(), None::<String>, None::<String>)
|
||||||
|
});
|
||||||
|
|
||||||
|
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
|
||||||
|
let mut map = std::collections::HashMap::new();
|
||||||
|
$(
|
||||||
|
let alias: &str = $alias;
|
||||||
|
map.insert(alias, testutils!( $($key_type)* ));
|
||||||
|
)+
|
||||||
|
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use miniscript::descriptor::Descriptor;
|
||||||
|
use miniscript::TranslatePk;
|
||||||
|
|
||||||
|
#[allow(unused_assignments, unused_mut)]
|
||||||
|
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
||||||
|
$(
|
||||||
|
keys = testutils!{ @keys $( $keys )* };
|
||||||
|
)*
|
||||||
|
|
||||||
|
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
||||||
|
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
|
||||||
|
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
||||||
|
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||||
|
} else {
|
||||||
|
k.clone()
|
||||||
|
}
|
||||||
|
}, |kh| {
|
||||||
|
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
||||||
|
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
|
||||||
|
} else {
|
||||||
|
kh.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
let external = external.to_string();
|
||||||
|
|
||||||
|
let internal = None::<String>$(.or({
|
||||||
|
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
||||||
|
|
||||||
|
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
|
||||||
|
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
||||||
|
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||||
|
} else {
|
||||||
|
k.clone()
|
||||||
|
}
|
||||||
|
}, |kh| {
|
||||||
|
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
||||||
|
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
|
||||||
|
} else {
|
||||||
|
kh.clone()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(string_internal.to_string())
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
(external, internal)
|
||||||
|
})
|
||||||
|
}
|
||||||
198
src/types.rs
198
src/types.rs
@@ -1,31 +1,19 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
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;
|
use bitcoin::{hash_types::Txid, util::psbt};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -69,19 +57,40 @@ impl FeeRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||||
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
pub const fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||||
FeeRate(sat_per_vb)
|
FeeRate(sat_per_vb)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||||
pub fn default_min_relay_fee() -> Self {
|
pub const fn default_min_relay_fee() -> Self {
|
||||||
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 {
|
||||||
@@ -90,9 +99,32 @@ impl std::default::Default for FeeRate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wallet unspent output
|
impl Sub for FeeRate {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
type Output = Self;
|
||||||
pub struct UTXO {
|
|
||||||
|
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`].
|
||||||
|
///
|
||||||
|
/// [`Wallet`]: crate::Wallet
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct LocalUtxo {
|
||||||
/// Reference to a transaction output
|
/// Reference to a transaction output
|
||||||
pub outpoint: OutPoint,
|
pub outpoint: OutPoint,
|
||||||
/// Transaction output
|
/// Transaction output
|
||||||
@@ -101,6 +133,64 @@ pub struct UTXO {
|
|||||||
pub keychain: KeychainKind,
|
pub keychain: KeychainKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct WeightedUtxo {
|
||||||
|
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||||
|
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||||
|
///
|
||||||
|
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
|
||||||
|
pub satisfaction_weight: usize,
|
||||||
|
/// The UTXO
|
||||||
|
pub utxo: Utxo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
/// An unspent transaction output (UTXO).
|
||||||
|
pub enum Utxo {
|
||||||
|
/// A UTXO owned by the local wallet.
|
||||||
|
Local(LocalUtxo),
|
||||||
|
/// A UTXO owned by another wallet.
|
||||||
|
Foreign {
|
||||||
|
/// The location of the output.
|
||||||
|
outpoint: OutPoint,
|
||||||
|
/// The information about the input we require to add it to a PSBT.
|
||||||
|
// Box it to stop the type being too big.
|
||||||
|
psbt_input: Box<psbt::Input>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Utxo {
|
||||||
|
/// Get the location of the UTXO
|
||||||
|
pub fn outpoint(&self) -> OutPoint {
|
||||||
|
match &self {
|
||||||
|
Utxo::Local(local) => local.outpoint,
|
||||||
|
Utxo::Foreign { outpoint, .. } => *outpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `TxOut` of the UTXO
|
||||||
|
pub fn txout(&self) -> &TxOut {
|
||||||
|
match &self {
|
||||||
|
Utxo::Local(local) => &local.txout,
|
||||||
|
Utxo::Foreign {
|
||||||
|
outpoint,
|
||||||
|
psbt_input,
|
||||||
|
} => {
|
||||||
|
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||||
|
return &prev_tx.output[outpoint.vout as usize];
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(txout) = &psbt_input.witness_utxo {
|
||||||
|
return txout;
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("Foreign UTXOs will always have one of these set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A wallet transaction
|
/// A wallet transaction
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct TransactionDetails {
|
pub struct TransactionDetails {
|
||||||
@@ -108,14 +198,56 @@ pub struct TransactionDetails {
|
|||||||
pub transaction: Option<Transaction>,
|
pub transaction: Option<Transaction>,
|
||||||
/// Transaction id
|
/// Transaction id
|
||||||
pub txid: Txid,
|
pub txid: Txid,
|
||||||
/// Timestamp
|
|
||||||
pub timestamp: u64,
|
|
||||||
/// Received value (sats)
|
/// Received value (sats)
|
||||||
pub received: u64,
|
pub received: u64,
|
||||||
/// Sent value (sats)
|
/// Sent value (sats)
|
||||||
pub sent: u64,
|
pub sent: u64,
|
||||||
/// Fee value (sats)
|
/// Fee value (sats) if available.
|
||||||
pub fees: u64,
|
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||||
/// Confirmed in block height, `None` means unconfirmed
|
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||||
pub height: Option<u32>,
|
/// funds while offline.
|
||||||
|
pub fee: Option<u64>,
|
||||||
|
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||||
|
/// transaction, unconfirmed transaction contains `None`.
|
||||||
|
pub confirmation_time: Option<ConfirmationTime>,
|
||||||
|
/// 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
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct ConfirmationTime {
|
||||||
|
/// confirmation block height
|
||||||
|
pub height: u32,
|
||||||
|
/// confirmation block timestamp
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfirmationTime {
|
||||||
|
/// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some`
|
||||||
|
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
|
||||||
|
match (height, timestamp) {
|
||||||
|
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_store_feerate_in_const() {
|
||||||
|
const _MY_RATE: FeeRate = FeeRate::from_sat_per_vb(10.0);
|
||||||
|
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Address validation callbacks
|
//! Address validation callbacks
|
||||||
//!
|
//!
|
||||||
@@ -33,7 +20,7 @@
|
|||||||
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
|
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
|
||||||
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
|
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
|
||||||
//! whenever a new address is generated (either explicitly by the user with
|
//! whenever a new address is generated (either explicitly by the user with
|
||||||
//! [`Wallet::get_new_address`](super::Wallet::get_new_address) or internally to create a change
|
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
|
||||||
//! address) all the attached validators will be polled, in sequence. All of them must complete
|
//! address) all the attached validators will be polled, in sequence. All of them must complete
|
||||||
//! successfully to continue.
|
//! successfully to continue.
|
||||||
//!
|
//!
|
||||||
@@ -45,13 +32,15 @@
|
|||||||
//! # use bdk::address_validator::*;
|
//! # use bdk::address_validator::*;
|
||||||
//! # use bdk::database::*;
|
//! # use bdk::database::*;
|
||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
|
//! # use bdk::wallet::AddressIndex::New;
|
||||||
|
//! #[derive(Debug)]
|
||||||
//! struct PrintAddressAndContinue;
|
//! struct PrintAddressAndContinue;
|
||||||
//!
|
//!
|
||||||
//! impl AddressValidator for PrintAddressAndContinue {
|
//! impl AddressValidator for PrintAddressAndContinue {
|
||||||
//! fn validate(
|
//! fn validate(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! keychain: KeychainKind,
|
//! keychain: KeychainKind,
|
||||||
//! hd_keypaths: &HDKeyPaths,
|
//! hd_keypaths: &HdKeyPaths,
|
||||||
//! script: &Script
|
//! script: &Script
|
||||||
//! ) -> Result<(), AddressValidatorError> {
|
//! ) -> Result<(), AddressValidatorError> {
|
||||||
//! let address = Address::from_script(script, Network::Testnet)
|
//! let address = Address::from_script(script, Network::Testnet)
|
||||||
@@ -69,7 +58,7 @@
|
|||||||
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||||
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
|
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
|
||||||
//!
|
//!
|
||||||
//! let address = wallet.get_new_address()?;
|
//! let address = wallet.get_address(New)?;
|
||||||
//! println!("Address: {}", address);
|
//! println!("Address: {}", address);
|
||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
@@ -78,7 +67,7 @@ use std::fmt;
|
|||||||
|
|
||||||
use bitcoin::Script;
|
use bitcoin::Script;
|
||||||
|
|
||||||
use crate::descriptor::HDKeyPaths;
|
use crate::descriptor::HdKeyPaths;
|
||||||
use crate::types::KeychainKind;
|
use crate::types::KeychainKind;
|
||||||
|
|
||||||
/// Errors that can be returned to fail the validation of an address
|
/// Errors that can be returned to fail the validation of an address
|
||||||
@@ -111,12 +100,12 @@ impl std::error::Error for AddressValidatorError {}
|
|||||||
/// validator will be propagated up to the original caller that triggered the address generation.
|
/// validator will be propagated up to the original caller that triggered the address generation.
|
||||||
///
|
///
|
||||||
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
||||||
pub trait AddressValidator: Send + Sync {
|
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
||||||
/// Validate or inspect an address
|
/// Validate or inspect an address
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
hd_keypaths: &HDKeyPaths,
|
hd_keypaths: &HdKeyPaths,
|
||||||
script: &Script,
|
script: &Script,
|
||||||
) -> Result<(), AddressValidatorError>;
|
) -> Result<(), AddressValidatorError>;
|
||||||
}
|
}
|
||||||
@@ -127,14 +116,15 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
|
||||||
use crate::wallet::TxBuilder;
|
use crate::wallet::AddressIndex::New;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct TestValidator;
|
struct TestValidator;
|
||||||
impl AddressValidator for TestValidator {
|
impl AddressValidator for TestValidator {
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
_keychain: KeychainKind,
|
_keychain: KeychainKind,
|
||||||
_hd_keypaths: &HDKeyPaths,
|
_hd_keypaths: &HdKeyPaths,
|
||||||
_script: &bitcoin::Script,
|
_script: &bitcoin::Script,
|
||||||
) -> Result<(), AddressValidatorError> {
|
) -> Result<(), AddressValidatorError> {
|
||||||
Err(AddressValidatorError::InvalidScript)
|
Err(AddressValidatorError::InvalidScript)
|
||||||
@@ -147,7 +137,7 @@ mod test {
|
|||||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
wallet.add_address_validator(Arc::new(TestValidator));
|
||||||
|
|
||||||
wallet.get_new_address().unwrap();
|
wallet.get_address(New).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -156,12 +146,9 @@ mod test {
|
|||||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||||
wallet.add_address_validator(Arc::new(TestValidator));
|
wallet.add_address_validator(Arc::new(TestValidator));
|
||||||
|
|
||||||
let addr = testutils!(@external descriptors, 10);
|
let addr = crate::testutils!(@external descriptors, 10);
|
||||||
wallet
|
let mut builder = wallet.build_tx();
|
||||||
.create_tx(TxBuilder::with_recipients(vec![(
|
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||||
addr.script_pubkey(),
|
builder.finish().unwrap();
|
||||||
25_000,
|
|
||||||
)]))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,32 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Coin selection
|
//! Coin selection
|
||||||
//!
|
//!
|
||||||
//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to
|
//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to
|
||||||
//! define custom coin selection algorithms.
|
//! define custom coin selection algorithms.
|
||||||
//!
|
//!
|
||||||
//! The coin selection algorithm is not globally part of a [`Wallet`](super::Wallet), instead it
|
//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on
|
||||||
//! is selected whenever a [`Wallet::create_tx`](super::Wallet::create_tx) call is made, through
|
//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will
|
||||||
//! the use of the [`TxBuilder`] structure, specifically with
|
//! be used if it is not explicitly set.
|
||||||
//! [`TxBuilder::coin_selection`](super::tx_builder::TxBuilder::coin_selection) method.
|
|
||||||
//!
|
|
||||||
//! The [`DefaultCoinSelectionAlgorithm`] selects the default coin selection algorithm that
|
|
||||||
//! [`TxBuilder`] uses, if it's not explicitly overridden.
|
|
||||||
//!
|
//!
|
||||||
//! [`TxBuilder`]: super::tx_builder::TxBuilder
|
//! [`TxBuilder`]: super::tx_builder::TxBuilder
|
||||||
|
//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection
|
||||||
//!
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```
|
||||||
//! # 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;
|
||||||
@@ -53,60 +37,67 @@
|
|||||||
//! fn coin_select(
|
//! fn coin_select(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! database: &D,
|
//! database: &D,
|
||||||
//! required_utxos: Vec<(UTXO, usize)>,
|
//! required_utxos: Vec<WeightedUtxo>,
|
||||||
//! optional_utxos: Vec<(UTXO, usize)>,
|
//! 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;
|
||||||
//! let all_utxos_selected = required_utxos
|
//! let all_utxos_selected = required_utxos
|
||||||
//! .into_iter().chain(optional_utxos)
|
//! .into_iter()
|
||||||
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), (utxo, weight)| {
|
//! .chain(optional_utxos)
|
||||||
//! **selected_amount += utxo.txout.value;
|
//! .scan(
|
||||||
//! **additional_weight += TXIN_BASE_WEIGHT + weight;
|
//! (&mut selected_amount, &mut additional_weight),
|
||||||
//!
|
//! |(selected_amount, additional_weight), weighted_utxo| {
|
||||||
//! Some(utxo)
|
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
//! })
|
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
|
||||||
|
//! Some(weighted_utxo.utxo)
|
||||||
|
//! },
|
||||||
|
//! )
|
||||||
//! .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 = (fee_amount + additional_fees).ceil() as u64 + amount_needed;
|
//! let amount_needed_with_fees = (fee_amount + additional_fees) + amount_needed;
|
||||||
//! if amount_needed_with_fees > selected_amount {
|
//! if amount_needed_with_fees > selected_amount {
|
||||||
//! return Err(bdk::Error::InsufficientFunds{ needed: amount_needed_with_fees, available: selected_amount });
|
//! return Err(bdk::Error::InsufficientFunds {
|
||||||
|
//! needed: amount_needed_with_fees,
|
||||||
|
//! available: selected_amount,
|
||||||
|
//! });
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! Ok(CoinSelectionResult {
|
//! Ok(CoinSelectionResult {
|
||||||
//! selected: all_utxos_selected,
|
//! selected: all_utxos_selected,
|
||||||
//! selected_amount,
|
|
||||||
//! fee_amount: fee_amount + additional_fees,
|
//! fee_amount: fee_amount + additional_fees,
|
||||||
//! })
|
//! })
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! # let wallet = Wallet::new_offline("", None, Network::Testnet, bdk::database::MemoryDatabase::default())?;
|
//! # let wallet = doctest_wallet!();
|
||||||
//! // create wallet, sync, ...
|
//! // create wallet, sync, ...
|
||||||
//!
|
//!
|
||||||
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
//! let (psbt, details) = wallet.create_tx(
|
//! let (psbt, details) = {
|
||||||
//! TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
|
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
|
||||||
//! .coin_selection(AlwaysSpendEverything),
|
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
|
||||||
//! )?;
|
//! builder.finish()?
|
||||||
|
//! };
|
||||||
//!
|
//!
|
||||||
//! // inspect, sign, broadcast, ...
|
//! // inspect, sign, broadcast, ...
|
||||||
//!
|
//!
|
||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::types::FeeRate;
|
||||||
use crate::error::Error;
|
use crate::{database::Database, WeightedUtxo};
|
||||||
use crate::types::{FeeRate, UTXO};
|
use crate::{error::Error, Utxo};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use rand::{rngs::StdRng, SeedableRng};
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
|
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
|
||||||
/// overridden
|
/// overridden
|
||||||
@@ -123,11 +114,27 @@ pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoinSelectionResult {
|
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>,
|
||||||
/// Sum of the selected inputs' value
|
|
||||||
pub selected_amount: u64,
|
|
||||||
/// Total fee amount in satoshi
|
/// Total fee amount in satoshi
|
||||||
pub fee_amount: f32,
|
pub fee_amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoinSelectionResult {
|
||||||
|
/// The total value of the inputs selected.
|
||||||
|
pub fn selected_amount(&self) -> u64 {
|
||||||
|
self.selected.iter().map(|u| u.txout().value).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total value of the inputs selected from the local wallet.
|
||||||
|
pub fn local_selected_amount(&self) -> u64 {
|
||||||
|
self.selected
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| match u {
|
||||||
|
Utxo::Local(_) => Some(u.txout().value),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for generalized coin selection algorithms
|
/// Trait for generalized coin selection algorithms
|
||||||
@@ -152,11 +159,11 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
|
|||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
database: &D,
|
database: &D,
|
||||||
required_utxos: Vec<(UTXO, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
optional_utxos: Vec<(UTXO, usize)>,
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,21 +171,19 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
|
|||||||
///
|
///
|
||||||
/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
|
/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
|
||||||
/// from the largest ones until the required amount is reached.
|
/// from the largest ones until the required amount is reached.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct LargestFirstCoinSelection;
|
pub struct LargestFirstCoinSelection;
|
||||||
|
|
||||||
impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
_database: &D,
|
_database: &D,
|
||||||
required_utxos: Vec<(UTXO, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
mut optional_utxos: Vec<(UTXO, usize)>,
|
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,
|
||||||
@@ -189,7 +194,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
|
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
|
||||||
// initially smallest to largest, before being reversed with `.rev()`.
|
// initially smallest to largest, before being reversed with `.rev()`.
|
||||||
let utxos = {
|
let utxos = {
|
||||||
optional_utxos.sort_unstable_by_key(|(utxo, _)| utxo.txout.value);
|
optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
|
||||||
required_utxos
|
required_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|utxo| (true, utxo))
|
.map(|utxo| (true, utxo))
|
||||||
@@ -202,18 +207,19 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
let selected = utxos
|
let selected = utxos
|
||||||
.scan(
|
.scan(
|
||||||
(&mut selected_amount, &mut fee_amount),
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|(selected_amount, fee_amount), (must_use, (utxo, weight))| {
|
|(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 += calc_fee_bytes(TXIN_BASE_WEIGHT + weight);
|
**fee_amount +=
|
||||||
**selected_amount += utxo.txout.value;
|
fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||||
|
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Selected {}, updated fee_amount = `{}`",
|
"Selected {}, updated fee_amount = `{}`",
|
||||||
utxo.outpoint,
|
weighted_utxo.utxo.outpoint(),
|
||||||
fee_amount
|
fee_amount
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(utxo)
|
Some(weighted_utxo.utxo)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -221,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,
|
||||||
@@ -232,7 +238,6 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
Ok(CoinSelectionResult {
|
Ok(CoinSelectionResult {
|
||||||
selected,
|
selected,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
selected_amount,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,24 +245,21 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
// Adds fee information to an UTXO.
|
// Adds fee information to an UTXO.
|
||||||
struct OutputGroup {
|
struct OutputGroup {
|
||||||
utxo: UTXO,
|
weighted_utxo: WeightedUtxo,
|
||||||
// weight needed to satisfy the UTXO, as described in `Descriptor::max_satisfaction_weight`
|
|
||||||
satisfaction_weight: usize,
|
|
||||||
// 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(utxo: UTXO, satisfaction_weight: usize, fee_rate: FeeRate) -> Self {
|
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||||
let fee = (TXIN_BASE_WEIGHT + satisfaction_weight) as f32 / 4.0 * fee_rate.as_sat_vb();
|
let fee = fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||||
let effective_value = utxo.txout.value as i64 - fee.ceil() as i64;
|
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
|
||||||
OutputGroup {
|
OutputGroup {
|
||||||
utxo,
|
weighted_utxo,
|
||||||
satisfaction_weight,
|
|
||||||
effective_value,
|
|
||||||
fee,
|
fee,
|
||||||
|
effective_value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,45 +294,60 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
_database: &D,
|
_database: &D,
|
||||||
required_utxos: Vec<(UTXO, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
optional_utxos: Vec<(UTXO, usize)>,
|
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
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Mapping every (UTXO, usize) to an output group.
|
// Mapping every (UTXO, usize) to an output group.
|
||||||
// Filtering UTXOs with an effective_value < 0, as the fee paid for
|
|
||||||
// adding them is more than their value
|
|
||||||
let optional_utxos: Vec<OutputGroup> = optional_utxos
|
let optional_utxos: Vec<OutputGroup> = optional_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.filter(|u| u.effective_value > 0)
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_value = required_utxos
|
let curr_value = required_utxos
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let curr_available_value = optional_utxos
|
let curr_available_value = optional_utxos
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let actual_target = fee_amount.ceil() as u64 + amount_needed;
|
let 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();
|
||||||
|
|
||||||
if curr_available_value + curr_value < actual_target {
|
let expected = (curr_available_value + curr_value)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::Generic("Sum of UTXO spendable values does not fit into u64".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if expected < actual_target {
|
||||||
return Err(Error::InsufficientFunds {
|
return Err(Error::InsufficientFunds {
|
||||||
needed: actual_target,
|
needed: actual_target,
|
||||||
available: curr_available_value + curr_value,
|
available: expected,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let actual_target = actual_target
|
||||||
|
.try_into()
|
||||||
|
.expect("Bitcoin amount to fit into i64");
|
||||||
|
|
||||||
|
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(),
|
||||||
@@ -361,10 +378,10 @@ impl BranchAndBoundCoinSelection {
|
|||||||
&self,
|
&self,
|
||||||
required_utxos: Vec<OutputGroup>,
|
required_utxos: Vec<OutputGroup>,
|
||||||
mut optional_utxos: Vec<OutputGroup>,
|
mut optional_utxos: Vec<OutputGroup>,
|
||||||
mut curr_value: u64,
|
mut curr_value: i64,
|
||||||
mut curr_available_value: u64,
|
mut curr_available_value: i64,
|
||||||
actual_target: u64,
|
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],
|
||||||
@@ -389,7 +406,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
// or the selected value is out of range.
|
// or the selected value is out of range.
|
||||||
// Go back and try other branch
|
// Go back and try other branch
|
||||||
if curr_value + curr_available_value < actual_target
|
if curr_value + curr_available_value < actual_target
|
||||||
|| curr_value > actual_target + cost_of_change as u64
|
|| curr_value > actual_target + cost_of_change as i64
|
||||||
{
|
{
|
||||||
backtrack = true;
|
backtrack = true;
|
||||||
} else if curr_value >= actual_target {
|
} else if curr_value >= actual_target {
|
||||||
@@ -415,8 +432,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
|
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
|
||||||
while let Some(false) = current_selection.last() {
|
while let Some(false) = current_selection.last() {
|
||||||
current_selection.pop();
|
current_selection.pop();
|
||||||
curr_available_value +=
|
curr_available_value += optional_utxos[current_selection.len()].effective_value;
|
||||||
optional_utxos[current_selection.len()].effective_value as u64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_selection.last_mut().is_none() {
|
if current_selection.last_mut().is_none() {
|
||||||
@@ -434,17 +450,17 @@ impl BranchAndBoundCoinSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let utxo = &optional_utxos[current_selection.len() - 1];
|
let utxo = &optional_utxos[current_selection.len() - 1];
|
||||||
curr_value -= utxo.effective_value as u64;
|
curr_value -= utxo.effective_value;
|
||||||
} else {
|
} else {
|
||||||
// Moving forwards, continuing down this branch
|
// Moving forwards, continuing down this branch
|
||||||
let utxo = &optional_utxos[current_selection.len()];
|
let utxo = &optional_utxos[current_selection.len()];
|
||||||
|
|
||||||
// Remove this utxo from the curr_available_value utxo amount
|
// Remove this utxo from the curr_available_value utxo amount
|
||||||
curr_available_value -= utxo.effective_value as u64;
|
curr_available_value -= utxo.effective_value;
|
||||||
|
|
||||||
// Inclusion branch first (Largest First Exploration)
|
// Inclusion branch first (Largest First Exploration)
|
||||||
current_selection.push(true);
|
current_selection.push(true);
|
||||||
curr_value += utxo.effective_value as u64;
|
curr_value += utxo.effective_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,9 +487,9 @@ impl BranchAndBoundCoinSelection {
|
|||||||
&self,
|
&self,
|
||||||
required_utxos: Vec<OutputGroup>,
|
required_utxos: Vec<OutputGroup>,
|
||||||
mut optional_utxos: Vec<OutputGroup>,
|
mut optional_utxos: Vec<OutputGroup>,
|
||||||
curr_value: u64,
|
curr_value: i64,
|
||||||
actual_target: u64,
|
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());
|
||||||
@@ -490,7 +506,7 @@ impl BranchAndBoundCoinSelection {
|
|||||||
if *curr_value >= actual_target {
|
if *curr_value >= actual_target {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
*curr_value += utxo.effective_value as u64;
|
*curr_value += utxo.effective_value;
|
||||||
Some(utxo)
|
Some(utxo)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -502,20 +518,18 @@ 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.utxo)
|
.map(|u| u.weighted_utxo.utxo)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let selected_amount = selected.iter().map(|u| u.txout.value).sum();
|
|
||||||
|
|
||||||
CoinSelectionResult {
|
CoinSelectionResult {
|
||||||
selected,
|
selected,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
selected_amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,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;
|
||||||
@@ -536,44 +551,42 @@ mod test {
|
|||||||
|
|
||||||
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
||||||
|
|
||||||
fn get_test_utxos() -> Vec<(UTXO, usize)> {
|
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> {
|
||||||
vec![
|
vec![
|
||||||
(
|
utxo(100_000, 0),
|
||||||
UTXO {
|
utxo(FEE_AMOUNT as u64 - 40, 1),
|
||||||
outpoint: OutPoint::from_str(
|
utxo(200_000, 2),
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
txout: TxOut {
|
|
||||||
value: 100_000,
|
|
||||||
script_pubkey: Script::new(),
|
|
||||||
},
|
|
||||||
keychain: KeychainKind::External,
|
|
||||||
},
|
|
||||||
P2WPKH_WITNESS_SIZE,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
UTXO {
|
|
||||||
outpoint: OutPoint::from_str(
|
|
||||||
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
txout: TxOut {
|
|
||||||
value: 200_000,
|
|
||||||
script_pubkey: Script::new(),
|
|
||||||
},
|
|
||||||
keychain: KeychainKind::Internal,
|
|
||||||
},
|
|
||||||
P2WPKH_WITNESS_SIZE,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(UTXO, usize)> {
|
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
for _ in 0..utxos_number {
|
for _ in 0..utxos_number {
|
||||||
res.push((
|
res.push(WeightedUtxo {
|
||||||
UTXO {
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||||
)
|
)
|
||||||
@@ -583,16 +596,16 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
});
|
||||||
));
|
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(UTXO, usize)> {
|
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||||
let utxo = (
|
let utxo = WeightedUtxo {
|
||||||
UTXO {
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||||
)
|
)
|
||||||
@@ -602,18 +615,18 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
};
|
||||||
);
|
|
||||||
vec![utxo; utxos_number]
|
vec![utxo; utxos_number]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(UTXO, usize)>) -> u64 {
|
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
||||||
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
|
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
|
||||||
utxos.shuffle(&mut rng);
|
utxos.shuffle(&mut rng);
|
||||||
utxos[..utxos_picked_len]
|
utxos[..utxos_picked_len]
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.0.txout.value)
|
.map(|u| u.utxo.txout().value)
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -628,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(), 2);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 254)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -649,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(), 2);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -670,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_eq!(result.fee_amount, 118.0);
|
assert_eq!(result.fee_amount, 118);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -692,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();
|
||||||
}
|
}
|
||||||
@@ -710,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();
|
||||||
}
|
}
|
||||||
@@ -730,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_eq!(result.fee_amount, 254.0);
|
assert_eq!(result.fee_amount, 254);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -751,13 +764,65 @@ 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(), 2);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_010);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 254);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bnb_coin_selection_optional_are_enough() {
|
||||||
|
let utxos = get_test_utxos();
|
||||||
|
let database = MemoryDatabase::default();
|
||||||
|
|
||||||
|
let result = BranchAndBoundCoinSelection::default()
|
||||||
|
.coin_select(
|
||||||
|
&database,
|
||||||
|
vec![],
|
||||||
|
utxos,
|
||||||
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
|
299756,
|
||||||
|
FEE_AMOUNT,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.selected.len(), 3);
|
||||||
|
assert_eq!(result.selected_amount(), 300010);
|
||||||
|
assert_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]
|
||||||
@@ -773,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();
|
||||||
}
|
}
|
||||||
@@ -791,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();
|
||||||
}
|
}
|
||||||
@@ -805,18 +870,18 @@ mod test {
|
|||||||
.coin_select(
|
.coin_select(
|
||||||
&database,
|
&database,
|
||||||
vec![],
|
vec![],
|
||||||
utxos.clone(),
|
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]
|
||||||
@@ -835,10 +900,10 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,12 +913,10 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = get_test_utxos()
|
let utxos: Vec<OutputGroup> = get_test_utxos()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_available_value = utxos
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
.iter()
|
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -864,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();
|
||||||
@@ -876,12 +939,10 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_available_value = utxos
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
.iter()
|
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -893,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();
|
||||||
@@ -905,22 +966,19 @@ 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()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_value = 0;
|
let curr_value = 0;
|
||||||
|
|
||||||
let curr_available_value = utxos
|
let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
|
||||||
.iter()
|
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
|
||||||
|
|
||||||
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
// 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
|
||||||
// cost_of_change + 5.
|
// cost_of_change + 5.
|
||||||
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as u64 + 5;
|
let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::new(size_of_change)
|
let result = BranchAndBoundCoinSelection::new(size_of_change)
|
||||||
.bnb(
|
.bnb(
|
||||||
@@ -929,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_eq!(result.fee_amount, 186.0);
|
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
|
||||||
@@ -947,17 +1005,17 @@ mod test {
|
|||||||
for _ in 0..200 {
|
for _ in 0..200 {
|
||||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_value = 0;
|
let curr_value = 0;
|
||||||
|
|
||||||
let curr_available_value = optional_utxos
|
let curr_available_value = optional_utxos
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.effective_value as u64);
|
.fold(0, |acc, x| acc + x.effective_value);
|
||||||
|
|
||||||
let target_amount = optional_utxos[3].effective_value as u64
|
let target_amount =
|
||||||
+ optional_utxos[23].effective_value as u64;
|
optional_utxos[3].effective_value + optional_utxos[23].effective_value;
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::new(0)
|
let result = BranchAndBoundCoinSelection::new(0)
|
||||||
.bnb(
|
.bnb(
|
||||||
@@ -966,11 +1024,11 @@ 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();
|
||||||
assert_eq!(result.selected_amount, target_amount);
|
assert_eq!(result.selected_amount(), target_amount as u64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,21 +1042,18 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let utxos: Vec<OutputGroup> = utxos
|
let utxos: Vec<OutputGroup> = utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::default().single_random_draw(
|
let result = BranchAndBoundCoinSelection::default().single_random_draw(
|
||||||
vec![],
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
0,
|
0,
|
||||||
target_amount,
|
target_amount as i64,
|
||||||
50.0,
|
FEE_AMOUNT,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(result.selected_amount > target_amount);
|
assert!(result.selected_amount() > target_amount);
|
||||||
assert_eq!(
|
assert_eq!(result.fee_amount, (50 + result.selected.len() * 68) as u64);
|
||||||
result.fee_amount,
|
|
||||||
50.0 + result.selected.len() as f32 * 68.0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Wallet export
|
//! Wallet export
|
||||||
//!
|
//!
|
||||||
@@ -76,6 +63,7 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use miniscript::descriptor::{ShInner, WshInner};
|
||||||
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
|
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
|
||||||
|
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
@@ -107,6 +95,10 @@ impl FromStr for WalletExport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_checksum(s: String) -> String {
|
||||||
|
s.splitn(2, '#').next().map(String::from).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
impl WalletExport {
|
impl WalletExport {
|
||||||
/// Export a wallet
|
/// Export a wallet
|
||||||
///
|
///
|
||||||
@@ -127,6 +119,7 @@ impl WalletExport {
|
|||||||
let descriptor = wallet
|
let descriptor = wallet
|
||||||
.descriptor
|
.descriptor
|
||||||
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
|
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
|
||||||
|
let descriptor = remove_checksum(descriptor);
|
||||||
Self::is_compatible_with_core(&descriptor)?;
|
Self::is_compatible_with_core(&descriptor)?;
|
||||||
|
|
||||||
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
||||||
@@ -135,7 +128,7 @@ impl WalletExport {
|
|||||||
Ok(txs) => {
|
Ok(txs) => {
|
||||||
let mut heights = txs
|
let mut heights = txs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tx| tx.height.unwrap_or(0))
|
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
heights.sort_unstable();
|
heights.sort_unstable();
|
||||||
|
|
||||||
@@ -150,7 +143,9 @@ impl WalletExport {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
|
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
|
||||||
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()))
|
let descriptor =
|
||||||
|
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()));
|
||||||
|
remove_checksum(descriptor)
|
||||||
};
|
};
|
||||||
if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) {
|
if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) {
|
||||||
return Err("Incompatible change descriptor");
|
return Err("Incompatible change descriptor");
|
||||||
@@ -161,7 +156,7 @@ impl WalletExport {
|
|||||||
|
|
||||||
fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> {
|
fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> {
|
||||||
fn check_ms<Ctx: ScriptContext>(
|
fn check_ms<Ctx: ScriptContext>(
|
||||||
terminal: Terminal<String, Ctx>,
|
terminal: &Terminal<String, Ctx>,
|
||||||
) -> Result<(), &'static str> {
|
) -> Result<(), &'static str> {
|
||||||
if let Terminal::Multi(_, _) = terminal {
|
if let Terminal::Multi(_, _) = terminal {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -170,13 +165,22 @@ impl WalletExport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti()
|
||||||
match Descriptor::<String>::from_str(descriptor).map_err(|_| "Invalid descriptor")? {
|
match Descriptor::<String>::from_str(descriptor).map_err(|_| "Invalid descriptor")? {
|
||||||
Descriptor::Pk(_)
|
Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()),
|
||||||
| Descriptor::Pkh(_)
|
Descriptor::Sh(sh) => match sh.as_inner() {
|
||||||
| Descriptor::Wpkh(_)
|
ShInner::Wpkh(_) => Ok(()),
|
||||||
| Descriptor::ShWpkh(_) => Ok(()),
|
ShInner::SortedMulti(_) => Ok(()),
|
||||||
Descriptor::Sh(ms) => check_ms(ms.node),
|
ShInner::Wsh(wsh) => match wsh.as_inner() {
|
||||||
Descriptor::Wsh(ms) | Descriptor::ShWsh(ms) => check_ms(ms.node),
|
WshInner::SortedMulti(_) => Ok(()),
|
||||||
|
WshInner::Ms(ms) => check_ms(&ms.node),
|
||||||
|
},
|
||||||
|
ShInner::Ms(ms) => check_ms(&ms.node),
|
||||||
|
},
|
||||||
|
Descriptor::Wsh(wsh) => match wsh.as_inner() {
|
||||||
|
WshInner::SortedMulti(_) => Ok(()),
|
||||||
|
WshInner::Ms(ms) => check_ms(&ms.node),
|
||||||
|
},
|
||||||
_ => Err("The descriptor is not compatible with Bitcoin Core"),
|
_ => Err("The descriptor is not compatible with Bitcoin Core"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +212,7 @@ mod test {
|
|||||||
use crate::database::{memory::MemoryDatabase, BatchOperations};
|
use crate::database::{memory::MemoryDatabase, BatchOperations};
|
||||||
use crate::types::TransactionDetails;
|
use crate::types::TransactionDetails;
|
||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
|
use crate::ConfirmationTime;
|
||||||
|
|
||||||
fn get_test_db() -> MemoryDatabase {
|
fn get_test_db() -> MemoryDatabase {
|
||||||
let mut db = MemoryDatabase::new();
|
let mut db = MemoryDatabase::new();
|
||||||
@@ -217,11 +222,15 @@ mod test {
|
|||||||
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
|
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
timestamp: 12345678,
|
|
||||||
received: 100_000,
|
received: 100_000,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
fees: 500,
|
fee: Some(500),
|
||||||
height: Some(5000),
|
confirmation_time: Some(ConfirmationTime {
|
||||||
|
timestamp: 12345678,
|
||||||
|
height: 5000,
|
||||||
|
}),
|
||||||
|
verified: true,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
3003
src/wallet/mod.rs
3003
src/wallet/mod.rs
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Generalized signers
|
//! Generalized signers
|
||||||
//!
|
//!
|
||||||
@@ -33,7 +20,6 @@
|
|||||||
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
||||||
//! # use bitcoin::*;
|
//! # use bitcoin::*;
|
||||||
//! # use bitcoin::util::psbt;
|
//! # use bitcoin::util::psbt;
|
||||||
//! # use bitcoin::util::bip32::Fingerprint;
|
|
||||||
//! # use bdk::signer::*;
|
//! # use bdk::signer::*;
|
||||||
//! # use bdk::database::*;
|
//! # use bdk::database::*;
|
||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
@@ -46,6 +32,9 @@
|
|||||||
//! # fn connect() -> Self {
|
//! # fn connect() -> Self {
|
||||||
//! # CustomHSM
|
//! # CustomHSM
|
||||||
//! # }
|
//! # }
|
||||||
|
//! # fn get_id(&self) -> SignerId {
|
||||||
|
//! # SignerId::Dummy(0)
|
||||||
|
//! # }
|
||||||
//! # }
|
//! # }
|
||||||
//! #[derive(Debug)]
|
//! #[derive(Debug)]
|
||||||
//! struct CustomSigner {
|
//! struct CustomSigner {
|
||||||
@@ -71,6 +60,10 @@
|
|||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
|
//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||||
|
//! self.device.get_id()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
//! fn sign_whole_tx(&self) -> bool {
|
//! fn sign_whole_tx(&self) -> bool {
|
||||||
//! false
|
//! false
|
||||||
//! }
|
//! }
|
||||||
@@ -82,7 +75,6 @@
|
|||||||
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||||
//! wallet.add_signer(
|
//! wallet.add_signer(
|
||||||
//! KeychainKind::External,
|
//! KeychainKind::External,
|
||||||
//! Fingerprint::from_str("e30f11b8").unwrap().into(),
|
|
||||||
//! SignerOrdering(200),
|
//! SignerOrdering(200),
|
||||||
//! Arc::new(custom_signer)
|
//! Arc::new(custom_signer)
|
||||||
//! );
|
//! );
|
||||||
@@ -100,7 +92,7 @@ use bitcoin::blockdata::opcodes;
|
|||||||
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
||||||
use bitcoin::hashes::{hash160, Hash};
|
use bitcoin::hashes::{hash160, Hash};
|
||||||
use bitcoin::secp256k1::{Message, Secp256k1};
|
use bitcoin::secp256k1::{Message, Secp256k1};
|
||||||
use bitcoin::util::bip32::{ExtendedPrivKey, Fingerprint};
|
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||||
use bitcoin::util::{bip143, psbt};
|
use bitcoin::util::{bip143, psbt};
|
||||||
use bitcoin::{PrivateKey, Script, SigHash, SigHashType};
|
use bitcoin::{PrivateKey, Script, SigHash, SigHashType};
|
||||||
|
|
||||||
@@ -118,6 +110,8 @@ pub enum SignerId {
|
|||||||
PkHash(hash160::Hash),
|
PkHash(hash160::Hash),
|
||||||
/// The fingerprint of a BIP32 extended key
|
/// The fingerprint of a BIP32 extended key
|
||||||
Fingerprint(Fingerprint),
|
Fingerprint(Fingerprint),
|
||||||
|
/// Dummy identifier
|
||||||
|
Dummy(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<hash160::Hash> for SignerId {
|
impl From<hash160::Hash> for SignerId {
|
||||||
@@ -152,7 +146,13 @@ pub enum SignerError {
|
|||||||
/// The `witness_script` field of the transaction is requied to sign this input
|
/// The `witness_script` field of the transaction is requied to sign this input
|
||||||
MissingWitnessScript,
|
MissingWitnessScript,
|
||||||
/// The fingerprint and derivation path are missing from the psbt input
|
/// The fingerprint and derivation path are missing from the psbt input
|
||||||
MissingHDKeypath,
|
MissingHdKeypath,
|
||||||
|
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
|
||||||
|
/// explicitly allowed them
|
||||||
|
///
|
||||||
|
/// To enable signing transactions with non-standard sighashes set
|
||||||
|
/// [`SignOptions::allow_all_sighashes`] to `true`.
|
||||||
|
NonStandardSighash,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SignerError {
|
impl fmt::Display for SignerError {
|
||||||
@@ -184,6 +184,12 @@ pub trait Signer: fmt::Debug + Send + Sync {
|
|||||||
/// input individually
|
/// input individually
|
||||||
fn sign_whole_tx(&self) -> bool;
|
fn sign_whole_tx(&self) -> bool;
|
||||||
|
|
||||||
|
/// Return the [`SignerId`] for this signer
|
||||||
|
///
|
||||||
|
/// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to
|
||||||
|
/// compare two signers.
|
||||||
|
fn id(&self, secp: &SecpCtx) -> SignerId;
|
||||||
|
|
||||||
/// Return the secret key for the signer
|
/// Return the secret key for the signer
|
||||||
///
|
///
|
||||||
/// This is used internally to reconstruct the original descriptor that may contain secrets.
|
/// This is used internally to reconstruct the original descriptor that may contain secrets.
|
||||||
@@ -206,11 +212,17 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
|||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (public_key, deriv_path) = match psbt.inputs[input_index]
|
if psbt.inputs[input_index].final_script_sig.is_some()
|
||||||
.hd_keypaths
|
|| psbt.inputs[input_index].final_script_witness.is_some()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (public_key, full_path) = match psbt.inputs[input_index]
|
||||||
|
.bip32_derivation
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(pk, &(fingerprint, ref path))| {
|
.filter_map(|(pk, &(fingerprint, ref path))| {
|
||||||
if self.matches(&(fingerprint, path.clone()), &secp).is_some() {
|
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
|
||||||
Some((pk, path))
|
Some((pk, path))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -222,8 +234,18 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
|||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let derived_key = self.xkey.derive_priv(&secp, &deriv_path).unwrap();
|
let derived_key = match self.origin.clone() {
|
||||||
if &derived_key.private_key.public_key(&secp) != public_key {
|
Some((_fingerprint, origin_path)) => {
|
||||||
|
let deriv_path = DerivationPath::from(
|
||||||
|
&full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
|
||||||
|
[origin_path.len()..],
|
||||||
|
);
|
||||||
|
self.xkey.derive_priv(secp, &deriv_path).unwrap()
|
||||||
|
}
|
||||||
|
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if &derived_key.private_key.public_key(secp) != public_key {
|
||||||
Err(SignerError::InvalidKey)
|
Err(SignerError::InvalidKey)
|
||||||
} else {
|
} else {
|
||||||
derived_key.private_key.sign(psbt, Some(input_index), secp)
|
derived_key.private_key.sign(psbt, Some(input_index), secp)
|
||||||
@@ -234,6 +256,10 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
|
SignerId::from(self.root_fingerprint(secp))
|
||||||
|
}
|
||||||
|
|
||||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||||
Some(DescriptorSecretKey::XPrv(self.clone()))
|
Some(DescriptorSecretKey::XPrv(self.clone()))
|
||||||
}
|
}
|
||||||
@@ -247,11 +273,17 @@ impl Signer for PrivateKey {
|
|||||||
secp: &SecpCtx,
|
secp: &SecpCtx,
|
||||||
) -> Result<(), SignerError> {
|
) -> Result<(), SignerError> {
|
||||||
let input_index = input_index.unwrap();
|
let input_index = input_index.unwrap();
|
||||||
if input_index >= psbt.inputs.len() {
|
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pubkey = self.public_key(&secp);
|
if psbt.inputs[input_index].final_script_sig.is_some()
|
||||||
|
|| psbt.inputs[input_index].final_script_witness.is_some()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pubkey = self.public_key(secp);
|
||||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -285,6 +317,10 @@ impl Signer for PrivateKey {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||||
|
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
||||||
|
}
|
||||||
|
|
||||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||||
key: *self,
|
key: *self,
|
||||||
@@ -345,12 +381,7 @@ impl From<KeyMap> for SignersContainer {
|
|||||||
for (_, secret) in keymap {
|
for (_, secret) in keymap {
|
||||||
match secret {
|
match secret {
|
||||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
||||||
SignerId::from(
|
SignerId::from(private_key.key.public_key(&secp).to_pubkeyhash()),
|
||||||
private_key
|
|
||||||
.key
|
|
||||||
.public_key(&Secp256k1::signing_only())
|
|
||||||
.to_pubkeyhash(),
|
|
||||||
),
|
|
||||||
SignerOrdering::default(),
|
SignerOrdering::default(),
|
||||||
Arc::new(private_key.key),
|
Arc::new(private_key.key),
|
||||||
),
|
),
|
||||||
@@ -414,6 +445,50 @@ impl SignersContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Options for a software signer
|
||||||
|
///
|
||||||
|
/// Adjust the behavior of our software signers and the way a transaction is finalized
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SignOptions {
|
||||||
|
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
|
||||||
|
/// provided
|
||||||
|
///
|
||||||
|
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into
|
||||||
|
/// paying a fee larger than expected.
|
||||||
|
///
|
||||||
|
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
|
||||||
|
/// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
|
||||||
|
/// should correctly produce a signature, at the expense of an increased trust in the creator
|
||||||
|
/// of the PSBT.
|
||||||
|
///
|
||||||
|
/// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
|
||||||
|
pub trust_witness_utxo: bool,
|
||||||
|
|
||||||
|
/// Whether the wallet should assume a specific height has been reached when trying to finalize
|
||||||
|
/// a transaction
|
||||||
|
///
|
||||||
|
/// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
|
||||||
|
/// timelock height has already been reached. This option allows overriding the "current height" to let the
|
||||||
|
/// wallet use timelocks in the future to spend a coin.
|
||||||
|
pub assume_height: Option<u32>,
|
||||||
|
|
||||||
|
/// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
|
||||||
|
/// what its value is
|
||||||
|
///
|
||||||
|
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||||
|
pub allow_all_sighashes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SignOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
SignOptions {
|
||||||
|
trust_witness_utxo: false,
|
||||||
|
assume_height: None,
|
||||||
|
allow_all_sighashes: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) trait ComputeSighash {
|
pub(crate) trait ComputeSighash {
|
||||||
fn sighash(
|
fn sighash(
|
||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
@@ -426,7 +501,7 @@ impl ComputeSighash for Legacy {
|
|||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||||
if input_index >= psbt.inputs.len() {
|
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,32 +549,49 @@ impl ComputeSighash for Segwitv0 {
|
|||||||
psbt: &psbt::PartiallySignedTransaction,
|
psbt: &psbt::PartiallySignedTransaction,
|
||||||
input_index: usize,
|
input_index: usize,
|
||||||
) -> Result<(SigHash, SigHashType), SignerError> {
|
) -> Result<(SigHash, SigHashType), SignerError> {
|
||||||
if input_index >= psbt.inputs.len() {
|
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
|
||||||
return Err(SignerError::InputIndexOutOfRange);
|
return Err(SignerError::InputIndexOutOfRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
let psbt_input = &psbt.inputs[input_index];
|
let psbt_input = &psbt.inputs[input_index];
|
||||||
|
let tx_input = &psbt.global.unsigned_tx.input[input_index];
|
||||||
|
|
||||||
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
|
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
|
||||||
|
|
||||||
let witness_utxo = psbt_input
|
// Always try first with the non-witness utxo
|
||||||
.witness_utxo
|
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||||
.as_ref()
|
// Check the provided prev-tx
|
||||||
.ok_or(SignerError::MissingNonWitnessUtxo)?;
|
if prev_tx.txid() != tx_input.previous_output.txid {
|
||||||
let value = witness_utxo.value;
|
return Err(SignerError::InvalidNonWitnessUtxo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output should be present, if it's missing the `non_witness_utxo` is invalid
|
||||||
|
prev_tx
|
||||||
|
.output
|
||||||
|
.get(tx_input.previous_output.vout as usize)
|
||||||
|
.ok_or(SignerError::InvalidNonWitnessUtxo)?
|
||||||
|
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
|
||||||
|
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
|
||||||
|
// before we get to this point
|
||||||
|
witness_utxo
|
||||||
|
} else {
|
||||||
|
// Nothing has been provided
|
||||||
|
return Err(SignerError::MissingNonWitnessUtxo);
|
||||||
|
};
|
||||||
|
let value = utxo.value;
|
||||||
|
|
||||||
let script = match psbt_input.witness_script {
|
let script = match psbt_input.witness_script {
|
||||||
Some(ref witness_script) => witness_script.clone(),
|
Some(ref witness_script) => witness_script.clone(),
|
||||||
None => {
|
None => {
|
||||||
if witness_utxo.script_pubkey.is_v0_p2wpkh() {
|
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||||
p2wpkh_script_code(&witness_utxo.script_pubkey)
|
p2wpkh_script_code(&utxo.script_pubkey)
|
||||||
} else if psbt_input
|
} else if psbt_input
|
||||||
.redeem_script
|
.redeem_script
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(Script::is_v0_p2wpkh)
|
.map(Script::is_v0_p2wpkh)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
p2wpkh_script_code(&psbt_input.redeem_script.as_ref().unwrap())
|
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
|
||||||
} else {
|
} else {
|
||||||
return Err(SignerError::MissingWitnessScript);
|
return Err(SignerError::MissingWitnessScript);
|
||||||
}
|
}
|
||||||
@@ -544,24 +636,33 @@ impl Eq for SignersContainerKey {}
|
|||||||
mod signers_container_tests {
|
mod signers_container_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::descriptor;
|
use crate::descriptor;
|
||||||
use crate::descriptor::ToWalletDescriptor;
|
use crate::descriptor::IntoWalletDescriptor;
|
||||||
use crate::keys::{DescriptorKey, ToDescriptorKey};
|
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||||
use bitcoin::secp256k1::All;
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::util::bip32;
|
use bitcoin::util::bip32;
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
use bitcoin::Network;
|
use bitcoin::Network;
|
||||||
use miniscript::ScriptContext;
|
use miniscript::ScriptContext;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn is_equal(this: &Arc<dyn Signer>, that: &Arc<DummySigner>) -> bool {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
this.id(&secp) == that.id(&secp)
|
||||||
|
}
|
||||||
|
|
||||||
// Signers added with the same ordering (like `Ordering::default`) created from `KeyMap`
|
// Signers added with the same ordering (like `Ordering::default`) created from `KeyMap`
|
||||||
// should be preserved and not overwritten.
|
// should be preserved and not overwritten.
|
||||||
// This happens usually when a set of signers is created from a descriptor with private keys.
|
// This happens usually when a set of signers is created from a descriptor with private keys.
|
||||||
#[test]
|
#[test]
|
||||||
fn signers_with_same_ordering() {
|
fn signers_with_same_ordering() {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
|
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
|
||||||
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
|
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
|
||||||
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
|
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
|
||||||
let (_, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap();
|
let (_, keymap) = desc
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let signers = SignersContainer::from(keymap);
|
let signers = SignersContainer::from(keymap);
|
||||||
assert_eq!(signers.ids().len(), 2);
|
assert_eq!(signers.ids().len(), 2);
|
||||||
@@ -573,73 +674,58 @@ mod signers_container_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn signers_sorted_by_ordering() {
|
fn signers_sorted_by_ordering() {
|
||||||
let mut signers = SignersContainer::new();
|
let mut signers = SignersContainer::new();
|
||||||
let signer1 = Arc::new(DummySigner);
|
let signer1 = Arc::new(DummySigner { number: 1 });
|
||||||
let signer2 = Arc::new(DummySigner);
|
let signer2 = Arc::new(DummySigner { number: 2 });
|
||||||
let signer3 = Arc::new(DummySigner);
|
let signer3 = Arc::new(DummySigner { number: 3 });
|
||||||
|
|
||||||
signers.add_external(
|
// Mixed order insertions verifies we are not inserting at head or tail.
|
||||||
SignerId::Fingerprint(b"cafe"[..].into()),
|
signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone());
|
||||||
SignerOrdering(1),
|
signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone());
|
||||||
signer1.clone(),
|
signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone());
|
||||||
);
|
|
||||||
signers.add_external(
|
|
||||||
SignerId::Fingerprint(b"babe"[..].into()),
|
|
||||||
SignerOrdering(2),
|
|
||||||
signer2.clone(),
|
|
||||||
);
|
|
||||||
signers.add_external(
|
|
||||||
SignerId::Fingerprint(b"feed"[..].into()),
|
|
||||||
SignerOrdering(3),
|
|
||||||
signer3.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check that signers are sorted from lowest to highest ordering
|
// Check that signers are sorted from lowest to highest ordering
|
||||||
let signers = signers.signers();
|
let signers = signers.signers();
|
||||||
assert_eq!(Arc::as_ptr(signers[0]), Arc::as_ptr(&signer1));
|
|
||||||
assert_eq!(Arc::as_ptr(signers[1]), Arc::as_ptr(&signer2));
|
assert!(is_equal(signers[0], &signer1));
|
||||||
assert_eq!(Arc::as_ptr(signers[2]), Arc::as_ptr(&signer3));
|
assert!(is_equal(signers[1], &signer2));
|
||||||
|
assert!(is_equal(signers[2], &signer3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn find_signer_by_id() {
|
fn find_signer_by_id() {
|
||||||
let mut signers = SignersContainer::new();
|
let mut signers = SignersContainer::new();
|
||||||
let signer1: Arc<dyn Signer> = Arc::new(DummySigner);
|
let signer1 = Arc::new(DummySigner { number: 1 });
|
||||||
let signer2: Arc<dyn Signer> = Arc::new(DummySigner);
|
let signer2 = Arc::new(DummySigner { number: 2 });
|
||||||
let signer3: Arc<dyn Signer> = Arc::new(DummySigner);
|
let signer3 = Arc::new(DummySigner { number: 3 });
|
||||||
let signer4: Arc<dyn Signer> = Arc::new(DummySigner);
|
let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering.
|
||||||
|
|
||||||
let id1 = SignerId::Fingerprint(b"cafe"[..].into());
|
let id1 = SignerId::Dummy(1);
|
||||||
let id2 = SignerId::Fingerprint(b"babe"[..].into());
|
let id2 = SignerId::Dummy(2);
|
||||||
let id3 = SignerId::Fingerprint(b"feed"[..].into());
|
let id3 = SignerId::Dummy(3);
|
||||||
let id_nonexistent = SignerId::Fingerprint(b"fefe"[..].into());
|
let id_nonexistent = SignerId::Dummy(999);
|
||||||
|
|
||||||
signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone());
|
signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone());
|
||||||
signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
|
signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
|
||||||
signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
|
signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)));
|
||||||
matches!(signers.find(id1), Some(signer) if Arc::as_ptr(&signer1) == Arc::as_ptr(signer))
|
assert!(matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)));
|
||||||
);
|
assert!(matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)));
|
||||||
assert!(
|
|
||||||
matches!(signers.find(id2), Some(signer) if Arc::as_ptr(&signer2) == Arc::as_ptr(signer))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
matches!(signers.find(id3.clone()), Some(signer) if Arc::as_ptr(&signer3) == Arc::as_ptr(signer))
|
|
||||||
);
|
|
||||||
|
|
||||||
// The `signer4` has the same ID as `signer3` but lower ordering.
|
// The `signer4` has the same ID as `signer3` but lower ordering.
|
||||||
// It should be found by `id3` instead of `signer3`.
|
// It should be found by `id3` instead of `signer3`.
|
||||||
signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
|
signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
|
||||||
assert!(
|
assert!(matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)));
|
||||||
matches!(signers.find(id3), Some(signer) if Arc::as_ptr(&signer4) == Arc::as_ptr(signer))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Can't find anything with ID that doesn't exist
|
// Can't find anything with ID that doesn't exist
|
||||||
assert!(matches!(signers.find(id_nonexistent), None));
|
assert!(matches!(signers.find(id_nonexistent), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct DummySigner;
|
struct DummySigner {
|
||||||
|
number: u64,
|
||||||
|
}
|
||||||
|
|
||||||
impl Signer for DummySigner {
|
impl Signer for DummySigner {
|
||||||
fn sign(
|
fn sign(
|
||||||
&self,
|
&self,
|
||||||
@@ -650,6 +736,10 @@ mod signers_container_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn id(&self, _secp: &SecpCtx) -> SignerId {
|
||||||
|
SignerId::Dummy(self.number)
|
||||||
|
}
|
||||||
|
|
||||||
fn sign_whole_tx(&self) -> bool {
|
fn sign_whole_tx(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -668,8 +758,8 @@ mod signers_container_tests {
|
|||||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||||
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
|
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
|
||||||
let fingerprint = tprv.fingerprint(&secp);
|
let fingerprint = tprv.fingerprint(&secp);
|
||||||
let prvkey = (tprv, path.clone()).to_descriptor_key().unwrap();
|
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
let pubkey = (tpub, path).to_descriptor_key().unwrap();
|
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||||
|
|
||||||
(prvkey, pubkey, fingerprint)
|
(prvkey, pubkey, fingerprint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Cross-platform time
|
//! Cross-platform time
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
//! Transaction builder
|
//! Transaction builder
|
||||||
//!
|
//!
|
||||||
@@ -32,14 +19,21 @@
|
|||||||
//! # use bdk::*;
|
//! # use bdk::*;
|
||||||
//! # use bdk::wallet::tx_builder::CreateTx;
|
//! # use bdk::wallet::tx_builder::CreateTx;
|
||||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi, with a custom fee rate
|
//! # let wallet = doctest_wallet!();
|
||||||
//! // of 5.0 satoshi/vbyte, only spending non-change outputs and with RBF signaling
|
//! // create a TxBuilder from a wallet
|
||||||
//! // enabled
|
//! let mut tx_builder = wallet.build_tx();
|
||||||
//! let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
|
//!
|
||||||
|
//! tx_builder
|
||||||
|
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||||
|
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||||
|
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||||
|
//! // Only spend non-change outputs
|
||||||
//! .do_not_spend_change()
|
//! .do_not_spend_change()
|
||||||
|
//! // Turn on RBF signaling
|
||||||
//! .enable_rbf();
|
//! .enable_rbf();
|
||||||
//! # let builder: TxBuilder<bdk::database::MemoryDatabase, _, CreateTx> = builder;
|
//! let (psbt, tx_details) = tx_builder.finish()?;
|
||||||
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -47,56 +41,124 @@ use std::collections::HashSet;
|
|||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
||||||
|
|
||||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
use miniscript::descriptor::DescriptorTrait;
|
||||||
use crate::database::Database;
|
|
||||||
use crate::types::{FeeRate, KeychainKind, UTXO};
|
|
||||||
|
|
||||||
|
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||||
|
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||||
|
use crate::{
|
||||||
|
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
|
||||||
|
TransactionDetails,
|
||||||
|
};
|
||||||
/// Context in which the [`TxBuilder`] is valid
|
/// Context in which the [`TxBuilder`] is valid
|
||||||
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
|
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
|
||||||
|
|
||||||
/// [`Wallet::create_tx`](super::Wallet::create_tx) context
|
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
|
||||||
|
/// to bumping the fee of an existing one).
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct CreateTx;
|
pub struct CreateTx;
|
||||||
impl TxBuilderContext for CreateTx {}
|
impl TxBuilderContext for CreateTx {}
|
||||||
|
|
||||||
/// [`Wallet::bump_fee`](super::Wallet::bump_fee) context
|
/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct BumpFee;
|
pub struct BumpFee;
|
||||||
impl TxBuilderContext for BumpFee {}
|
impl TxBuilderContext for BumpFee {}
|
||||||
|
|
||||||
/// A transaction builder
|
/// A transaction builder
|
||||||
///
|
///
|
||||||
/// This structure contains the configuration that the wallet must follow to build a transaction.
|
/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After
|
||||||
|
/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and
|
||||||
|
/// generate the transaction.
|
||||||
///
|
///
|
||||||
/// For an example see [this module](super::tx_builder)'s documentation;
|
/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls
|
||||||
|
/// as in the following example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bdk::*;
|
||||||
|
/// # use bdk::wallet::tx_builder::*;
|
||||||
|
/// # use bitcoin::*;
|
||||||
|
/// # use core::str::FromStr;
|
||||||
|
/// # let wallet = doctest_wallet!();
|
||||||
|
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
|
/// # let addr2 = addr1.clone();
|
||||||
|
/// // chaining
|
||||||
|
/// let (psbt1, details) = {
|
||||||
|
/// let mut builder = wallet.build_tx();
|
||||||
|
/// builder
|
||||||
|
/// .ordering(TxOrdering::Untouched)
|
||||||
|
/// .add_recipient(addr1.script_pubkey(), 50_000)
|
||||||
|
/// .add_recipient(addr2.script_pubkey(), 50_000);
|
||||||
|
/// builder.finish()?
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // non-chaining
|
||||||
|
/// let (psbt2, details) = {
|
||||||
|
/// let mut builder = wallet.build_tx();
|
||||||
|
/// builder.ordering(TxOrdering::Untouched);
|
||||||
|
/// for addr in &[addr1, addr2] {
|
||||||
|
/// builder.add_recipient(addr.script_pubkey(), 50_000);
|
||||||
|
/// }
|
||||||
|
/// builder.finish()?
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// psbt1.global.unsigned_tx.output[..2],
|
||||||
|
/// psbt2.global.unsigned_tx.output[..2]
|
||||||
|
/// );
|
||||||
|
/// # Ok::<(), bdk::Error>(())
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
|
||||||
|
/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it.
|
||||||
|
///
|
||||||
|
/// For further examples see [this module](super::tx_builder)'s documentation;
|
||||||
|
///
|
||||||
|
/// [`build_tx`]: Wallet::build_tx
|
||||||
|
/// [`build_fee_bump`]: Wallet::build_fee_bump
|
||||||
|
/// [`finish`]: Self::finish
|
||||||
|
/// [`coin_selection`]: Self::coin_selection
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> {
|
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
|
||||||
|
pub(crate) wallet: &'a Wallet<B, D>,
|
||||||
|
pub(crate) params: TxParams,
|
||||||
|
pub(crate) coin_selection: Cs,
|
||||||
|
pub(crate) phantom: PhantomData<Ctx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parameters for transaction creation sans coin selection algorithm.
|
||||||
|
//TODO: TxParams should eventually be exposed publicly.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
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>>>,
|
||||||
pub(crate) utxos: Vec<OutPoint>,
|
pub(crate) utxos: Vec<WeightedUtxo>,
|
||||||
pub(crate) unspendable: HashSet<OutPoint>,
|
pub(crate) unspendable: HashSet<OutPoint>,
|
||||||
pub(crate) manually_selected_only: bool,
|
pub(crate) manually_selected_only: bool,
|
||||||
pub(crate) sighash: Option<SigHashType>,
|
pub(crate) sighash: Option<SigHashType>,
|
||||||
pub(crate) ordering: TxOrdering,
|
pub(crate) ordering: TxOrdering,
|
||||||
pub(crate) locktime: Option<u32>,
|
pub(crate) locktime: Option<u32>,
|
||||||
pub(crate) rbf: Option<RBFValue>,
|
pub(crate) rbf: Option<RbfValue>,
|
||||||
pub(crate) version: Option<Version>,
|
pub(crate) version: Option<Version>,
|
||||||
pub(crate) change_policy: ChangeSpendPolicy,
|
pub(crate) change_policy: ChangeSpendPolicy,
|
||||||
pub(crate) force_non_witness_utxo: bool,
|
pub(crate) only_witness_utxo: bool,
|
||||||
pub(crate) add_global_xpubs: bool,
|
pub(crate) add_global_xpubs: bool,
|
||||||
pub(crate) coin_selection: Cs,
|
|
||||||
pub(crate) include_output_redeem_witness_script: bool,
|
pub(crate) include_output_redeem_witness_script: bool,
|
||||||
|
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||||
phantom: PhantomData<(D, Ctx)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct PreviousFee {
|
||||||
|
pub absolute: u64,
|
||||||
|
pub rate: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub(crate) enum FeePolicy {
|
pub(crate) enum FeePolicy {
|
||||||
FeeRate(FeeRate),
|
FeeRate(FeeRate),
|
||||||
FeeAmount(u64),
|
FeeAmount(u64),
|
||||||
@@ -108,58 +170,30 @@ impl std::default::Default for FeePolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately derive doesn't work with `PhantomData`: https://github.com/rust-lang/rust/issues/26925
|
impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
|
||||||
impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> Default
|
fn clone(&self) -> Self {
|
||||||
for TxBuilder<D, Cs, Ctx>
|
|
||||||
where
|
|
||||||
Cs: Default,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
TxBuilder {
|
TxBuilder {
|
||||||
recipients: Default::default(),
|
wallet: self.wallet,
|
||||||
drain_wallet: Default::default(),
|
params: self.params.clone(),
|
||||||
single_recipient: Default::default(),
|
coin_selection: self.coin_selection.clone(),
|
||||||
fee_policy: Default::default(),
|
|
||||||
internal_policy_path: Default::default(),
|
|
||||||
external_policy_path: Default::default(),
|
|
||||||
utxos: Default::default(),
|
|
||||||
unspendable: Default::default(),
|
|
||||||
manually_selected_only: Default::default(),
|
|
||||||
sighash: Default::default(),
|
|
||||||
ordering: Default::default(),
|
|
||||||
locktime: Default::default(),
|
|
||||||
rbf: Default::default(),
|
|
||||||
version: Default::default(),
|
|
||||||
change_policy: Default::default(),
|
|
||||||
force_non_witness_utxo: Default::default(),
|
|
||||||
add_global_xpubs: Default::default(),
|
|
||||||
coin_selection: Default::default(),
|
|
||||||
include_output_redeem_witness_script: Default::default(),
|
|
||||||
|
|
||||||
phantom: PhantomData,
|
phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// methods supported by both contexts, but only for `DefaultCoinSelectionAlgorithm`
|
|
||||||
impl<D: Database, Ctx: TxBuilderContext> TxBuilder<D, DefaultCoinSelectionAlgorithm, Ctx> {
|
|
||||||
/// Create an empty builder
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||||
impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilder<D, Cs, Ctx> {
|
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||||
|
TxBuilder<'a, B, D, Cs, Ctx>
|
||||||
|
{
|
||||||
/// Set a custom fee rate
|
/// Set a custom fee rate
|
||||||
pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
|
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
|
||||||
self.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
|
self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set an absolute fee
|
/// Set an absolute fee
|
||||||
pub fn fee_absolute(mut self, fee_amount: u64) -> Self {
|
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
|
||||||
self.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
|
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,96 +245,189 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
/// # use bitcoin::*;
|
/// # use bitcoin::*;
|
||||||
/// # use bdk::*;
|
/// # use bdk::*;
|
||||||
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||||
|
/// # let wallet = doctest_wallet!();
|
||||||
/// let mut path = BTreeMap::new();
|
/// let mut path = BTreeMap::new();
|
||||||
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
|
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
|
||||||
///
|
///
|
||||||
/// let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)])
|
/// let builder = wallet
|
||||||
|
/// .build_tx()
|
||||||
|
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||||
/// .policy_path(path, KeychainKind::External);
|
/// .policy_path(path, KeychainKind::External);
|
||||||
/// # let builder: TxBuilder<bdk::database::MemoryDatabase, _, _> = builder;
|
///
|
||||||
|
/// # Ok::<(), bdk::Error>(())
|
||||||
/// ```
|
/// ```
|
||||||
pub fn policy_path(
|
pub fn policy_path(
|
||||||
mut self,
|
&mut self,
|
||||||
policy_path: BTreeMap<String, Vec<usize>>,
|
policy_path: BTreeMap<String, Vec<usize>>,
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
) -> Self {
|
) -> &mut Self {
|
||||||
let to_update = match keychain {
|
let to_update = match keychain {
|
||||||
KeychainKind::Internal => &mut self.internal_policy_path,
|
KeychainKind::Internal => &mut self.params.internal_policy_path,
|
||||||
KeychainKind::External => &mut self.external_policy_path,
|
KeychainKind::External => &mut self.params.external_policy_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
*to_update = Some(policy_path);
|
*to_update = Some(policy_path);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the internal list of utxos that **must** be spent with a new list
|
/// Add the list of outpoints to the internal list of UTXOs that **must** be spent.
|
||||||
|
///
|
||||||
|
/// If an error occurs while adding any of the UTXOs then none of them are added and the error is returned.
|
||||||
///
|
///
|
||||||
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
||||||
/// the "utxos" and the "unspendable" list, it will be spent.
|
/// the "utxos" and the "unspendable" list, it will be spent.
|
||||||
pub fn utxos(mut self, utxos: Vec<OutPoint>) -> Self {
|
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> {
|
||||||
self.utxos = utxos;
|
let utxos = outpoints
|
||||||
self
|
.iter()
|
||||||
|
.map(|outpoint| self.wallet.get_utxo(*outpoint)?.ok_or(Error::UnknownUtxo))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
for utxo in utxos {
|
||||||
|
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||||
|
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
||||||
|
self.params.utxos.push(WeightedUtxo {
|
||||||
|
satisfaction_weight,
|
||||||
|
utxo: Utxo::Local(utxo),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a utxo to the internal list of utxos that **must** be spent
|
/// Add a utxo to the internal list of utxos that **must** be spent
|
||||||
///
|
///
|
||||||
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
||||||
/// the "utxos" and the "unspendable" list, it will be spent.
|
/// the "utxos" and the "unspendable" list, it will be spent.
|
||||||
pub fn add_utxo(mut self, utxo: OutPoint) -> Self {
|
pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, Error> {
|
||||||
self.utxos.push(utxo);
|
self.add_utxos(&[outpoint])
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Only spend utxos added by [`add_utxo`] and [`utxos`].
|
/// Add a foreign UTXO i.e. a UTXO not owned by this wallet.
|
||||||
|
///
|
||||||
|
/// At a minimum to add a foreign UTXO we need:
|
||||||
|
///
|
||||||
|
/// 1. `outpoint`: To add it to the raw transaction.
|
||||||
|
/// 2. `psbt_input`: To know the value.
|
||||||
|
/// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation.
|
||||||
|
///
|
||||||
|
/// There are several security concerns about adding foregin UTXOs that application
|
||||||
|
/// developers should consider. First, how do you know the value of the input is correct? If a
|
||||||
|
/// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the
|
||||||
|
/// value by checking it against the transaction. If only a `witness_utxo` is provided then this
|
||||||
|
/// method doesn't verify the value but just takes it as a given -- it is up to you to check
|
||||||
|
/// that whoever sent you the `input_psbt` was not lying!
|
||||||
|
///
|
||||||
|
/// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your
|
||||||
|
/// application it may be important that this be known precisely. If not, a malicious
|
||||||
|
/// counterparty may fool you into putting in a value that is too low, giving the transaction a
|
||||||
|
/// lower than expected feerate. They could also fool you into putting a value that is too high
|
||||||
|
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
||||||
|
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
||||||
|
///
|
||||||
|
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
|
||||||
|
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
||||||
|
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
|
||||||
|
///
|
||||||
|
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This method returns errors in the following circumstances:
|
||||||
|
///
|
||||||
|
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
|
||||||
|
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
|
||||||
|
///
|
||||||
|
/// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
|
||||||
|
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
|
||||||
|
///
|
||||||
|
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||||
|
/// [`finish`]: Self::finish
|
||||||
|
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||||
|
pub fn add_foreign_utxo(
|
||||||
|
&mut self,
|
||||||
|
outpoint: OutPoint,
|
||||||
|
psbt_input: psbt::Input,
|
||||||
|
satisfaction_weight: usize,
|
||||||
|
) -> Result<&mut Self, Error> {
|
||||||
|
if psbt_input.witness_utxo.is_none() {
|
||||||
|
match psbt_input.non_witness_utxo.as_ref() {
|
||||||
|
Some(tx) => {
|
||||||
|
if tx.txid() != outpoint.txid {
|
||||||
|
return Err(Error::Generic(
|
||||||
|
"Foreign utxo outpoint does not match PSBT input".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if tx.output.len() <= outpoint.vout as usize {
|
||||||
|
return Err(Error::InvalidOutpoint(outpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(Error::Generic(
|
||||||
|
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.params.utxos.push(WeightedUtxo {
|
||||||
|
satisfaction_weight,
|
||||||
|
utxo: Utxo::Foreign {
|
||||||
|
outpoint,
|
||||||
|
psbt_input: Box::new(psbt_input),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only spend utxos added by [`add_utxo`].
|
||||||
///
|
///
|
||||||
/// The wallet will **not** add additional utxos to the transaction even if they are needed to
|
/// The wallet will **not** add additional utxos to the transaction even if they are needed to
|
||||||
/// make the transaction valid.
|
/// make the transaction valid.
|
||||||
///
|
///
|
||||||
/// [`add_utxo`]: Self::add_utxo
|
/// [`add_utxo`]: Self::add_utxo
|
||||||
/// [`utxos`]: Self::utxos
|
pub fn manually_selected_only(&mut self) -> &mut Self {
|
||||||
pub fn manually_selected_only(mut self) -> Self {
|
self.params.manually_selected_only = true;
|
||||||
self.manually_selected_only = true;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the internal list of unspendable utxos with a new list
|
/// Replace the internal list of unspendable utxos with a new list
|
||||||
///
|
///
|
||||||
/// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::utxos`] and
|
/// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
|
||||||
/// [`TxBuilder::add_utxo`] have priority over these. See the docs of the two linked methods
|
/// have priority over these. See the docs of the two linked methods for more details.
|
||||||
/// for more details.
|
pub fn unspendable(&mut self, unspendable: Vec<OutPoint>) -> &mut Self {
|
||||||
pub fn unspendable(mut self, unspendable: Vec<OutPoint>) -> Self {
|
self.params.unspendable = unspendable.into_iter().collect();
|
||||||
self.unspendable = unspendable.into_iter().collect();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a utxo to the internal list of unspendable utxos
|
/// Add a utxo to the internal list of unspendable utxos
|
||||||
///
|
///
|
||||||
/// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::utxos`] and
|
/// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`]
|
||||||
/// [`TxBuilder::add_utxo`] have priority over this. See the docs of the two linked methods
|
/// have priority over this. See the docs of the two linked methods for more details.
|
||||||
/// for more details.
|
pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut Self {
|
||||||
pub fn add_unspendable(mut self, unspendable: OutPoint) -> Self {
|
self.params.unspendable.insert(unspendable);
|
||||||
self.unspendable.insert(unspendable);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign with a specific sig hash
|
/// Sign with a specific sig hash
|
||||||
///
|
///
|
||||||
/// **Use this option very carefully**
|
/// **Use this option very carefully**
|
||||||
pub fn sighash(mut self, sighash: SigHashType) -> Self {
|
pub fn sighash(&mut self, sighash: SigHashType) -> &mut Self {
|
||||||
self.sighash = Some(sighash);
|
self.params.sighash = Some(sighash);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Choose the ordering for inputs and outputs of the transaction
|
/// Choose the ordering for inputs and outputs of the transaction
|
||||||
pub fn ordering(mut self, ordering: TxOrdering) -> Self {
|
pub fn ordering(&mut self, ordering: TxOrdering) -> &mut Self {
|
||||||
self.ordering = ordering;
|
self.params.ordering = ordering;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a specific nLockTime while creating the transaction
|
/// Use a specific nLockTime while creating the transaction
|
||||||
///
|
///
|
||||||
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
||||||
pub fn nlocktime(mut self, locktime: u32) -> Self {
|
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self {
|
||||||
self.locktime = Some(locktime);
|
self.params.locktime = Some(locktime);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,8 +435,8 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
///
|
///
|
||||||
/// The `version` should always be greater than `0` and greater than `1` if the wallet's
|
/// The `version` should always be greater than `0` and greater than `1` if the wallet's
|
||||||
/// descriptors contain an "older" (OP_CSV) operator.
|
/// descriptors contain an "older" (OP_CSV) operator.
|
||||||
pub fn version(mut self, version: i32) -> Self {
|
pub fn version(&mut self, version: i32) -> &mut Self {
|
||||||
self.version = Some(Version(version));
|
self.params.version = Some(Version(version));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,8 +444,8 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
///
|
///
|
||||||
/// This effectively adds all the change outputs to the "unspendable" list. See
|
/// This effectively adds all the change outputs to the "unspendable" list. See
|
||||||
/// [`TxBuilder::unspendable`].
|
/// [`TxBuilder::unspendable`].
|
||||||
pub fn do_not_spend_change(mut self) -> Self {
|
pub fn do_not_spend_change(&mut self) -> &mut Self {
|
||||||
self.change_policy = ChangeSpendPolicy::ChangeForbidden;
|
self.params.change_policy = ChangeSpendPolicy::ChangeForbidden;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,24 +453,25 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
///
|
///
|
||||||
/// This effectively adds all the non-change outputs to the "unspendable" list. See
|
/// This effectively adds all the non-change outputs to the "unspendable" list. See
|
||||||
/// [`TxBuilder::unspendable`].
|
/// [`TxBuilder::unspendable`].
|
||||||
pub fn only_spend_change(mut self) -> Self {
|
pub fn only_spend_change(&mut self) -> &mut Self {
|
||||||
self.change_policy = ChangeSpendPolicy::OnlyChange;
|
self.params.change_policy = ChangeSpendPolicy::OnlyChange;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and
|
/// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and
|
||||||
/// [`TxBuilder::only_spend_change`] for some shortcuts.
|
/// [`TxBuilder::only_spend_change`] for some shortcuts.
|
||||||
pub fn change_policy(mut self, change_policy: ChangeSpendPolicy) -> Self {
|
pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self {
|
||||||
self.change_policy = change_policy;
|
self.params.change_policy = change_policy;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit
|
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
|
||||||
/// descriptors.
|
/// SegWit descriptors.
|
||||||
///
|
///
|
||||||
/// This is useful for signers which always require it, like Trezor hardware wallets.
|
/// This reduces the size of the PSBT, but some signers might reject them due to the lack of
|
||||||
pub fn force_non_witness_utxo(mut self) -> Self {
|
/// the `non_witness_utxo`.
|
||||||
self.force_non_witness_utxo = true;
|
pub fn only_witness_utxo(&mut self) -> &mut Self {
|
||||||
|
self.params.only_witness_utxo = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +479,8 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
/// [`psbt::Output::witness_script`](bitcoin::util::psbt::Output::witness_script) fields.
|
/// [`psbt::Output::witness_script`](bitcoin::util::psbt::Output::witness_script) fields.
|
||||||
///
|
///
|
||||||
/// This is useful for signers which always require it, like ColdCard hardware wallets.
|
/// This is useful for signers which always require it, like ColdCard hardware wallets.
|
||||||
pub fn include_output_redeem_witness_script(mut self) -> Self {
|
pub fn include_output_redeem_witness_script(&mut self) -> &mut Self {
|
||||||
self.include_output_redeem_witness_script = true;
|
self.params.include_output_redeem_witness_script = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,98 +489,48 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilde
|
|||||||
///
|
///
|
||||||
/// This is useful for offline signers that take part to a multisig. Some hardware wallets like
|
/// This is useful for offline signers that take part to a multisig. Some hardware wallets like
|
||||||
/// BitBox and ColdCard are known to require this.
|
/// BitBox and ColdCard are known to require this.
|
||||||
pub fn add_global_xpubs(mut self) -> Self {
|
pub fn add_global_xpubs(&mut self) -> &mut Self {
|
||||||
self.add_global_xpubs = true;
|
self.params.add_global_xpubs = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy.
|
/// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy.
|
||||||
pub fn drain_wallet(mut self) -> Self {
|
pub fn drain_wallet(&mut self) -> &mut Self {
|
||||||
self.drain_wallet = true;
|
self.params.drain_wallet = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Choose the coin selection algorithm
|
/// Choose the coin selection algorithm
|
||||||
///
|
///
|
||||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
|
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
|
||||||
|
///
|
||||||
|
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
|
||||||
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
|
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
|
||||||
self,
|
self,
|
||||||
coin_selection: P,
|
coin_selection: P,
|
||||||
) -> TxBuilder<D, P, Ctx> {
|
) -> TxBuilder<'a, B, D, P, Ctx> {
|
||||||
TxBuilder {
|
TxBuilder {
|
||||||
recipients: self.recipients,
|
wallet: self.wallet,
|
||||||
drain_wallet: self.drain_wallet,
|
params: self.params,
|
||||||
single_recipient: self.single_recipient,
|
|
||||||
fee_policy: self.fee_policy,
|
|
||||||
internal_policy_path: self.internal_policy_path,
|
|
||||||
external_policy_path: self.external_policy_path,
|
|
||||||
utxos: self.utxos,
|
|
||||||
unspendable: self.unspendable,
|
|
||||||
manually_selected_only: self.manually_selected_only,
|
|
||||||
sighash: self.sighash,
|
|
||||||
ordering: self.ordering,
|
|
||||||
locktime: self.locktime,
|
|
||||||
rbf: self.rbf,
|
|
||||||
version: self.version,
|
|
||||||
change_policy: self.change_policy,
|
|
||||||
force_non_witness_utxo: self.force_non_witness_utxo,
|
|
||||||
add_global_xpubs: self.add_global_xpubs,
|
|
||||||
include_output_redeem_witness_script: self.include_output_redeem_witness_script,
|
|
||||||
coin_selection,
|
coin_selection,
|
||||||
|
|
||||||
phantom: PhantomData,
|
phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// methods supported only by create_tx, and only for `DefaultCoinSelectionAlgorithm`
|
/// Finish the building the transaction.
|
||||||
impl<D: Database> TxBuilder<D, DefaultCoinSelectionAlgorithm, CreateTx> {
|
|
||||||
/// Create a builder starting from a list of recipients
|
|
||||||
pub fn with_recipients(recipients: Vec<(Script, u64)>) -> Self {
|
|
||||||
Self::default().set_recipients(recipients)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods supported only by create_tx, for any `CoinSelectionAlgorithm`
|
|
||||||
impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs, CreateTx> {
|
|
||||||
/// Replace the recipients already added with a new list
|
|
||||||
pub fn set_recipients(mut self, recipients: Vec<(Script, u64)>) -> Self {
|
|
||||||
self.recipients = recipients;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a recipient to the internal list
|
|
||||||
pub fn add_recipient(mut self, script_pubkey: Script, amount: u64) -> Self {
|
|
||||||
self.recipients.push((script_pubkey, amount));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a single recipient that will get all the selected funds minus the fee. No change will
|
|
||||||
/// be created
|
|
||||||
///
|
///
|
||||||
/// This method overrides any recipient set with [`set_recipients`](Self::set_recipients) or
|
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||||
/// [`add_recipient`](Self::add_recipient).
|
|
||||||
///
|
///
|
||||||
/// It can only be used in conjunction with [`drain_wallet`](Self::drain_wallet) to send the
|
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||||
/// entire content of the wallet (minus filters) to a single recipient or with a
|
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
|
||||||
/// list of manually selected UTXOs by enabling [`manually_selected_only`](Self::manually_selected_only)
|
self.wallet.create_tx(self.coin_selection, self.params)
|
||||||
/// and selecting them with [`utxos`](Self::utxos) or [`add_utxo`](Self::add_utxo).
|
|
||||||
///
|
|
||||||
/// When bumping the fees of a transaction made with this option, the user should remeber to
|
|
||||||
/// add [`maintain_single_recipient`](Self::maintain_single_recipient) to correctly update the
|
|
||||||
/// single output instead of adding one more for the change.
|
|
||||||
pub fn set_single_recipient(mut self, recipient: Script) -> Self {
|
|
||||||
self.single_recipient = Some(recipient);
|
|
||||||
self.recipients.clear();
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable signaling RBF
|
/// Enable signaling RBF
|
||||||
///
|
///
|
||||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||||
pub fn enable_rbf(mut self) -> Self {
|
pub fn enable_rbf(&mut self) -> &mut Self {
|
||||||
self.rbf = Some(RBFValue::Default);
|
self.params.rbf = Some(RbfValue::Default);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,30 +541,100 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs, CreateTx> {
|
|||||||
///
|
///
|
||||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||||
/// be a valid nSequence to signal RBF.
|
/// be a valid nSequence to signal RBF.
|
||||||
pub fn enable_rbf_with_sequence(mut self, nsequence: u32) -> Self {
|
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
||||||
self.rbf = Some(RBFValue::Value(nsequence));
|
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D, Cs, CreateTx> {
|
||||||
|
/// Replace the recipients already added with a new list
|
||||||
|
pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self {
|
||||||
|
self.params.recipients = recipients;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a recipient to the internal list
|
||||||
|
pub fn add_recipient(&mut self, script_pubkey: Script, amount: u64) -> &mut Self {
|
||||||
|
self.params.recipients.push((script_pubkey, amount));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the address to *drain* excess coins to.
|
||||||
|
///
|
||||||
|
/// Usually, when there are excess coins they are sent to a change address generated by the
|
||||||
|
/// 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).
|
||||||
|
///
|
||||||
|
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||||
|
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
|
||||||
|
/// single address.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # 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<D: Database> TxBuilder<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`] or [`utxos`], 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
|
||||||
/// [`utxos`]: Self::utxos
|
.iter()
|
||||||
pub fn maintain_single_recipient(mut self) -> Self {
|
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
|
||||||
self.single_recipient = Some(Script::default());
|
{
|
||||||
self
|
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
|
||||||
|
))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +646,7 @@ pub enum TxOrdering {
|
|||||||
/// Unchanged
|
/// Unchanged
|
||||||
Untouched,
|
Untouched,
|
||||||
/// BIP69 / Lexicographic
|
/// BIP69 / Lexicographic
|
||||||
BIP69Lexicographic,
|
Bip69Lexicographic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TxOrdering {
|
impl Default for TxOrdering {
|
||||||
@@ -524,7 +672,7 @@ impl TxOrdering {
|
|||||||
|
|
||||||
tx.output.shuffle(&mut rng);
|
tx.output.shuffle(&mut rng);
|
||||||
}
|
}
|
||||||
TxOrdering::BIP69Lexicographic => {
|
TxOrdering::Bip69Lexicographic => {
|
||||||
tx.input.sort_unstable_by_key(|txin| {
|
tx.input.sort_unstable_by_key(|txin| {
|
||||||
(txin.previous_output.txid, txin.previous_output.vout)
|
(txin.previous_output.txid, txin.previous_output.vout)
|
||||||
});
|
});
|
||||||
@@ -551,16 +699,16 @@ impl Default for Version {
|
|||||||
///
|
///
|
||||||
/// Has a default value of `0xFFFFFFFD`
|
/// Has a default value of `0xFFFFFFFD`
|
||||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
pub(crate) enum RBFValue {
|
pub(crate) enum RbfValue {
|
||||||
Default,
|
Default,
|
||||||
Value(u32),
|
Value(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RBFValue {
|
impl RbfValue {
|
||||||
pub(crate) fn get_value(&self) -> u32 {
|
pub(crate) fn get_value(&self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
RBFValue::Default => 0xFFFFFFFD,
|
RbfValue::Default => 0xFFFFFFFD,
|
||||||
RBFValue::Value(v) => *v,
|
RbfValue::Value(v) => *v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,7 +731,7 @@ impl Default for ChangeSpendPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChangeSpendPolicy {
|
impl ChangeSpendPolicy {
|
||||||
pub(crate) fn is_satisfied_by(&self, utxo: &UTXO) -> bool {
|
pub(crate) fn is_satisfied_by(&self, utxo: &LocalUtxo) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ChangeSpendPolicy::ChangeAllowed => true,
|
ChangeSpendPolicy::ChangeAllowed => true,
|
||||||
ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
|
ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
|
||||||
@@ -594,12 +742,12 @@ impl ChangeSpendPolicy {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
const ORDERING_TEST_TX: &'static str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\
|
const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\
|
||||||
85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\
|
85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\
|
||||||
79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\
|
79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\
|
||||||
dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\
|
dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\
|
||||||
03e80300000000000002aaeee80300000000000001aa200300000000000001ff\
|
03e80300000000000002aaeee80300000000000001aa200300000000000001ff\
|
||||||
00000000";
|
00000000";
|
||||||
macro_rules! ordering_test_tx {
|
macro_rules! ordering_test_tx {
|
||||||
() => {
|
() => {
|
||||||
deserialize::<bitcoin::Transaction>(&Vec::<u8>::from_hex(ORDERING_TEST_TX).unwrap())
|
deserialize::<bitcoin::Transaction>(&Vec::<u8>::from_hex(ORDERING_TEST_TX).unwrap())
|
||||||
@@ -643,9 +791,9 @@ mod test {
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
let original_tx = ordering_test_tx!();
|
let original_tx = ordering_test_tx!();
|
||||||
let mut tx = original_tx.clone();
|
let mut tx = original_tx;
|
||||||
|
|
||||||
TxOrdering::BIP69Lexicographic.sort_tx(&mut tx);
|
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx.input[0].previous_output,
|
tx.input[0].previous_output,
|
||||||
@@ -674,9 +822,9 @@ mod test {
|
|||||||
assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE]));
|
assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE]));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_test_utxos() -> Vec<UTXO> {
|
fn get_test_utxos() -> Vec<LocalUtxo> {
|
||||||
vec![
|
vec![
|
||||||
UTXO {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
txid: Default::default(),
|
txid: Default::default(),
|
||||||
vout: 0,
|
vout: 0,
|
||||||
@@ -684,7 +832,7 @@ mod test {
|
|||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
},
|
||||||
UTXO {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
txid: Default::default(),
|
txid: Default::default(),
|
||||||
vout: 1,
|
vout: 1,
|
||||||
@@ -701,9 +849,9 @@ mod test {
|
|||||||
let filtered = get_test_utxos()
|
let filtered = get_test_utxos()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|u| change_spend_policy.is_satisfied_by(u))
|
.filter(|u| change_spend_policy.is_satisfied_by(u))
|
||||||
.collect::<Vec<_>>();
|
.count();
|
||||||
|
|
||||||
assert_eq!(filtered.len(), 2);
|
assert_eq!(filtered, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
// Magical Bitcoin Library
|
// Bitcoin Dev Kit
|
||||||
// Written in 2020 by
|
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
//
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
// in the Software without restriction, including without limitation the rights
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// You may not use this file except in accordance with one or both of these
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// licenses.
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
use bitcoin::secp256k1::{All, Secp256k1};
|
use bitcoin::secp256k1::{All, Secp256k1};
|
||||||
use bitcoin::util::bip32;
|
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorPublicKeyCtx;
|
|
||||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||||
|
|
||||||
// De-facto standard "dust limit" (even though it should change based on the output type)
|
// De-facto standard "dust limit" (even though it should change based on the output type)
|
||||||
@@ -110,7 +95,7 @@ pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ToPkCtx: Copy, Pk: MiniscriptKey + ToPublicKey<ToPkCtx>> Satisfier<ToPkCtx, Pk> for After {
|
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
||||||
fn check_after(&self, n: u32) -> bool {
|
fn check_after(&self, n: u32) -> bool {
|
||||||
if let Some(current_height) = self.current_height {
|
if let Some(current_height) = self.current_height {
|
||||||
current_height >= n
|
current_height >= n
|
||||||
@@ -140,7 +125,7 @@ impl Older {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ToPkCtx: Copy, Pk: MiniscriptKey + ToPublicKey<ToPkCtx>> Satisfier<ToPkCtx, Pk> for Older {
|
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||||
fn check_older(&self, n: u32) -> bool {
|
fn check_older(&self, n: u32) -> bool {
|
||||||
if let Some(current_height) = self.current_height {
|
if let Some(current_height) = self.current_height {
|
||||||
// TODO: test >= / >
|
// TODO: test >= / >
|
||||||
@@ -152,20 +137,14 @@ impl<ToPkCtx: Copy, Pk: MiniscriptKey + ToPublicKey<ToPkCtx>> Satisfier<ToPkCtx,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type SecpCtx = Secp256k1<All>;
|
pub(crate) type SecpCtx = Secp256k1<All>;
|
||||||
pub(crate) fn descriptor_to_pk_ctx(secp: &SecpCtx) -> DescriptorPublicKeyCtx<'_, All> {
|
|
||||||
// Create a `to_pk_ctx` with a dummy derivation index, since we always use this on descriptor
|
|
||||||
// that have already been derived with `Descriptor::derive()`, so the child number added here
|
|
||||||
// is ignored.
|
|
||||||
DescriptorPublicKeyCtx::new(secp, bip32::ChildNumber::Normal { index: 0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChunksIterator<I: Iterator> {
|
pub struct ChunksIterator<I: Iterator> {
|
||||||
iter: I,
|
iter: I,
|
||||||
size: usize,
|
size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "electrum", feature = "esplora"))]
|
||||||
impl<I: Iterator> ChunksIterator<I> {
|
impl<I: Iterator> ChunksIterator<I> {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn new(iter: I, size: usize) -> Self {
|
pub fn new(iter: I, size: usize) -> Self {
|
||||||
ChunksIterator { iter, size }
|
ChunksIterator { iter, size }
|
||||||
}
|
}
|
||||||
@@ -222,31 +201,31 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_msb_set() {
|
fn test_check_nsequence_rbf_msb_set() {
|
||||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_lt_csv() {
|
fn test_check_nsequence_rbf_lt_csv() {
|
||||||
let result = check_nsequence_rbf(4000, 5000);
|
let result = check_nsequence_rbf(4000, 5000);
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_different_unit() {
|
fn test_check_nsequence_rbf_different_unit() {
|
||||||
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_mask() {
|
fn test_check_nsequence_rbf_mask() {
|
||||||
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nsequence_rbf_same_unit_blocks() {
|
fn test_check_nsequence_rbf_same_unit_blocks() {
|
||||||
let result = check_nsequence_rbf(10_000, 5000);
|
let result = check_nsequence_rbf(10_000, 5000);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -255,25 +234,25 @@ mod test {
|
|||||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
||||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
||||||
);
|
);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nlocktime_lt_cltv() {
|
fn test_check_nlocktime_lt_cltv() {
|
||||||
let result = check_nlocktime(4000, 5000);
|
let result = check_nlocktime(4000, 5000);
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nlocktime_different_unit() {
|
fn test_check_nlocktime_different_unit() {
|
||||||
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_nlocktime_same_unit_blocks() {
|
fn test_check_nlocktime_same_unit_blocks() {
|
||||||
let result = check_nlocktime(10_000, 5000);
|
let result = check_nlocktime(10_000, 5000);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -282,6 +261,6 @@ mod test {
|
|||||||
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
||||||
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
||||||
);
|
);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bdk-testutils-macros"
|
|
||||||
version = "0.2.0"
|
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
homepage = "https://bitcoindevkit.org"
|
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
|
||||||
documentation = "https://docs.rs/bdk-testutils-macros"
|
|
||||||
description = "Supporting testing macros for `bdk`"
|
|
||||||
keywords = ["bdk"]
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
name = "testutils_macros"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = { version = "1.0", features = ["parsing", "full"] }
|
|
||||||
proc-macro2 = "1.0"
|
|
||||||
quote = "1.0"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
debug = ["syn/extra-traits"]
|
|
||||||
@@ -1,545 +0,0 @@
|
|||||||
// Magical Bitcoin Library
|
|
||||||
// Written in 2020 by
|
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate quote;
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
|
|
||||||
use syn::spanned::Spanned;
|
|
||||||
use syn::{parse, parse2, Ident, ReturnType};
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
let root_ident = if !attr.is_empty() {
|
|
||||||
match parse::<syn::ExprPath>(attr) {
|
|
||||||
Ok(parsed) => parsed,
|
|
||||||
Err(e) => {
|
|
||||||
let error_string = e.to_string();
|
|
||||||
return (quote! {
|
|
||||||
compile_error!("Invalid crate path: {:?}", #error_string)
|
|
||||||
})
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parse2::<syn::ExprPath>(quote! { bdk }).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
match parse::<syn::ItemFn>(item) {
|
|
||||||
Err(_) => (quote! {
|
|
||||||
compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s")
|
|
||||||
})
|
|
||||||
.into(),
|
|
||||||
Ok(parsed) => {
|
|
||||||
let parsed_sig_ident = parsed.sig.ident.clone();
|
|
||||||
let mod_name = Ident::new(
|
|
||||||
&format!("generated_tests_{}", parsed_sig_ident.to_string()),
|
|
||||||
parsed.span(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let return_type = match parsed.sig.output {
|
|
||||||
ReturnType::Type(_, ref t) => t.clone(),
|
|
||||||
ReturnType::Default => {
|
|
||||||
return (quote! {
|
|
||||||
compile_error!("The tagged function must return a type that impl `Blockchain`")
|
|
||||||
}).into();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = quote! {
|
|
||||||
|
|
||||||
#parsed
|
|
||||||
|
|
||||||
mod #mod_name {
|
|
||||||
use bitcoin::Network;
|
|
||||||
|
|
||||||
use miniscript::Descriptor;
|
|
||||||
|
|
||||||
use testutils::{TestClient, serial};
|
|
||||||
|
|
||||||
use #root_ident::blockchain::{Blockchain, noop_progress};
|
|
||||||
use #root_ident::descriptor::ExtendedDescriptor;
|
|
||||||
use #root_ident::database::MemoryDatabase;
|
|
||||||
use #root_ident::types::KeychainKind;
|
|
||||||
use #root_ident::{Wallet, TxBuilder, FeeRate};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn get_blockchain() -> #return_type {
|
|
||||||
#parsed_sig_ident()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<#return_type, MemoryDatabase> {
|
|
||||||
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option<String>), TestClient) {
|
|
||||||
let descriptors = testutils! {
|
|
||||||
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
|
|
||||||
};
|
|
||||||
|
|
||||||
let test_client = TestClient::new();
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
|
||||||
|
|
||||||
(wallet, descriptors, test_client)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_simple() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
let tx = testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
};
|
|
||||||
println!("{:?}", tx);
|
|
||||||
let txid = test_client.receive(tx);
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, txid);
|
|
||||||
assert_eq!(list_tx_item.received, 50_000);
|
|
||||||
assert_eq!(list_tx_item.sent, 0);
|
|
||||||
assert_eq!(list_tx_item.height, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_stop_gap_20() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 5) => 50_000 )
|
|
||||||
});
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 25) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_before_and_after_receive() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_multiple_outputs_same_tx() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
let txid = test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 105_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, txid);
|
|
||||||
assert_eq!(list_tx_item.received, 105_000);
|
|
||||||
assert_eq!(list_tx_item.sent, 0);
|
|
||||||
assert_eq!(list_tx_item.height, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_multi() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 5) => 25_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_address_reuse() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 25_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_rbf_replaced() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
let txid = test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, txid);
|
|
||||||
assert_eq!(list_tx_item.received, 50_000);
|
|
||||||
assert_eq!(list_tx_item.sent, 0);
|
|
||||||
assert_eq!(list_tx_item.height, None);
|
|
||||||
|
|
||||||
let new_txid = test_client.bump_fee(&txid);
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, new_txid);
|
|
||||||
assert_eq!(list_tx_item.received, 50_000);
|
|
||||||
assert_eq!(list_tx_item.sent, 0);
|
|
||||||
assert_eq!(list_tx_item.height, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_reorg_block() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
|
|
||||||
let txid = test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, txid);
|
|
||||||
assert!(list_tx_item.height.is_some());
|
|
||||||
|
|
||||||
// Invalidate 1 block
|
|
||||||
test_client.invalidate(1);
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
|
||||||
assert_eq!(list_tx_item.txid, txid);
|
|
||||||
assert_eq!(list_tx_item.height, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_after_send() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
println!("{}", descriptors.0);
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
let tx = psbt.extract_tx();
|
|
||||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
|
||||||
wallet.broadcast(tx).unwrap();
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
|
||||||
|
|
||||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
|
|
||||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_outgoing_from_scratch() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
let received_txid = test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
|
||||||
|
|
||||||
// empty wallet
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
|
||||||
|
|
||||||
let received = tx_map.get(&received_txid).unwrap();
|
|
||||||
assert_eq!(received.received, 50_000);
|
|
||||||
assert_eq!(received.sent, 0);
|
|
||||||
|
|
||||||
let sent = tx_map.get(&sent_txid).unwrap();
|
|
||||||
assert_eq!(sent.received, details.received);
|
|
||||||
assert_eq!(sent.sent, details.sent);
|
|
||||||
assert_eq!(sent.fees, details.fees);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_long_change_chain() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let mut total_sent = 0;
|
|
||||||
for _ in 0..5 {
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)])).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
|
|
||||||
total_sent += 5_000 + details.fees;
|
|
||||||
}
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
|
|
||||||
|
|
||||||
// empty wallet
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)]).enable_rbf()).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
|
||||||
|
|
||||||
let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(2.1))).unwrap();
|
|
||||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
|
|
||||||
|
|
||||||
assert!(new_details.fees > details.fees);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_remove_change() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), details.received);
|
|
||||||
|
|
||||||
let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(5.0))).unwrap();
|
|
||||||
|
|
||||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
|
||||||
assert_eq!(new_details.received, 0);
|
|
||||||
|
|
||||||
assert!(new_details.fees > details.fees);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_add_input() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
|
|
||||||
assert_eq!(details.received, 1_000 - details.fees);
|
|
||||||
|
|
||||||
let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(10.0))).unwrap();
|
|
||||||
|
|
||||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(new_details.sent, 75_000);
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_bump_fee_add_input_no_change() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let node_addr = test_client.get_node_address(None);
|
|
||||||
|
|
||||||
test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000);
|
|
||||||
|
|
||||||
let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap();
|
|
||||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
|
|
||||||
assert_eq!(details.received, 1_000 - details.fees);
|
|
||||||
|
|
||||||
let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(123.0))).unwrap();
|
|
||||||
println!("{:#?}", new_details);
|
|
||||||
|
|
||||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(new_details.sent, 75_000);
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
|
||||||
assert_eq!(new_details.received, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn test_sync_receive_coinbase() {
|
|
||||||
let (wallet, descriptors, mut test_client) = init_single_sig();
|
|
||||||
let wallet_addr = wallet.get_new_address().unwrap();
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
|
||||||
|
|
||||||
test_client.generate(1, Some(wallet_addr));
|
|
||||||
|
|
||||||
wallet.sync(noop_progress(), None).unwrap();
|
|
||||||
assert!(wallet.get_balance().unwrap() > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
output.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
testutils/.gitignore
vendored
2
testutils/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
target/
|
|
||||||
Cargo.lock
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bdk-testutils"
|
|
||||||
version = "0.2.0"
|
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
homepage = "https://bitcoindevkit.org"
|
|
||||||
repository = "https://github.com/bitcoindevkit/bdk"
|
|
||||||
documentation = "https://docs.rs/bdk-testutils"
|
|
||||||
description = "Supporting testing utilities for `bdk`"
|
|
||||||
keywords = ["bdk"]
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "testutils"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = "0.4.8"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
serial_test = "0.4"
|
|
||||||
bitcoin = "0.25"
|
|
||||||
bitcoincore-rpc = "0.12"
|
|
||||||
electrum-client = "0.4.0-beta.1"
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
// Magical Bitcoin Library
|
|
||||||
// Written in 2020 by
|
|
||||||
// Alekos Filini <alekos.filini@gmail.com>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2020 Magical Bitcoin
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
pub use serial_test::serial;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::env;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use log::{debug, error, info, trace};
|
|
||||||
|
|
||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
|
||||||
use bitcoin::hashes::sha256d;
|
|
||||||
use bitcoin::{Address, Amount, Script, Transaction, Txid};
|
|
||||||
|
|
||||||
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
|
|
||||||
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
|
|
||||||
|
|
||||||
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
|
|
||||||
|
|
||||||
// TODO: we currently only support env vars, we could also parse a toml file
|
|
||||||
fn get_auth() -> Auth {
|
|
||||||
match env::var("MAGICAL_RPC_AUTH").as_ref().map(String::as_ref) {
|
|
||||||
Ok("USER_PASS") => Auth::UserPass(
|
|
||||||
env::var("MAGICAL_RPC_USER").unwrap(),
|
|
||||||
env::var("MAGICAL_RPC_PASS").unwrap(),
|
|
||||||
),
|
|
||||||
_ => Auth::CookieFile(PathBuf::from(
|
|
||||||
env::var("MAGICAL_RPC_COOKIEFILE")
|
|
||||||
.unwrap_or("/home/user/.bitcoin/regtest/.cookie".to_string()),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_electrum_url() -> String {
|
|
||||||
env::var("MAGICAL_ELECTRUM_URL").unwrap_or("tcp://127.0.0.1:50001".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestClient {
|
|
||||||
client: RpcClient,
|
|
||||||
electrum: ElectrumClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TestIncomingOutput {
|
|
||||||
pub value: u64,
|
|
||||||
pub to_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestIncomingOutput {
|
|
||||||
pub fn new(value: u64, to_address: Address) -> Self {
|
|
||||||
Self {
|
|
||||||
value,
|
|
||||||
to_address: to_address.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TestIncomingTx {
|
|
||||||
pub output: Vec<TestIncomingOutput>,
|
|
||||||
pub min_confirmations: Option<u64>,
|
|
||||||
pub locktime: Option<i64>,
|
|
||||||
pub replaceable: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestIncomingTx {
|
|
||||||
pub fn new(
|
|
||||||
output: Vec<TestIncomingOutput>,
|
|
||||||
min_confirmations: Option<u64>,
|
|
||||||
locktime: Option<i64>,
|
|
||||||
replaceable: Option<bool>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
output,
|
|
||||||
min_confirmations,
|
|
||||||
locktime,
|
|
||||||
replaceable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
|
||||||
self.output.push(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! testutils {
|
|
||||||
( @external $descriptors:expr, $child:expr ) => ({
|
|
||||||
use bitcoin::secp256k1::Secp256k1;
|
|
||||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorPublicKeyCtx};
|
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
let deriv_ctx = DescriptorPublicKeyCtx::new(&secp, bitcoin::util::bip32::ChildNumber::from_normal_idx(0).unwrap());
|
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
|
|
||||||
parsed.derive(bitcoin::util::bip32::ChildNumber::from_normal_idx($child).unwrap()).address(bitcoin::Network::Regtest, deriv_ctx).expect("No address form")
|
|
||||||
});
|
|
||||||
( @internal $descriptors:expr, $child:expr ) => ({
|
|
||||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
|
||||||
|
|
||||||
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
|
|
||||||
parsed.derive(bitcoin::util::bip32::ChildNumber::from_normal_idx($child).unwrap()).address(bitcoin::Network::Regtest).expect("No address form")
|
|
||||||
});
|
|
||||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
|
||||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
|
||||||
|
|
||||||
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
|
|
||||||
let mut outs = Vec::new();
|
|
||||||
$( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
|
|
||||||
|
|
||||||
let mut locktime = None::<i64>;
|
|
||||||
$( locktime = Some($locktime); )*
|
|
||||||
|
|
||||||
let mut min_confirmations = None::<u64>;
|
|
||||||
$( min_confirmations = Some($confirmations); )*
|
|
||||||
|
|
||||||
let mut replaceable = None::<bool>;
|
|
||||||
$( replaceable = Some($replaceable); )*
|
|
||||||
|
|
||||||
testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
|
|
||||||
});
|
|
||||||
|
|
||||||
( @literal $key:expr ) => ({
|
|
||||||
let key = $key.to_string();
|
|
||||||
(key, None::<String>, None::<String>)
|
|
||||||
});
|
|
||||||
( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
let mut seed = [0u8; 32];
|
|
||||||
rand::thread_rng().fill(&mut seed[..]);
|
|
||||||
|
|
||||||
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
|
|
||||||
bitcoin::Network::Testnet,
|
|
||||||
&seed,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut external_path = None::<String>;
|
|
||||||
$( external_path = Some($external_path.to_string()); )*
|
|
||||||
|
|
||||||
let mut internal_path = None::<String>;
|
|
||||||
$( internal_path = Some($internal_path.to_string()); )*
|
|
||||||
|
|
||||||
(key.unwrap().to_string(), external_path, internal_path)
|
|
||||||
});
|
|
||||||
( @generate_wif ) => ({
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
|
|
||||||
rand::thread_rng().fill(&mut key[..]);
|
|
||||||
|
|
||||||
(bitcoin::PrivateKey {
|
|
||||||
compressed: true,
|
|
||||||
network: bitcoin::Network::Testnet,
|
|
||||||
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
|
|
||||||
}.to_string(), None::<String>, None::<String>)
|
|
||||||
});
|
|
||||||
|
|
||||||
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
|
|
||||||
let mut map = std::collections::HashMap::new();
|
|
||||||
$(
|
|
||||||
let alias: &str = $alias;
|
|
||||||
map.insert(alias, testutils!( $($key_type)* ));
|
|
||||||
)+
|
|
||||||
|
|
||||||
map
|
|
||||||
});
|
|
||||||
|
|
||||||
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
|
||||||
|
|
||||||
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
|
|
||||||
$(
|
|
||||||
keys = testutils!{ @keys $( $keys )* };
|
|
||||||
)*
|
|
||||||
|
|
||||||
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
|
|
||||||
let external: Descriptor<String> = external.translate_pk::<_, _, _, &'static str>(|k| {
|
|
||||||
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
|
|
||||||
Ok(format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into())))
|
|
||||||
} else {
|
|
||||||
Ok(k.clone())
|
|
||||||
}
|
|
||||||
}, |kh| {
|
|
||||||
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
|
|
||||||
Ok(format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into())))
|
|
||||||
} else {
|
|
||||||
Ok(kh.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
}).unwrap();
|
|
||||||
let external = external.to_string();
|
|
||||||
|
|
||||||
let mut internal = None::<String>;
|
|
||||||
$(
|
|
||||||
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
|
|
||||||
|
|
||||||
let string_internal: Descriptor<String> = string_internal.translate_pk::<_, _, _, &'static str>(|k| {
|
|
||||||
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
|
|
||||||
Ok(format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into())))
|
|
||||||
} else {
|
|
||||||
Ok(k.clone())
|
|
||||||
}
|
|
||||||
}, |kh| {
|
|
||||||
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
|
|
||||||
Ok(format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into())))
|
|
||||||
} else {
|
|
||||||
Ok(kh.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
}).unwrap();
|
|
||||||
internal = Some(string_internal.to_string());
|
|
||||||
)*
|
|
||||||
|
|
||||||
(external, internal)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
|
|
||||||
where
|
|
||||||
F: FnMut() -> Option<T>,
|
|
||||||
{
|
|
||||||
let mut delay = Duration::from_millis(64);
|
|
||||||
loop {
|
|
||||||
match poll() {
|
|
||||||
Some(data) => break data,
|
|
||||||
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::sleep(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestClient {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let url = env::var("MAGICAL_RPC_URL").unwrap_or("127.0.0.1:18443".to_string());
|
|
||||||
let client = RpcClient::new(format!("http://{}", url), get_auth()).unwrap();
|
|
||||||
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
|
|
||||||
|
|
||||||
TestClient { client, electrum }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
|
|
||||||
// wait for electrs to index the tx
|
|
||||||
exponential_backoff_poll(|| {
|
|
||||||
trace!("wait_for_tx {}", txid);
|
|
||||||
|
|
||||||
self.electrum
|
|
||||||
.script_get_history(monitor_script)
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.position(|entry| entry.tx_hash == txid)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_block(&mut self, min_height: usize) {
|
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let header = exponential_backoff_poll(|| {
|
|
||||||
self.electrum.ping().unwrap();
|
|
||||||
self.electrum.block_headers_pop().unwrap()
|
|
||||||
});
|
|
||||||
if header.height >= min_height {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
|
|
||||||
assert!(
|
|
||||||
meta_tx.output.len() > 0,
|
|
||||||
"can't create a transaction with no outputs"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
|
|
||||||
let mut required_balance = 0;
|
|
||||||
for out in &meta_tx.output {
|
|
||||||
required_balance += out.value;
|
|
||||||
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
|
||||||
panic!("Insufficient funds in bitcoind. Plase generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: core can't create a tx with two outputs to the same address
|
|
||||||
let tx = self
|
|
||||||
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
|
|
||||||
.unwrap();
|
|
||||||
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
|
||||||
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
|
||||||
|
|
||||||
if let Some(true) = meta_tx.replaceable {
|
|
||||||
// for some reason core doesn't set this field right
|
|
||||||
for input in &mut tx.input {
|
|
||||||
input.sequence = 0xFFFFFFFD;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx = self
|
|
||||||
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// broadcast through electrum so that it caches the tx immediately
|
|
||||||
let txid = self
|
|
||||||
.electrum
|
|
||||||
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Some(num) = meta_tx.min_confirmations {
|
|
||||||
self.generate(num, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
|
|
||||||
.unwrap()
|
|
||||||
.script_pubkey();
|
|
||||||
self.wait_for_tx(txid, &monitor_script);
|
|
||||||
|
|
||||||
debug!("Sent tx: {}", txid);
|
|
||||||
|
|
||||||
txid
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
|
|
||||||
let tx = self.get_raw_transaction_info(txid, None).unwrap();
|
|
||||||
assert!(
|
|
||||||
tx.confirmations.is_none(),
|
|
||||||
"Can't bump tx {} because it's already confirmed",
|
|
||||||
txid
|
|
||||||
);
|
|
||||||
|
|
||||||
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
|
|
||||||
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
|
|
||||||
|
|
||||||
let monitor_script =
|
|
||||||
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
|
|
||||||
self.wait_for_tx(new_txid, &monitor_script);
|
|
||||||
|
|
||||||
debug!("Bumped {}, new txid {}", txid, new_txid);
|
|
||||||
|
|
||||||
new_txid
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
|
|
||||||
use bitcoin::blockdata::block::{Block, BlockHeader};
|
|
||||||
use bitcoin::blockdata::script::Builder;
|
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
|
|
||||||
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
|
|
||||||
|
|
||||||
let block_template: serde_json::Value = self
|
|
||||||
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
|
|
||||||
.unwrap();
|
|
||||||
trace!("getblocktemplate: {:#?}", block_template);
|
|
||||||
|
|
||||||
let header = BlockHeader {
|
|
||||||
version: block_template["version"].as_i64().unwrap() as i32,
|
|
||||||
prev_blockhash: BlockHash::from_hex(
|
|
||||||
block_template["previousblockhash"].as_str().unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
merkle_root: TxMerkleNode::default(),
|
|
||||||
time: block_template["curtime"].as_u64().unwrap() as u32,
|
|
||||||
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
|
|
||||||
nonce: 0,
|
|
||||||
};
|
|
||||||
debug!("header: {:#?}", header);
|
|
||||||
|
|
||||||
let height = block_template["height"].as_u64().unwrap() as i64;
|
|
||||||
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
|
|
||||||
// burn block subsidy and fees, not a big deal
|
|
||||||
let mut coinbase_tx = Transaction {
|
|
||||||
version: 1,
|
|
||||||
lock_time: 0,
|
|
||||||
input: vec![TxIn {
|
|
||||||
previous_output: OutPoint::null(),
|
|
||||||
script_sig: Builder::new().push_int(height).into_script(),
|
|
||||||
sequence: 0xFFFFFFFF,
|
|
||||||
witness: vec![witness_reserved_value],
|
|
||||||
}],
|
|
||||||
output: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut txdata = vec![coinbase_tx.clone()];
|
|
||||||
txdata.extend_from_slice(&txs);
|
|
||||||
|
|
||||||
let mut block = Block { header, txdata };
|
|
||||||
|
|
||||||
let witness_root = block.witness_root();
|
|
||||||
let witness_commitment =
|
|
||||||
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
|
|
||||||
|
|
||||||
// now update and replace the coinbase tx
|
|
||||||
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
|
|
||||||
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
|
|
||||||
|
|
||||||
coinbase_tx.output.push(TxOut {
|
|
||||||
value: 0,
|
|
||||||
script_pubkey: coinbase_witness_commitment_script.into(),
|
|
||||||
});
|
|
||||||
block.txdata[0] = coinbase_tx;
|
|
||||||
|
|
||||||
// set merkle root
|
|
||||||
let merkle_root = block.merkle_root();
|
|
||||||
block.header.merkle_root = merkle_root;
|
|
||||||
|
|
||||||
assert!(block.check_merkle_root());
|
|
||||||
assert!(block.check_witness_commitment());
|
|
||||||
|
|
||||||
// now do PoW :)
|
|
||||||
let target = block.header.target();
|
|
||||||
while block.header.validate_pow(&target).is_err() {
|
|
||||||
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
|
|
||||||
}
|
|
||||||
|
|
||||||
let block_hex: String = serialize(&block).to_hex();
|
|
||||||
debug!("generated block hex: {}", block_hex);
|
|
||||||
|
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
|
||||||
|
|
||||||
let submit_result: serde_json::Value =
|
|
||||||
self.call("submitblock", &[block_hex.into()]).unwrap();
|
|
||||||
debug!("submitblock: {:?}", submit_result);
|
|
||||||
assert!(
|
|
||||||
submit_result.is_null(),
|
|
||||||
"submitblock error: {:?}",
|
|
||||||
submit_result.as_str()
|
|
||||||
);
|
|
||||||
|
|
||||||
self.wait_for_block(height as usize);
|
|
||||||
|
|
||||||
block.header.block_hash().to_hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
|
|
||||||
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
|
|
||||||
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
|
|
||||||
let best_hash = hashes.last().unwrap();
|
|
||||||
let height = self.get_block_info(best_hash).unwrap().height;
|
|
||||||
|
|
||||||
self.wait_for_block(height);
|
|
||||||
|
|
||||||
debug!("Generated blocks to new height {}", height);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn invalidate(&mut self, num_blocks: u64) {
|
|
||||||
self.electrum.block_headers_subscribe().unwrap();
|
|
||||||
|
|
||||||
let best_hash = self.get_best_block_hash().unwrap();
|
|
||||||
let initial_height = self.get_block_info(&best_hash).unwrap().height;
|
|
||||||
|
|
||||||
let mut to_invalidate = best_hash;
|
|
||||||
for i in 1..=num_blocks {
|
|
||||||
trace!(
|
|
||||||
"Invalidating block {}/{} ({})",
|
|
||||||
i,
|
|
||||||
num_blocks,
|
|
||||||
to_invalidate
|
|
||||||
);
|
|
||||||
|
|
||||||
self.invalidate_block(&to_invalidate).unwrap();
|
|
||||||
to_invalidate = self.get_best_block_hash().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.wait_for_block(initial_height - num_blocks as usize);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Invalidated {} blocks to new height of {}",
|
|
||||||
num_blocks,
|
|
||||||
initial_height - num_blocks as usize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reorg(&mut self, num_blocks: u64) {
|
|
||||||
self.invalidate(num_blocks);
|
|
||||||
self.generate(num_blocks, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
|
|
||||||
Address::from_str(
|
|
||||||
&self
|
|
||||||
.get_new_address(None, address_type)
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for TestClient {
|
|
||||||
type Target = RpcClient;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user