Compare commits

..

119 Commits

Author SHA1 Message Date
Steve Myers
fa4c73a4d1 Bump version to 0.19.0 2022-06-10 09:08:43 -07:00
Steve Myers
2bf8121b18 Update CHANGELOG.md to 0.19.0 2022-06-08 15:18:54 -07:00
Steve Myers
ed3ef94071 Bump version to 0.19.0-rc.1 2022-06-07 13:13:23 -07:00
Steve Myers
ed78d18f60 Merge bitcoindevkit/bdk#628: rpc: use importdescriptors with Core >= 0.21
e1a1372bae rpc: use `importdescriptors` with Core >= 0.21 (Alekos Filini)

Pull request description:

  ### Description

  Only use the old `importmulti` with Core versions that don't support descriptor-based (sqlite) wallets.

  Add an extra feature to test against Core 0.20 called `test-rpc-legacy`.

  This also makes us compatible with Core 23.0 and is thus a replacement for #613, which actually looking back at it was adding support for 23.0 but probably breaking older wallets by adding the extra argument to `createwallet`.

  I believe #613 should now only focus on getting the tests to work against 23.0, which is still important but not such a high priority as being compatible with the latest version of Core.

  Also fixes #598

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK e1a1372bae

Tree-SHA512: 7891b8ab2fc900ea2a186f64a32aea970f28a50339063ed0e1a8d13248e5c038b8fff3d9e26b93cb7daafd0c873379e64a28836dbe4e4b82f1983577a88971ff
2022-06-07 13:02:13 -07:00
Alekos Filini
e1a1372bae rpc: use importdescriptors with Core >= 0.21
Only use the old `importmulti` with Core versions that don't support
descriptor-based (sqlite) wallets.

Add an extra feature to test against Core 0.20 called `test-rpc-legacy`
2022-06-07 15:07:58 +02:00
Steve Myers
32699234b6 Merge bitcoindevkit/bdk#619: Fix index out of bound error
d9b9b3dc46 Fix InvalidColumnIndex error (Philipp Hoenisch)

Pull request description:

  This query returns 7 rows, so last row is index 6

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [x] I've added tests to reproduce the issue which are now passing

ACKs for top commit:
  danielabrozzoni:
    tACK d9b9b3dc46
  rajarshimaitra:
    tACK d9b9b3dc46

Tree-SHA512: 8a3d8a291daa4af86a2a2eacc31f002972dd9cdb9bf300a4b09e2e015c4a967dc4fa7e925afbcce8b104a01e1d7f7c8cb0badda8e1ac5ade511681f490c719d5
2022-06-05 10:13:49 -07:00
Alekos Filini
8fbe40a918 Merge bitcoindevkit/bdk#593: Add support for Taproot and tr() descriptors
20d36c71d4 Update CHANGELOG.md for Taproot (Alekos Filini)
ef08fbd3c7 Update to the newest release of rust-bitcoin (Alekos Filini)
5320c8353e taproot-tests: validate `tap_tree` in psbt outputs (Alekos Filini)
c1bfaf9b1e Add blockchain tests for parsing, signing, finalizing taproot core psbts (Steve Myers)
0643f76c1f taproot-tests: Add tests for the policy module (Alekos Filini)
89cb425e69 taproot-tests: Add test coverage for tx signing (Alekos Filini)
461397e590 taproot-tests: Test taproot key and script spend in the blockchain tests (Alekos Filini)
c67116fb55 policy: Consider `tap_key_origins` when looking for sigs in PSBTs (Alekos Filini)
572c3ee70d policy: Build `SatisfiableItem::*Signature` based on the context (Alekos Filini)
ff1abc63e0 policy: Refactor `PkOrF` into an enum (Alekos Filini)
308708952b Fix type inference for the `tr()` descriptor, add basic tests (Alekos Filini)
fe1877fb18 Support `tr()` descriptors in dsl (Alekos Filini)
cdc7057813 Add `tr()` descriptors to the `descriptor!()` macro (Alekos Filini)
c121dd0252 Use `tap_key_origins` in PSBTs to derive descriptors (Alekos Filini)
8553821133 Populate more taproot fields in PSBTs (Alekos Filini)
8a5a87b075 Populate `tap_key_origin` in PSBT inputs and outputs (Alekos Filini)
1312184ed7 Attach a context to our software signers (Alekos Filini)
906598ad92 Refactor signer traits, add support for taproot signatures (Alekos Filini)

Pull request description:

  ### Description

  This is a work-in-progress PR to update BDK to rust-bitcoin `0.28` which introduces taproot support and a few other improvements. While updating we also introduce taproot support in BDK.

  High level list of subtasks for this PR:
  - [x] Update rust-bitcoin and rust-miniscript
  - [x] Stop using deprecated structs
  - [x] Add taproot metadata to psbts
  - [x] Produce schnorr signatures
  - [x] Finalize taproot txs
  - [x] Support `tr()` descriptors in the `descriptor!()` macro
  - [x] Write a lot of tests
    - [x] Interoperability with other wallets (Core + ?)
       - [x] Signing/finalizing a psbt made by core
       - [x] Producing a psbt that core can sign and finalize
    - [x] Creating psbts
      - [x] Verify the metadata are correct
      - [x] Verify sighashes are applied correctly
      - [x] Create a tx with a foreign taproot and non-taproot utxo
    - [x] Signing psbts
      - [x] Signing for a key spend
      - [x] Signing for a script spend
      - [x] Signing with a single (wif) key
      - [x] Signing with an xprv (with and without knowing the utxo being spent in the db)
      - [x] Signing with weird sighashes
    - [x] Policy module
      - [x] Simple key spend
      - [x] More complex tap tree with a few keys
      - [x] Verify both `contribution` and `satisfaction` of a PSBT input
    - [x] Wallet module
      - [x] Generate addresses

  Fixes #63

  ### Notes to the reviewers

  #### Milestone

  I'm adding this to the `0.19` milestone because now that rust-bitcoin and rust-miniscript have been released we should not waiting too long to release a version of BDK that supports the new libraries.

  #### API Breaks

  Since this is an API-break because of the new version of rust-bitcoin and rust-miniscript, I'm also taking the chance to update a few things in our lib that I had been thinking about for a while.

  One example is the signer interface, which had that weird `sign_whole_tx()` method. This has now been removed, and the `Signer` trait replaced with `TransactionSigner` and `InputSigner`. I'm also starting to think that the signer should not only look at the psbt to figure out what to do, but ideally it should also receive some information about the descriptor (for example, the type) to simplify the code.

  One option is to add an extra parameter, but that would probably only be used by our internal signers and not much else (for example, if you ask an hardware wallet to sign, it will probably already know what kind of wallet you have).

  Another option is to wrap `PrivateKey` and `DescriptorXKey<ExtendedPrivKey>` which are the two internal signers we support with a struct that contains metadata about the descriptor, and then implement the signer traits on that struct. We could construct this in `Wallet::new()`, after miniscript parses the descriptor.

  #### MSRV Bump

  Due to the update of `rust-electrum-client`, which in turn depends on an updated `webpki`, we will have to bump our MSRV beacuse 1.46 is not supported by the new `webpki` version.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

Top commit has no ACKs.

Tree-SHA512: 44ec6c4e7fe0bc87862bb76581ddae7905c8796a4a130c90b48b73b4b7d01ea1a20043b8a0ff449fc2db2f7aecc490def8daee2d420809ded1ece7f085a48f55
2022-06-03 16:43:20 +02:00
Philipp Hoenisch
d9b9b3dc46 Fix InvalidColumnIndex error
This query returns 7 columns, so last columns is index 6
2022-06-03 15:28:43 +10:00
Alekos Filini
20d36c71d4 Update CHANGELOG.md for Taproot 2022-06-01 14:55:43 +02:00
Alekos Filini
ef08fbd3c7 Update to the newest release of rust-bitcoin 2022-06-01 14:51:40 +02:00
Alekos Filini
5320c8353e taproot-tests: validate tap_tree in psbt outputs
Co-authored-by: Daniela Brozzoni <danielabrozzoni@protonmail.com>
2022-06-01 14:51:38 +02:00
Steve Myers
c1bfaf9b1e Add blockchain tests for parsing, signing, finalizing taproot core psbts 2022-06-01 14:51:36 +02:00
Alekos Filini
0643f76c1f taproot-tests: Add tests for the policy module 2022-06-01 14:51:34 +02:00
Alekos Filini
89cb425e69 taproot-tests: Add test coverage for tx signing 2022-06-01 14:51:32 +02:00
Alekos Filini
461397e590 taproot-tests: Test taproot key and script spend in the blockchain tests
This is to ensure a Bitcoin node accepts our transactions
2022-06-01 14:51:30 +02:00
Alekos Filini
c67116fb55 policy: Consider tap_key_origins when looking for sigs in PSBTs
We used to only look at `bip32_derivations` which is only used for ECDSA
keys.
2022-06-01 14:51:28 +02:00
Alekos Filini
572c3ee70d policy: Build SatisfiableItem::*Signature based on the context
Also refactor our code to lookup signatures in PSBTs to use the context
2022-06-01 14:51:26 +02:00
Alekos Filini
ff1abc63e0 policy: Refactor PkOrF into an enum
For whatever reason we were using a struct as an enum, so we might as
well fix it in this PR since we are already breaking the API quite
badly.
2022-06-01 14:51:16 +02:00
Alekos Filini
308708952b Fix type inference for the tr() descriptor, add basic tests 2022-05-31 18:16:24 +02:00
Alekos Filini
fe1877fb18 Support tr() descriptors in dsl 2022-05-31 18:16:22 +02:00
Alekos Filini
cdc7057813 Add tr() descriptors to the descriptor!() macro 2022-05-31 18:16:21 +02:00
Alekos Filini
c121dd0252 Use tap_key_origins in PSBTs to derive descriptors 2022-05-31 18:16:17 +02:00
Alekos Filini
8553821133 Populate more taproot fields in PSBTs 2022-05-31 18:13:08 +02:00
Alekos Filini
8a5a87b075 Populate tap_key_origin in PSBT inputs and outputs 2022-05-31 18:06:59 +02:00
Alekos Filini
1312184ed7 Attach a context to our software signers
This allows the signer to know the signing context precisely without
relying on heuristics on the psbt fields.

Due to the context being static, we still have to look at the PSBT when
producing taproot signatures to determine the set of leaf hashes that
the key can sign for.
2022-05-27 11:48:50 +02:00
Alekos Filini
906598ad92 Refactor signer traits, add support for taproot signatures 2022-05-27 11:48:41 +02:00
Steve Myers
fbd98b4c5a Merge bitcoindevkit/bdk#605: Fix sqlite database set_utxo to insert or update utxos
35feb107ed [CI] Fix cont_integration test-blockchains to run all tests (Steve Myers)
2471908151 Update CHANGELOG with warning about sqlite-db deleted wallet data (Steve Myers)
0b1a399f4e Update sqlite schema with unique index for utxos, change insert_utxo to upsert (Steve Myers)
cea79872d7 Update database tests to verify set_utxo upserts (Steve Myers)

Pull request description:

  ### Description

  This PR fixes #591 by:
  1. Add sqlite `MIGRATIONS` statements to remove duplicate utxos and add unique utxos index on txid and vout.
  2. Do an upsert (if insert fails update) instead of an insert in `set_utxo()`.
  3. Update database::test::test_utxo to also verify `set_utxo()` doesn't insert duplicate utxos.

  ### Notes to the reviewers

  I verified the updated `test_utxo` fails as expected before my fix and passes after the fix. I tested the new migrations using the below `bdk-cli` command and a manually updated sqlite db with duplicate utxos.
  ```shell
  cargo run --no-default-features --features cli,sqlite-db,esplora-ureq -- wallet -w test1 --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" sync
  ```

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    utACK 35feb107ed - Code looks good, but I didn't do any local test to see if the db gets wiped

Tree-SHA512: 753c7a0cfd0e803b5e12f39181d9a718791c4ce229d5072e6498db75a7008e94d447b3d0b4b0c205e7a8f127f60102e12bac2d271b8bad3a3038856bfd54e99c
2022-05-24 08:25:24 -07:00
Alekos Filini
87b07456bd Merge bitcoindevkit/bdk#610: Populate the redeemScript for sh(wsh(sortedmulti()))
82de8b50da Populate the redeemScript for `sh(wsh(sortedmulti()))` (Alekos Filini)

Pull request description:

  ### Description

  Also explicitly match all the individual variants to ensure a similar problem
  doesn't happen again.

  Fixes #609

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK 82de8b50da

Tree-SHA512: 7cccae44679089b70e8d9df466a165debe17c4cf30ae11cb27357fbda830d3d7ce53161e414b354b1f2be46efe413b8e44132f5e6d625298b740d44112ca286a
2022-05-24 10:15:34 +02:00
Alekos Filini
82de8b50da Populate the redeemScript for sh(wsh(sortedmulti()))
Also explicitly match all the individual variants to ensure a similar problem
doesn't happen again.

Fixes #609
2022-05-23 21:02:42 +02:00
Steve Myers
35feb107ed [CI] Fix cont_integration test-blockchains to run all tests 2022-05-19 14:00:40 -07:00
Steve Myers
2471908151 Update CHANGELOG with warning about sqlite-db deleted wallet data 2022-05-19 13:20:54 -07:00
Steve Myers
0b1a399f4e Update sqlite schema with unique index for utxos, change insert_utxo to upsert 2022-05-19 13:20:11 -07:00
Steve Myers
cea79872d7 Update database tests to verify set_utxo upserts 2022-05-19 13:20:09 -07:00
Steve Myers
4c1749a13a Merge bitcoindevkit/bdk#604: unpinning dependency tokio to just 1
939a1156c6 unpinning dependency tokio to 1 (Richard Ulrich)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  unpinning dependency tokio to just 1

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK 939a1156c6

Tree-SHA512: 7b33522411f6b63d844d12cc86c8992aa4105c29e91dbef67362a6c600f3ef39e802ff5893054bf8feeb7fa5bf383765025096cbc2ccb9a6b83bc5c919a5a43d
2022-05-17 11:14:30 -07:00
Richard Ulrich
939a1156c6 unpinning dependency tokio to 1 2022-05-17 14:44:45 +02:00
Alekos Filini
e5486536ae Merge bitcoindevkit/bdk#606: Upgrade to rust-bitcoin 0.28
00164588f2 Stop using deprecated structs (Alekos Filini)
a16c18255c Upgrade to rust-bitcoin 0.28 and miniscript 7.0 (Alekos Filini)

Pull request description:

  ### Description

  Upgrade all our dependencies to work with the new release of rust-bitcoin

  ### Notes to the reviewers

  The commits in this pr were originally part of #593

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  rajarshimaitra:
    ACK 00164588f2

Tree-SHA512: eef7e94246e619686b4dfffd6e4cb685630fe2eaf9447f2f0b49ed2643d67f81c50e0d89b66267db4552a05e58f638d885eb7056270648403f716803fce9e275
2022-05-12 20:29:59 +02:00
Alekos Filini
00164588f2 Stop using deprecated structs 2022-05-12 17:31:48 +02:00
Alekos Filini
a16c18255c Upgrade to rust-bitcoin 0.28 and miniscript 7.0 2022-05-12 12:51:21 +02:00
Steve Myers
7aa2746c51 Merge bitcoindevkit/bdk#569: [blockchain] Add traits to reuse Blockchains across multiple wallets
8795da4839 wallet: Move `wallet_name_from_descriptor` above the tests (Alekos Filini)
9c405e9c70 [blockchain] Add traits to reuse `Blockchain`s across multiple wallets (Alekos Filini)
2d83af4905 Move testutils macro module before the others (Alekos Filini)

Pull request description:

  ### Description

  Add three new traits:
  - `StatelessBlockchain` is used to tag `Blockchain`s that don't have any wallet-specic state, i.e. they can be used as-is to sync multiple wallets.
  - `StatefulBlockchain` is the opposite of `StatelessBlockchain`: it provides a method to "clone" a `Blockchain` with an updated internal state (a new wallet checksum and, optionally, a different number of blocks to skip from genesis). Potentially this also allows reusing the underlying connection on `Blockchain` types that support it.
  - `MultiBlockchain` is a generalization of this concept: it's implemented automatically for every type that implements `StatefulBlockchain` and for every `Arc<T>` where `T` is a `StatelessBlockchain`. This allows a piece of code that deals with multiple sub-wallets to just get a `&B: MultiBlockchain` without having to deal with stateful and statless blockchains individually.

  These new traits have been implemented for Electrum, Esplora and RPC (the first two being stateless and the latter stateful). It hasn't been implemented on the CBF blockchain, because I don't think it would work in its current form (it throws away old block filters, so it's hard to go back and rescan).

  This is the first step for #549, as BIP47 needs to sync many different descriptors internally.

  It's also very useful for #486.

  ### Notes to the reviewers

  This is still a draft because:
  - I'm still wondering if these traits should "inherit" from `Blockchain` instead of the less-restrictive `WalletSync` + `GetHeight` which is the bare minimum to sync a wallet
  - I need to write tests, at least for rpc which is stateful
  - I need to add examples

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  rajarshimaitra:
    tACK 8795da4839

Tree-SHA512: 03f3b63d51199b26a20d58cf64929fd690e2530f873120291a7ffea14a6237334845ceb37bff20d6c5466fca961699460af42134d561935d77b830e2e131df9d
2022-05-10 21:35:28 -07:00
Alekos Filini
616aa8259a Merge bitcoindevkit/bdk#602: wrong Network path fixed
b4100a7189 wrong Network path fixed (Pedro Felix)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description
  This change needs to be made for this example to compile correctly.
  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  ### Notes to the reviewers

  <!-- In this section you can include notes directed to the reviewers, like explaining why some parts
  of the PR were done in a specific way -->

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK b4100a7189

Tree-SHA512: b4b7d186cabc7b3ed2d5b6411414d56eeee09c4e8d9d704a26f8817d6573b6b182d7ef1ff8139b6f4f6fb3e4ec99c2c09758804a6bb25051e45b0885c0def5e3
2022-05-10 17:25:57 +02:00
Alekos Filini
8795da4839 wallet: Move wallet_name_from_descriptor above the tests 2022-05-09 19:34:06 +02:00
Alekos Filini
9c405e9c70 [blockchain] Add traits to reuse Blockchains across multiple wallets
Add two new traits:
- `StatelessBlockchain` is used to tag `Blockchain`s that don't have any
  wallet-specic state, i.e. they can be used as-is to sync multiple wallets.
- `BlockchainFactory` is a trait for objects that can build multiple
  blockchains for different descriptors. It's implemented automatically
  for every `Arc<T>` where `T` is a `StatelessBlockchain`. This allows a
  piece of code that deals with multiple sub-wallets to just get a
  `&B: BlockchainFactory` to sync all of them.

These new traits have been implemented for Electrum, Esplora and RPC
(the first two being stateless and the latter having a dedicated
`RpcBlockchainFactory` struct). It hasn't been implemented on the CBF
blockchain, because I don't think it would work in its current form
(it throws away old block filters, so it's hard to go back and rescan).

This is the first step for #549, as BIP47 needs to sync many different
descriptors internally.

It's also very useful for #486.
2022-05-09 19:34:04 +02:00
Alekos Filini
2d83af4905 Move testutils macro module before the others
This allows using the `testuitils` macro in their tests as well
2022-05-09 19:30:51 +02:00
Pedro Felix
b4100a7189 wrong Network path fixed
Signed-off-by: Pedro Felix <NEWAGAIN@Pedros-MacBook-Pro.local>
Signed-off-by: Pedro Felix <p.felixgarcia@gmail.com>
2022-05-06 17:30:21 -07:00
Alekos Filini
cfb67fc25b Merge bitcoindevkit/bdk#600: Change wallet::get_funded_wallet to return Wallet<AnyDatabase>
e7a56a9268 Change wallet::get_funded_wallet to return Wallet<AnyDatabase> (Steve Myers)

Pull request description:

  ### Description

  Change testing function `wallet::get_funded_wallet` to return `Wallet<AnyDatabase>` instead of `Wallet<MemoryDatabase>`. This will allow us to use this function for testing `bdk-ffi` which only works with `Wallet<AnyDatabase>`.

  ### Notes to the reviewers

  This is required to complete https://github.com/bitcoindevkit/bdk-ffi/pull/148.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  afilini:
    ACK e7a56a9268

Tree-SHA512: 47b53ab6dcee63fc7b24666d3cf9a0ad832782081dd2fe92961c8c9b4c302df90db96b0b518af71d6cbc85319434971219100f8cedb35ce7212d944db29a4295
2022-05-06 10:28:57 +02:00
Alekos Filini
7201e09db9 Merge bitcoindevkit/bdk#599: Fix typo in docs for TxBuilder allow_shrinking method
4628a10191 Fix typo in docs for TxBuilder allow_shrinking method (thunderbiscuit)

Pull request description:

  ### Description
  This PR fixes a small typo in the documentation for the `allow_shrinking()` method on the `TxBuilder`.

  ### Notes to the reviewers
  The sentence
  ```txt
  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.
  ```
  was changed for
  ```txt
  Explicitly tells the wallet that it is allowed to reduce the amount 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.
  ```

  To reflect the fact that it's the _amount_ of the output that is being reduced in order to leave more for the fees.

  ### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    ACK 4628a10191

Tree-SHA512: ca135764d78144295fdbdb4673ca6dec153c6f83ee7fb5d7d3051e0d46b04d23b2f3c3fd66e44491da842c778e72b52f8996d00e4c20986d7ccb11587f0d7d27
2022-05-06 10:27:00 +02:00
Steve Myers
e7a56a9268 Change wallet::get_funded_wallet to return Wallet<AnyDatabase> 2022-05-05 16:43:10 -07:00
thunderbiscuit
4628a10191 Fix typo in docs for TxBuilder allow_shrinking method 2022-05-05 15:15:04 -04:00
Steve Myers
2f325328c5 Merge bitcoindevkit/bdk#596: Bump MSRV to 1.56
cca69481eb Bump MSRV to 1.56 (Alekos Filini)

Pull request description:

  ### Description

  Following the discussion in #331, bump the MSRV to `1.56`. We already have other PRs bumping it to at least `1.51` (#593), but I'm felling like we are always lagging behind and our CI breaks regularly. As @LLFourn suggested, this PR makes a relatively large bump, hoping this buys us enough time to finish splitting up BDK, which will allow us to have a lower MSRV for the "core" crate.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK cca69481eb

Tree-SHA512: bc6572dc94e1c4cbb6d21f6e06a9730af5763fb4811311a61a6a6ec850b5a65664a21e4a7070a3ebcd702529fbba97b2e9a43c1277b9b9f092e194f16a39bc1a
2022-05-04 09:57:30 -07:00
Alekos Filini
cca69481eb Bump MSRV to 1.56 2022-05-04 17:29:07 +02:00
Alekos Filini
6e8744d59d Merge bitcoindevkit/bdk#557: add OldestFirstCoinSelection
6931d0bd1f add private function select_sorted_utxso to be resued by multiple CoinSelection impl (KaFai Choi)
545beec743 add OldestFirstCoinSelection (KaFai Choi)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description
  This PR is to add `OldestFirstCoinSelection`. See this issue for detail https://github.com/bitcoindevkit/bdk/issues/120
  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  ### Notes to the reviewers
  Apologize in advance if the quality of this PR is too low.(I am newbie in both bitcoin wallet and rust).

  While this PR seemed very straight-forward to me in the first glance, it's actually a bit more complicated than I thought as it involves calling DB get the blockheight before sorting it.

  The current implementation should be pretty naive but I would like to get some opinion to see if I am heading to a right direction first before working on optimizations like

  ~~1. Avoiding calling DB for optional_utxos if  if the amount from required_utxos are already enough.~~ Probably not worth to do such optimization to keep code simpler?

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  afilini:
    ACK 6931d0bd1f

Tree-SHA512: d297bbad847d99cfdd8c6b1450c3777c5d55bc51c7934f287975c4d114a21840d428a75a172bfb7eacbac95413535452b644cab971efb8c0b5caf0d06d6d8356
2022-04-25 16:46:41 +02:00
Alekos Filini
79f73df545 Merge bitcoindevkit/bdk#582: Expose bip39::Error
c752ccbdde Expose bip39::Error (志宇)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  Expose `bip39::Error` (fixes #581 )

  ### Notes to the reviewers

  I am aware that the `bip39` module plans to be rewritten (as per #561 ), however this seems like a rather straightforward and quick change that may be useful in the short/mid term.

  ### Checklists

  #### Bugfixes:

  * [x] Expose `bip39::Error`

ACKs for top commit:
  afilini:
    ACK c752ccbdde

Tree-SHA512: 98b7ac1ba88aed07d9160830ee80496c32d531c15ada0e9b50a97f0883fbfced22fa83a7c7f8366aadb7e7a667d8a63dde869d31cc375206d277e55b2ec3089d
2022-04-25 16:14:46 +02:00
Alekos Filini
9db8d3a410 Merge bitcoindevkit/bdk#584: Release 0.18.0
b5c8ce924b Bump version to 0.19.0-dev (Steve Myers)
e3ce50059f Bump version to 0.18.0 (Steve Myers)
8a2a6bbcee Add `n:` wrapper to vulnerable scripts (Alekos Filini)
122e6e7140 Bump 'miniscript' dependency version to '^6.1' (Steve Myers)
9ed36875f1 export: Rename `WalletExport` to `FullyNodedExport` (Alekos Filini)
f90e3f978e Update changelog with addition of keychain to AddressInfo (Steve Myers)
68e1b32d81 Bump version to 0.18.0-rc.1 (Steve Myers)

Pull request description:

  ### Description

  Release 0.18.0.

  ### Notes to the reviewers

  This branch also includes the following changes which are not yet in the `master` branch:

  - Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
  - Bump `miniscript` dependency version to `^6.1`.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  afilini:
    ACK b5c8ce924b

Tree-SHA512: d6a8c1ca2c13d3a84704592053cc450557c96229b0092e655ccf97775c3e0a23e9e1499cf8c9fc5a24fc0665ec0333d0ec6d6b06cb15eb2e14c033f06c3032c2
2022-04-21 17:17:32 +02:00
Steve Myers
b5c8ce924b Bump version to 0.19.0-dev 2022-04-20 10:31:31 -07:00
Steve Myers
e3ce50059f Bump version to 0.18.0 2022-04-20 09:58:13 -07:00
Alekos Filini
8a2a6bbcee Add n: wrapper to vulnerable scripts 2022-04-20 18:25:26 +02:00
Steve Myers
122e6e7140 Bump 'miniscript' dependency version to '^6.1' 2022-04-20 08:16:53 -07:00
Steve Myers
1018bb2b17 Merge bitcoindevkit/bdk#589: export: Rename WalletExport to FullyNodedExport
9ed36875f1 export: Rename `WalletExport` to `FullyNodedExport` (Alekos Filini)

Pull request description:

  ### Description

  The `WalletExport` type can still be used as an alias to `FullyNodedExport` but it has been marked as deprecated and will be removed in the next release.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  bucko13:
    Concept ACK 9ed3687
  notmandatory:
    ACK 9ed36875f1

Tree-SHA512: fe07307092d3040b30ae96901d5fc4afecee635412bca4a0d2e78d90f07990e7bbcd6b87ed923bf5804385356f419cacadbfefbaf97ebc9a622e34548438ee00
2022-04-19 19:21:11 -07:00
Alekos Filini
9ed36875f1 export: Rename WalletExport to FullyNodedExport
The `WalletExport` type can still be used as an alias to
`FullyNodedExport` but it has been marked as deprecated and will be
removed in the next release.
2022-04-19 11:44:50 +02:00
Steve Myers
502882d27c Merge branch 'master' into release/0.18.0 2022-04-14 10:06:47 -07:00
Steve Myers
a328607d27 Merge bitcoindevkit/bdk#583: Improve keys traits, simplify rpcwallet example
44758f9483 Simplify the `rpcwallet` example using the improved keys traits (Alekos Filini)
c350064dae Update changelog changes in `keys` mod (Alekos Filini)
92746440db [keys] Make `GenerateKey<K>` clonable if K is (Alekos Filini)
e4eb95fb9c [keys] Implement `DerivableKey<Ctx>` for `(GeneratedKey<Mnemonic, Ctx>, Option<String>)` (Alekos Filini)

Pull request description:

  ### Description

  I recently looked at the `rpcwallet` example and immediately thought the key generation part was very confusing. I made a few little changes to our traits and now I believe it's much better.

  I'm copying the next few paragraphs from the first commit in this PR which explains why it's better not to extract the key from the `GeneratedKey` wrapper:

  > This lets us use a tuple of (generated mnemonic, optional passphrase) as a `DerivableKey` directly, without extracting the inner mnemonic from the `GeneratedKey` wrapper. For BIP39 keys specifically it doesn't make much difference because the mnemonic format doesn't encode the network, but in general this is not the case and having a consistent API will make it harder for people to make mistakes.

  > To explain why we should not extract the key: some key formats (like BIP32 extended keys) are network-specific, meaning that if somebody tries to use a Testnet key on a Mainnet BDK wallet, it won't work.

  > However, when we generate a new key we would like to be able to use that key on any network, but we need to set some kind of placeholder for the `network` field in the structure. This is why (or at least one of the reasons why) we wrap the key in the `GeneratedKey` struct: we keep track of the "valid_networks" separately, which means that even if we set our BIP32 xprv to be a "Mainnet" key, once we go try creating a wallet with that key BDK is smart enough to understand that `GeneratedKey`s have  their own separate set of valid networks and it will use that set to validate whether the key can be used in the wallet or not.

  ### Notes to the reviewers

  I'm adding this to the `0.18` milestone even if the feature freeze is today. I don't think we should wait for this PR to start the release branch, but since it's just a minor addition to our traits I believe we could merge this afterwards to the release branch if we manage to get it reviewed and ready within the next week.

  Otherwise it'll just wait for `0.19`, it's not a big deal.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    tACK 44758f9483

Tree-SHA512: 31a8b5a3cd56c7c39688c4804026a8b6cee950c50ea880efcddc150ef523e2ddcc544831bf996a58b5cf3b7bc4a086c1008dc0728604e5134d331c68855c2d5b
2022-04-14 09:58:10 -07:00
Steve Myers
f90e3f978e Update changelog with addition of keychain to AddressInfo 2022-04-13 13:51:12 -07:00
Steve Myers
68e1b32d81 Bump version to 0.18.0-rc.1 2022-04-13 13:40:16 -07:00
Alekos Filini
44758f9483 Simplify the rpcwallet example using the improved keys traits 2022-04-13 12:56:43 +02:00
Alekos Filini
c350064dae Update changelog changes in keys mod 2022-04-13 12:56:04 +02:00
Alekos Filini
92746440db [keys] Make GenerateKey<K> clonable if K is 2022-04-13 12:55:23 +02:00
Alekos Filini
e4eb95fb9c [keys] Implement DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>)
This lets us use a tuple of (generated mnemonic, optional passphrase) as
a `DerivableKey` directly, without extracting the inner mnemonic from the
`GeneratedKey` wrapper. For BIP39 keys specifically it doesn't make much
difference because the mnemonic format doesn't encode the network, but in
general this is not the case and having a consistent API will make it
harder for people to make mistakes.

To explain why we should not extract the key: some key formats (like
BIP32 extended keys) are network-specific, meaning that if somebody
tries to use a Testnet key on a Mainnet BDK wallet, it won't work.

However, when we generate a new key we would like to be able to use that
key on any network, but we need to set some kind of placeholder for the
`network` field in the structure. This is why (or at least one of the
reasons why) we wrap the key in the `GeneratedKey` struct: we keep track
of the "valid_networks" separately, which means that even if we set our
BIP32 xprv to be a "Mainnet" key, once we go try creating a wallet with
that key BDK is smart enough to understand that `GeneratedKey`s have
their own separate set of valid networks and it will use that set to
validate whether the key can be used in the wallet or not.
2022-04-13 12:37:27 +02:00
Alekos Filini
c307bacb9c Merge bitcoindevkit/bdk#577: Deprecate Database::flush() function
a111d25476 Deprecate Database::flush() function (Steve Myers)

Pull request description:

  ### Description

  The Database::flush() function is only needed for the sled database on mobile, instead for mobile use the sqlite database.

  ### Notes to the reviewers

  This PR is in preparation for removing the Database::flush() function. See https://github.com/bitcoindevkit/bdk/pull/575#issuecomment-1082916667.

  After the `release/0.18.0` feature freeze branch is created then #575 should be merged.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  afilini:
    ACK a111d25476

Tree-SHA512: 18434dc95dbef47118a0d4fface908bdf920a7ffcef927b36bb740c15f8efcf11dea9198b364648f16f74aaec4aa18e92a3c5e925299b2f3b9d69e566f89e790
2022-04-13 12:00:35 +02:00
Steve Myers
a111d25476 Deprecate Database::flush() function
The Database::flush() function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
2022-04-12 14:16:09 -07:00
志宇
c752ccbdde Expose bip39::Error 2022-04-07 19:50:17 +08:00
Steve Myers
0621ca89d5 Merge bitcoindevkit/bdk#579: Faster sync by collecting esplora ureq thread handles
adef166b22 Create vector of thread handles to spawn threads (nickfarrow)

Pull request description:

  ### Description
  Speeds up esplora ureq syncing. Taken from https://github.com/bitcoindevkit/bdk/pull/560

  The current sync just creates a map of scripts to joinhandles which doesn't yet spawn the sync threads due to lazy evaluation. In the following `handles.map()`, the thread handles are *sequentially* evaluated and joined. With the fix, the handles are collected so that the threads spawn in parallel, and then joined

  ### Notes to the reviewers

  I had to add a `#[allow(clippy::needless_collect)]` so that it wouldn't complain about collecting and then iterating. (Perhaps clippy is partially responsible for this issue!)

  Tested sync performance by doing a fresh sync on an existing [gun](https://gun.fun) wallet.
  ```
  ---- Before fix ---
  real0m13.121s
  real0m13.367s
  real0m13.211s

  ---- After fix ----
  real0m5.516s
  real0m5.251s
  real0m5.594s
  ```

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK adef166b22

Tree-SHA512: 47c617117afde9b4706bfa63759bf06d1ec60ff95d8a80931e5b7e40e3293c855d2f7dac0c681173d43aecf77201a842e739b82291da09ac81909cf526a51c8d
2022-04-06 12:42:03 -07:00
nickfarrow
adef166b22 Create vector of thread handles to spawn threads
Signed-off-by: nickfarrow <nick@nickfarrow.com>
2022-04-06 16:10:59 +10:00
Steve Myers
213f18f7b7 Merge bitcoindevkit/bdk#562: Expose more getters in Wallet and other useful descriptor traits
1b9014846c Update changelog (Alekos Filini)
86abd8698f [descriptor] Expose utilities to deal with derived descriptors (Alekos Filini)
0d9c2f76e0 [export] Use the new getters on `Wallet` to generate export JSONs (Alekos Filini)
63d5bcee93 [wallet] Add more getters (Alekos Filini)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  Expose more getters and internal utilities for people who need to work with descriptors.

  A good example of how this can be leveraged is in the second commit, which refactors the wallet export functionality to use the new public APIs rather than using members on `Wallet` directly.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK 8cd055090d

Tree-SHA512: 3e8833670ebc56316fce01fc572fcc9391d602ef85f0cde8edcb446295570a9012e18f6ba8af0984153e4688f66f7eea6803ef610ceb395867e464e05c01c137
2022-04-04 20:53:18 -07:00
Steve Myers
8cd055090d Merge branch 'master' into feature/more-getters 2022-04-04 19:32:55 -07:00
Alekos Filini
1b9014846c Update changelog 2022-04-04 18:57:33 -07:00
Steve Myers
9c0141b5e3 Merge bitcoindevkit/bdk#563: update AddressInfo struct
2698fc0219 update AddressInfo struct (eunoia_1729)

Pull request description:

  ### Description

  Resolves #541.
  - Updates `AddressInfo` struct to include `keychainKind`
  - Updates the related `get_address` functions to pass in this field
  - Updates corresponding tests
  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

Top commit has no ACKs.

Tree-SHA512: b1d22e5f322e1a35390fb06a5185457bd53af7c4a9c4e6e2fd5e93b967fa3f56af5848ac314e63e2b0becd60dbc8a4eeb761a2bdebfe8ba43ce49b13da4190b2
2022-04-04 17:36:16 -07:00
eunoia_1729
2698fc0219 update AddressInfo struct 2022-04-04 11:14:00 +05:30
KaFai Choi
6931d0bd1f add private function select_sorted_utxso to be resued by multiple CoinSelection impl 2022-04-02 10:59:08 +07:00
KaFai Choi
545beec743 add OldestFirstCoinSelection 2022-04-02 10:59:04 +07:00
Steve Myers
bac15bb207 Merge bitcoindevkit/bdk#554: Fix hierarchy of headers on docs landing page
364ad95e85 Fix hierarchy of headers on docs landing page (thunderbiscuit)

Pull request description:

  ### Description

  This PR fixes the hierarchy of headers on the lib.rs docs. I noticed it as I was reading through the docs. The new hierarchy is the following:

  - About (h1)
  - A tour of BDK (h1)
  - Examples (h1)
    - Sync the balance of a descriptor (h2)
    - Generate a few addresses (h2)
    - Create a transaction (h2)
    - Sign a transaction (h2)
  - Feature flags (h1)
  - Internal features (h1)

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  rajarshimaitra:
    tACK 364ad95e85

Tree-SHA512: b4439a200c320a1815c572ac36b81c4d684209cb5cf1b90d3f761893ad672a270722be7d7b61d77434a7f98c4276d6fc9694d0c86cacf03c72dc35ca17b5e7d3
2022-04-01 19:33:21 -07:00
Steve Myers
06b80fdb15 Merge branch 'master', commit 'refs/pull/566/head' of github.com:bitcoindevkit/bdk 2022-03-30 09:54:34 -07:00
Steve Myers
ff6db18726 Merge bitcoindevkit/bdk#519: Add wallet creation example using RPC
8a98e69e78 Add rpc wallet creation example (rajarshimaitra)

Pull request description:

  ### Description

  As mentioned in https://github.com/bitcoindevkit/bitcoindevkit.org/issues/83#issuecomment-1005190174 it would be helpful
  to have wallet creation example code in the `example` directory.

  This is an attempt to create the most simplistic bdk wallet with a RPC backend and demonstrate receiving, creating, signing
  and broadcasting transaction.

  The code is a refinement of the RPC tutorial https://bitcoindevkit.org/blog/bitcoin-core-rpc-demo/ with more elaborate doc comments.

  ### Notes to the reviewers

  To automate the background RPC process `electrsd` is used which is already a `dev-dependency` of bdk.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    tACK 8a98e69e78

Tree-SHA512: f00c1ce57ef91c3d3ab2ce08af3571c41bbe98f18f705c61ba4217c2f8061196e5eaf6122742293cd6fa16939bbbfacd0f8506cd4151b55f19a05325fd94a74a
2022-03-29 09:56:09 -07:00
Alekos Filini
86abd8698f [descriptor] Expose utilities to deal with derived descriptors 2022-03-25 11:18:56 +01:00
Alekos Filini
0d9c2f76e0 [export] Use the new getters on Wallet to generate export JSONs 2022-03-25 11:18:54 +01:00
Alekos Filini
63d5bcee93 [wallet] Add more getters 2022-03-25 11:18:52 +01:00
rajarshimaitra
8a98e69e78 Add rpc wallet creation example
This adds an example wallet creation code with sled DB and RPC
Blockchain.

The backend RPC is managed using electrsd::bitcoind
2022-03-23 11:59:51 +05:30
Steve Myers
c6eeb7b989 Add sqlite-bundled feature 2022-03-22 10:00:55 -05:00
Steve Myers
3334c8da07 Merge commit 'refs/pull/564/head' of github.com:bitcoindevkit/bdk 2022-03-21 09:58:01 -05:00
Steve Myers
ce09203431 Bump version to 0.17.1-dev 2022-03-18 09:35:46 -05:00
Steve Myers
cac312d34f Bump version to 0.17.0 2022-03-17 11:17:55 -05:00
Steve Myers
4b1be68965 Merge bitcoindevkit/bdk#545: Update electrsd to v0.15
1803f5ea8a Update `electrsd` to v0.15 (Alekos Filini)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  PR RCasatta/electrsd#33 has been merged and the new changes released as part of `v0.15.0`: the PR allows setting an env variable (`ELECTRSD_EXE`) to use a custom electrsd binary, which can be useful when the one that is downloaded automatically doesn't work for whatever reason.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  LLFourn:
    ACK 1803f5e
  notmandatory:
    ACK 1803f5ea8a

Tree-SHA512: 01d84763a5acf3aa1686b91ba7cb0c7ec3a0ca6c7b9bac29aa74a72f54ed31c476b7a8b0f8c6c8a8ccf530ad8a19e90e144ac4cacbe436699ef9e319b5732f1c
2022-03-17 10:50:41 -05:00
LLFourn
559cfc4373 Fix pre-segwit inputs with esplora 2022-03-17 10:37:03 -05:00
Steve Myers
1e9a684b54 Merge bitcoindevkit/bdk#571: Fix pre-segwit inputs with esplora
52bc63e48f Fix pre-segwit inputs with esplora (LLFourn)

Pull request description:

  Unexpectedly pre-segwit inputs have an empty JSON witness field in esplora.

  Fixes #570

  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK 52bc63e48f
  afilini:
    ACK 52bc63e48f

Tree-SHA512: 1da82aeb2739111e1a516d94c79fe7d7c7879526a8cd780dcd63ff5ae9ccb1bed4eb20e5c575a45e37b6d1818d63ce6d3812e7b9ae34ebb02bc190a47b9750f8
2022-03-17 10:06:05 -05:00
LLFourn
52bc63e48f Fix pre-segwit inputs with esplora 2022-03-16 10:39:53 +11:00
Alekos Filini
9a6db15d26 Merge bitcoindevkit/bdk#515: Never delete spent utxos from the database
f2f0efc0b3 Never delete spent utxos from the database (Daniela Brozzoni)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  A `is_spent` field is added to LocalUtxo; when a txo is spent we set
  this field to true instead of deleting the entire utxo from the
  database.
  This allows us to create txs double-spending txs already in blockchain.
  Listunspent won't return spent in mempool utxos, effectively excluding them from the coin selection and balance calculation
  Fixes #414

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've updated `CHANGELOG.md`
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  afilini:
    Re-ACK f2f0efc

Tree-SHA512: 7984b69c56b3b12746c2301072d637dea1346730aa132f1ea67c5bd8bf685bd557fc7cbdc2fc1185e63c71db07d36cd979514921b2c83c55737cd4b96102377e
2022-03-14 16:06:23 +01:00
Steve Myers
52bcd105eb Bump version to 0.17.0-rc.1 2022-03-10 17:01:44 -06:00
Alekos Filini
1803f5ea8a Update electrsd to v0.15 2022-03-10 12:03:15 +01:00
Daniela Brozzoni
f2f0efc0b3 Never delete spent utxos from the database
A `is_spent` field is added to LocalUtxo; when a txo is spent we set
this field to true instead of deleting the entire utxo from the
database.
This allows us to create txs double-spending txs already in blockchain.
Listunspent won't return spent utxos, effectively excluding them from the
coin selection and balance calculation
2022-03-10 11:58:23 +01:00
Steve Myers
3e4678d8e3 Merge bitcoindevkit/bdk#535: Remove blockchain from wallet
0cc4700bd6 Fix typo in CHANGELOG and doc in wallet/mod.rs (Steve Myers)
660faab1e2 Fix typo in CHANGELOG (LLFourn)
45767fcaf7 Remove max_addresses sync param (LLFourn)
fbb50ad1c8 apply doc suggestions from @notmandatory (Lloyd Fournier)
035307ef54 [rpc] Filter out unrelated transactions (LLFourn)
c0e75fc1a8 Split get_tx into its own trait (LLFourn)
dcd90f8b61 Restore but depreciate new_offline (LLFourn)
410a51355b Add SyncOptions as the second argument to Wallet::sync (LLFourn)
326bfe82a8 Remove Blockchain from wallet (LLFourn)

Pull request description:

  While trying to finish #490 I thought that it'd be better to try the idea of getting rid of a lot of the async macros and just having tow different traits for sync and async stuff. While trying to do that I felt that this needed to be done first.

  The goal of this change is to decouple the wallet from the blockchain trait. A wallet is something that keeps track of UTXOs and transactions (and can sign things). The only reason it should need to talk to the blockchain is if doing a `sync`. So we remove all superfluous calls to the blockchain and precisely define the requirements for the blockchain to be used to sync with two new traits: `WalletSync` and `GetHeight`.

  1. Stop requesting height when wallet is created
  2. `new_offline` is just `new` now.
  3. a `WalletSync + GetHeight` is now the first argument to `sync`.
  4. `SyncOptions` replaces the existing options to `sync` and allows for less friction when making breaking changes in the fuutre (no more noop_progress!).
  5. We `Box` the `Progress` now to avoid type parameters
  6. broadcast has been removed from `Wallet`. You just use the blockchain directly to broadcast now.

  ### Notes to the reviewers

  This needs #502 before it can be merged but can reviewed now.
  Be on the look up for stale documentation from this change.
  Our doc build looks broken because of ureq or something.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing
  * [x] I've updated `CHANGELOG.md`

Top commit has no ACKs.

Tree-SHA512: a8f3e21e45359be642b1f30d4ac1ba74785439e1b770dbeab0a5a4b8aab1eef4bb6b855aea4382e289257bc890fa713ca832a8b6c9655f7a59e96d412b4da3e6
2022-03-09 11:15:52 -06:00
Steve Myers
0cc4700bd6 Fix typo in CHANGELOG and doc in wallet/mod.rs 2022-03-08 20:02:47 -06:00
LLFourn
660faab1e2 Fix typo in CHANGELOG 2022-03-08 14:00:29 +11:00
LLFourn
45767fcaf7 Remove max_addresses sync param
You can do this with ensure_addresses_cached if you really want to.
2022-03-07 10:44:41 +11:00
LLFourn
d03aa85108 Merge branch 'master' into remove-blockchain-from-wallet 2022-03-03 13:13:45 +11:00
Steve Myers
adf7d0c126 Merge bitcoindevkit/bdk#537: refactor wallet address caching into its own public method
edf2f0ce06 refactor wallet address caching into its own public method for offline wallet use (a5an0)

Pull request description:

  ### Description

  Currently, the only way to ensure that a wallet's internal database has addresses loaded and cached is through `Wallet::sync`: that function generates and caches up to a number of addresses if they aren't already in the database, and then uses the wallet's blockchain client to sync those addresses. If you are using an offline wallet, there is no mechanism to ensure that the database has addresses loaded. This is a problem for usecases like an offline wallet being used as a multisig signer and wanting to validate change addresses as `Wallet::is_mine` will only work properly if the owned-address is loaded in the database.

  This PR takes the address caching functionality out of `Wallet::sync` and puts it in a new public method that is available to offline wallets. `Wallet::sync` uses this method internally.

  ### Checklists

  #### All Submissions:

  * [X] I've signed all my commits
  * [X] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [X] I ran `cargo fmt` and `cargo clippy` before committing
  * [X] I've added docs for the new feature
  * [X] I've updated `CHANGELOG.md`

Top commit has no ACKs.

Tree-SHA512: 8ddc58d71457163bb20ff663ac508feb4e77000688161b63841a94db30b3f29f60f35fa2467bd99546123148873e3aed11e2f13ae6cbceda6605b83c227d9079
2022-03-01 21:28:31 -08:00
Steve Myers
4291f84d79 Merge branch 'master' into offline-address-cache 2022-03-01 21:00:56 -08:00
Steve Myers
f0188f49a8 Merge bitcoindevkit/bdk#522: Add API for internal addresses
022256c91a Fix comment on peek_address (Lloyd Fournier)
00f0901bac Add API for internal addresses (LLFourn)

Pull request description:

  There are good reasons for applications to need to get internal
  addresses too. For example creating a transactions that splits an output
  into several smaller ones.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK 022256c91a

Tree-SHA512: 0ead6669c9974332708ae6cba1a5be69cd4b3b6c7a21896efdce178d4406265ba072e1cf02d1913178d0a551f3b276d99b75676c632833964f87484e262a61b1
2022-03-01 20:54:07 -08:00
a5an0
edf2f0ce06 refactor wallet address caching into its own public method for offline wallet use 2022-02-25 12:15:08 -05:00
thunderbiscuit
364ad95e85 Fix hierarchy of headers on docs landing page 2022-02-24 10:48:47 -05:00
Lloyd Fournier
fbb50ad1c8 apply doc suggestions from @notmandatory
Co-authored-by: Steve Myers <github@notmandatory.org>
2022-02-24 20:59:21 +11:00
LLFourn
035307ef54 [rpc] Filter out unrelated transactions
For some reason while doing the "remove blockchain from wallet PR" I
changed the tests around in what I thought was a benign way. But it
meant (apparently) that both parties "test_sync_double_receive" were
using the same "blockchain". This meant that when the blockchain was RPC
they both imported their addresses to it and got each other's results
when syncing. This bugged out the sync and this commit fixes that.
2022-02-24 20:39:00 +11:00
LLFourn
c0e75fc1a8 Split get_tx into its own trait
to make supporting verify_tx easier
2022-02-24 20:39:00 +11:00
LLFourn
dcd90f8b61 Restore but depreciate new_offline 2022-02-24 20:39:00 +11:00
LLFourn
410a51355b Add SyncOptions as the second argument to Wallet::sync
The current options are awkward and it would be good if we could
introduce more in the future without breaking changes.
2022-02-24 20:39:00 +11:00
LLFourn
326bfe82a8 Remove Blockchain from wallet
Although somewhat convenient to have, coupling the Wallet with
the blockchain trait causes development friction and complexity.
What if sometimes the wallet is "offline" (no access to the blockchain)
but sometimes its online?
The only thing the Wallet needs the blockchain for is to sync.
But not all applications will even use the sync method and the sync
method doesn't require the full blockchain functionality.
So we instead pass the blockchain in when we want to sync.

- To further reduce the coupling with blockchain I removed the get_height call from `new` and just use the height of the
last sync in the database.
- I split up the blockchain trait a bit into subtraits.
2022-02-24 20:39:00 +11:00
Steve Myers
b23a0747b5 Merge commit 'refs/pull/552/head' of github.com:bitcoindevkit/bdk 2022-02-23 14:10:28 -08:00
Steve Myers
552765bb58 Bump version to 0.16.2-dev 2022-02-19 13:10:04 -08:00
Steve Myers
f3e479fa7f Bump version to 0.16.1 2022-02-19 12:46:17 -08:00
Steve Myers
5698c683c6 Pin tokio version to ~1.14 2022-02-19 11:33:57 -08:00
Steve Myers
a83aa0461c [ci] Pin nightly docs workflow rust version to nightly-2022-01-25 2022-02-19 11:33:01 -08:00
42 changed files with 4262 additions and 1523 deletions

View File

@@ -10,9 +10,9 @@ jobs:
strategy:
matrix:
rust:
- version: 1.56.0 # STABLE
- version: 1.60.0 # STABLE
clippy: true
- version: 1.46.0 # MSRV
- version: 1.56.0 # MSRV
features:
- default
- minimal
@@ -28,6 +28,7 @@ jobs:
- async-interface
- use-esplora-reqwest
- sqlite
- sqlite-bundled
steps:
- name: checkout
uses: actions/checkout@v2
@@ -89,12 +90,19 @@ jobs:
matrix:
blockchain:
- name: electrum
testprefix: blockchain::electrum::test
features: test-electrum,verify
- name: rpc
testprefix: blockchain::rpc::test
features: test-rpc
- name: rpc-legacy
testprefix: blockchain::rpc::test
features: test-rpc-legacy
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-reqwest,verify
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-ureq,verify
steps:
- name: Checkout
@@ -113,8 +121,8 @@ jobs:
toolchain: stable
override: true
- name: Test
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.testprefix }}::bdk_blockchain_tests
check-wasm:
name: Check WASM
runs-on: ubuntu-20.04

View File

@@ -6,10 +6,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
## [v0.19.0] - [v0.18.0]
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
- New MSRV set to `1.56`
- Unpinned tokio to `1`
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
- Upgrade to rust-bitcoin `0.28`
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
- Update `PkOrF` in the policy module to become an enum
- Add experimental support for Taproot, including:
- Support for `tr()` descriptors with complex tapscript trees
- Creation of Taproot PSBTs (BIP-371)
- Signing Taproot PSBTs (key spend and script spend)
- Support for `tr()` descriptors in the `descriptor!()` macro
- Add support for Bitcoin Core 23.0 when using the `rpc` blockchain
## [v0.18.0] - [v0.17.0]
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
- Improve key generation traits
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
- Bump `miniscript` dependency version to `^6.1`.
## [v0.17.0] - [v0.16.1]
- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
- `verify` flag removed from `TransactionDetails`.
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
### Sync API change
To decouple the `Wallet` from the `Blockchain` we've made major changes:
- Removed `Blockchain` from Wallet.
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
- Deprecated `Wallet::new_offline` (all wallets are offline now)
- Changed `Wallet::sync` to take a `Blockchain`.
- Stop making a request for the block height when calling `Wallet:new`.
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
## [v0.16.1] - [v0.16.0]
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
## [v0.16.0] - [v0.15.0]
@@ -399,7 +444,6 @@ final transaction is created by calling `finish` on the builder.
- Use `MemoryDatabase` in the compiler example
- Make the REPL return JSON
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...HEAD
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
@@ -416,4 +460,9 @@ final transaction is created by calling `finish` on the builder.
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
[v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0
[v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...HEAD

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.16.1-dev"
version = "0.19.0"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -14,15 +14,15 @@ license = "MIT OR Apache-2.0"
[dependencies]
bdk-macros = "^0.6"
log = "^0.4"
miniscript = { version = "^6.0", features = ["use-serde"] }
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
miniscript = { version = "7.0", features = ["use-serde"] }
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.7"
# Optional dependencies
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.8", optional = true }
electrum-client = { version = "0.10", optional = true }
rusqlite = { version = "0.25.3", optional = true }
ahash = { version = "=0.7.4", optional = true }
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
@@ -37,12 +37,12 @@ lazy_static = { version = "1.4", optional = true }
bip39 = { version = "1.0.1", optional = true }
bitcoinconsensus = { version = "0.19.0-3", optional = true }
# Needed by bdk_blockchain_tests macro
bitcoincore-rpc = { version = "0.14", optional = true }
# Needed by bdk_blockchain_tests macro and the `rpc` feature
bitcoincore-rpc = { version = "0.15", optional = true }
# Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "~1.14", features = ["rt"] }
tokio = { version = "1", features = ["rt"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
async-trait = "0.1"
@@ -55,6 +55,7 @@ compiler = ["miniscript/compiler"]
verify = ["bitcoinconsensus"]
default = ["key-value-db", "electrum"]
sqlite = ["rusqlite", "ahash"]
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
key-value-db = ["sled"]
all-keys = ["keys-bip39"]
@@ -87,16 +88,17 @@ reqwest-default-tls = ["reqwest/default-tls"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
test-md-docs = ["electrum"]
[dev-dependencies]
lazy_static = "1.4"
env_logger = "0.7"
clap = "2.33"
electrsd = { version= "0.13", features = ["trigger", "bitcoind_22_0"] }
electrsd = "0.19.1"
[[example]]
name = "address_validator"
@@ -109,6 +111,11 @@ name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]
[[example]]
name = "rpcwallet"
path = "examples/rpcwallet.rs"
required-features = ["keys-bip39", "key-value-db", "rpc"]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]

View File

@@ -13,7 +13,7 @@
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
<a href="https://blog.rust-lang.org/2020/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://blog.rust-lang.org/2020/08/27/Rust-1.56.0.html"><img alt="Rustc Version 1.56+" src="https://img.shields.io/badge/rustc-1.56%2B-lightgrey.svg"/></a>
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
</p>
@@ -41,21 +41,21 @@ The `bdk` library aims to be the core building block for Bitcoin wallets of any
```rust,no_run
use bdk::Wallet;
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::SyncOptions;
use bdk::electrum_client::Client;
use bdk::bitcoin::Network;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(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,
Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
@@ -70,7 +70,7 @@ use bdk::{Wallet, database::MemoryDatabase};
use bdk::wallet::AddressIndex::New;
fn main() -> Result<(), bdk::Error> {
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
@@ -88,9 +88,9 @@ fn main() -> Result<(), bdk::Error> {
### Create a transaction
```rust,no_run
use bdk::{FeeRate, Wallet};
use bdk::{FeeRate, Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New;
@@ -98,16 +98,15 @@ use bdk::wallet::AddressIndex::New;
use bitcoin::consensus::serialize;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(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)?;
wallet.sync(&blockchain, SyncOptions::default())?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
@@ -135,7 +134,7 @@ use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::consensus::deserialize;
fn main() -> Result<(), bdk::Error> {
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
bitcoin::Network::Testnet,
@@ -167,7 +166,7 @@ Integration testing require testing features, for example:
cargo test --features test-electrum
```
The other options are `test-esplora` or `test-rpc`.
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
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

View File

@@ -48,8 +48,7 @@ impl AddressValidator for DummyValidator {
fn main() -> Result<(), bdk::Error> {
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
let mut wallet =
Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
wallet.add_address_validator(Arc::new(DummyValidator));

View File

@@ -10,7 +10,6 @@
// licenses.
use bdk::blockchain::compact_filters::*;
use bdk::blockchain::noop_progress;
use bdk::database::MemoryDatabase;
use bdk::*;
use bitcoin::*;
@@ -35,9 +34,8 @@ fn main() -> Result<(), CompactFiltersError> {
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();
let wallet = Arc::new(Wallet::new(descriptor, None, Network::Testnet, database).unwrap());
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
info!("balance: {}", wallet.get_balance()?);
Ok(())
}

View File

@@ -85,11 +85,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let network = matches
.value_of("network")
.map(|n| Network::from_str(n))
.map(Network::from_str)
.transpose()
.unwrap()
.unwrap_or(Network::Testnet);
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database)?;
let wallet = Wallet::new(&format!("{}", descriptor), None, network, database)?;
info!("... First address: {}", wallet.get_address(New)?);

229
examples/rpcwallet.rs Normal file
View File

@@ -0,0 +1,229 @@
// 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::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::Amount;
use bdk::bitcoin::Network;
use bdk::bitcoincore_rpc::RpcApi;
use bdk::blockchain::rpc::{Auth, RpcBlockchain, RpcConfig};
use bdk::blockchain::ConfigurableBlockchain;
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{DerivableKey, GeneratableKey, GeneratedKey};
use bdk::miniscript::miniscript::Segwitv0;
use bdk::sled;
use bdk::template::Bip84;
use bdk::wallet::{signer::SignOptions, wallet_name_from_descriptor, AddressIndex, SyncOptions};
use bdk::KeychainKind;
use bdk::Wallet;
use bdk::blockchain::Blockchain;
use electrsd;
use std::error::Error;
use std::path::PathBuf;
use std::str::FromStr;
/// This example demonstrates a typical way to create a wallet and work with bdk.
///
/// This example bdk wallet is connected to a bitcoin core rpc regtest node,
/// and will attempt to receive, create and broadcast transactions.
///
/// To start a bitcoind regtest node programmatically, this example uses
/// `electrsd` library, which is also a bdk dev-dependency.
///
/// But you can start your own bitcoind backend, and the rest of the example should work fine.
fn main() -> Result<(), Box<dyn Error>> {
// -- Setting up background bitcoind process
println!(">> Setting up bitcoind");
// Start the bitcoind process
let bitcoind_conf = electrsd::bitcoind::Conf::default();
// electrsd will automatically download the bitcoin core binaries
let bitcoind_exe =
electrsd::bitcoind::downloaded_exe_path().expect("We should always have downloaded path");
// Launch bitcoind and gather authentication access
let bitcoind = electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap();
let bitcoind_auth = Auth::Cookie {
file: bitcoind.params.cookie_file.clone(),
};
// Get a new core address
let core_address = bitcoind.client.get_new_address(None, None)?;
// Generate 101 blocks and use the above address as coinbase
bitcoind.client.generate_to_address(101, &core_address)?;
println!(">> bitcoind setup complete");
println!(
"Available coins in Core wallet : {}",
bitcoind.client.get_balance(None, None)?
);
// -- Setting up the Wallet
println!("\n>> Setting up BDK wallet");
// Get a random private key
let xprv = generate_random_ext_privkey()?;
// Use the derived descriptors from the privatekey to
// create unique wallet name.
// This is a special utility function exposed via `bdk::wallet_name_from_descriptor()`
let wallet_name = wallet_name_from_descriptor(
Bip84(xprv.clone(), KeychainKind::External),
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
Network::Regtest,
&Secp256k1::new(),
)?;
// Create a database (using default sled type) to store wallet data
let mut datadir = PathBuf::from_str("/tmp/")?;
datadir.push(".bdk-example");
let database = sled::open(datadir)?;
let database = database.open_tree(wallet_name.clone())?;
// Create a RPC configuration of the running bitcoind backend we created in last step
// Note: If you are using custom regtest node, use the appropriate url and auth
let rpc_config = RpcConfig {
url: bitcoind.params.rpc_socket.to_string(),
auth: bitcoind_auth,
network: Network::Regtest,
wallet_name,
skip_blocks: None,
};
// Use the above configuration to create a RPC blockchain backend
let blockchain = RpcBlockchain::from_config(&rpc_config)?;
// Combine Database + Descriptor to create the final wallet
let wallet = Wallet::new(
Bip84(xprv.clone(), KeychainKind::External),
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
Network::Regtest,
database,
)?;
// The `wallet` and the `blockchain` are independent structs.
// The wallet will be used to do all wallet level actions
// The blockchain can be used to do all blockchain level actions.
// For certain actions (like sync) the wallet will ask for a blockchain.
// Sync the wallet
// The first sync is important as this will instantiate the
// wallet files.
wallet.sync(&blockchain, SyncOptions::default())?;
println!(">> BDK wallet setup complete.");
println!(
"Available initial coins in BDK wallet : {} sats",
wallet.get_balance()?
);
// -- Wallet transaction demonstration
println!("\n>> Sending coins: Core --> BDK, 10 BTC");
// Get a new address to receive coins
let bdk_new_addr = wallet.get_address(AddressIndex::New)?.address;
// Send 10 BTC from core wallet to bdk wallet
bitcoind.client.send_to_address(
&bdk_new_addr,
Amount::from_btc(10.0)?,
None,
None,
None,
None,
None,
None,
)?;
// Confirm transaction by generating 1 block
bitcoind.client.generate_to_address(1, &core_address)?;
// Sync the BDK wallet
// This time the sync will fetch the new transaction and update it in
// wallet database
wallet.sync(&blockchain, SyncOptions::default())?;
println!(">> Received coins in BDK wallet");
println!(
"Available balance in BDK wallet: {} sats",
wallet.get_balance()?
);
println!("\n>> Sending coins: BDK --> Core, 5 BTC");
// Attempt to send back 5.0 BTC to core address by creating a transaction
//
// Transactions are created using a `TxBuilder`.
// This helps us to systematically build a transaction with all
// required customization.
// A full list of APIs offered by `TxBuilder` can be found at
// https://docs.rs/bdk/latest/bdk/wallet/tx_builder/struct.TxBuilder.html
let mut tx_builder = wallet.build_tx();
// For a regular transaction, just set the recipient and amount
tx_builder.set_recipients(vec![(core_address.script_pubkey(), 500000000)]);
// Finalize the transaction and extract the PSBT
let (mut psbt, _) = tx_builder.finish()?;
// Set signing option
let signopt = SignOptions {
assume_height: None,
..Default::default()
};
// Sign the psbt
wallet.sign(&mut psbt, signopt)?;
// Extract the signed transaction
let tx = psbt.extract_tx();
// Broadcast the transaction
blockchain.broadcast(&tx)?;
// Confirm transaction by generating some blocks
bitcoind.client.generate_to_address(1, &core_address)?;
// Sync the BDK wallet
wallet.sync(&blockchain, SyncOptions::default())?;
println!(">> Coins sent to Core wallet");
println!(
"Remaining BDK wallet balance: {} sats",
wallet.get_balance()?
);
println!("\nCongrats!! you made your first test transaction with bdk and bitcoin core.");
Ok(())
}
// Helper function demonstrating privatekey extraction using bip39 mnemonic
// The mnemonic can be shown to user to safekeeping and the same wallet
// private descriptors can be recreated from it.
fn generate_random_ext_privkey() -> Result<impl DerivableKey<Segwitv0> + Clone, Box<dyn Error>> {
// a Bip39 passphrase can be set optionally
let password = Some("random password".to_string());
// Generate a random mnemonic, and use that to create a "DerivableKey"
let mnemonic: GeneratedKey<_, _> = Mnemonic::generate((WordCount::Words12, Language::English))
.map_err(|e| e.expect("Unknown Error"))?;
// `Ok(mnemonic)` would also work if there's no passphrase and it would
// yield the same result as this construct with `password` = `None`.
Ok((mnemonic, password))
}

View File

@@ -16,61 +16,17 @@
//!
//! ## Example
//!
//! In this example both `wallet_electrum` and `wallet_esplora` have the same type of
//! `Wallet<AnyBlockchain, MemoryDatabase>`. This means that they could both, for instance, be
//! assigned to a struct member.
//!
//! ```no_run
//! # use bitcoin::Network;
//! # use bdk::blockchain::*;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
//! # #[cfg(feature = "electrum")]
//! # {
//! let electrum_blockchain = ElectrumBlockchain::from(electrum_client::Client::new("...")?);
//! let wallet_electrum: Wallet<AnyBlockchain, _> = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! electrum_blockchain.into(),
//! )?;
//! # }
//!
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
//! # {
//! let esplora_blockchain = EsploraBlockchain::new("...", 20);
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! esplora_blockchain.into(),
//! )?;
//! # }
//!
//! # Ok::<(), bdk::Error>(())
//! ```
//!
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating wallets with any
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating any
//! blockchain type supported using a single line of code:
//!
//! ```no_run
//! # use bitcoin::Network;
//! # use bdk::blockchain::*;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
//! # {
//! let config = serde_json::from_str("...")?;
//! let blockchain = AnyBlockchain::from_config(&config)?;
//! let wallet = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! blockchain,
//! )?;
//! let height = blockchain.get_height();
//! # }
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -78,6 +34,14 @@
use super::*;
macro_rules! impl_from {
( boxed $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => {
$( $cfg )*
impl From<$from> for $to {
fn from(inner: $from) -> Self {
<$to>::$variant(Box::new(inner))
}
}
};
( $from:ty, $to:ty, $variant:ident, $( $cfg:tt )* ) => {
$( $cfg )*
impl From<$from> for $to {
@@ -112,19 +76,19 @@ pub enum AnyBlockchain {
#[cfg(feature = "electrum")]
#[cfg_attr(docsrs, doc(cfg(feature = "electrum")))]
/// Electrum client
Electrum(electrum::ElectrumBlockchain),
Electrum(Box<electrum::ElectrumBlockchain>),
#[cfg(feature = "esplora")]
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
/// Esplora client
Esplora(esplora::EsploraBlockchain),
Esplora(Box<esplora::EsploraBlockchain>),
#[cfg(feature = "compact_filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
/// Compact filters client
CompactFilters(compact_filters::CompactFiltersBlockchain),
CompactFilters(Box<compact_filters::CompactFiltersBlockchain>),
#[cfg(feature = "rpc")]
#[cfg_attr(docsrs, doc(cfg(feature = "rpc")))]
/// RPC client
Rpc(rpc::RpcBlockchain),
Rpc(Box<rpc::RpcBlockchain>),
}
#[maybe_async]
@@ -133,40 +97,62 @@ impl Blockchain for AnyBlockchain {
maybe_await!(impl_inner_method!(self, get_capabilities))
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, setup, database, progress_update))
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, sync, database, progress_update))
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(impl_inner_method!(self, get_tx, txid))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, broadcast, tx))
}
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(impl_inner_method!(self, get_height))
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
maybe_await!(impl_inner_method!(self, estimate_fee, target))
}
}
impl_from!(electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
impl_from!(esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
impl_from!(compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
impl_from!(rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
#[maybe_async]
impl GetHeight for AnyBlockchain {
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(impl_inner_method!(self, get_height))
}
}
#[maybe_async]
impl GetTx for AnyBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(impl_inner_method!(self, get_tx, txid))
}
}
#[maybe_async]
impl WalletSync for AnyBlockchain {
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
self,
wallet_sync,
database,
progress_update
))
}
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
self,
wallet_setup,
database,
progress_update
))
}
}
impl_from!(boxed electrum::ElectrumBlockchain, AnyBlockchain, Electrum, #[cfg(feature = "electrum")]);
impl_from!(boxed esplora::EsploraBlockchain, AnyBlockchain, Esplora, #[cfg(feature = "esplora")]);
impl_from!(boxed compact_filters::CompactFiltersBlockchain, AnyBlockchain, CompactFilters, #[cfg(feature = "compact_filters")]);
impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]);
/// Type that can contain any of the blockchain configurations defined by the library
///
@@ -229,19 +215,19 @@ impl ConfigurableBlockchain for AnyBlockchain {
Ok(match config {
#[cfg(feature = "electrum")]
AnyBlockchainConfig::Electrum(inner) => {
AnyBlockchain::Electrum(electrum::ElectrumBlockchain::from_config(inner)?)
AnyBlockchain::Electrum(Box::new(electrum::ElectrumBlockchain::from_config(inner)?))
}
#[cfg(feature = "esplora")]
AnyBlockchainConfig::Esplora(inner) => {
AnyBlockchain::Esplora(esplora::EsploraBlockchain::from_config(inner)?)
AnyBlockchain::Esplora(Box::new(esplora::EsploraBlockchain::from_config(inner)?))
}
#[cfg(feature = "compact_filters")]
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(
AnyBlockchainConfig::CompactFilters(inner) => AnyBlockchain::CompactFilters(Box::new(
compact_filters::CompactFiltersBlockchain::from_config(inner)?,
),
)),
#[cfg(feature = "rpc")]
AnyBlockchainConfig::Rpc(inner) => {
AnyBlockchain::Rpc(rpc::RpcBlockchain::from_config(inner)?)
AnyBlockchain::Rpc(Box::new(rpc::RpcBlockchain::from_config(inner)?))
}
})
}

View File

@@ -67,7 +67,7 @@ mod peer;
mod store;
mod sync;
use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::blockchain::*;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
@@ -163,11 +163,19 @@ impl CompactFiltersBlockchain {
if let Some(previous_output) = database.get_previous_output(&input.previous_output)? {
inputs_sum += previous_output.value;
if database.is_mine(&previous_output.script_pubkey)? {
// this output is ours, we have a path to derive it
if let Some((keychain, _)) =
database.get_path_from_script_pubkey(&previous_output.script_pubkey)?
{
outgoing += previous_output.value;
debug!("{} input #{} is mine, removing from utxo", tx.txid(), i);
updates.del_utxo(&input.previous_output)?;
debug!("{} input #{} is mine, setting utxo as spent", tx.txid(), i);
updates.set_utxo(&LocalUtxo {
outpoint: input.previous_output,
txout: previous_output.clone(),
keychain,
is_spent: true,
})?;
}
}
}
@@ -185,6 +193,7 @@ impl CompactFiltersBlockchain {
outpoint: OutPoint::new(tx.txid(), i as u32),
txout: output.clone(),
keychain,
is_spent: false,
})?;
incoming += output.value;
@@ -225,11 +234,38 @@ impl Blockchain for CompactFiltersBlockchain {
vec![Capability::FullHistory].into_iter().collect()
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.peers[0].broadcast_tx(tx.clone())?;
Ok(())
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
// TODO
Ok(FeeRate::default())
}
}
impl GetHeight for CompactFiltersBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.headers.get_height()? as u32)
}
}
impl GetTx for CompactFiltersBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
}
impl WalletSync for CompactFiltersBlockchain {
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
fn setup<D: BatchDatabase, P: 'static + Progress>(
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let first_peer = &self.peers[0];
@@ -430,27 +466,6 @@ impl Blockchain for CompactFiltersBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.peers[0].broadcast_tx(tx.clone())?;
Ok(())
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.headers.get_height()? as u32)
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
// TODO
Ok(FeeRate::default())
}
}
/// Data to connect to a Bitcoin P2P peer

View File

@@ -10,6 +10,7 @@
// licenses.
use std::collections::HashMap;
use std::io::BufReader;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::thread;
@@ -19,14 +20,13 @@ use socks::{Socks5Stream, ToTargetAddr};
use rand::{thread_rng, Rng};
use bitcoin::consensus::Encodable;
use bitcoin::consensus::{Decodable, Encodable};
use bitcoin::hash_types::BlockHash;
use bitcoin::network::constants::ServiceFlags;
use bitcoin::network::message::{NetworkMessage, RawNetworkMessage};
use bitcoin::network::message_blockdata::*;
use bitcoin::network::message_filter::*;
use bitcoin::network::message_network::VersionMessage;
use bitcoin::network::stream_reader::StreamReader;
use bitcoin::network::Address;
use bitcoin::{Block, Network, Transaction, Txid, Wtxid};
@@ -94,8 +94,7 @@ impl Mempool {
TxIdentifier::Wtxid(wtxid) => self.0.read().unwrap().wtxids.get(&wtxid).cloned(),
};
txid.map(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
.flatten()
txid.and_then(|txid| self.0.read().unwrap().txs.get(&txid).cloned())
}
/// Return whether or not the mempool contains a transaction with a given txid
@@ -111,6 +110,7 @@ impl Mempool {
/// A Bitcoin peer
#[derive(Debug)]
#[allow(dead_code)]
pub struct Peer {
writer: Arc<Mutex<TcpStream>>,
responses: Arc<RwLock<ResponsesMap>>,
@@ -327,9 +327,10 @@ impl Peer {
};
}
let mut reader = StreamReader::new(connection, None);
let mut reader = BufReader::new(connection);
loop {
let raw_message: RawNetworkMessage = check_disconnect!(reader.read_next());
let raw_message: RawNetworkMessage =
check_disconnect!(Decodable::consensus_decode(&mut reader));
let in_message = if raw_message.magic != network.magic() {
continue;

View File

@@ -68,10 +68,41 @@ impl Blockchain for ElectrumBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::from_btc_per_kvb(
self.client.estimate_fee(target)? as f32
))
}
}
impl StatelessBlockchain for ElectrumBlockchain {}
impl GetHeight for ElectrumBlockchain {
fn get_height(&self) -> Result<u32, Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
Ok(self
.client
.block_headers_subscribe()
.map(|data| data.height as u32)?)
}
}
impl GetTx for ElectrumBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
}
impl WalletSync for ElectrumBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut request = script_sync::start(database, self.stop_gap)?;
let mut block_times = HashMap::<u32, u32>::new();
@@ -207,29 +238,6 @@ impl Blockchain for ElectrumBlockchain {
database.commit_batch(batch_update)?;
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
Ok(self
.client
.block_headers_subscribe()
.map(|data| data.height as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::from_btc_per_kvb(
self.client.estimate_fee(target)? as f32
))
}
}
struct TxCache<'a, 'b, D> {
@@ -314,8 +322,67 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
#[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())
mod test {
use std::sync::Arc;
use super::*;
use crate::database::MemoryDatabase;
use crate::testutils::blockchain_tests::TestClient;
use crate::wallet::{AddressIndex, Wallet};
crate::bdk_blockchain_tests! {
fn test_instance(test_client: &TestClient) -> ElectrumBlockchain {
ElectrumBlockchain::from(Client::new(&test_client.electrsd.electrum_url).unwrap())
}
}
fn get_factory() -> (TestClient, Arc<ElectrumBlockchain>) {
let test_client = TestClient::default();
let factory = Arc::new(ElectrumBlockchain::from(
Client::new(&test_client.electrsd.electrum_url).unwrap(),
));
(test_client, factory)
}
#[test]
fn test_electrum_blockchain_factory() {
let (_test_client, factory) = get_factory();
let a = factory.build("aaaaaa", None).unwrap();
let b = factory.build("bbbbbb", None).unwrap();
assert_eq!(
a.client.block_headers_subscribe().unwrap().height,
b.client.block_headers_subscribe().unwrap().height
);
}
#[test]
fn test_electrum_blockchain_factory_sync_wallet() {
let (mut test_client, factory) = get_factory();
let db = MemoryDatabase::new();
let wallet = Wallet::new(
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
None,
bitcoin::Network::Regtest,
db,
)
.unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap();
let tx = testutils! {
@tx ( (@addr address.address) => 50_000 )
};
test_client.receive(tx);
factory
.sync_wallet(&wallet, None, Default::default())
.unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
}
}

View File

@@ -2,7 +2,7 @@
//!
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
use crate::BlockTime;
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid};
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness};
#[derive(serde::Deserialize, Clone, Debug)]
pub struct PrevOut {
@@ -17,7 +17,7 @@ pub struct Vin {
// None if coinbase
pub prevout: Option<PrevOut>,
pub scriptsig: Script,
#[serde(deserialize_with = "deserialize_witness")]
#[serde(deserialize_with = "deserialize_witness", default)]
pub witness: Vec<Vec<u8>>,
pub sequence: u32,
pub is_coinbase: bool,
@@ -63,7 +63,7 @@ impl Tx {
},
script_sig: vin.scriptsig,
sequence: vin.sequence,
witness: vin.witness,
witness: Witness::from_vec(vin.witness),
})
.collect(),
output: self

View File

@@ -91,10 +91,38 @@ impl Blockchain for EsploraBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
super::into_fee_rate(target, estimates)
}
}
impl StatelessBlockchain for EsploraBlockchain {}
#[maybe_async]
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(await_or_block!(self.url_client._get_height())?)
}
}
#[maybe_async]
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
}
#[maybe_async]
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
use crate::blockchain::script_sync::Request;
let mut request = script_sync::start(database, self.stop_gap)?;
@@ -180,23 +208,6 @@ impl Blockchain for EsploraBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(await_or_block!(self.url_client._get_height())?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
super::into_fee_rate(target, estimates)
}
}
impl UrlClient {

View File

@@ -87,10 +87,36 @@ impl Blockchain for EsploraBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
let _txid = self.url_client._broadcast(tx)?;
Ok(())
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = self.url_client._get_fee_estimates()?;
super::into_fee_rate(target, estimates)
}
}
impl StatelessBlockchain for EsploraBlockchain {}
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
}
}
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client._get_tx(txid)?)
}
}
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
use crate::blockchain::script_sync::Request;
let mut request = script_sync::start(database, self.stop_gap)?;
@@ -103,10 +129,11 @@ impl Blockchain for EsploraBlockchain {
.take(self.concurrency as usize)
.cloned();
let handles = scripts.map(move |script| {
let mut handles = vec![];
for script in scripts {
let client = self.url_client.clone();
// make each request in its own thread.
std::thread::spawn(move || {
handles.push(std::thread::spawn(move || {
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
let n_confirmed =
@@ -128,10 +155,11 @@ impl Blockchain for EsploraBlockchain {
}
}
Result::<_, Error>::Ok(related_txs)
})
});
}));
}
let txs_per_script: Vec<Vec<Tx>> = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Result<_, _>>()?;
let mut satisfaction = vec![];
@@ -179,24 +207,6 @@ impl Blockchain for EsploraBlockchain {
Ok(())
}
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 {

View File

@@ -25,7 +25,8 @@ use bitcoin::{Transaction, Txid};
use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
use crate::wallet::{wallet_name_from_descriptor, Wallet};
use crate::{FeeRate, KeychainKind};
#[cfg(any(
feature = "electrum",
@@ -86,28 +87,50 @@ pub enum Capability {
/// Trait that defines the actions that must be supported by a blockchain backend
#[maybe_async]
pub trait Blockchain {
pub trait Blockchain: WalletSync + GetHeight + GetTx {
/// Return the set of [`Capability`] supported by this backend
fn get_capabilities(&self) -> HashSet<Capability>;
/// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
}
/// Trait for getting the current height of the blockchain.
#[maybe_async]
pub trait GetHeight {
/// Return the current height
fn get_height(&self) -> Result<u32, Error>;
}
#[maybe_async]
/// Trait for getting a transaction by txid
pub trait GetTx {
/// Fetch a transaction given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
}
/// Trait for blockchains that can sync by updating the database directly.
#[maybe_async]
pub trait WalletSync {
/// Setup the backend and populate the internal database for the first time
///
/// This method is the equivalent of [`Blockchain::sync`], but it's guaranteed to only be
/// This method is the equivalent of [`Self::wallet_sync`], but it's guaranteed to only be
/// called once, at the first [`Wallet::sync`](crate::wallet::Wallet::sync).
///
/// The rationale behind the distinction between `sync` and `setup` is that some custom backends
/// might need to perform specific actions only the first time they are synced.
///
/// For types that do not have that distinction, only this method can be implemented, since
/// [`Blockchain::sync`] defaults to calling this internally if not overridden.
fn setup<D: BatchDatabase, P: 'static + Progress>(
/// [`WalletSync::wallet_sync`] defaults to calling this internally if not overridden.
/// Populate the internal database with transactions and UTXOs
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error>;
/// Populate the internal database with transactions and UTXOs
///
/// If not overridden, it defaults to calling [`Blockchain::setup`] internally.
/// If not overridden, it defaults to calling [`Self::wallet_setup`] internally.
///
/// This method should implement the logic required to iterate over the list of the wallet's
/// script_pubkeys using [`Database::iter_script_pubkeys`] and look for relevant transactions
@@ -124,23 +147,13 @@ pub trait Blockchain {
/// [`BatchOperations::set_tx`]: crate::database::BatchOperations::set_tx
/// [`BatchOperations::set_utxo`]: crate::database::BatchOperations::set_utxo
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
fn sync<D: BatchDatabase, P: 'static + Progress>(
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.setup(database, progress_update))
maybe_await!(self.wallet_setup(database, progress_update))
}
/// Fetch a transaction from the blockchain given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
/// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
/// Return the current height
fn get_height(&self) -> Result<u32, Error>;
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
}
/// Trait for [`Blockchain`] types that can be created given a configuration
@@ -152,12 +165,112 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
fn from_config(config: &Self::Config) -> Result<Self, Error>;
}
/// Trait for blockchains that don't contain any state
///
/// Statless blockchains can be used to sync multiple wallets with different descriptors.
///
/// [`BlockchainFactory`] is automatically implemented for `Arc<T>` where `T` is a stateless
/// blockchain.
pub trait StatelessBlockchain: Blockchain {}
/// Trait for a factory of blockchains that share the underlying connection or configuration
#[cfg_attr(
not(feature = "async-interface"),
doc = r##"
## Example
This example shows how to sync multiple walles and return the sum of their balances
```no_run
# use bdk::Error;
# use bdk::blockchain::*;
# use bdk::database::*;
# use bdk::wallet::*;
# use bdk::*;
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
Ok(wallets
.iter()
.map(|w| -> Result<_, Error> {
blockchain_factory.sync_wallet(&w, None, SyncOptions::default())?;
w.get_balance()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.sum())
}
```
"##
)]
pub trait BlockchainFactory {
/// The type returned when building a blockchain from this factory
type Inner: Blockchain;
/// Build a new blockchain for the given descriptor wallet_name
///
/// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks
/// from the factory. Since it's not possible to override the value to `None`, set it to
/// `Some(0)` to rescan from the genesis.
fn build(
&self,
wallet_name: &str,
override_skip_blocks: Option<u32>,
) -> Result<Self::Inner, Error>;
/// Build a new blockchain for a given wallet
///
/// Internally uses [`wallet_name_from_descriptor`] to derive the name, and then calls
/// [`BlockchainFactory::build`] to create the blockchain instance.
fn build_for_wallet<D: BatchDatabase>(
&self,
wallet: &Wallet<D>,
override_skip_blocks: Option<u32>,
) -> Result<Self::Inner, Error> {
let wallet_name = wallet_name_from_descriptor(
wallet.public_descriptor(KeychainKind::External)?.unwrap(),
wallet.public_descriptor(KeychainKind::Internal)?,
wallet.network(),
wallet.secp_ctx(),
)?;
self.build(&wallet_name, override_skip_blocks)
}
/// Use [`BlockchainFactory::build_for_wallet`] to get a blockchain, then sync the wallet
///
/// This can be used when a new blockchain would only be used to sync a wallet and then
/// immediately dropped. Keep in mind that specific blockchain factories may perform slow
/// operations to build a blockchain for a given wallet, so if a wallet needs to be synced
/// often it's recommended to use [`BlockchainFactory::build_for_wallet`] to reuse the same
/// blockchain multiple times.
#[cfg(not(any(target_arch = "wasm32", feature = "async-interface")))]
#[cfg_attr(
docsrs,
doc(cfg(not(any(target_arch = "wasm32", feature = "async-interface"))))
)]
fn sync_wallet<D: BatchDatabase>(
&self,
wallet: &Wallet<D>,
override_skip_blocks: Option<u32>,
sync_options: crate::wallet::SyncOptions,
) -> Result<(), Error> {
let blockchain = self.build_for_wallet(wallet, override_skip_blocks)?;
wallet.sync(&blockchain, sync_options)
}
}
impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
type Inner = Self;
fn build(&self, _wallet_name: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
Ok(Arc::clone(self))
}
}
/// Data sent with a progress update over a [`channel`]
pub type ProgressData = (f32, Option<String>);
/// Trait for types that can receive and process progress updates during [`Blockchain::sync`] and
/// [`Blockchain::setup`]
pub trait Progress: Send {
/// Trait for types that can receive and process progress updates during [`WalletSync::wallet_sync`] and
/// [`WalletSync::wallet_setup`]
pub trait Progress: Send + 'static + core::fmt::Debug {
/// Send a new progress update
///
/// The `progress` value should be in the range 0.0 - 100.0, and the `message` value is an
@@ -182,7 +295,7 @@ impl Progress for Sender<ProgressData> {
}
/// Type that implements [`Progress`] and drops every update received
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default, Debug)]
pub struct NoopProgress;
/// Create a new instance of [`NoopProgress`]
@@ -197,7 +310,7 @@ impl Progress for NoopProgress {
}
/// Type that implements [`Progress`] and logs at level `INFO` every update received
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default, Debug)]
pub struct LogProgress;
/// Create a new instance of [`LogProgress`]
@@ -223,33 +336,44 @@ impl<T: Blockchain> Blockchain for Arc<T> {
maybe_await!(self.deref().get_capabilities())
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(self.deref().setup(database, progress_update))
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(self.deref().sync(database, progress_update))
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(self.deref().get_tx(txid))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
maybe_await!(self.deref().broadcast(tx))
}
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(self.deref().get_height())
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
maybe_await!(self.deref().estimate_fee(target))
}
}
#[maybe_async]
impl<T: GetTx> GetTx for Arc<T> {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(self.deref().get_tx(txid))
}
}
#[maybe_async]
impl<T: GetHeight> GetHeight for Arc<T> {
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(self.deref().get_height())
}
}
#[maybe_async]
impl<T: WalletSync> WalletSync for Arc<T> {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.deref().wallet_setup(database, progress_update))
}
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.deref().wallet_sync(database, progress_update))
}
}

View File

@@ -32,15 +32,17 @@
//! ```
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::hashes::hex::ToHex;
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::blockchain::*;
use crate::database::{BatchDatabase, DatabaseUtils};
use crate::descriptor::get_checksum;
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoincore_rpc::json::{
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
};
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::jsonrpc::serde_json::{json, Value};
use bitcoincore_rpc::Auth as RpcAuth;
use bitcoincore_rpc::{Client, RpcApi};
use log::debug;
@@ -54,8 +56,8 @@ use std::str::FromStr;
pub struct RpcBlockchain {
/// Rpc client to the node, includes the wallet name
client: Client,
/// Network used
network: Network,
/// Whether the wallet is a "descriptor" or "legacy" wallet in Core
is_descriptors: bool,
/// 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
@@ -139,10 +141,39 @@ impl Blockchain for RpcBlockchain {
self.capabilities.clone()
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
}
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 GetTx for RpcBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
}
impl GetHeight for RpcBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
}
}
impl WalletSync for RpcBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
@@ -150,22 +181,53 @@ impl Blockchain for RpcBlockchain {
"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))?;
if self.is_descriptors {
// Core still doesn't support complex descriptors like BDK, but when the wallet type is
// "descriptors" we should import individual addresses using `importdescriptors` rather
// than `importmulti`, using the `raw()` descriptor which allows us to specify an
// arbitrary script
let requests = Value::Array(
scripts_pubkeys
.iter()
.map(|s| {
let desc = format!("raw({})", s.to_hex());
json!({
"timestamp": "now",
"desc": format!("{}#{}", desc, get_checksum(&desc).unwrap()),
})
})
.collect(),
);
let res: Vec<Value> = self.client.call("importdescriptors", &[requests])?;
res.into_iter()
.map(|v| match v["success"].as_bool() {
Some(true) => Ok(()),
Some(false) => Err(Error::Generic(
v["error"]["message"]
.as_str()
.unwrap_or("Unknown error")
.to_string(),
)),
_ => Err(Error::Generic("Unexpected response from Core".to_string())),
})
.collect::<Result<Vec<_>, _>>()?;
} else {
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),
};
self.client.import_multi(&requests, Some(&options))?;
}
loop {
let current_height = self.get_height()?;
@@ -187,13 +249,13 @@ impl Blockchain for RpcBlockchain {
}
}
self.sync(database, progress_update)
self.wallet_sync(database, progress_update)
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
fn wallet_sync<D: BatchDatabase>(
&self,
db: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut indexes = HashMap::new();
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
@@ -220,7 +282,7 @@ impl Blockchain for RpcBlockchain {
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
// list_txs returns all conflicting txs, 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()
}) {
@@ -289,32 +351,37 @@ impl Blockchain for RpcBlockchain {
}
}
let current_utxos: HashSet<_> = current_utxo
// Filter out trasactions that are for script pubkeys that aren't in this wallet.
let current_utxos = 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>>()?;
.filter_map(
|u| match db.get_path_from_script_pubkey(&u.script_pub_key) {
Err(e) => Some(Err(e)),
Ok(None) => None,
Ok(Some(path)) => Some(Ok(LocalUtxo {
outpoint: OutPoint::new(u.txid, u.vout),
keychain: path.0,
txout: TxOut {
value: u.amount.as_sat(),
script_pubkey: u.script_pub_key,
},
is_spent: false,
})),
},
)
.collect::<Result<HashSet<_>, Error>>()?;
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
for s in spent {
debug!("removing utxo: {:?}", s);
db.del_utxo(&s.outpoint)?;
for utxo in spent {
debug!("setting as spent utxo: {:?}", utxo);
let mut spent_utxo = utxo.clone();
spent_utxo.is_spent = true;
db.set_utxo(&spent_utxo)?;
}
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
for s in received {
debug!("adding utxo: {:?}", s);
db.set_utxo(s)?;
for utxo in received {
debug!("adding utxo: {:?}", utxo);
db.set_utxo(utxo)?;
}
for (keykind, index) in indexes {
@@ -324,29 +391,6 @@ impl Blockchain for RpcBlockchain {
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 {
@@ -360,20 +404,36 @@ impl ConfigurableBlockchain for RpcBlockchain {
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?;
let rpc_version = client.version()?;
let loaded_wallets = client.list_wallets()?;
if loaded_wallets.contains(&wallet_name) {
debug!("wallet already loaded {:?}", wallet_name);
} else if list_wallet_dir(&client)?.contains(&wallet_name) {
client.load_wallet(&wallet_name)?;
debug!("wallet 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 {
// pre-0.21 use legacy wallets
if rpc_version < 210_000 {
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
debug!("wallet created {:?}", wallet_name);
} else {
// TODO: move back to api call when https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/225 is closed
let args = [
Value::String(wallet_name.clone()),
Value::Bool(true),
Value::Bool(false),
Value::Null,
Value::Bool(false),
Value::Bool(true),
];
let _: Value = client.call("createwallet", &args)?;
}
debug!("wallet created {:?}", wallet_name);
}
let is_descriptors = is_wallet_descriptor(&client)?;
let blockchain_info = client.get_blockchain_info()?;
let network = match blockchain_info.chain.as_str() {
"main" => Network::Bitcoin,
@@ -390,7 +450,6 @@ impl ConfigurableBlockchain for RpcBlockchain {
}
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") {
@@ -406,8 +465,8 @@ impl ConfigurableBlockchain for RpcBlockchain {
Ok(RpcBlockchain {
client,
network,
capabilities,
is_descriptors,
_storage_address: storage_address,
skip_blocks: config.skip_blocks,
})
@@ -430,18 +489,141 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
Ok(result.wallets.into_iter().map(|n| n.name).collect())
}
#[cfg(test)]
#[cfg(feature = "test-rpc")]
crate::bdk_blockchain_tests! {
/// Returns whether a wallet is legacy or descriptors by calling `getwalletinfo`.
///
/// This API is mapped by bitcoincore_rpc, but it doesn't have the fields we need (either
/// "descriptors" or "format") so we have to call the RPC manually
fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
#[derive(Deserialize)]
struct CallResult {
descriptors: Option<bool>,
}
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()
let result: CallResult = client.call("getwalletinfo", &[])?;
Ok(result.descriptors.unwrap_or(false))
}
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
///
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
/// objects for different wallet names and with different rescan heights.
///
/// ## Example
///
/// ```no_run
/// # use bdk::bitcoin::Network;
/// # use bdk::blockchain::BlockchainFactory;
/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let factory = RpcBlockchainFactory {
/// url: "http://127.0.0.1:18332".to_string(),
/// auth: Auth::Cookie {
/// file: "/home/user/.bitcoin/.cookie".into(),
/// },
/// network: Network::Testnet,
/// wallet_name_prefix: Some("prefix-".to_string()),
/// default_skip_blocks: 100_000,
/// };
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct RpcBlockchainFactory {
/// 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 optional prefix used to build the full wallet name for blockchains
pub wallet_name_prefix: Option<String>,
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
pub default_skip_blocks: u32,
}
impl BlockchainFactory for RpcBlockchainFactory {
type Inner = RpcBlockchain;
fn build(
&self,
checksum: &str,
override_skip_blocks: Option<u32>,
) -> Result<Self::Inner, Error> {
RpcBlockchain::from_config(&RpcConfig {
url: self.url.clone(),
auth: self.auth.clone(),
network: self.network,
wallet_name: format!(
"{}{}",
self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
checksum
),
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
})
}
}
#[cfg(test)]
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
mod test {
use super::*;
use crate::testutils::blockchain_tests::TestClient;
use bitcoin::Network;
use bitcoincore_rpc::RpcApi;
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().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() ),
skip_blocks: None,
};
RpcBlockchain::from_config(&config).unwrap()
}
}
fn get_factory() -> (TestClient, RpcBlockchainFactory) {
let test_client = TestClient::default();
let factory = RpcBlockchainFactory {
url: test_client.bitcoind.rpc_url(),
auth: Auth::Cookie {
file: test_client.bitcoind.params.cookie_file.clone(),
},
network: Network::Regtest,
wallet_name_prefix: Some("prefix-".into()),
default_skip_blocks: 0,
};
(test_client, factory)
}
#[test]
fn test_rpc_blockchain_factory() {
let (_test_client, factory) = get_factory();
let a = factory.build("aaaaaa", None).unwrap();
assert_eq!(a.skip_blocks, Some(0));
assert_eq!(
a.client
.get_wallet_info()
.expect("Node connection isn't working")
.wallet_name,
"prefix-aaaaaa"
);
let b = factory.build("bbbbbb", Some(100)).unwrap();
assert_eq!(b.skip_blocks, Some(100));
assert_eq!(
b.client
.get_wallet_info()
.expect("Node connection isn't working")
.wallet_name,
"prefix-bbbbbb"
);
}
}

View File

@@ -332,7 +332,23 @@ impl<'a, D: BatchDatabase> State<'a, D> {
batch.del_tx(txid, true)?;
}
// Set every tx we observed
let mut spent_utxos = HashSet::new();
// track all the spent utxos
for finished_tx in &finished_txs {
let tx = finished_tx
.transaction
.as_ref()
.expect("transaction will always be present here");
for input in &tx.input {
spent_utxos.insert(&input.previous_output);
}
}
// set every utxo we observed, unless it's already spent
// we don't do this in the loop above as we want to know all the spent outputs before
// adding the non-spent to the batch in case there are new tranasactions
// that spend form each other.
for finished_tx in &finished_txs {
let tx = finished_tx
.transaction
@@ -343,30 +359,22 @@ impl<'a, D: BatchDatabase> State<'a, D> {
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
{
// add utxos we own from the new transactions we've seen.
let outpoint = OutPoint {
txid: finished_tx.txid,
vout: i as u32,
};
batch.set_utxo(&LocalUtxo {
outpoint: OutPoint {
txid: finished_tx.txid,
vout: i as u32,
},
outpoint,
txout: output.clone(),
keychain,
// Is this UTXO in the spent_utxos set?
is_spent: spent_utxos.get(&outpoint).is_some(),
})?;
}
}
batch.set_tx(finished_tx)?;
}
// we don't do this in the loop above since we may want to delete some of the utxos we
// just added in case there are new tranasactions that spend form each other.
for finished_tx in &finished_txs {
let tx = finished_tx
.transaction
.as_ref()
.expect("transaction will always be present here");
for input in &tx.input {
// Delete any spent utxos
batch.del_utxo(&input.previous_output)?;
}
batch.set_tx(finished_tx)?;
}
for (keychain, last_active_index) in self.last_active_index {

View File

@@ -23,12 +23,12 @@
//! # use bdk::database::{AnyDatabase, MemoryDatabase};
//! # use bdk::{Wallet};
//! let memory = MemoryDatabase::default();
//! let wallet_memory = Wallet::new_offline("...", None, Network::Testnet, memory)?;
//! let wallet_memory = Wallet::new("...", None, Network::Testnet, memory)?;
//!
//! # #[cfg(feature = "key-value-db")]
//! # {
//! let sled = sled::open("my-database")?.open_tree("default_tree")?;
//! let wallet_sled = Wallet::new_offline("...", None, Network::Testnet, sled)?;
//! let wallet_sled = Wallet::new("...", None, Network::Testnet, sled)?;
//! # }
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -42,7 +42,7 @@
//! # use bdk::{Wallet};
//! let config = serde_json::from_str("...")?;
//! let database = AnyDatabase::from_config(&config)?;
//! let wallet = Wallet::new_offline("...", None, Network::Testnet, database)?;
//! let wallet = Wallet::new("...", None, Network::Testnet, database)?;
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -61,6 +61,7 @@ macro_rules! impl_from {
macro_rules! impl_inner_method {
( $enum_name:ident, $self:expr, $name:ident $(, $args:expr)* ) => {
#[allow(deprecated)]
match $self {
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
#[cfg(feature = "key-value-db")]

View File

@@ -43,6 +43,7 @@ macro_rules! impl_batch_operations {
let value = json!({
"t": utxo.txout,
"i": utxo.keychain,
"s": utxo.is_spent,
});
self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*;
@@ -125,8 +126,9 @@ macro_rules! impl_batch_operations {
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
let txout = serde_json::from_value(val["t"].take())?;
let keychain = serde_json::from_value(val["i"].take())?;
let is_spent = val.get_mut("s").and_then(|s| s.take().as_bool()).unwrap_or(false);
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain }))
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain, is_spent, }))
}
}
}
@@ -246,11 +248,16 @@ impl Database for Tree {
let mut val: serde_json::Value = serde_json::from_slice(&v)?;
let txout = serde_json::from_value(val["t"].take())?;
let keychain = serde_json::from_value(val["i"].take())?;
let is_spent = val
.get_mut("s")
.and_then(|s| s.take().as_bool())
.unwrap_or(false);
Ok(LocalUtxo {
outpoint,
txout,
keychain,
is_spent,
})
})
.collect()
@@ -314,11 +321,16 @@ impl Database for Tree {
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
let txout = serde_json::from_value(val["t"].take())?;
let keychain = serde_json::from_value(val["i"].take())?;
let is_spent = val
.get_mut("s")
.and_then(|s| s.take().as_bool())
.unwrap_or(false);
Ok(LocalUtxo {
outpoint: *outpoint,
txout,
keychain,
is_spent,
})
})
.transpose()

View File

@@ -150,8 +150,10 @@ impl BatchOperations for MemoryDatabase {
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
self.map
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
self.map.insert(
key,
Box::new((utxo.txout.clone(), utxo.keychain, utxo.is_spent)),
);
Ok(())
}
@@ -228,11 +230,12 @@ impl BatchOperations for MemoryDatabase {
match res {
None => Ok(None),
Some(b) => {
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
Ok(Some(LocalUtxo {
outpoint: *outpoint,
txout,
keychain,
is_spent,
}))
}
}
@@ -326,11 +329,12 @@ impl Database for MemoryDatabase {
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
.map(|(k, v)| {
let outpoint = deserialize(&k[1..]).unwrap();
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
let (txout, keychain, is_spent) = v.downcast_ref().cloned().unwrap();
Ok(LocalUtxo {
outpoint,
txout,
keychain,
is_spent,
})
})
.collect()
@@ -389,11 +393,12 @@ impl Database for MemoryDatabase {
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
Ok(self.map.get(&key).map(|b| {
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
LocalUtxo {
outpoint: *outpoint,
txout,
keychain,
is_spent,
}
}))
}
@@ -526,6 +531,7 @@ macro_rules! populate_test_db {
vout: vout as u32,
},
keychain: $crate::KeychainKind::External,
is_spent: false,
})
.unwrap();
}
@@ -554,7 +560,7 @@ macro_rules! doctest_wallet {
Some(100),
);
$crate::Wallet::new_offline(
$crate::Wallet::new(
&descriptors.0,
descriptors.1.as_ref(),
Network::Regtest,

View File

@@ -159,6 +159,10 @@ pub trait Database: BatchOperations {
/// It should insert and return `0` if not present in the database
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
#[deprecated(
since = "0.18.0",
note = "The flush function is only needed for the sled database on mobile, instead for mobile use the sqlite database."
)]
/// Force changes to be written to disk
fn flush(&mut self) -> Result<(), Error>;
}
@@ -196,8 +200,7 @@ pub(crate) trait DatabaseUtils: Database {
D: FnOnce() -> Result<Option<Transaction>, Error>,
{
self.get_tx(txid, true)?
.map(|t| t.transaction)
.flatten()
.and_then(|t| t.transaction)
.map_or_else(default, |t| Ok(Some(t)))
}
@@ -316,10 +319,12 @@ pub mod test {
txout,
outpoint,
keychain: KeychainKind::External,
is_spent: true,
};
tree.set_utxo(&utxo).unwrap();
tree.set_utxo(&utxo).unwrap();
assert_eq!(tree.iter_utxos().unwrap().len(), 1);
assert_eq!(tree.get_utxo(&outpoint).unwrap(), Some(utxo));
}
@@ -371,6 +376,34 @@ pub mod test {
);
}
pub fn test_list_transaction<D: Database>(mut tree: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
let mut tx_details = TransactionDetails {
transaction: Some(tx),
txid,
received: 1337,
sent: 420420,
fee: Some(140),
confirmation_time: Some(BlockTime {
timestamp: 123456,
height: 1000,
}),
};
tree.set_tx(&tx_details).unwrap();
// get raw tx
assert_eq!(tree.iter_txs(true).unwrap(), vec![tx_details.clone()]);
// now get without raw tx
tx_details.transaction = None;
// get not raw tx
assert_eq!(tree.iter_txs(false).unwrap(), vec![tx_details.clone()]);
}
pub fn test_last_index<D: Database>(mut tree: D) {
tree.set_last_index(KeychainKind::External, 1337).unwrap();

View File

@@ -40,6 +40,17 @@ static MIGRATIONS: &[&str] = &[
"CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER);",
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
"DROP TABLE transaction_details_old;",
"ALTER TABLE utxos ADD COLUMN is_spent;",
// drop all data due to possible inconsistencies with duplicate utxos, re-sync required
"DELETE FROM checksums;",
"DELETE FROM last_derivation_indices;",
"DELETE FROM script_pubkeys;",
"DELETE FROM sync_time;",
"DELETE FROM transaction_details;",
"DELETE FROM transactions;",
"DELETE FROM utxos;",
"DROP INDEX idx_txid_vout;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
];
/// Sqlite database stored on filesystem
@@ -83,14 +94,16 @@ impl SqliteDatabase {
vout: u32,
txid: &[u8],
script: &[u8],
is_spent: bool,
) -> Result<i64, Error> {
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script) VALUES (:value, :keychain, :vout, :txid, :script)")?;
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script, is_spent) VALUES (:value, :keychain, :vout, :txid, :script, :is_spent) ON CONFLICT(txid, vout) DO UPDATE SET value=:value, keychain=:keychain, script=:script, is_spent=:is_spent")?;
statement.execute(named_params! {
":value": value,
":keychain": keychain,
":vout": vout,
":txid": txid,
":script": script
":script": script,
":is_spent": is_spent,
})?;
Ok(self.connection.last_insert_rowid())
@@ -291,7 +304,7 @@ impl SqliteDatabase {
fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
let mut statement = self
.connection
.prepare_cached("SELECT value, keychain, vout, txid, script FROM utxos")?;
.prepare_cached("SELECT value, keychain, vout, txid, script, is_spent FROM utxos")?;
let mut utxos: Vec<LocalUtxo> = vec![];
let mut rows = statement.query([])?;
while let Some(row) = rows.next()? {
@@ -300,6 +313,7 @@ impl SqliteDatabase {
let vout = row.get(2)?;
let txid: Vec<u8> = row.get(3)?;
let script: Vec<u8> = row.get(4)?;
let is_spent: bool = row.get(5)?;
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
@@ -310,19 +324,16 @@ impl SqliteDatabase {
script_pubkey: script.into(),
},
keychain,
is_spent,
})
}
Ok(utxos)
}
fn select_utxo_by_outpoint(
&self,
txid: &[u8],
vout: u32,
) -> Result<Option<(u64, KeychainKind, Script)>, Error> {
fn select_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<Option<LocalUtxo>, Error> {
let mut statement = self.connection.prepare_cached(
"SELECT value, keychain, script FROM utxos WHERE txid=:txid AND vout=:vout",
"SELECT value, keychain, script, is_spent FROM utxos WHERE txid=:txid AND vout=:vout",
)?;
let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
match rows.next()? {
@@ -331,9 +342,18 @@ impl SqliteDatabase {
let keychain: String = row.get(1)?;
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
let script: Vec<u8> = row.get(2)?;
let script: Script = script.into();
let script_pubkey: Script = script.into();
let is_spent: bool = row.get(3)?;
Ok(Some((value, keychain, script)))
Ok(Some(LocalUtxo {
outpoint: OutPoint::new(deserialize(txid)?, vout),
txout: TxOut {
value,
script_pubkey,
},
keychain,
is_spent,
}))
}
None => Ok(None),
}
@@ -380,7 +400,7 @@ impl SqliteDatabase {
let sent: u64 = row.get(3)?;
let fee: Option<u64> = row.get(4)?;
let height: Option<u32> = row.get(5)?;
let raw_tx: Option<Vec<u8>> = row.get(7)?;
let raw_tx: Option<Vec<u8>> = row.get(6)?;
let tx: Option<Transaction> = match raw_tx {
Some(raw_tx) => {
let tx: Transaction = deserialize(&raw_tx)?;
@@ -620,6 +640,7 @@ impl BatchOperations for SqliteDatabase {
utxo.outpoint.vout,
&utxo.outpoint.txid,
utxo.txout.script_pubkey.as_bytes(),
utxo.is_spent,
)?;
Ok(())
}
@@ -694,16 +715,9 @@ impl BatchOperations for SqliteDatabase {
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
Some((value, keychain, script_pubkey)) => {
Some(local_utxo) => {
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
Ok(Some(LocalUtxo {
outpoint: *outpoint,
txout: TxOut {
value,
script_pubkey,
},
keychain,
}))
Ok(Some(local_utxo))
}
None => Ok(None),
}
@@ -832,17 +846,7 @@ impl Database for SqliteDatabase {
}
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
Some((value, keychain, script_pubkey)) => Ok(Some(LocalUtxo {
outpoint: *outpoint,
txout: TxOut {
value,
script_pubkey,
},
keychain,
})),
None => Ok(None),
}
self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)
}
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
@@ -1026,4 +1030,9 @@ pub mod test {
fn test_sync_time() {
crate::database::test::test_sync_time(get_database());
}
#[test]
fn test_txs() {
crate::database::test::test_list_transaction(get_database());
}
}

View File

@@ -10,6 +10,41 @@
// licenses.
//! Derived descriptor keys
//!
//! The [`DerivedDescriptorKey`] type is a wrapper over the standard [`DescriptorPublicKey`] which
//! guarantees that all the extended keys have a fixed derivation path, i.e. all the wildcards have
//! been replaced by actual derivation indexes.
//!
//! The [`AsDerived`] trait provides a quick way to derive descriptors to obtain a
//! `Descriptor<DerivedDescriptorKey>` type. This, in turn, can be used to derive public
//! keys for arbitrary derivation indexes.
//!
//! Combining this with [`Wallet::get_signers`], secret keys can also be derived.
//!
//! # Example
//!
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::{AsDerived, DescriptorPublicKey};
//! use bdk::miniscript::{ToPublicKey, TranslatePk, MiniscriptKey};
//!
//! let secp = Secp256k1::gen_new();
//!
//! let key = DescriptorPublicKey::from_str("[aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/*")?;
//! let (descriptor, _, _) = bdk::descriptor!(wpkh(key))?;
//!
//! // derived: wpkh([aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/42)#3ladd0t2
//! let derived = descriptor.as_derived(42, &secp);
//! println!("derived: {}", derived);
//!
//! // with_pks: wpkh(02373ecb54c5e83bd7e0d40adf78b65efaf12fafb13571f0261fc90364eee22e1e)#p4jjgvll
//! let with_pks = derived.translate_pk_infallible(|pk| pk.to_public_key(), |pkh| pkh.to_public_key().to_pubkeyhash());
//! println!("with_pks: {}", with_pks);
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers
use std::cmp::Ordering;
use std::fmt;
@@ -17,13 +52,10 @@ use std::hash::{Hash, Hasher};
use std::ops::Deref;
use bitcoin::hashes::hash160;
use bitcoin::PublicKey;
use bitcoin::{PublicKey, XOnlyPublicKey};
pub use miniscript::{
descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript,
ScriptContext, Segwitv0,
};
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard};
use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk};
use crate::wallet::utils::SecpCtx;
@@ -96,21 +128,44 @@ impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
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) => {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(_),
..
}) => panic!("Found x-only public key in non-tr descriptor"),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(ref pk),
..
}) => *pk,
DescriptorPublicKey::XPub(ref xpub) => PublicKey::new(
xpub.xkey
.derive_pub(self.1, &xpub.derivation_path)
.expect("Shouldn't fail, only normal derivations")
.public_key
}
.public_key,
),
}
}
fn to_x_only_pubkey(&self) -> XOnlyPublicKey {
match &self.0 {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(ref pk),
..
}) => *pk,
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(ref pk),
..
}) => XOnlyPublicKey::from(pk.inner),
DescriptorPublicKey::XPub(ref xpub) => XOnlyPublicKey::from(
xpub.xkey
.derive_pub(self.1, &xpub.derivation_path)
.expect("Shouldn't fail, only normal derivations")
.public_key,
),
}
}
@@ -119,14 +174,19 @@ impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
}
}
pub(crate) trait AsDerived {
// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
/// Utilities to derive descriptors
///
/// Check out the [module level] documentation for more.
///
/// [module level]: crate::descriptor::derived
pub 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
/// 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>>;
}

View File

@@ -73,6 +73,48 @@ macro_rules! impl_top_level_pk {
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_top_level_tr {
( $internal_key:expr, $tap_tree:expr ) => {{
use $crate::miniscript::descriptor::{
Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr,
};
use $crate::miniscript::Tap;
#[allow(unused_imports)]
use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
$internal_key
.into_descriptor_key()
.and_then(|key: DescriptorKey<Tap>| key.extract(&secp))
.map_err($crate::descriptor::DescriptorError::Key)
.and_then(|(pk, mut key_map, mut valid_networks)| {
let tap_tree = $tap_tree.map(
|(tap_tree, tree_keymap, tree_networks): (
TapTree<DescriptorPublicKey>,
KeyMap,
ValidNetworks,
)| {
key_map.extend(tree_keymap.into_iter());
valid_networks =
$crate::keys::merge_networks(&valid_networks, &tree_networks);
tap_tree
},
);
Ok((
Descriptor::<DescriptorPublicKey>::Tr(Tr::new(pk, tap_tree)?),
key_map,
valid_networks,
))
})
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_leaf_opcode {
@@ -228,6 +270,62 @@ macro_rules! impl_sortedmulti {
}
#[doc(hidden)]
#[macro_export]
macro_rules! parse_tap_tree {
( @merge $tree_a:expr, $tree_b:expr) => {{
use std::sync::Arc;
use $crate::miniscript::descriptor::TapTree;
$tree_a
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
a_keymap.extend(b_keymap.into_iter());
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
})
}};
// Two sub-trees
( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{
let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
$crate::parse_tap_tree!(@merge tree_a, tree_b)
}};
// One leaf and a sub-tree
( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{
let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } );
$crate::parse_tap_tree!(@merge tree_a, tree_b)
}};
( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } );
let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
$crate::parse_tap_tree!(@merge tree_a, tree_b)
}};
// Two leaves
( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{
let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) );
let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) );
$crate::parse_tap_tree!(@merge tree_a, tree_b)
}};
// Single leaf
( $op:ident ( $( $minisc:tt )* ) ) => {{
use std::sync::Arc;
use $crate::miniscript::descriptor::TapTree;
$crate::fragment!( $op ( $( $minisc )* ) )
.map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks))
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! apply_modifier {
@@ -336,7 +434,7 @@ macro_rules! apply_modifier {
/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
///
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sdv:older(...)))`
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))`
///
/// ```
/// # use std::str::FromStr;
@@ -349,7 +447,7 @@ macro_rules! apply_modifier {
///
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
/// wsh (
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:d:v:older(my_timelock))
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
/// )
/// }?;
///
@@ -357,7 +455,7 @@ macro_rules! apply_modifier {
/// let b_items = vec![
/// bdk::fragment!(pk(my_key_1))?,
/// bdk::fragment!(s:pk(my_key_2))?,
/// bdk::fragment!(s:d:v:older(my_timelock))?,
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
/// ];
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
///
@@ -441,6 +539,15 @@ macro_rules! descriptor {
( wsh ( $( $minisc:tt )* ) ) => ({
$crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*)
});
( tr ( $internal_key:expr ) ) => ({
$crate::impl_top_level_tr!($internal_key, None)
});
( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({
let tap_tree = $crate::parse_tap_tree!( $( $taptree )* );
tap_tree
.and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree)))
});
}
#[doc(hidden)]
@@ -480,6 +587,23 @@ impl<A, B, C> From<(A, (B, (C, ())))> for TupleThree<A, B, C> {
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! group_multi_keys {
( $( $key:expr ),+ ) => {{
use $crate::keys::IntoDescriptorKey;
let keys = vec![
$(
$key.into_descriptor_key(),
)*
];
keys.into_iter().collect::<Result<Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! fragment_internal {
@@ -640,21 +764,22 @@ macro_rules! fragment {
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
});
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
$crate::keys::make_multi($thresh, $keys)
});
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
use $crate::keys::IntoDescriptorKey;
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
let keys = vec![
$(
$key.into_descriptor_key(),
)*
];
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp)
});
( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({
$crate::group_multi_keys!( $( $key ),* )
.and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) ))
});
( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({
let secp = $crate::bitcoin::secp256k1::Secp256k1::new();
keys.into_iter().collect::<Result<Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
.and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp))
$crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp)
});
( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({
$crate::group_multi_keys!( $( $key ),* )
.and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) ))
});
// `sortedmulti()` is handled separately
@@ -1048,13 +1173,15 @@ mod test {
let private_key =
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
let (descriptor, _, _) =
descriptor!(wsh(thresh(2,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
descriptor!(wsh(thresh(2,n: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)))#cfdcqs3s")
assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
}
#[test]
#[should_panic(expected = "Miniscript(ContextError(CompressedOnly))")]
#[should_panic(
expected = "Miniscript(ContextError(CompressedOnly(\"04b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a87378ec38ff91d43e8c2092ebda601780485263da089465619e0358a5c1be7ac91f4\")))"
)]
fn test_dsl_miniscript_checks() {
let mut uncompressed_pk =
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
@@ -1062,4 +1189,35 @@ mod test {
descriptor!(wsh(v: pk(uncompressed_pk))).unwrap();
}
#[test]
fn test_dsl_tr_only_key() {
let private_key =
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap();
assert_eq!(
descriptor.to_string(),
"tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v"
)
}
#[test]
fn test_dsl_tr_simple_tree() {
let private_key =
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
let (descriptor, _, _) =
descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap();
assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d")
}
#[test]
fn test_dsl_tr_single_leaf() {
let private_key =
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap();
assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7")
}
}

View File

@@ -14,23 +14,25 @@
//! This module contains generic utilities to work with descriptors, plus some re-exported types
//! from [`miniscript`].
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::util::psbt;
use bitcoin::{Network, PublicKey, Script, TxOut};
use bitcoin::util::{psbt, taproot};
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
use bitcoin::{Network, Script, TxOut};
use miniscript::descriptor::{
DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard,
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
pub use miniscript::{
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
};
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum;
pub(crate) mod derived;
pub mod derived;
#[doc(hidden)]
pub mod dsl;
pub mod error;
@@ -38,8 +40,7 @@ pub mod policy;
pub mod template;
pub use self::checksum::get_checksum;
use self::derived::AsDerived;
pub use self::derived::DerivedDescriptorKey;
pub use self::derived::{AsDerived, DerivedDescriptorKey};
pub use self::error::Error as DescriptorError;
pub use self::policy::Policy;
use self::template::DescriptorTemplateOut;
@@ -58,7 +59,14 @@ pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
///
/// [`psbt::Input`]: bitcoin::util::psbt::Input
/// [`psbt::Output`]: bitcoin::util::psbt::Output
pub type HdKeyPaths = BTreeMap<PublicKey, KeySource>;
pub type HdKeyPaths = BTreeMap<secp256k1::PublicKey, KeySource>;
/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or
/// [`psbt::Output`]
///
/// [`psbt::Input`]: bitcoin::util::psbt::Input
/// [`psbt::Output`]: bitcoin::util::psbt::Output
pub type TapKeyOrigins = BTreeMap<bitcoin::XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
pub trait IntoWalletDescriptor {
@@ -301,17 +309,29 @@ where
}
pub(crate) trait DerivedDescriptorMeta {
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError>;
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths;
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins;
}
pub(crate) trait DescriptorMeta {
fn is_witness(&self) -> bool;
fn is_taproot(&self) -> bool;
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
fn derive_from_hd_keypaths<'s>(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
fn derive_from_tap_key_origins<'s>(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
fn derive_from_psbt_key_origins<'s>(
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
fn derive_from_psbt_input<'s>(
&self,
psbt_input: &psbt::Input,
@@ -328,23 +348,34 @@ pub(crate) trait DescriptorScripts {
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
fn psbt_redeem_script(&self) -> Option<Script> {
match self.desc_type() {
DescriptorType::ShWpkh => Some(self.explicit_script()),
DescriptorType::ShWsh => Some(self.explicit_script().to_v0_p2wsh()),
DescriptorType::Sh => Some(self.explicit_script()),
DescriptorType::Bare => Some(self.explicit_script()),
DescriptorType::ShSortedMulti => Some(self.explicit_script()),
_ => None,
DescriptorType::ShWpkh => Some(self.explicit_script().unwrap()),
DescriptorType::ShWsh => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
DescriptorType::Sh => Some(self.explicit_script().unwrap()),
DescriptorType::Bare => Some(self.explicit_script().unwrap()),
DescriptorType::ShSortedMulti => Some(self.explicit_script().unwrap()),
DescriptorType::ShWshSortedMulti => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::Tr
| DescriptorType::Wsh
| DescriptorType::WshSortedMulti => None,
}
}
fn psbt_witness_script(&self) -> Option<Script> {
match self.desc_type() {
DescriptorType::Wsh => Some(self.explicit_script()),
DescriptorType::ShWsh => Some(self.explicit_script()),
DescriptorType::Wsh => Some(self.explicit_script().unwrap()),
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
Some(self.explicit_script())
Some(self.explicit_script().unwrap())
}
_ => None,
DescriptorType::Bare
| DescriptorType::Sh
| DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::ShSortedMulti
| DescriptorType::Tr
| DescriptorType::ShWpkh => None,
}
}
}
@@ -362,6 +393,10 @@ impl DescriptorMeta for ExtendedDescriptor {
)
}
fn is_taproot(&self) -> bool {
self.desc_type() == DescriptorType::Tr
}
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
let mut answer = Vec::new();
@@ -376,61 +411,124 @@ impl DescriptorMeta for ExtendedDescriptor {
Ok(answer)
}
fn derive_from_hd_keypaths<'s>(
fn derive_from_psbt_key_origins<'s>(
&self,
hd_keypaths: &HdKeyPaths,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
// Ensure that deriving `xpub` with `path` yields `expected`
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
path: &DerivationPath,
expected: &SinglePubKey| {
let derived = xpub
.xkey
.derive_pub(secp, path)
.expect("The path should never contain hardened derivation steps")
.public_key;
match expected {
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
_ => false,
}
};
let mut path_found = None;
self.for_each_key(|key| {
if path_found.is_some() {
// already found a matching path, we are done
return true;
}
// using `for_any_key` should make this stop as soon as we return `true`
self.for_any_key(|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 `key_origins`. If it does, `matches()` will
// 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 `key_origins` 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.
let root_fingerprint = xpub.root_fingerprint(secp);
let derivation_path: Option<Vec<ChildNumber>> = index
let derive_path = key_origins
.get_key_value(&root_fingerprint)
.and_then(|(fingerprint, path)| {
xpub.matches(&(**fingerprint, (*path).clone()), secp)
.and_then(|(fingerprint, (path, expected))| {
xpub.matches(&(*fingerprint, (*path).clone()), secp)
.zip(Some((path, expected)))
})
.map(|prefix| {
index
.get(&xpub.root_fingerprint(secp))
.unwrap()
.and_then(|(prefix, (full_path, expected))| {
let derive_path = full_path
.into_iter()
.skip(prefix.into_iter().count())
.cloned()
.collect()
.collect::<DerivationPath>();
// `derive_path` only contains the replacement index for the wildcard, if present, or
// an empty path for fixed descriptors. To verify the key we also need the normal steps
// that come before the wildcard, so we take them directly from `xpub` and then append
// the final index
if verify_key(
xpub,
&xpub.derivation_path.extend(derive_path.clone()),
expected,
) {
Some(derive_path)
} else {
log::debug!(
"Key `{}` derived with {} yields an unexpected key",
root_fingerprint,
derive_path
);
None
}
});
match derivation_path {
match derive_path {
Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
// Ignore hardened wildcards
if let ChildNumber::Normal { index } = path[0] {
path_found = Some(index)
path_found = Some(index);
return true;
}
}
Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
path_found = Some(0)
path_found = Some(0);
return true;
}
_ => {}
}
}
true
false
});
path_found.map(|path| self.as_derived(path, secp))
}
fn derive_from_hd_keypaths<'s>(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
let key_origins = hd_keypaths
.iter()
.map(|(pk, (fingerprint, path))| {
(
*fingerprint,
(path, SinglePubKey::FullKey(PublicKey::new(*pk))),
)
})
.collect();
self.derive_from_psbt_key_origins(key_origins, secp)
}
fn derive_from_tap_key_origins<'s>(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
let key_origins = tap_key_origins
.iter()
.map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk))))
.collect();
self.derive_from_psbt_key_origins(key_origins, secp)
}
fn derive_from_psbt_input<'s>(
&self,
psbt_input: &psbt::Input,
@@ -440,6 +538,9 @@ impl DescriptorMeta for ExtendedDescriptor {
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
return Some(derived);
}
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
return Some(derived);
}
if self.is_deriveable() {
// We can't try to bruteforce the derivation index, exit here
return None;
@@ -448,7 +549,10 @@ impl DescriptorMeta for ExtendedDescriptor {
let descriptor = self.as_derived_fixed(secp);
match descriptor.desc_type() {
// TODO: add pk() here
DescriptorType::Pkh | DescriptorType::Wpkh | DescriptorType::ShWpkh
DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::ShWpkh
| DescriptorType::Tr
if utxo.is_some()
&& descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey =>
{
@@ -456,7 +560,7 @@ impl DescriptorMeta for ExtendedDescriptor {
}
DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti
if psbt_input.redeem_script.is_some()
&& &descriptor.explicit_script()
&& &descriptor.explicit_script().unwrap()
== psbt_input.redeem_script.as_ref().unwrap() =>
{
Some(descriptor)
@@ -466,7 +570,7 @@ impl DescriptorMeta for ExtendedDescriptor {
| DescriptorType::ShWshSortedMulti
| DescriptorType::WshSortedMulti
if psbt_input.witness_script.is_some()
&& &descriptor.explicit_script()
&& &descriptor.explicit_script().unwrap()
== psbt_input.witness_script.as_ref().unwrap() =>
{
Some(descriptor)
@@ -477,7 +581,7 @@ impl DescriptorMeta for ExtendedDescriptor {
}
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result<HdKeyPaths, DescriptorError> {
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths {
let mut answer = BTreeMap::new();
self.for_each_key(|key| {
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
@@ -495,7 +599,64 @@ impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
true
});
Ok(answer)
answer
}
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins {
use miniscript::ToPublicKey;
let mut answer = BTreeMap::new();
let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| {
let key_origin = match pk.deref() {
DescriptorPublicKey::XPub(xpub) => {
Some((xpub.root_fingerprint(secp), xpub.full_path(&[])))
}
DescriptorPublicKey::SinglePub(_) => None,
};
// If this is the internal key, we only insert the key origin if it's not None.
// For keys found in the tap tree we always insert a key origin (because the signer
// looks for it to know which leaves to sign for), even though it may be None
match (lh, key_origin) {
(None, Some(ko)) => {
answer
.entry(pk.to_x_only_pubkey())
.or_insert_with(|| (vec![], ko));
}
(Some(lh), origin) => {
answer
.entry(pk.to_x_only_pubkey())
.or_insert_with(|| (vec![], origin.unwrap_or_default()))
.0
.push(lh);
}
_ => {}
}
};
if let Descriptor::Tr(tr) = &self {
// Internal key first, then iterate the scripts
insert_path(tr.internal_key(), None);
for (_, ms) in tr.iter_scripts() {
// Assume always the same leaf version
let leaf_hash = taproot::TapLeafHash::from_script(
&ms.encode(),
taproot::LeafVersion::TapScript,
);
for key in ms.iter_pk_pkh() {
let key = match key {
miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk,
miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk,
};
insert_path(&key, Some(leaf_hash));
}
}
}
answer
}
}
@@ -507,6 +668,7 @@ mod test {
use bitcoin::hashes::hex::FromHex;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::{bip32, psbt};
use bitcoin::Script;
use super::*;
use crate::psbt::PsbtUtils;
@@ -770,4 +932,22 @@ mod test {
DescriptorError::DuplicatedKeys
));
}
#[test]
fn test_sh_wsh_sortedmulti_redeemscript() {
use super::{AsDerived, DescriptorScripts};
let secp = Secp256k1::new();
let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
let (descriptor, _) =
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
let descriptor = descriptor.as_derived(0, &secp);
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
assert_eq!(descriptor.psbt_witness_script(), Some(script));
}
}

View File

@@ -21,6 +21,7 @@
//! ```
//! # use std::sync::Arc;
//! # use bdk::descriptor::*;
//! # use bdk::wallet::signer::*;
//! # use bdk::bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new();
@@ -29,7 +30,7 @@
//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?;
//! println!("{:?}", extended_desc);
//!
//! let signers = Arc::new(key_map.into());
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy)?);
//! # Ok::<(), bdk::Error>(())
@@ -44,59 +45,64 @@ use serde::{Serialize, Serializer};
use bitcoin::hashes::*;
use bitcoin::util::bip32::Fingerprint;
use bitcoin::PublicKey;
use bitcoin::{PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner};
use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner,
};
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use crate::descriptor::ExtractPolicy;
use crate::keys::ExtScriptContext;
use crate::wallet::signer::{SignerId, SignersContainer};
use crate::wallet::utils::{self, After, Older, SecpCtx};
use super::checksum::get_checksum;
use super::error::Error;
use super::XKeyUtils;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
use miniscript::psbt::PsbtInputSatisfier;
/// Raw public key or extended key fingerprint
#[derive(Debug, Clone, Default, Serialize)]
pub struct PkOrF {
#[serde(skip_serializing_if = "Option::is_none")]
pubkey: Option<PublicKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pubkey_hash: Option<hash160::Hash>,
#[serde(skip_serializing_if = "Option::is_none")]
fingerprint: Option<Fingerprint>,
/// A unique identifier for a key
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PkOrF {
/// A legacy public key
Pubkey(PublicKey),
/// A x-only public key
XOnlyPubkey(XOnlyPublicKey),
/// An extended key fingerprint
Fingerprint(Fingerprint),
}
impl PkOrF {
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
match k {
DescriptorPublicKey::SinglePub(pubkey) => PkOrF {
pubkey: Some(pubkey.key),
..Default::default()
},
DescriptorPublicKey::XPub(xpub) => PkOrF {
fingerprint: Some(xpub.root_fingerprint(secp)),
..Default::default()
},
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => PkOrF::Pubkey(*pk),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => PkOrF::XOnlyPubkey(*pk),
DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
}
}
}
/// An item that needs to be satisfied
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "UPPERCASE")]
pub enum SatisfiableItem {
// Leaves
/// Signature for a raw public key
Signature(PkOrF),
/// Signature for an extended key fingerprint
SignatureKey(PkOrF),
/// ECDSA Signature for a raw public key
EcdsaSignature(PkOrF),
/// Schnorr Signature for a raw public key
SchnorrSignature(PkOrF),
/// SHA256 preimage hash
Sha256Preimage {
/// The digest value
@@ -243,7 +249,7 @@ where
}
/// Represent if and how much a policy item is satisfied by the wallet's descriptor
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "UPPERCASE")]
pub enum Satisfaction {
/// Only a partial satisfaction of some kind of threshold policy
@@ -357,7 +363,8 @@ impl Satisfaction {
// we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one
// condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we
// consider every possible options and check whether or not they are compatible.
.map(|i_vec| {
// since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out
.flat_map(|i_vec| {
mix(i_vec
.iter()
.map(|i| {
@@ -371,9 +378,6 @@ impl Satisfaction {
.map(|x| (i_vec.clone(), x))
.collect::<Vec<(Vec<usize>, Vec<Condition>)>>()
})
// .inspect(|x: &Vec<(Vec<usize>, Vec<Condition>)>| println!("fetch {:?}", x))
// since the previous step can turn one item of the iterator into multiple ones, we call flatten to expand them out
.flatten()
// .inspect(|x| println!("flat {:?}", x))
// try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err
.map(|(key, val)| {
@@ -419,7 +423,7 @@ impl From<bool> for Satisfaction {
}
/// Descriptor spending policy
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Policy {
/// Identifier for this policy node
pub id: String,
@@ -567,7 +571,7 @@ impl Policy {
Ok(Some(policy))
}
fn make_multisig(
fn make_multisig<Ctx: ScriptContext + 'static>(
keys: &[DescriptorPublicKey],
signers: &SignersContainer,
build_sat: BuildSatisfaction,
@@ -601,7 +605,7 @@ impl Policy {
}
if let Some(psbt) = build_sat.psbt() {
if signature_in_psbt(psbt, key, secp) {
if Ctx::find_signature(psbt, key, secp) {
satisfaction.add(
&Satisfaction::Complete {
condition: Default::default(),
@@ -717,18 +721,27 @@ impl From<SatisfiableItem> for Policy {
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
match key {
DescriptorPublicKey::SinglePub(pubkey) => pubkey.key.to_pubkeyhash().into(),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => pk.to_pubkeyhash().into(),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => pk.to_pubkeyhash().into(),
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
}
}
fn signature(
fn make_generic_signature<M: Fn() -> SatisfiableItem, F: Fn(&Psbt) -> bool>(
key: &DescriptorPublicKey,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
make_policy: M,
find_sig: F,
) -> Policy {
let mut policy: Policy = SatisfiableItem::Signature(PkOrF::from_key(key, secp)).into();
let mut policy: Policy = make_policy().into();
policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
Satisfaction::Complete {
@@ -739,7 +752,7 @@ fn signature(
};
if let Some(psbt) = build_sat.psbt() {
policy.satisfaction = if signature_in_psbt(psbt, key, secp) {
policy.satisfaction = if find_sig(psbt) {
Satisfaction::Complete {
condition: Default::default(),
}
@@ -751,26 +764,119 @@ fn signature(
policy
}
fn signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
fn generic_sig_in_psbt<
// C is for "check", it's a closure we use to *check* if a psbt input contains the signature
// for a specific key
C: Fn(&PsbtInput, &SinglePubKey) -> bool,
// E is for "extract", it extracts a key from the bip32 derivations found in the psbt input
E: Fn(&PsbtInput, Fingerprint) -> Option<SinglePubKey>,
>(
psbt: &Psbt,
key: &DescriptorPublicKey,
secp: &SecpCtx,
check: C,
extract: E,
) -> bool {
//TODO check signature validity
psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(key) => input.partial_sigs.contains_key(&key.key),
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::XPub(xpub) => {
let pubkey = input
.bip32_derivation
.iter()
.find(|(_, (f, _))| *f == xpub.root_fingerprint(secp))
.map(|(p, _)| p);
//TODO check actual derivation matches
match pubkey {
Some(pubkey) => input.partial_sigs.contains_key(pubkey),
match extract(input, xpub.root_fingerprint(secp)) {
Some(pubkey) => check(input, &pubkey),
None => false,
}
}
})
}
impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
trait SigExt: ScriptContext {
fn make_signature(
key: &DescriptorPublicKey,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Policy;
fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool;
}
impl<T: ScriptContext + 'static> SigExt for T {
fn make_signature(
key: &DescriptorPublicKey,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Policy {
if T::as_enum().is_taproot() {
make_generic_signature(
key,
signers,
build_sat,
secp,
|| SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)),
|psbt| Self::find_signature(psbt, key, secp),
)
} else {
make_generic_signature(
key,
signers,
build_sat,
secp,
|| SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)),
|psbt| Self::find_signature(psbt, key, secp),
)
}
}
fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
if T::as_enum().is_taproot() {
generic_sig_in_psbt(
psbt,
key,
secp,
|input, pk| {
let pk = match pk {
SinglePubKey::XOnly(pk) => pk,
_ => return false,
};
if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() {
true
} else {
input.tap_script_sigs.keys().any(|(sk, _)| sk == pk)
}
},
|input, fing| {
input
.tap_key_origins
.iter()
.find(|(_, (_, (f, _)))| f == &fing)
.map(|(pk, _)| SinglePubKey::XOnly(*pk))
},
)
} else {
generic_sig_in_psbt(
psbt,
key,
secp,
|input, pk| match pk {
SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk),
_ => false,
},
|input, fing| {
input
.bip32_derivation
.iter()
.find(|(_, (f, _))| f == &fing)
.map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk)))
},
)
}
}
}
impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx> {
fn extract_policy(
&self,
signers: &SignersContainer,
@@ -780,8 +886,10 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
Ok(match &self.node {
// Leaves
Terminal::True | Terminal::False => None,
Terminal::PkK(pubkey) => Some(signature(pubkey, signers, build_sat, secp)),
Terminal::PkH(pubkey_hash) => Some(signature(pubkey_hash, signers, build_sat, secp)),
Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)),
Terminal::PkH(pubkey_hash) => {
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
}
Terminal::After(value) => {
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
policy.contribution = Satisfaction::Complete {
@@ -842,8 +950,8 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
Terminal::Hash160(hash) => {
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
}
Terminal::Multi(k, pks) => {
Policy::make_multisig(pks, signers, build_sat, *k, false, secp)?
Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => {
Policy::make_multisig::<Ctx>(pks, signers, build_sat, *k, false, secp)?
}
// Identities
Terminal::Alt(inner)
@@ -934,13 +1042,13 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
fn make_sortedmulti<Ctx: ScriptContext>(
fn make_sortedmulti<Ctx: ScriptContext + 'static>(
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Result<Option<Policy>, Error> {
Ok(Policy::make_multisig(
Ok(Policy::make_multisig::<Ctx>(
keys.pks.as_ref(),
signers,
build_sat,
@@ -951,10 +1059,25 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
}
match self {
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature(
pk.as_inner(),
signers,
build_sat,
secp,
))),
Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
pk.as_inner(),
signers,
build_sat,
secp,
))),
Descriptor::Sh(sh) => match sh.as_inner() {
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature(
pk.as_inner(),
signers,
build_sat,
secp,
))),
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
ShInner::Wsh(wsh) => match wsh.as_inner() {
@@ -969,6 +1092,28 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
},
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
Descriptor::Tr(tr) => {
// If there's no tap tree, treat this as a single sig, otherwise build a `Thresh`
// node with threshold = 1 and the key spend signature plus all the tree leaves
let key_spend_sig =
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
if tr.taptree().is_none() {
Ok(Some(key_spend_sig))
} else {
let mut items = vec![key_spend_sig];
items.append(
&mut tr
.iter_scripts()
.filter_map(|(_, ms)| {
ms.extract_policy(signers, build_sat, secp).transpose()
})
.collect::<Result<Vec<_>, _>>()?,
);
Ok(Policy::make_thresh(items, 1)?)
}
}
}
}
}
@@ -980,7 +1125,7 @@ mod test {
use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{Multisig, Signature, Thresh};
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use crate::wallet::signer::SignersContainer;
use bitcoin::secp256k1::Secp256k1;
@@ -1002,7 +1147,7 @@ mod test {
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
let path = bip32::DerivationPath::from_str(path).unwrap();
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_private(secp, &tprv);
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
let fingerprint = tprv.fingerprint(secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();
@@ -1021,30 +1166,26 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
);
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(matches!(&policy.contribution, Satisfaction::None));
let desc = descriptor!(wpkh(prvkey)).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
);
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
);
@@ -1060,7 +1201,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1068,8 +1209,8 @@ mod test {
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
);
// TODO should this be "Satisfaction::None" since we have no prv keys?
// TODO should items and conditions not be empty?
@@ -1092,15 +1233,15 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
);
assert!(
@@ -1124,7 +1265,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1132,8 +1273,8 @@ mod test {
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
@@ -1156,7 +1297,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1164,8 +1305,8 @@ mod test {
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
);
assert!(
@@ -1189,15 +1330,13 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
);
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(matches!(&policy.contribution, Satisfaction::None));
let desc = descriptor!(wpkh(prvkey)).unwrap();
@@ -1205,15 +1344,13 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Signature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint)
);
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
);
@@ -1232,7 +1369,7 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1240,8 +1377,8 @@ mod test {
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
&& keys[0].fingerprint.unwrap() == fingerprint0
&& keys[1].fingerprint.unwrap() == fingerprint1)
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
@@ -1275,7 +1412,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1314,7 +1451,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1339,7 +1476,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1357,7 +1494,7 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
@@ -1379,10 +1516,10 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers = keymap.into();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers, BuildSatisfaction::None, &secp)
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
@@ -1445,7 +1582,7 @@ mod test {
addr.to_string()
);
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
@@ -1501,19 +1638,19 @@ mod test {
let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc =
descriptor!(wsh(thresh(2,d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
descriptor!(wsh(thresh(2,n:d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let addr = wallet_desc
.as_derived(0, &secp)
.address(Network::Testnet)
.unwrap();
assert_eq!(
"tb1qhpemaacpeu8ajlnh8k9v55ftg0px58r8630fz8t5mypxcwdk5d8sum522g",
"tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58",
addr.to_string()
);
@@ -1594,9 +1731,185 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp);
assert!(policy.is_ok());
}
#[test]
fn test_extract_tr_key_spend() {
let secp = Secp256k1::new();
let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc = descriptor!(tr(prvkey)).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap();
assert_eq!(
policy,
Some(Policy {
id: "48u0tz0n".to_string(),
item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)),
satisfaction: Satisfaction::None,
contribution: Satisfaction::Complete {
condition: Condition::default()
}
})
);
}
#[test]
fn test_extract_tr_script_spend() {
let secp = Secp256k1::new();
let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
);
assert!(
matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1])
);
let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
let thresh_items = match policy.item {
SatisfiableItem::Thresh { items, .. } => items,
_ => unreachable!(),
};
assert_eq!(thresh_items[0].item, bob_sig);
assert_eq!(thresh_items[1].item, alice_sig);
}
#[test]
fn test_extract_tr_satisfaction_key_spend() {
const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA==";
const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA=";
let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
let secp = Secp256k1::new();
let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc = descriptor!(tr(pubkey)).unwrap();
let (wallet_desc, _) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let policy_unsigned = wallet_desc
.extract_policy(
&SignersContainer::default(),
BuildSatisfaction::Psbt(&unsigned_psbt),
&secp,
)
.unwrap()
.unwrap();
let policy_signed = wallet_desc
.extract_policy(
&SignersContainer::default(),
BuildSatisfaction::Psbt(&signed_psbt),
&secp,
)
.unwrap()
.unwrap();
assert_eq!(policy_unsigned.satisfaction, Satisfaction::None);
assert_eq!(
policy_signed.satisfaction,
Satisfaction::Complete {
condition: Default::default()
}
);
}
#[test]
fn test_extract_tr_satisfaction_script_spend() {
const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA==";
let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap();
let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap();
let secp = Secp256k1::new();
let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap();
let (wallet_desc, _) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let policy_unsigned = wallet_desc
.extract_policy(
&SignersContainer::default(),
BuildSatisfaction::Psbt(&unsigned_psbt),
&secp,
)
.unwrap()
.unwrap();
let policy_signed = wallet_desc
.extract_policy(
&SignersContainer::default(),
BuildSatisfaction::Psbt(&signed_psbt),
&secp,
)
.unwrap()
.unwrap();
assert!(
matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
);
assert!(
matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty())
);
assert!(
matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
);
assert!(
matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1])
);
let satisfied_items = match policy_signed.item {
SatisfiableItem::Thresh { items, .. } => items,
_ => unreachable!(),
};
assert_eq!(
satisfied_items[0].satisfaction,
Satisfaction::Complete {
condition: Default::default()
}
);
assert_eq!(
satisfied_items[1].satisfaction,
Satisfaction::Complete {
condition: Default::default()
}
);
}
}

View File

@@ -79,7 +79,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Pkh(key),
/// None,
/// Network::Testnet,
@@ -113,7 +113,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Wpkh_P2Sh(key),
/// None,
/// Network::Testnet,
@@ -148,7 +148,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Wpkh(key),
/// None,
/// Network::Testnet,
@@ -186,7 +186,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// use bdk::template::Bip44;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip44(key.clone(), KeychainKind::External),
/// Some(Bip44(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -226,7 +226,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
@@ -262,7 +262,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// use bdk::template::Bip49;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip49(key.clone(), KeychainKind::External),
/// Some(Bip49(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -302,7 +302,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
@@ -338,7 +338,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// use bdk::template::Bip84;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip84(key.clone(), KeychainKind::External),
/// Some(Bip84(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -378,7 +378,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,

View File

@@ -19,7 +19,7 @@ use bitcoin::Network;
use miniscript::ScriptContext;
pub use bip39::{Language, Mnemonic};
pub use bip39::{Error, Language, Mnemonic};
type Seed = [u8; 64];
@@ -94,6 +94,23 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
impl<Ctx: ScriptContext> DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>) {
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
let (mnemonic, passphrase) = self;
(mnemonic.into_key(), passphrase).into_extended_key()
}
fn into_descriptor_key(
self,
source: Option<bip32::KeySource>,
derivation_path: bip32::DerivationPath,
) -> Result<DescriptorKey<Ctx>, KeyError> {
let (mnemonic, passphrase) = self;
(mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {

View File

@@ -20,12 +20,12 @@ use std::str::FromStr;
use bitcoin::secp256k1::{self, Secp256k1, Signing};
use bitcoin::util::bip32;
use bitcoin::{Network, PrivateKey, PublicKey};
use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
pub use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
SortedMultiVec,
SinglePubKey, SortedMultiVec,
};
pub use miniscript::ScriptContext;
use miniscript::{Miniscript, Terminal};
@@ -127,6 +127,8 @@ pub enum ScriptContextEnum {
Legacy,
/// Segwitv0 scripts
Segwitv0,
/// Taproot scripts
Tap,
}
impl ScriptContextEnum {
@@ -139,6 +141,11 @@ impl ScriptContextEnum {
pub fn is_segwit_v0(&self) -> bool {
self == &ScriptContextEnum::Segwitv0
}
/// Returns whether the script context is [`ScriptContextEnum::Tap`]
pub fn is_taproot(&self) -> bool {
self == &ScriptContextEnum::Tap
}
}
/// Trait that adds extra useful methods to [`ScriptContext`]s
@@ -155,6 +162,11 @@ pub trait ExtScriptContext: ScriptContext {
fn is_segwit_v0() -> bool {
Self::as_enum().is_segwit_v0()
}
/// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1
fn is_taproot() -> bool {
Self::as_enum().is_taproot()
}
}
impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
@@ -162,6 +174,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
match TypeId::of::<Ctx>() {
t if t == TypeId::of::<miniscript::Legacy>() => ScriptContextEnum::Legacy,
t if t == TypeId::of::<miniscript::Segwitv0>() => ScriptContextEnum::Segwitv0,
t if t == TypeId::of::<miniscript::Tap>() => ScriptContextEnum::Tap,
_ => unimplemented!("Unknown ScriptContext type"),
}
}
@@ -212,7 +225,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
///
/// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
/// IntoDescriptorKey, KeyError, ScriptContext,
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
/// };
///
/// pub struct MyKeyType {
@@ -224,7 +237,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Ok(DescriptorKey::from_public(
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
/// origin: None,
/// key: self.pubkey,
/// key: SinglePubKey::FullKey(self.pubkey),
/// }),
/// mainnet_network(),
/// ))
@@ -319,7 +332,6 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
match self {
ExtendedKey::Private((mut xprv, _)) => {
xprv.network = network;
xprv.private_key.network = network;
Some(xprv)
}
ExtendedKey::Public(_) => None,
@@ -334,7 +346,7 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
secp: &Secp256k1<C>,
) -> bip32::ExtendedPubKey {
let mut xpub = match self {
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_private(secp, &xprv),
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
ExtendedKey::Public((xpub, _)) => xpub,
};
@@ -388,7 +400,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// network: self.network,
/// depth: 0,
/// parent_fingerprint: bip32::Fingerprint::default(),
/// private_key: self.key_data,
/// private_key: self.key_data.inner,
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
/// child_number: bip32::ChildNumber::Normal { index: 0 },
/// };
@@ -420,7 +432,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
/// depth: 0,
/// parent_fingerprint: bip32::Fingerprint::default(),
/// private_key: self.key_data,
/// private_key: self.key_data.inner,
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
/// child_number: bip32::ChildNumber::Normal { index: 0 },
/// };
@@ -548,6 +560,16 @@ impl<K, Ctx: ScriptContext> Deref for GeneratedKey<K, Ctx> {
}
}
impl<K: Clone, Ctx: ScriptContext> Clone for GeneratedKey<K, Ctx> {
fn clone(&self) -> GeneratedKey<K, Ctx> {
GeneratedKey {
key: self.key.clone(),
valid_networks: self.valid_networks.clone(),
phantom: self.phantom,
}
}
}
// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
// right `valid_networks`.
impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
@@ -688,11 +710,11 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for PrivateKey {
entropy: Self::Entropy,
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
// pick a arbitrary network here, but say that we support all of them
let key = secp256k1::SecretKey::from_slice(&entropy)?;
let inner = secp256k1::SecretKey::from_slice(&entropy)?;
let private_key = PrivateKey {
compressed: options.compressed,
network: Network::Bitcoin,
key,
inner,
};
Ok(GeneratedKey::new(private_key, any_network()))
@@ -770,13 +792,18 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
// Used internally by `bdk::fragment!` to build `multi()` fragments
#[doc(hidden)]
pub fn make_multi<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
pub fn make_multi<
Pk: IntoDescriptorKey<Ctx>,
Ctx: ScriptContext,
V: Fn(usize, Vec<DescriptorPublicKey>) -> Terminal<DescriptorPublicKey, Ctx>,
>(
thresh: usize,
variant: V,
pks: Vec<Pk>,
secp: &SecpCtx,
) -> Result<(Miniscript<DescriptorPublicKey, Ctx>, KeyMap, ValidNetworks), DescriptorError> {
let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?;
let minisc = Miniscript::from_ast(Terminal::Multi(thresh, pks))?;
let minisc = Miniscript::from_ast(variant(thresh, pks))?;
minisc.check_miniscript()?;
@@ -831,7 +858,17 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: self,
key: SinglePubKey::FullKey(self),
origin: None,
})
.into_descriptor_key()
}
}
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(self),
origin: None,
})
.into_descriptor_key()
@@ -933,29 +970,6 @@ pub mod test {
);
}
#[test]
fn test_keys_wif_network() {
// test mainnet wif
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
let xkey = generated_xprv.into_extended_key().unwrap();
let network = Network::Bitcoin;
let xprv = xkey.into_xprv(network).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, network);
// test testnet wif
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
let xkey = generated_xprv.into_extended_key().unwrap();
let network = Network::Testnet;
let xprv = xkey.into_xprv(network).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, network);
}
#[cfg(feature = "keys-bip39")]
#[test]
fn test_keys_wif_network_bip39() {
@@ -967,8 +981,7 @@ pub mod test {
.into_extended_key()
.unwrap();
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, Network::Testnet);
assert_eq!(xprv.network, Network::Testnet);
}
}

View File

@@ -44,31 +44,32 @@
//! interact with the bitcoin P2P network.
//!
//! ```toml
//! bdk = "0.16.0"
//! bdk = "0.18.0"
//! ```
//!
//! # Examples
#![cfg_attr(
feature = "electrum",
doc = r##"
## Sync the balance of a descriptor
### Example
```no_run
use bdk::Wallet;
use bdk::{Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(client);
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)?;
wallet.sync(&blockchain, SyncOptions::default())?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
@@ -80,14 +81,13 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ## Generate a few addresses
//!
//! ### Example
//! ```
//! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase;
//! use bdk::wallet::AddressIndex::New;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
//! bitcoin::Network::Testnet,
@@ -106,11 +106,10 @@ fn main() -> Result<(), bdk::Error> {
doc = r##"
## Create a transaction
### Example
```no_run
use bdk::{FeeRate, Wallet};
use bdk::{FeeRate, Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bitcoin::consensus::serialize;
@@ -123,10 +122,10 @@ fn main() -> Result<(), bdk::Error> {
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
let blockchain = ElectrumBlockchain::from(client);
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
@@ -150,7 +149,6 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ## Sign a transaction
//!
//! ### Example
//! ```no_run
//! use std::str::FromStr;
//!
@@ -160,7 +158,7 @@ fn main() -> Result<(), bdk::Error> {
//! use bdk::database::MemoryDatabase;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
//! bitcoin::Network::Testnet,
@@ -192,7 +190,7 @@ fn main() -> Result<(), bdk::Error> {
//! * `async-interface`: async functions in bdk traits
//! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys
//!
//! ## Internal features
//! # Internal features
//!
//! These features do not expose any new API, but influence internal implementation aspects of
//! BDK.
@@ -251,6 +249,14 @@ pub extern crate sled;
#[cfg(feature = "sqlite")]
pub extern crate rusqlite;
// We should consider putting this under a feature flag but we need the macro in doctests 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)]
#[macro_use]
pub mod testutils;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
@@ -272,16 +278,10 @@ pub use wallet::address_validator;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::SyncOptions;
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 doctests 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;

View File

@@ -19,7 +19,7 @@ pub trait PsbtUtils {
impl PsbtUtils for Psbt {
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
let tx = &self.global.unsigned_tx;
let tx = &self.unsigned_tx;
if input_index >= tx.input.len() {
return None;
@@ -93,7 +93,7 @@ mod test {
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());
psbt.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
@@ -112,10 +112,9 @@ mod test {
// add a finalized input
psbt.inputs.push(psbt_bip.inputs[0].clone());
psbt.global
.unsigned_tx
psbt.unsigned_tx
.input
.push(psbt_bip.global.unsigned_tx.input[0].clone());
.push(psbt_bip.unsigned_tx.input[0].clone());
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
}

View File

@@ -2,7 +2,7 @@ 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};
use bitcoin::{Address, Amount, Script, Transaction, Txid, Witness};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr;
@@ -193,7 +193,7 @@ impl TestClient {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
witness: vec![witness_reserved_value],
witness: Witness::from_vec(vec![witness_reserved_value]),
}],
output: vec![],
};
@@ -203,23 +203,31 @@ impl TestClient {
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]);
if let Some(witness_root) = block.witness_root() {
let witness_commitment = Block::compute_witness_commitment(
&witness_root,
&coinbase_tx.input[0]
.witness
.last()
.expect("Should contain the witness reserved value"),
);
// 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);
// 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(),
});
}
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;
if let Some(merkle_root) = block.compute_merkle_root() {
block.header.merkle_root = merkle_root;
}
assert!(block.check_merkle_root());
assert!(block.check_witness_commitment());
@@ -320,7 +328,7 @@ impl Default for TestClient {
fn default() -> Self {
let bitcoind_exe = env::var("BITCOIND_EXE")
.ok()
.or(bitcoind::downloaded_exe_path())
.or(bitcoind::downloaded_exe_path().ok())
.expect(
"you should provide env var BITCOIND_EXE or specifiy a bitcoind version feature",
);
@@ -361,10 +369,10 @@ macro_rules! bdk_blockchain_tests {
mod bdk_blockchain_tests {
use $crate::bitcoin::{Transaction, Network};
use $crate::testutils::blockchain_tests::TestClient;
use $crate::blockchain::noop_progress;
use $crate::blockchain::Blockchain;
use $crate::database::MemoryDatabase;
use $crate::types::KeychainKind;
use $crate::{Wallet, FeeRate};
use $crate::{Wallet, FeeRate, SyncOptions};
use $crate::testutils;
use super::*;
@@ -375,25 +383,53 @@ macro_rules! bdk_blockchain_tests {
$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 get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap()
}
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
#[allow(dead_code)]
enum WalletType {
WpkhSingleSig,
TaprootKeySpend,
TaprootScriptSpend,
TaprootScriptSpend2,
TaprootScriptSpend3,
}
fn init_wallet(ty: WalletType) -> (Wallet<MemoryDatabase>, $blockchain, (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 descriptors = match ty {
WalletType::WpkhSingleSig => testutils! {
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
},
WalletType::TaprootKeySpend => testutils! {
@descriptors ( "tr(Alice)" ) ( "tr(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
},
WalletType::TaprootScriptSpend => testutils! {
@descriptors ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( @keys ( "Key" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Script" => (@generate_xprv "/0/*", "/1/*") ) )
},
WalletType::TaprootScriptSpend2 => testutils! {
@descriptors ( "tr(Alice,pk(Bob))" ) ( "tr(Alice,pk(Bob))" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*") ) )
},
WalletType::TaprootScriptSpend3 => testutils! {
@descriptors ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*"), "Carol" => (@generate_xprv "/0/*", "/1/*") ) )
},
};
let test_client = TestClient::default();
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
let blockchain = get_blockchain(&test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
#[cfg(feature = "test-rpc")]
wallet.sync(noop_progress(), None).unwrap();
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
(wallet, descriptors, test_client)
(wallet, blockchain, descriptors, test_client)
}
fn init_single_sig() -> (Wallet<MemoryDatabase>, $blockchain, (String, Option<String>), TestClient) {
init_wallet(WalletType::WpkhSingleSig)
}
#[test]
@@ -401,7 +437,7 @@ macro_rules! bdk_blockchain_tests {
use std::ops::Deref;
use crate::database::Database;
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@@ -411,10 +447,10 @@ macro_rules! bdk_blockchain_tests {
// the RPC blockchain needs to call `sync()` during initialization to import the
// addresses (see `init_single_sig()`), so we skip this assertion
#[cfg(not(feature = "test-rpc"))]
#[cfg(not(any(feature = "test-rpc", feature = "test-rpc-legacy")))]
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
@@ -429,7 +465,7 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
@@ -438,7 +474,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
@@ -446,16 +482,16 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -463,13 +499,13 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -484,7 +520,7 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@@ -493,7 +529,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
@@ -502,32 +538,32 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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();
wallet.sync(&blockchain, SyncOptions::default()).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 (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -541,7 +577,7 @@ macro_rules! bdk_blockchain_tests {
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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");
@@ -559,13 +595,13 @@ macro_rules! bdk_blockchain_tests {
#[cfg(not(feature = "esplora"))]
#[test]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -578,7 +614,7 @@ macro_rules! bdk_blockchain_tests {
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
@@ -589,15 +625,15 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -607,8 +643,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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");
@@ -619,24 +655,24 @@ macro_rules! bdk_blockchain_tests {
/// The coins should only be received once!
#[test]
fn test_sync_double_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None), &test_client);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None));
// need to sync so rpc can start watching
receiver_wallet.sync(noop_progress(), None).unwrap();
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
let tx1 = {
let mut builder = wallet.build_tx();
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf();
let (mut psbt, _details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
let (mut psbt, _details) = builder.finish().expect("building first tx");
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing first tx");
assert!(finalized, "Cannot finalize transaction");
psbt.extract_tx()
};
@@ -644,22 +680,22 @@ macro_rules! bdk_blockchain_tests {
let tx2 = {
let mut builder = wallet.build_tx();
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf().fee_rate(FeeRate::from_sat_per_vb(5.0));
let (mut psbt, _details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
let (mut psbt, _details) = builder.finish().expect("building replacement tx");
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing replacement tx");
assert!(finalized, "Cannot finalize transaction");
psbt.extract_tx()
};
wallet.broadcast(&tx1).unwrap();
wallet.broadcast(&tx2).unwrap();
blockchain.broadcast(&tx1).expect("broadcasting first");
blockchain.broadcast(&tx2).expect("broadcasting replacement");
receiver_wallet.sync(noop_progress(), None).unwrap();
assert_eq!(receiver_wallet.get_balance().unwrap(), 49_000, "should have received coins once and only once");
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
}
#[test]
fn test_sync_many_sends_to_a_single_address() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
for _ in 0..4 {
// split this up into multiple blocks so rpc doesn't get angry
@@ -678,22 +714,22 @@ macro_rules! bdk_blockchain_tests {
});
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
}
#[test]
fn test_update_confirmation_time_after_generate() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).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<_, _>>();
@@ -701,7 +737,7 @@ macro_rules! bdk_blockchain_tests {
assert!(details.confirmation_time.is_none());
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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();
@@ -711,13 +747,13 @@ macro_rules! bdk_blockchain_tests {
#[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 (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -726,25 +762,26 @@ macro_rules! bdk_blockchain_tests {
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(&psbt.extract_tx()).unwrap();
let sent_tx = psbt.extract_tx();
blockchain.broadcast(&sent_tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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();
let sent = tx_map.get(&sent_tx.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");
@@ -752,14 +789,14 @@ macro_rules! bdk_blockchain_tests {
#[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);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut total_sent = 0;
@@ -769,38 +806,38 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
total_sent += 5_000 + details.fee.unwrap_or(0);
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -808,8 +845,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
@@ -818,8 +855,8 @@ macro_rules! bdk_blockchain_tests {
let (mut new_psbt, new_details) = builder.finish().expect("fee bump tx");
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();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
@@ -828,14 +865,14 @@ macro_rules! bdk_blockchain_tests {
#[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);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -843,8 +880,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
@@ -853,8 +890,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
assert_eq!(new_details.received, 0, "incorrect received after change removal");
@@ -863,14 +900,14 @@ macro_rules! bdk_blockchain_tests {
#[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);
let (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -878,8 +915,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
@@ -888,22 +925,22 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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 (wallet, blockchain, 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();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -911,8 +948,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
@@ -923,8 +960,8 @@ macro_rules! bdk_blockchain_tests {
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();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).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");
@@ -933,13 +970,13 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_add_data() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -952,22 +989,22 @@ macro_rules! bdk_blockchain_tests {
let tx = psbt.extract_tx();
let serialized_tx = bitcoin::consensus::encode::serialize(&tx);
assert!(serialized_tx.windows(data.len()).any(|e| e==data), "cannot find op_return data in transaction");
let sent_txid = wallet.broadcast(&tx).unwrap();
blockchain.broadcast(&tx).unwrap();
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let _ = tx_map.get(&sent_txid).unwrap();
let _ = tx_map.get(&tx.txid()).unwrap();
}
#[test]
fn test_sync_receive_coinbase() {
let (wallet, _, mut test_client) = init_single_sig();
let (wallet, blockchain, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
test_client.generate(1, Some(wallet_addr));
@@ -980,11 +1017,12 @@ macro_rules! bdk_blockchain_tests {
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_send_to_bech32m_addr() {
use std::str::FromStr;
use serde;
@@ -993,7 +1031,7 @@ macro_rules! bdk_blockchain_tests {
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{Auth, Client, RpcApi};
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
// TODO remove once rust-bitcoincore-rpc with PR 199 released
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/199
@@ -1056,7 +1094,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
@@ -1067,8 +1105,8 @@ macro_rules! bdk_blockchain_tests {
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "wallet cannot finalize transaction");
let tx = psbt.extract_tx();
wallet.broadcast(&tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
@@ -1095,7 +1133,7 @@ macro_rules! bdk_blockchain_tests {
// 2.
// Core (#2) -> Us (#4)
let (wallet, _, mut test_client) = init_single_sig();
let (wallet, blockchain, _, mut test_client) = init_single_sig();
let bdk_address = wallet.get_address(AddressIndex::New).unwrap().address;
let core_address = test_client.get_new_address(None, None).unwrap();
let tx = testutils! {
@@ -1106,7 +1144,7 @@ macro_rules! bdk_blockchain_tests {
let txid_1 = test_client.receive(tx);
let tx_1: Transaction = deserialize(&test_client.get_transaction(&txid_1, None).unwrap().hex).unwrap();
let vout_1 = tx_1.output.into_iter().position(|o| o.script_pubkey == core_address.script_pubkey()).unwrap() as u32;
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_1 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_1).unwrap();
assert_eq!(tx_1.received, 50_000);
assert_eq!(tx_1.sent, 0);
@@ -1117,11 +1155,212 @@ macro_rules! bdk_blockchain_tests {
};
let txid_2 = test_client.receive(tx);
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_2 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_2).unwrap();
assert_eq!(tx_2.received, 10_000);
assert_eq!(tx_2.sent, 0);
}
#[test]
fn test_double_spend() {
// We create a tx and then we try to double spend it; BDK will always allow
// us to do so, as it never forgets about spent UTXOs
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
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 initial_tx = psbt.extract_tx();
let _sent_txid = blockchain.broadcast(&initial_tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
for utxo in wallet.list_unspent().unwrap() {
// Making sure the TXO we just spent is not returned by list_unspent
assert!(utxo.outpoint != initial_tx.input[0].previous_output, "wallet displays spent txo in unspents");
}
// We can still create a transaction double spending `initial_tx`
let mut builder = wallet.build_tx();
builder
.add_utxo(initial_tx.input[0].previous_output)
.expect("Can't manually add an UTXO spent");
test_client.generate(1, Some(node_addr));
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
// Even after confirmation, we can still create a tx double spend it
let mut builder = wallet.build_tx();
builder
.add_utxo(initial_tx.input[0].previous_output)
.expect("Can't manually add an UTXO spent");
for utxo in wallet.list_unspent().unwrap() {
// Making sure the TXO we just spent is not returned by list_unspent
assert!(utxo.outpoint != initial_tx.input[0].previous_output, "wallet displays spent txo in unspents");
}
}
#[test]
fn test_send_receive_pkh() {
let descriptors = ("pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None);
let mut test_client = TestClient::default();
let blockchain = get_blockchain(&test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let tx = {
let mut builder = wallet.build_tx();
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000);
let (mut psbt, _details) = builder.finish().unwrap();
wallet.sign(&mut psbt, Default::default()).unwrap();
psbt.extract_tx()
};
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_taproot_key_spend() {
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootKeySpend);
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let tx = {
let mut builder = wallet.build_tx();
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000);
let (mut psbt, _details) = builder.finish().unwrap();
wallet.sign(&mut psbt, Default::default()).unwrap();
psbt.extract_tx()
};
blockchain.broadcast(&tx).unwrap();
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_taproot_script_spend() {
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootScriptSpend);
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();
let ext_path = vec![(ext_policy.id.clone(), vec![1])].into_iter().collect();
let int_path = vec![(int_policy.id.clone(), vec![1])].into_iter().collect();
let tx = {
let mut builder = wallet.build_tx();
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000)
.policy_path(ext_path, KeychainKind::External)
.policy_path(int_path, KeychainKind::Internal);
let (mut psbt, _details) = builder.finish().unwrap();
wallet.sign(&mut psbt, Default::default()).unwrap();
psbt.extract_tx()
};
blockchain.broadcast(&tx).unwrap();
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_sign_taproot_core_keyspend_psbt() {
test_sign_taproot_core_psbt(WalletType::TaprootKeySpend);
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_sign_taproot_core_scriptspend2_psbt() {
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend2);
}
#[test]
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_sign_taproot_core_scriptspend3_psbt() {
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend3);
}
#[cfg(not(feature = "test-rpc-legacy"))]
fn test_sign_taproot_core_psbt(wallet_type: WalletType) {
use std::str::FromStr;
use serde_json;
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{Auth, Client, RpcApi};
let (wallet, _blockchain, _descriptors, test_client) = init_wallet(wallet_type);
// TODO replace once rust-bitcoincore-rpc with PR 174 released
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/174
let _createwallet_result: Value = test_client.bitcoind.client.call("createwallet", &["taproot_wallet".into(), true.into(), true.into(), serde_json::to_value("").unwrap(), false.into(), true.into(), true.into(), false.into()]).expect("created wallet");
let external_descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External);
// TODO replace once bitcoind released with support for rust-bitcoincore-rpc PR 174
let taproot_wallet_client = Client::new(&test_client.bitcoind.rpc_url_with_wallet("taproot_wallet"), Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone())).unwrap();
let descriptor_info = taproot_wallet_client.get_descriptor_info(external_descriptor.to_string().as_str()).expect("descriptor info");
let import_descriptor_args = json!([{
"desc": descriptor_info.descriptor,
"active": true,
"timestamp": "now",
"label":"taproot key spend",
}]);
let _importdescriptors_result: Value = taproot_wallet_client.call("importdescriptors", &[import_descriptor_args]).expect("import wallet");
let generate_to_address: bitcoin::Address = taproot_wallet_client.call("getnewaddress", &["test address".into(), "bech32m".into()]).expect("new address");
let _generatetoaddress_result = taproot_wallet_client.generate_to_address(101, &generate_to_address).expect("generated to address");
let send_to_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string();
let change_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string();
let send_addr_amounts = json!([{
send_to_address: "0.4321"
}]);
let send_options = json!({
"change_address": change_address,
"psbt": true,
});
let send_result: Value = taproot_wallet_client.call("send", &[send_addr_amounts, Value::Null, "unset".into(), Value::Null, send_options]).expect("send psbt");
let core_psbt = send_result["psbt"].as_str().expect("core psbt str");
use bitcoin::util::psbt::PartiallySignedTransaction;
// Test parsing core created PSBT
let mut psbt = PartiallySignedTransaction::from_str(&core_psbt).expect("core taproot psbt");
// Test signing core created PSBT
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true);
// Test with updated psbt
let update_result: Value = taproot_wallet_client.call("utxoupdatepsbt", &[core_psbt.into()]).expect("update psbt utxos");
let core_updated_psbt = update_result.as_str().expect("core updated psbt");
// Test parsing core created and updated PSBT
let mut psbt = PartiallySignedTransaction::from_str(&core_updated_psbt).expect("core taproot psbt");
// Test signing core created and updated PSBT
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true);
}
}
};

View File

@@ -14,11 +14,7 @@
#[cfg(feature = "test-blockchains")]
pub mod blockchain_tests;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, PublicKey, Txid};
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
use bitcoin::{Address, Txid};
#[derive(Clone, Debug)]
pub struct TestIncomingInput {
@@ -96,39 +92,6 @@ impl TestIncomingTx {
}
}
#[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 {
@@ -136,23 +99,23 @@ macro_rules! testutils {
use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
use $crate::descriptor::AsDerived;
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")
parsed.as_derived($child, &secp).address(bitcoin::Network::Regtest).expect("No address form")
});
( @internal $descriptors:expr, $child:expr ) => ({
use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
use $crate::descriptor::AsDerived;
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($crate::bitcoin::Network::Regtest).expect("No address form")
parsed.as_derived($child, &secp).address($crate::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) });

View File

@@ -131,6 +131,8 @@ pub struct LocalUtxo {
pub txout: TxOut,
/// Type of keychain
pub keychain: KeychainKind,
/// Whether this UTXO is spent or not
pub is_spent: bool,
}
/// A [`Utxo`] with its `satisfaction_weight`.

View File

@@ -55,7 +55,7 @@
//! }
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
//!
//! let address = wallet.get_address(New)?;

View File

@@ -97,6 +97,7 @@ use rand::seq::SliceRandom;
use rand::thread_rng;
#[cfg(test)]
use rand::{rngs::StdRng, SeedableRng};
use std::collections::HashMap;
use std::convert::TryInto;
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
@@ -182,7 +183,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
mut optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
amount_needed: u64,
mut fee_amount: u64,
fee_amount: u64,
) -> Result<CoinSelectionResult, Error> {
log::debug!(
"amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
@@ -201,47 +202,113 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
.chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
};
// Keep including inputs until we've got enough.
// Store the total input value in selected_amount and the total fee being paid in fee_amount
let mut selected_amount = 0;
let selected = utxos
.scan(
(&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
if must_use || **selected_amount < amount_needed + **fee_amount {
**fee_amount +=
fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
**selected_amount += weighted_utxo.utxo.txout().value;
log::debug!(
"Selected {}, updated fee_amount = `{}`",
weighted_utxo.utxo.outpoint(),
fee_amount
);
Some(weighted_utxo.utxo)
} else {
None
}
},
)
.collect::<Vec<_>>();
let amount_needed_with_fees = amount_needed + fee_amount;
if selected_amount < amount_needed_with_fees {
return Err(Error::InsufficientFunds {
needed: amount_needed_with_fees,
available: selected_amount,
});
}
Ok(CoinSelectionResult {
selected,
fee_amount,
})
select_sorted_utxos(utxos, fee_rate, amount_needed, fee_amount)
}
}
/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next
///
/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting
/// from the oldest ones until the required amount is reached.
#[derive(Debug, Default, Clone, Copy)]
pub struct OldestFirstCoinSelection;
impl<D: Database> CoinSelectionAlgorithm<D> for OldestFirstCoinSelection {
fn coin_select(
&self,
database: &D,
required_utxos: Vec<WeightedUtxo>,
mut optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
amount_needed: u64,
fee_amount: u64,
) -> Result<CoinSelectionResult, Error> {
// query db and create a blockheight lookup table
let blockheights = optional_utxos
.iter()
.map(|wu| wu.utxo.outpoint().txid)
// fold is used so we can skip db query for txid that already exist in hashmap acc
.fold(Ok(HashMap::new()), |bh_result_acc, txid| {
bh_result_acc.and_then(|mut bh_acc| {
if bh_acc.contains_key(&txid) {
Ok(bh_acc)
} else {
database.get_tx(&txid, false).map(|details| {
bh_acc.insert(
txid,
details.and_then(|d| d.confirmation_time.map(|ct| ct.height)),
);
bh_acc
})
}
})
})?;
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
// oldest to newest according to blocktime
// For utxo that doesn't exist in DB, they will have lowest priority to be selected
let utxos = {
optional_utxos.sort_unstable_by_key(|wu| {
match blockheights.get(&wu.utxo.outpoint().txid) {
Some(Some(blockheight)) => blockheight,
_ => &u32::MAX,
}
});
required_utxos
.into_iter()
.map(|utxo| (true, utxo))
.chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
};
select_sorted_utxos(utxos, fee_rate, amount_needed, fee_amount)
}
}
fn select_sorted_utxos(
utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
fee_rate: FeeRate,
amount_needed: u64,
mut fee_amount: u64,
) -> Result<CoinSelectionResult, Error> {
let mut selected_amount = 0;
let selected = utxos
.scan(
(&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
if must_use || **selected_amount < amount_needed + **fee_amount {
**fee_amount +=
fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
**selected_amount += weighted_utxo.utxo.txout().value;
log::debug!(
"Selected {}, updated fee_amount = `{}`",
weighted_utxo.utxo.outpoint(),
fee_amount
);
Some(weighted_utxo.utxo)
} else {
None
}
},
)
.collect::<Vec<_>>();
let amount_needed_with_fees = amount_needed + fee_amount;
if selected_amount < amount_needed_with_fees {
return Err(Error::InsufficientFunds {
needed: amount_needed_with_fees,
available: selected_amount,
});
}
Ok(CoinSelectionResult {
selected,
fee_amount,
})
}
#[derive(Debug, Clone)]
// Adds fee information to an UTXO.
struct OutputGroup {
@@ -541,7 +608,7 @@ mod test {
use bitcoin::{OutPoint, Script, TxOut};
use super::*;
use crate::database::MemoryDatabase;
use crate::database::{BatchOperations, MemoryDatabase};
use crate::types::*;
use crate::wallet::Vbytes;
@@ -569,6 +636,7 @@ mod test {
script_pubkey: Script::new(),
},
keychain: KeychainKind::External,
is_spent: false,
}),
}
}
@@ -581,6 +649,61 @@ mod test {
]
}
fn setup_database_and_get_oldest_first_test_utxos<D: Database>(
database: &mut D,
) -> Vec<WeightedUtxo> {
// ensure utxos are from different tx
let utxo1 = utxo(120_000, 1);
let utxo2 = utxo(80_000, 2);
let utxo3 = utxo(300_000, 3);
// add tx to DB so utxos are sorted by blocktime asc
// utxos will be selected by the following order
// utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (blockheight 3)
// timestamp are all set as the same to ensure that only block height is used in sorting
let utxo1_tx_details = TransactionDetails {
transaction: None,
txid: utxo1.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 1,
timestamp: 1231006505,
}),
};
let utxo2_tx_details = TransactionDetails {
transaction: None,
txid: utxo2.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 2,
timestamp: 1231006505,
}),
};
let utxo3_tx_details = TransactionDetails {
transaction: None,
txid: utxo3.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 3,
timestamp: 1231006505,
}),
};
database.set_tx(&utxo1_tx_details).unwrap();
database.set_tx(&utxo2_tx_details).unwrap();
database.set_tx(&utxo3_tx_details).unwrap();
vec![utxo1, utxo2, utxo3]
}
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
let mut res = Vec::new();
for _ in 0..utxos_number {
@@ -596,6 +719,7 @@ mod test {
script_pubkey: Script::new(),
},
keychain: KeychainKind::External,
is_spent: false,
}),
});
}
@@ -615,6 +739,7 @@ mod test {
script_pubkey: Script::new(),
},
keychain: KeychainKind::External,
is_spent: false,
}),
};
vec![utxo; utxos_number]
@@ -728,6 +853,164 @@ mod test {
.unwrap();
}
#[test]
fn test_oldest_first_coin_selection_success() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
180_000,
FEE_AMOUNT,
)
.unwrap();
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 186)
}
#[test]
fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() {
// ensure utxos are from different tx
let utxo1 = utxo(120_000, 1);
let utxo2 = utxo(80_000, 2);
let utxo3 = utxo(300_000, 3);
let mut database = MemoryDatabase::default();
// add tx to DB so utxos are sorted by blocktime asc
// utxos will be selected by the following order
// utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (not exist in DB)
// timestamp are all set as the same to ensure that only block height is used in sorting
let utxo1_tx_details = TransactionDetails {
transaction: None,
txid: utxo1.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 1,
timestamp: 1231006505,
}),
};
let utxo2_tx_details = TransactionDetails {
transaction: None,
txid: utxo2.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 2,
timestamp: 1231006505,
}),
};
database.set_tx(&utxo1_tx_details).unwrap();
database.set_tx(&utxo2_tx_details).unwrap();
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
vec![utxo3, utxo1, utxo2],
FeeRate::from_sat_per_vb(1.0),
180_000,
FEE_AMOUNT,
)
.unwrap();
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 186)
}
#[test]
fn test_oldest_first_coin_selection_use_all() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
20_000,
FEE_AMOUNT,
)
.unwrap();
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 500_000);
assert_eq!(result.fee_amount, 254);
}
#[test]
fn test_oldest_first_coin_selection_use_only_necessary() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
20_000,
FEE_AMOUNT,
)
.unwrap();
assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount(), 120_000);
assert_eq!(result.fee_amount, 118);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_oldest_first_coin_selection_insufficient_funds() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
600_000,
FEE_AMOUNT,
)
.unwrap();
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let amount_needed: u64 =
utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - (FEE_AMOUNT + 50);
OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1000.0),
amount_needed,
FEE_AMOUNT,
)
.unwrap();
}
#[test]
fn test_bnb_coin_selection_success() {
// In this case bnb won't find a suitable match and single random draw will

View File

@@ -29,8 +29,8 @@
//! "label":"testnet"
//! }"#;
//!
//! let import = WalletExport::from_str(import)?;
//! let wallet = Wallet::new_offline(
//! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new(
//! &import.descriptor(),
//! import.change_descriptor().as_ref(),
//! Network::Testnet,
@@ -45,13 +45,13 @@
//! # use bdk::database::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
//! Network::Testnet,
//! MemoryDatabase::default()
//! )?;
//! let export = WalletExport::export_wallet(&wallet, "exported wallet", true)
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
//! .map_err(ToString::to_string)
//! .map_err(bdk::Error::Generic)?;
//!
@@ -64,16 +64,21 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize};
use miniscript::descriptor::{ShInner, WshInner};
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
use miniscript::{Descriptor, ScriptContext, Terminal};
use crate::database::BatchDatabase;
use crate::types::KeychainKind;
use crate::wallet::Wallet;
/// Alias for [`FullyNodedExport`]
#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")]
pub type WalletExport = FullyNodedExport;
/// Structure that contains the export of a wallet
///
/// For a usage example see [this module](crate::wallet::export)'s documentation.
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletExport {
pub struct FullyNodedExport {
descriptor: String,
/// Earliest block to rescan when looking for the wallet's transactions
pub blockheight: u32,
@@ -81,13 +86,13 @@ pub struct WalletExport {
pub label: String,
}
impl ToString for WalletExport {
impl ToString for FullyNodedExport {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
impl FromStr for WalletExport {
impl FromStr for FullyNodedExport {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -96,10 +101,10 @@ impl FromStr for WalletExport {
}
fn remove_checksum(s: String) -> String {
s.splitn(2, '#').next().map(String::from).unwrap()
s.split_once('#').map(|(a, _)| String::from(a)).unwrap()
}
impl WalletExport {
impl FullyNodedExport {
/// Export a wallet
///
/// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
@@ -111,14 +116,18 @@ impl WalletExport {
///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`.
pub fn export_wallet<B, D: BatchDatabase>(
wallet: &Wallet<B, D>,
pub fn export_wallet<D: BatchDatabase>(
wallet: &Wallet<D>,
label: &str,
include_blockheight: bool,
) -> Result<Self, &'static str> {
let descriptor = wallet
.descriptor
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
.get_descriptor_for_keychain(KeychainKind::External)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::External)
.as_key_map(wallet.secp_ctx()),
);
let descriptor = remove_checksum(descriptor);
Self::is_compatible_with_core(&descriptor)?;
@@ -136,18 +145,30 @@ impl WalletExport {
}
};
let export = WalletExport {
let export = FullyNodedExport {
descriptor,
label: label.into(),
blockheight,
};
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
let descriptor =
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()));
remove_checksum(descriptor)
let change_descriptor = match wallet
.public_descriptor(KeychainKind::Internal)
.map_err(|_| "Invalid change descriptor")?
.is_some()
{
false => None,
true => {
let descriptor = wallet
.get_descriptor_for_keychain(KeychainKind::Internal)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::Internal)
.as_key_map(wallet.secp_ctx()),
);
Some(remove_checksum(descriptor))
}
};
if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) {
if export.change_descriptor() != change_descriptor {
return Err("Incompatible change descriptor");
}
@@ -241,14 +262,14 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
@@ -265,9 +286,8 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let wallet =
Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
#[test]
@@ -279,14 +299,14 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
#[test]
@@ -302,14 +322,14 @@ mod test {
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
@@ -322,14 +342,14 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
}
@@ -340,7 +360,7 @@ mod test {
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
let export = WalletExport::from_str(import_str).unwrap();
let export = FullyNodedExport::from_str(import_str).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
//! # #[derive(Debug)]
//! # struct CustomHSM;
//! # impl CustomHSM {
//! # fn sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
//! # Ok(())
//! # }
//! # fn connect() -> Self {
@@ -47,32 +47,29 @@
//! }
//! }
//!
//! impl Signer for CustomSigner {
//! fn sign(
//! &self,
//! psbt: &mut psbt::PartiallySignedTransaction,
//! input_index: Option<usize>,
//! _secp: &Secp256k1<All>,
//! ) -> Result<(), SignerError> {
//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?;
//! self.device.sign_input(psbt, input_index)?;
//!
//! Ok(())
//! }
//!
//! impl SignerCommon for CustomSigner {
//! fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
//! self.device.get_id()
//! }
//! }
//!
//! fn sign_whole_tx(&self) -> bool {
//! false
//! impl InputSigner for CustomSigner {
//! fn sign_input(
//! &self,
//! psbt: &mut psbt::PartiallySignedTransaction,
//! input_index: usize,
//! _secp: &Secp256k1<All>,
//! ) -> Result<(), SignerError> {
//! self.device.hsm_sign_input(psbt, input_index)?;
//!
//! Ok(())
//! }
//! }
//!
//! let custom_signer = CustomSigner::connect();
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
@@ -85,22 +82,26 @@
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Bound::Included;
use std::ops::{Bound::Included, Deref};
use std::sync::Arc;
use bitcoin::blockdata::opcodes;
use bitcoin::blockdata::script::Builder as ScriptBuilder;
use bitcoin::hashes::{hash160, Hash};
use bitcoin::secp256k1::{Message, Secp256k1};
use bitcoin::secp256k1::Message;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
use bitcoin::util::{bip143, psbt};
use bitcoin::{PrivateKey, Script, SigHash, SigHashType};
use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot};
use bitcoin::{secp256k1, XOnlyPublicKey};
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, KeyMap};
use miniscript::{Legacy, MiniscriptKey, Segwitv0};
use miniscript::descriptor::{
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
KeyMap, SinglePubKey,
};
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
use super::utils::SecpCtx;
use crate::descriptor::XKeyUtils;
use crate::descriptor::{DescriptorMeta, XKeyUtils};
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
/// multiple of them
@@ -153,6 +154,16 @@ pub enum SignerError {
/// To enable signing transactions with non-standard sighashes set
/// [`SignOptions::allow_all_sighashes`] to `true`.
NonStandardSighash,
/// Invalid SIGHASH for the signing context in use
InvalidSighash,
/// Error while computing the hash to sign
SighashError(sighash::Error),
}
impl From<sighash::Error> for SignerError {
fn from(e: sighash::Error) -> Self {
SignerError::SighashError(e)
}
}
impl fmt::Display for SignerError {
@@ -163,27 +174,46 @@ impl fmt::Display for SignerError {
impl std::error::Error for SignerError {}
/// Trait for signers
/// Signing context
///
/// This trait can be implemented to provide customized signers to the wallet. For an example see
/// [`this module`](crate::wallet::signer)'s documentation.
pub trait Signer: fmt::Debug + Send + Sync {
/// Sign a PSBT
///
/// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole
/// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and
/// can be ignored.
fn sign(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: Option<usize>,
secp: &SecpCtx,
) -> Result<(), SignerError>;
/// Used by our software signers to determine the type of signatures to make
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignerContext {
/// Legacy context
Legacy,
/// Segwit v0 context (BIP 143)
Segwitv0,
/// Taproot context (BIP 340)
Tap {
/// Whether the signer can sign for the internal key or not
is_internal_key: bool,
},
}
/// Return whether or not the signer signs the whole transaction in one go instead of every
/// input individually
fn sign_whole_tx(&self) -> bool;
/// Wrapper structure to pair a signer with its context
#[derive(Debug, Clone)]
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
signer: S,
ctx: SignerContext,
}
impl<S: Sized + fmt::Debug + Clone> SignerWrapper<S> {
/// Create a wrapped signer from a signer and a context
pub fn new(signer: S, ctx: SignerContext) -> Self {
SignerWrapper { signer, ctx }
}
}
impl<S: Sized + fmt::Debug + Clone> Deref for SignerWrapper<S> {
type Target = S;
fn deref(&self) -> &Self::Target {
&self.signer
}
}
/// Common signer methods
pub trait SignerCommon: fmt::Debug + Send + Sync {
/// 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
@@ -200,14 +230,65 @@ pub trait Signer: fmt::Debug + Send + Sync {
}
}
impl Signer for DescriptorXKey<ExtendedPrivKey> {
fn sign(
/// PSBT Input signer
///
/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing
/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation
/// for [`TransactionSigner`].
pub trait InputSigner: SignerCommon {
/// Sign a single psbt input
fn sign_input(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
secp: &SecpCtx,
) -> Result<(), SignerError>;
}
/// PSBT signer
///
/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction
/// at once.
pub trait TransactionSigner: SignerCommon {
/// Sign all the inputs of the psbt
fn sign_transaction(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
secp: &SecpCtx,
) -> Result<(), SignerError>;
}
impl<T: InputSigner> TransactionSigner for T {
fn sign_transaction(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: Option<usize>,
secp: &SecpCtx,
) -> Result<(), SignerError> {
let input_index = input_index.unwrap();
for input_index in 0..psbt.inputs.len() {
self.sign_input(psbt, input_index, secp)?;
}
Ok(())
}
}
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(secp))
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::XPrv(self.signer.clone()))
}
}
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
fn sign_input(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
secp: &SecpCtx,
) -> Result<(), SignerError> {
if input_index >= psbt.inputs.len() {
return Err(SignerError::InputIndexOutOfRange);
}
@@ -218,19 +299,23 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
return Ok(());
}
let tap_key_origins = psbt.inputs[input_index]
.tap_key_origins
.iter()
.map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource));
let (public_key, full_path) = match psbt.inputs[input_index]
.bip32_derivation
.iter()
.filter_map(|(pk, &(fingerprint, ref path))| {
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
Some((pk, path))
.map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
.chain(tap_key_origins)
.find_map(|(pk, keysource)| {
if self.matches(keysource, secp).is_some() {
Some((pk, keysource.1.clone()))
} else {
None
}
})
.next()
{
Some((pk, full_path)) => (pk, full_path.clone()),
}) {
Some((pk, full_path)) => (pk, full_path),
None => return Ok(()),
};
@@ -245,35 +330,48 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
};
if &derived_key.private_key.public_key(secp) != public_key {
let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key);
let valid_key = match public_key {
SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true,
SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true,
_ => false,
};
if !valid_key {
Err(SignerError::InvalidKey)
} else {
derived_key.private_key.sign(psbt, Some(input_index), secp)
// HD wallets imply compressed keys
let priv_key = PrivateKey {
compressed: true,
network: self.xkey.network,
inner: derived_key.private_key,
};
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp)
}
}
fn sign_whole_tx(&self) -> bool {
false
}
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(secp))
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::XPrv(self.clone()))
}
}
impl Signer for PrivateKey {
fn sign(
impl SignerCommon for SignerWrapper<PrivateKey> {
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.public_key(secp).to_pubkeyhash())
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
key: self.signer,
origin: None,
}))
}
}
impl InputSigner for SignerWrapper<PrivateKey> {
fn sign_input(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: Option<usize>,
input_index: usize,
secp: &SecpCtx,
) -> Result<(), SignerError> {
let input_index = input_index.unwrap();
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
@@ -283,49 +381,124 @@ impl Signer for PrivateKey {
return Ok(());
}
let pubkey = self.public_key(secp);
let pubkey = PublicKey::from_private_key(secp, self);
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
if let SignerContext::Tap { is_internal_key } = self.ctx {
if is_internal_key && psbt.inputs[input_index].tap_key_sig.is_none() {
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
None,
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
);
}
if let Some((leaf_hashes, _)) =
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
{
let leaf_hashes = leaf_hashes
.iter()
.filter(|lh| {
!psbt.inputs[input_index]
.tap_script_sigs
.contains_key(&(x_only_pubkey, **lh))
})
.cloned()
.collect::<Vec<_>>();
for lh in leaf_hashes {
let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
Some(lh),
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
);
}
}
return Ok(());
}
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(());
}
// FIXME: use the presence of `witness_utxo` as an indication that we should make a bip143
// sig. Does this make sense? Should we add an extra argument to explicitly switch between
// these? The original idea was to declare sign() as sign<Ctx: ScriptContex>() and use Ctx,
// but that violates the rules for trait-objects, so we can't do it.
let (hash, sighash) = match psbt.inputs[input_index].witness_utxo {
Some(_) => Segwitv0::sighash(psbt, input_index)?,
None => Legacy::sighash(psbt, input_index)?,
let (hash, hash_ty) = match self.ctx {
SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?,
SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?,
_ => return Ok(()), // handled above
};
let signature = secp.sign(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
&self.key,
sign_psbt_ecdsa(
&self.inner,
pubkey,
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
);
let mut final_signature = Vec::with_capacity(75);
final_signature.extend_from_slice(&signature.serialize_der());
final_signature.push(sighash.as_u32() as u8);
psbt.inputs[input_index]
.partial_sigs
.insert(pubkey, final_signature);
Ok(())
}
}
fn sign_whole_tx(&self) -> bool {
false
}
fn sign_psbt_ecdsa(
secret_key: &secp256k1::SecretKey,
pubkey: PublicKey,
psbt_input: &mut psbt::Input,
hash: bitcoin::Sighash,
hash_ty: EcdsaSighashType,
secp: &SecpCtx,
) {
let sig = secp.sign_ecdsa(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
secret_key,
);
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.public_key(secp).to_pubkeyhash())
}
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
psbt_input.partial_sigs.insert(pubkey, final_signature);
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
key: *self,
origin: None,
}))
// Calling this with `leaf_hash` = `None` will sign for key-spend
fn sign_psbt_schnorr(
secret_key: &secp256k1::SecretKey,
pubkey: XOnlyPublicKey,
leaf_hash: Option<taproot::TapLeafHash>,
psbt_input: &mut psbt::Input,
hash: taproot::TapSighashHash,
hash_ty: SchnorrSighashType,
secp: &SecpCtx,
) {
use schnorr::TapTweak;
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
let keypair = match leaf_hash {
None => keypair
.tap_tweak(secp, psbt_input.tap_merkle_root)
.into_inner(),
Some(_) => keypair, // no tweak for script spend
};
let sig = secp.sign_schnorr(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
&keypair,
);
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
if let Some(lh) = leaf_hash {
psbt_input
.tap_script_sigs
.insert((pubkey, lh), final_signature);
} else {
psbt_input.tap_key_sig = Some(final_signature);
}
}
@@ -360,7 +533,7 @@ impl From<(SignerId, SignerOrdering)> for SignersContainerKey {
/// Container for multiple signers
#[derive(Debug, Default, Clone)]
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn Signer>>);
pub struct SignersContainer(BTreeMap<SignersContainerKey, Arc<dyn TransactionSigner>>);
impl SignersContainer {
/// Create a map of public keys to secret keys
@@ -371,24 +544,37 @@ impl SignersContainer {
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
.collect()
}
}
impl From<KeyMap> for SignersContainer {
fn from(keymap: KeyMap) -> SignersContainer {
let secp = Secp256k1::new();
/// Build a new signer container from a [`KeyMap`]
///
/// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to
/// the signers
pub fn build(
keymap: KeyMap,
descriptor: &Descriptor<DescriptorPublicKey>,
secp: &SecpCtx,
) -> SignersContainer {
let mut container = SignersContainer::new();
for (_, secret) in keymap {
for (pubkey, secret) in keymap {
let ctx = match descriptor {
Descriptor::Tr(tr) => SignerContext::Tap {
is_internal_key: tr.internal_key() == &pubkey,
},
_ if descriptor.is_witness() => SignerContext::Segwitv0,
_ => SignerContext::Legacy,
};
match secret {
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
SignerId::from(private_key.key.public_key(&secp).to_pubkeyhash()),
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
SignerOrdering::default(),
Arc::new(private_key.key),
Arc::new(SignerWrapper::new(private_key.key, ctx)),
),
DescriptorSecretKey::XPrv(xprv) => container.add_external(
SignerId::from(xprv.root_fingerprint(&secp)),
SignerId::from(xprv.root_fingerprint(secp)),
SignerOrdering::default(),
Arc::new(xprv),
Arc::new(SignerWrapper::new(xprv, ctx)),
),
};
}
@@ -409,13 +595,17 @@ impl SignersContainer {
&mut self,
id: SignerId,
ordering: SignerOrdering,
signer: Arc<dyn Signer>,
) -> Option<Arc<dyn Signer>> {
signer: Arc<dyn TransactionSigner>,
) -> Option<Arc<dyn TransactionSigner>> {
self.0.insert((id, ordering).into(), signer)
}
/// Removes a signer from the container and returns it
pub fn remove(&mut self, id: SignerId, ordering: SignerOrdering) -> Option<Arc<dyn Signer>> {
pub fn remove(
&mut self,
id: SignerId,
ordering: SignerOrdering,
) -> Option<Arc<dyn TransactionSigner>> {
self.0.remove(&(id, ordering).into())
}
@@ -428,12 +618,12 @@ impl SignersContainer {
}
/// Returns the list of signers in the container, sorted by lowest to highest `ordering`
pub fn signers(&self) -> Vec<&Arc<dyn Signer>> {
pub fn signers(&self) -> Vec<&Arc<dyn TransactionSigner>> {
self.0.values().collect()
}
/// Finds the signer with lowest ordering for a given id in the container.
pub fn find(&self, id: SignerId) -> Option<&Arc<dyn Signer>> {
pub fn find(&self, id: SignerId) -> Option<&Arc<dyn TransactionSigner>> {
self.0
.range((
Included(&(id.clone(), SignerOrdering(0)).into()),
@@ -479,6 +669,7 @@ pub struct SignOptions {
pub allow_all_sighashes: bool,
}
#[allow(clippy::derivable_impls)]
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
@@ -490,25 +681,39 @@ impl Default for SignOptions {
}
pub(crate) trait ComputeSighash {
type Extra;
type Sighash;
type SighashType;
fn sighash(
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError>;
extra: Self::Extra,
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
}
impl ComputeSighash for Legacy {
type Extra = ();
type Sighash = bitcoin::Sighash;
type SighashType = EcdsaSighashType;
fn sighash(
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.global.unsigned_tx.input[input_index];
let tx_input = &psbt.unsigned_tx.input[input_index];
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
let sighash = psbt_input
.sighash_type
.unwrap_or_else(|| EcdsaSighashType::All.into())
.ecdsa_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let script = match psbt_input.redeem_script {
Some(ref redeem_script) => redeem_script.clone(),
None => {
@@ -526,9 +731,11 @@ impl ComputeSighash for Legacy {
};
Ok((
psbt.global
.unsigned_tx
.signature_hash(input_index, &script, sighash.as_u32()),
sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
input_index,
&script,
sighash.to_u32(),
)?,
sighash,
))
}
@@ -545,18 +752,27 @@ fn p2wpkh_script_code(script: &Script) -> Script {
}
impl ComputeSighash for Segwitv0 {
type Extra = ();
type Sighash = bitcoin::Sighash;
type SighashType = EcdsaSighashType;
fn sighash(
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.global.unsigned_tx.input[input_index];
let tx_input = &psbt.unsigned_tx.input[input_index];
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
let sighash = psbt_input
.sighash_type
.unwrap_or_else(|| EcdsaSighashType::All.into())
.ecdsa_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
// Always try first with the non-witness utxo
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
@@ -599,17 +815,72 @@ impl ComputeSighash for Segwitv0 {
};
Ok((
bip143::SigHashCache::new(&psbt.global.unsigned_tx).signature_hash(
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
input_index,
&script,
value,
sighash,
),
)?,
sighash,
))
}
}
impl ComputeSighash for Tap {
type Extra = Option<taproot::TapLeafHash>;
type Sighash = taproot::TapSighashHash;
type SighashType = SchnorrSighashType;
fn sighash(
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
extra: Self::Extra,
) -> Result<(Self::Sighash, SchnorrSighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let sighash_type = psbt_input
.sighash_type
.unwrap_or_else(|| SchnorrSighashType::Default.into())
.schnorr_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let witness_utxos = psbt
.inputs
.iter()
.cloned()
.map(|i| i.witness_utxo)
.collect::<Vec<_>>();
let mut all_witness_utxos = vec![];
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
let prevouts = if is_anyone_can_pay {
sighash::Prevouts::One(
input_index,
witness_utxos[input_index]
.as_ref()
.ok_or(SignerError::MissingWitnessUtxo)?,
)
} else if witness_utxos.iter().all(Option::is_some) {
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
sighash::Prevouts::All(&all_witness_utxos)
} else {
return Err(SignerError::MissingWitnessUtxo);
};
// Assume no OP_CODESEPARATOR
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
Ok((
cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
sighash_type,
))
}
}
impl PartialOrd for SignersContainerKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
@@ -640,12 +911,11 @@ mod signers_container_tests {
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::util::bip32;
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin::Network;
use miniscript::ScriptContext;
use std::str::FromStr;
fn is_equal(this: &Arc<dyn Signer>, that: &Arc<DummySigner>) -> bool {
fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
let secp = Secp256k1::new();
this.id(&secp) == that.id(&secp)
}
@@ -660,11 +930,11 @@ mod signers_container_tests {
let (prvkey1, _, _) = setup_keys(TPRV0_STR);
let (prvkey2, _, _) = setup_keys(TPRV1_STR);
let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap();
let (_, keymap) = desc
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers = SignersContainer::from(keymap);
let signers = SignersContainer::build(keymap, &wallet_desc, &secp);
assert_eq!(signers.ids().len(), 2);
let signers = signers.signers();
@@ -726,22 +996,19 @@ mod signers_container_tests {
number: u64,
}
impl Signer for DummySigner {
fn sign(
&self,
_psbt: &mut PartiallySignedTransaction,
_input_index: Option<usize>,
_secp: &SecpCtx,
) -> Result<(), SignerError> {
Ok(())
}
impl SignerCommon for DummySigner {
fn id(&self, _secp: &SecpCtx) -> SignerId {
SignerId::Dummy(self.number)
}
}
fn sign_whole_tx(&self) -> bool {
true
impl TransactionSigner for DummySigner {
fn sign_transaction(
&self,
_psbt: &mut psbt::PartiallySignedTransaction,
_secp: &SecpCtx,
) -> Result<(), SignerError> {
Ok(())
}
}
@@ -756,7 +1023,7 @@ mod signers_container_tests {
let secp: Secp256k1<All> = Secp256k1::new();
let path = bip32::DerivationPath::from_str(PATH).unwrap();
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
let fingerprint = tprv.fingerprint(&secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();

View File

@@ -42,7 +42,7 @@ use std::default::Default;
use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
use bitcoin::{OutPoint, Script, Transaction};
use miniscript::descriptor::DescriptorTrait;
@@ -103,10 +103,7 @@ impl TxBuilderContext for BumpFee {}
/// builder.finish()?
/// };
///
/// assert_eq!(
/// psbt1.global.unsigned_tx.output[..2],
/// psbt2.global.unsigned_tx.output[..2]
/// );
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
/// # Ok::<(), bdk::Error>(())
/// ```
///
@@ -120,8 +117,8 @@ impl TxBuilderContext for BumpFee {}
/// [`finish`]: Self::finish
/// [`coin_selection`]: Self::coin_selection
#[derive(Debug)]
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<B, D>,
pub struct TxBuilder<'a, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<D>,
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
@@ -140,7 +137,7 @@ pub(crate) struct TxParams {
pub(crate) utxos: Vec<WeightedUtxo>,
pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) manually_selected_only: bool,
pub(crate) sighash: Option<SigHashType>,
pub(crate) sighash: Option<psbt::PsbtSighashType>,
pub(crate) ordering: TxOrdering,
pub(crate) locktime: Option<u32>,
pub(crate) rbf: Option<RbfValue>,
@@ -170,7 +167,7 @@ impl std::default::Default for FeePolicy {
}
}
impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
fn clone(&self) -> Self {
TxBuilder {
wallet: self.wallet,
@@ -182,8 +179,8 @@ impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
}
// methods supported by both contexts, for any CoinSelectionAlgorithm
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
TxBuilder<'a, B, D, Cs, Ctx>
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
TxBuilder<'a, D, Cs, Ctx>
{
/// Set a custom fee rate
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
@@ -337,8 +334,9 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
///
/// Note 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.
/// Note unless you set [`only_witness_utxo`] any non-taproot `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
@@ -412,7 +410,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// Sign with a specific sig hash
///
/// **Use this option very carefully**
pub fn sighash(&mut self, sighash: SigHashType) -> &mut Self {
pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self {
self.params.sighash = Some(sighash);
self
}
@@ -508,7 +506,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
self,
coin_selection: P,
) -> TxBuilder<'a, B, D, P, Ctx> {
) -> TxBuilder<'a, D, P, Ctx> {
TxBuilder {
wallet: self.wallet,
params: self.params,
@@ -547,7 +545,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
}
}
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D, Cs, CreateTx> {
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, 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;
@@ -614,8 +612,8 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
}
// methods supported only by bump_fee
impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Explicitly tells the wallet that it is allowed to reduce the amount 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.
///
@@ -838,6 +836,7 @@ mod test {
},
txout: Default::default(),
keychain: KeychainKind::External,
is_spent: false,
},
LocalUtxo {
outpoint: OutPoint {
@@ -846,6 +845,7 @@ mod test {
},
txout: Default::default(),
keychain: KeychainKind::Internal,
is_spent: false,
},
]
}

View File

@@ -17,7 +17,7 @@ use std::fmt;
use bitcoin::consensus::serialize;
use bitcoin::{OutPoint, Transaction, Txid};
use crate::blockchain::Blockchain;
use crate::blockchain::GetTx;
use crate::database::Database;
use crate::error::Error;
@@ -29,7 +29,7 @@ use crate::error::Error;
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
/// with unconfirmed transactions that have been evicted from the backend's memory.
pub fn verify_tx<D: Database, B: Blockchain>(
pub fn verify_tx<D: Database, B: GetTx>(
tx: &Transaction,
database: &D,
blockchain: &B,
@@ -104,43 +104,18 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
use crate::database::{BatchOperations, MemoryDatabase};
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(())
}
impl GetTx for DummyBlockchain {
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]