Compare commits

..

295 Commits

Author SHA1 Message Date
Alekos Filini
f2e12d0ccd Bump version to 0.22.0-rc.1 2022-09-01 15:49:46 +02:00
Alekos Filini
13cf72ffa7 Merge bitcoindevkit/bdk#738: Fix docs.rs features
3451d1c12e Fix docs.rs features (Alekos Filini)

Pull request description:

  ### Description

  Fix docs.rs features

  ### 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`

ACKs for top commit:
  danielabrozzoni:
    utACK 3451d1c12e
  notmandatory:
    ACK 3451d1c12e

Tree-SHA512: 96dc4f816b21cf20fc2828dcfd56865f3f9add8e4aa643205879c810d09e6f2e73d6643061bf3ca98145b37e726907dedd31a145f92d6646c172a43ae2285aa8
2022-08-31 15:57:48 +02:00
Alekos Filini
cf13c80991 Merge bitcoindevkit/bdk#544: Update DEVELOPMENT_CYCLE.md to work with [patch.crates-io]
7c57965999 Bump version before making release branch, separate patch_release template (Steve Myers)
3d69f1c291 Update DEVELOPMENT_CYCLE.md to work with [patch.crates-io] (Steve Myers)

Pull request description:

  ### Description

  Update DEVELOPMENT_CYCLE and release instructions to make [overriding dependencies] possible for downstream projects with unreleased `bdk` versions for development and testing. Also simplifies the release process by capturing changelog information in the `pull_request_template` and recording release changelog information in the release tag message instead of in a `CHANGELOG.md` file which causes too many merge conflicts and complicates the release process.

  Fixes #536
  Fixes #496

  ### Notes to the reviewers

  The primary changes to our current release process are:

  1. Don't add `-dev` or `-rc.x` to unreleased `bdk` cargo versions because those extensions do not work with [overriding dependencies].
  2. Increment the `master` branch version as soon as a `release/MAJOR.MINOR` branch is created, the next release `release/MAJOR.MINOR` branch version with be **MAJOR.MINOR.PATCH**, and the `master` branch development version will be **MAJOR.MINOR+1.0**; either version can be used with [overriding dependencies].
  4. Remove the `bdk` version from the `src/lib.rs` file so that it doesn't need to be changed on every release, because it isn't needed in the rust docs for most developers and removing it will help simplify the release process.
  5. The new release process is now documented as a checklist in a new `release.md` github issue template.
  6. Putting changelog information in the release tag message is how the tokio project does it. ~~After this PR is merged I will replace old tags with new ones containing changelog information and then do a new PR to remove the CHANGELOG.md file.~~ After this PR is merged I don't think we need to update old tags, only rename the CHANGELOG.md file to CHANGELOG-OLD.md with a note to check tags for future change log info.

  [overriding dependencies]: https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html

  ### 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:
  afilini:
    ACK 7c57965999

Tree-SHA512: 818e2f9bc7a629cbbb190a83b9743e8f4de49a4093beae83ed0b9c506f33e6f96b2c1e376f788536d58c46908d278bde08140f43a625515401ea1f9efdb9153f
2022-08-31 15:18:20 +02:00
Steve Myers
7c57965999 Bump version before making release branch, separate patch_release template 2022-08-31 07:43:06 -05:00
Steve Myers
3d69f1c291 Update DEVELOPMENT_CYCLE.md to work with [patch.crates-io] 2022-08-31 07:43:03 -05:00
Alekos Filini
3451d1c12e Fix docs.rs features 2022-08-31 11:23:51 +02:00
Daniela Brozzoni
4fbd8520e6 Merge bitcoindevkit/bdk#689: Add allow_dust method to TxBuilder
bfd7b2f65d Allow creating transactions with dust outputs (Liam)

Pull request description:

  We needed this for testing our wallet with dust, does this look like a reasonable feature? If so, I'll go ahead and add a test, update the changelog, etc.

ACKs for top commit:
  danielabrozzoni:
    tACK bfd7b2f65d

Tree-SHA512: b467b365d8a68f5a868cc5cc88387677533e8fb0bf543bf4c7a5b984f8b28972281029a3be8d2c92cee7d6ee05c243d12af0841e7a7e1d652745567557f2bede
2022-08-31 10:31:01 +02:00
Liam
bfd7b2f65d Allow creating transactions with dust outputs
Add TxBuilder::allow_dust() that skips checking the dust limit
2022-08-30 11:25:34 -04:00
Alekos Filini
061f15af00 Merge bitcoindevkit/bdk#682: Add a custom signer for hardware wallets
138acc3b7d Change `populate_test_db` to not return empty input (wszdexdrf)
d6e1dd1040 Change CI to add test using ledger emulator (wszdexdrf)
76034772cb Add a custom signer for hardware wallets (wszdexdrf)

Pull request description:

  Also adds a new test in CI for building and testing on a virtual
  hardware wallet.

  ### Description

  This PR would enable BDK users to sign transactions using a hardware wallet. It is just the beginning hence there are no complex features, but I hope not for long.
  I have added a test in CI for building a ledger emulator and running the new test on it. The test is similar to the one on bitcoindevkit/rust-hwi.

  ### Notes to the reviewers
  The PR is incomplete (and wouldn't work, as the rust-hwi in `cargo.toml` is pointing to a local crate, temporarily) as a small change is required in rust-hwi (https://github.com/bitcoindevkit/rust-hwi/pull/42).

  ### 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:
  afilini:
    ACK 138acc3b7d

Tree-SHA512: 54337f06247829242b4dc60f733346173d957de8e9f8b80beb91304d679cfb4e0e4db722c967469265a5b6ede2bd641ba5c089760391c671995dc30de37897de
2022-08-29 16:15:02 +02:00
Alekos Filini
2bff4e5e56 Merge bitcoindevkit/bdk#726: [bug-fix] Set the db sync height
08668ac462 Set the db sync height (rajarshimaitra)

Pull request description:

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

  ### Description

  Fixes #719

  Previously we weren't setting the db sync height in populate_test_db
  macro even when current height is provided.. This creates a bug that
  get_funded_wallet will return 0 balance.

  This PR fixes the populate_test_db macro and updates tests which were
  previously dependent on the unsynced wallet behavior.

  ### 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

  #### Bugfixes:

  * [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:
  afilini:
    ACK 08668ac462

Tree-SHA512: 1dcc968e4b3551e916b450c5ff2fab6636083f104cc982eb3f7602c624382434e0170d9f0c0a356e6c9c5f834eebe5cb1365b37ef73d7b4ef15d652a364dc2ab
2022-08-29 10:24:40 +02:00
wszdexdrf
138acc3b7d Change populate_test_db to not return empty input 2022-08-29 13:54:01 +05:30
wszdexdrf
d6e1dd1040 Change CI to add test using ledger emulator 2022-08-29 13:54:01 +05:30
wszdexdrf
76034772cb Add a custom signer for hardware wallets
Also add function to get funded wallet with coinbase
2022-08-29 13:53:56 +05:30
Daniela Brozzoni
12507c707f Merge bitcoindevkit/bdk#678: Implement conversion for Lightning fee rate
de358f8cdc Implement conversion for Lightning fee rate (Vladimir Fomene)

Pull request description:

  This PR fixes #608.

  ### Description

  Lightning denotes transaction fee rate sats / 1000 weight units and sats / 1000 vbytes.
  Here we add support for creating BDK FeeRate from lightning fee rate. We also move all FeeRate tests to
  types.rs and rename as_sat_vb to as_sat_per_vb.

  ### Notes to the reviewers

  Matt was concerned that we might round down value in fee calculation in such a way that a transaction may not be relayed because it is below Bitcoin Core's min relay fee (1 sat/vbyte). I don't think we need to worry about that because we [round up(ceil)](https://github.com/bitcoindevkit/bdk/blob/master/src/types.rs#L91) during fee calculation, we don't round down. I will love to hear what you think. Is there something I'm missing? @johncantrell97, I will appreciate your review on this one.

  ### 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
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    ACK de358f8cdc

Tree-SHA512: aaa7da8284b668d15ad9c92168c149c4b3ee0f8faee9b7eb159745d23e38835189eaf5c336da14ba9272ee07cd366718eefb8365da9ddf53014e122b6393a087
2022-08-29 10:02:31 +02:00
Vladimir Fomene
de358f8cdc Implement conversion for Lightning fee rate
Lightning denotes transaction fee rate
sats / 1000 weight units and sats / 1000 vbytes.
Here we add support for creating BDK fee rate from
lightning fee rate. We also move all FeeRate test to
types.rs and rename as_sat_vb to as_sat_per_vb.
2022-08-28 21:37:07 +03:00
rajarshimaitra
08668ac462 Set the db sync height
Previously we weren't setting the db sync height in populate_test_db
macro even when current height is provided.. This creates a bug that
get_funded_wallet will return 0 balance.

This PR fixes the populate_test_db macro and updates tests which were
previously dependent on the unsynced wallet behavior.
2022-08-28 23:51:24 +05:30
Alekos Filini
0a3734ed2b Merge bitcoindevkit/bdk#718: Verify signatures after signing
7b1ad1b629 Verify signatures after signing (Scott Robinson)

Pull request description:

  ### Description

  Verify signatures after signing

  As per [BIP-340, footnote 14][fn]:
  > Verifying the signature before leaving the signer prevents random or
  > attacker provoked computation errors. This prevents publishing invalid
  > signatures which may leak information about the secret key. It is
  > recommended, but can be omitted if the computation cost is prohibitive.

  [fn]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#cite_note-14

  ### Notes to the reviewers

  How do we test this?

  ### Checklists

  #### All Submissions:

  * [ ] 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:
  afilini:
    re-ACK 7b1ad1b629

Tree-SHA512: 7319db1f8cec2fcfe4ac443ab5728893f9fb6133b33331b35ec6910662c45de8a7cdcf80ac1f3bb435815e914ccf639682a5c07ff0baef42605bf044a34a8232
2022-08-25 12:21:40 +02:00
Alekos Filini
a5d1a3d65c Merge bitcoindevkit/bdk#722: Implement Deref<Target=UrlClient> for EsploraBlockchain
baf7eaace6 Implement Deref<Target=UrlClient> for EsploraBlockchain (Vladimir Fomene)

Pull request description:

  ### Description

  There is currently no way to access the client from the EsploraBlockchain. This makes it difficult for users to extend it's functionality. This PR exposes both the reqwest and ureq clients. This PR is related to PR #705.

  ### 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 baf7eaace6

Tree-SHA512: e2f530058c88e06fc2972edfcd2df1b534d43b0214d710b62e4d5200ac0e38dad6a9f8db1e0c7a7ed19892e59411dcc07f3f6dc8ad58afae9d677169ca98bb38
2022-08-25 12:20:12 +02:00
Alekos Filini
7bc2980905 Merge bitcoindevkit/bdk#705: Implement Deref<Target=Client> for ElectrumBlockchain
c5952dd09a Implement `Deref<Target=Client>` for `ElectrumBlockchain` (Alekos Filini)

Pull request description:

  ### Description

  As pointed out in https://github.com/bitcoindevkit/rust-electrum-client/pull/58#issuecomment-1207890096 there was no way to keep using the client once it was given to 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:
  rajarshimaitra:
    ACK c5952dd09a

Tree-SHA512: fbfbada51c9426266c8960da5508ee07b196808f0d670a09a51962bd6eda9ccf585e209f5b99b5ab78a3d17af774bdb3e33ef36ac4f4d1ce7f2c3398ae4f6d0c
2022-08-25 12:19:21 +02:00
Alekos Filini
34e792e193 Merge bitcoindevkit/bdk#731: Implement Deref<Target=Client> for RpcBlockchain
a8f9f6c43a RpcBlockchain derefs to the underlying RPC Client (rajarshimaitra)

Pull request description:

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

  ### Description

  For the same reason as #705  and #722 ..

  ### 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:
  afilini:
    ACK a8f9f6c43a

Tree-SHA512: 81e596fe451c275ca0ce27ee7ac9cf7e88433775603021c2dd1cd26a26558531cf74f81ef05d0ae9d5d0e59e91196e3ac6d38c0f4853b1889ddf822d8e63e178
2022-08-25 12:18:22 +02:00
Scott Robinson
7b1ad1b629 Verify signatures after signing
As per [BIP-340, footnote 14][fn]:
> Verifying the signature before leaving the signer prevents random or
> attacker provoked computation errors. This prevents publishing invalid
> signatures which may leak information about the secret key. It is
> recommended, but can be omitted if the computation cost is prohibitive.

[fn]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#cite_note-14
2022-08-25 16:29:44 +10:00
rajarshimaitra
a8f9f6c43a RpcBlockchain derefs to the underlying RPC Client 2022-08-23 21:44:38 +05:30
Alekos Filini
c9b1b6d076 Merge bitcoindevkit/bdk#723: Fix P2WPKH_SATISFACTION_SIZE in CS tests
cd078903a7 Fix P2WPKH_SATISFACTION_SIZE in CS tests (Daniela Brozzoni)

Pull request description:

  Our costant for the P2WPKH satisfaction size was wrong: in
  7ac87b8f99 we added 1 WU for the script
  sig len - but actually, that's 4WU! This resulted in
  P2WPKH_SATISFACTION_SIZE being equal to 109 instead of 112.
  This also adds a comment for better readability.

  ### Description

  ### Notes to the reviewers

  ### 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:
  csralvall:
    utACK cd078903a7
  afilini:
    ACK cd078903a7

Tree-SHA512: 3e39735505e411392cf01885b5920443d23fa21d9c20cc7c8fdeaa2698df8bc2da86241b6c20f5e3f5941fe1a0aebe8f957d8145d4f9e7ad3f213e4658d6ea68
2022-08-17 13:46:48 +02:00
Daniela Brozzoni
cd078903a7 Fix P2WPKH_SATISFACTION_SIZE in CS tests
Our costant for the P2WPKH satisfaction size was wrong: in
7ac87b8f99 we added 1 WU for the script
sig len - but actually, that's 4WU! This resulted in
P2WPKH_SATISFACTION_SIZE being equal to 109 instead of 112.
This also adds a comment for better readability.
2022-08-16 18:27:59 +01:00
Alekos Filini
588c17ff69 Merge bitcoindevkit/bdk#711: Release/0.21.0
8026bd9476 Bump version to 0.21.1-dev (Alekos Filini)
e2bd96012a Bump version to 0.21.0 (Alekos Filini)
2c01b6118f Bump version to 0.21.0-rc.1 (Alekos Filini)

Pull request description:

  ### Description

  Merge the release branch back into master

  ### 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:
  danielabrozzoni:
    ACK 8026bd9476

Tree-SHA512: a2a924a60d551a823de035b609d4d51652a165a0695212af76dea87706919c8929dba977bb297f4787708470bf075d14dd0a37657bd3a76e7d44a746fb5439df
2022-08-16 13:39:49 +02:00
Vladimir Fomene
baf7eaace6 Implement Deref<Target=UrlClient> for EsploraBlockchain
There is currently no way to access the client
from the EsploraBlockchain. This makes it difficult
for users to extend it's functionality. This PR exposes
both the reqwest and ureq clients. This PR is related to
PR #705.
2022-08-15 19:34:57 +03:00
Alekos Filini
8026bd9476 Bump version to 0.21.1-dev 2022-08-11 20:56:03 +02:00
Alekos Filini
e2bd96012a Bump version to 0.21.0 2022-08-11 17:02:32 +02:00
Alekos Filini
9be63e66ec Merge commit 'refs/pull/703/head' of github.com:bitcoindevkit/bdk into release/0.21.0 2022-08-09 12:06:13 +02:00
Alekos Filini
9f9ffd0efd Merge bitcoindevkit/bdk#703: Fix minor typos in docs
134b19a9cb Fix minor typos in docs (thunderbiscuit)

Pull request description:

  ### Description
  This PR fixes:
  1. The use of "i.e." in docs, sometimes spelled as "ie."
  2. A small typo in the sentence "Note that this methods only operate on the internal database..."
  3. A small typo in the sentence "Finish the building the transaction"

  I came across these while building docs for bdk-kotlin.

  ### Notes to the reviewers

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  danielabrozzoni:
    ACK 134b19a9cb

Tree-SHA512: 67296999eba8ffe1fe64756fa023d85064774cb9d4c26e99054d467b5024baea4138f11d602d04e695412c61625ee4f5b4687b75f177cfec2604a6c61a5a6216
2022-08-09 12:05:35 +02:00
Alekos Filini
2db881519a Merge branch 'release/0.21.0', commit 'refs/pull/704/head' of github.com:bitcoindevkit/bdk into release/0.21.0 2022-08-09 11:54:23 +02:00
Alekos Filini
d9adfbe047 Merge bitcoindevkit/bdk#704: Fix rpc::CoreTxIter logic.
74e2c477f1 Replace `rpc::CoreTxIter` with `list_transactions` fn. (志宇)

Pull request description:

  ### Description

  This fixes a bug where `CoreTxIter` attempts to call `listtransactions` immediately after a tx result is filtered (instead of being returned), when in fact, the correct logic will be to pop another tx result.

  The new logic also ensures that tx results are returned in chonological order. The test `test_list_transactions` verifies this. We also now ensure that `page_size` is between the range `[0 to 1000]` otherwise an error is returned.

  Some needless cloning is removed from `from_config` as well as logging improvements.

  ### Notes to the reviewers

  This is an oversight by me (sorry) for PR #683

  ### 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:
  afilini:
    ACK 74e2c477f1

Tree-SHA512: f32314a9947067673d19d95da8cde36b350c0bb0ebe0924405ad50602c14590f7ccb09a3e03cdfdd227f938dccd0f556f3a2b4dd7fdd6eba1591c0f8d3e65182
2022-08-09 11:49:56 +02:00
志宇
74e2c477f1 Replace rpc::CoreTxIter with list_transactions fn.
This fixes a bug where `CoreTxIter` attempts to call `listtransactions`
immediately after a tx result is filtered (instead of being returned),
when in fact, the correct logic will be to pop another tx result.

The new logic also ensures that tx results are returned in chonological
order. The test `test_list_transactions` verifies this. We also now
ensure that `page_size` is between the range `[0 to 1000]` otherwise an
error is returned.

Some needless cloning is removed from `from_config` as well as logging
improvements.
2022-08-08 21:12:23 +08:00
Alekos Filini
c5952dd09a Implement Deref<Target=Client> for ElectrumBlockchain
As pointed out in https://github.com/bitcoindevkit/rust-electrum-client/pull/58#issuecomment-1207890096
there was no way to keep using the client once it was given to BDK.
2022-08-08 12:03:35 +02:00
thunderbiscuit
134b19a9cb Fix minor typos in docs 2022-08-05 12:45:18 -04:00
Alekos Filini
2c01b6118f Bump version to 0.21.0-rc.1 2022-08-04 11:59:56 +02:00
Alekos Filini
03d3c786f2 Merge bitcoindevkit/bdk#640: Get balance in categories
0f03831274 Change get_balance to return in categories. (wszdexdrf)

Pull request description:

  ### Description
  This changes `get_balance()` function so that it returns balance separated in 4 categories:
  - available
  - trusted-pending
  - untrusted-pending
  - immature

  Fixes #238

  ### Notes to the reviewers
  Based on #614

  ### 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 tests for the new feature
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  afilini:
    ACK 0f03831274

Tree-SHA512: 39f02c22c61b6c73dd8e6d27b1775a72e64ab773ee67c0ad00e817e555c52cdf648f482ca8be5fcc2f3d62134c35b720b1e61b311cb6debb3ad651e79c829b93
2022-08-04 11:43:40 +02:00
wszdexdrf
0f03831274 Change get_balance to return in categories.
Add type balance with add, display traits. Change affected tests.
Update `CHANGELOG.md`
2022-08-04 10:37:09 +02:00
Alekos Filini
dc7adb7161 Merge bitcoindevkit/bdk#683: Fix wallet sync for RpcBlockchain
5eeba6cced Various `RpcBlockchain` improvements (志宇)
5eb74af414  Rpc: Manually add immature coinbase utxos (志宇)
ac19c19f21 New `RpcBlockchain` implementation with various fixes (志宇)

Pull request description:

  Fixes #677

  ### Description

  Unfortunately to fix all the problems, I had to do a complete re-implementation of `RpcBlockchain`.

  **The new implementation fixes the following:**
  * We can track more than 100 scriptPubKeys
  * We can obtain more than 1000 transactions per sync
  * Transaction "metadata" for already-syned transactions are updated when we introduce new scriptPubKeys

  **`RpcConfig` changes:**
  * Introduce `RpcSyncParams`.
  * Remove `RpcConfig::skip_blocks` (this is replaced by `RpcSyncParams::start_time`).

  ### Notes to the reviewers

  * The `RpcConfig` structure is changed. It will be good to confirm whether this is an okay change.

  ### 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`

  #### Bugfixes:

  * [x] 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:
  afilini:
    ACK 5eeba6cced

Tree-SHA512: 7e0c9cfc4ef10fb07e4ac7f6fbf30cf28ca6395495c0237fa5bfa9a2fcbbd4d8ff980ffcf71ddd10bc052e4c07bc2c27f093dd3cd1c69cb29141455c693f2386
2022-08-04 10:23:19 +02:00
志宇
5eeba6cced Various RpcBlockchain improvements
These are as suggested by @danielabrozzoni and @afilini

Also introduced `RpcSyncParams::force_start_time` for users who
prioritise reliability above all else.

Also improved logging.
2022-08-04 11:29:38 +08:00
志宇
5eb74af414 Rpc: Manually add immature coinbase utxos
Before this commit, the rpc backend would not notice immature utxos
(`listunspent` does not return them), making the rpc balance different
to other blockchain implementations.

Co-authored-by: Daniela Brozzoni <danielabrozzoni@protonmail.com>
2022-08-04 11:27:50 +08:00
志宇
ac19c19f21 New RpcBlockchain implementation with various fixes
The new implementation fixes the following:
* We can track more than 100 scriptPubKeys
* We can obtain more than 1000 transactions per sync
* `TransactionDetails` for already-synced transactions are updated when
  new scriptPubKeys are introduced (fixing the missing balance/coins
      issue of supposedly tracked scriptPubKeys)

`RpcConfig` changes:
* Introduce `RpcSyncParams`.
* Remove `RpcConfig::skip_blocks` (this is replaced by
  `RpcSyncParams::start_time`).
2022-08-04 11:27:37 +08:00
Daniela Brozzoni
ef03da0a76 Merge bitcoindevkit/bdk#693: Fix the early InsufficientFunds error in the branch and bound
9d85c9667f Fix the early InsufficientFunds error in the branch and bound (Alekos Filini)

Pull request description:

  ### Description

  We were wrongly considering the sum of "effective value" (i.e. value -
  fee cost) when reporting an early "insufficient funds" error in the
  branch and bound coin selection.

  This commit fixes essentially two issues:
  - Very high fee rates could cause a panic during the i64 -> u64
    conversion because we assumed the sum of effective values would never
    be negative
  - Since we were comparing the sum of effective values of *all* the UTXOs
    (even the optional UTXOs with negative effective value) with the target
    we'd like to reach, we could in some cases error and tell the user we
    don't have enough funds, while in fact we do! Since we are not required
    to spend any of the optional UTXOs, so we could just ignore the ones
    that *cost us* money to spend and excluding them could potentially
    allow us to reach the target.

  There's a third issue that was present before and remains even with this
  fix: when we report the "available" funds in the error, we are ignoring
  UTXOs with negative effective value, so it may look like there are less
  funds in the wallet than there actually are.

  I don't know how to convey the right message the user: if we actually
  consider them we just make the "needed" value larger and larger (which
  may be confusing, because if the user asks BDK to send 10k satoshis, why
  do we say that we actually need 100k?), while if we don't we could report
  an incorrect "available" value.

  ### Notes to the reviewers

  I'm opening this as a draft before adding tests because I want to gather some feedback on the available vs needed error reporting. I personally think reporting a reasonable "needed" value is more important than the "available", because in a wallet app I would expect this is the value that would be shown to the user.

  ### 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:
  danielabrozzoni:
    utACK 9d85c9667f

Tree-SHA512: 9a06758cba61ade73198f35b08070987d5eb065e01750ce62409f86b37cd0b0894640e9f75c8b2c26543c0da04e3f77bd397fab540e789f221661aae828db224
2022-08-03 20:04:28 +02:00
Alekos Filini
9d85c9667f Fix the early InsufficientFunds error in the branch and bound
We were wrongly considering the sum of "effective value" (i.e. value -
fee cost) when reporting an early "insufficient funds" error in the
branch and bound coin selection.

This commit fixes essentially two issues:
- Very high fee rates could cause a panic during the i64 -> u64
  conversion because we assumed the sum of effective values would never
  be negative
- Since we were comparing the sum of effective values of *all* the UTXOs
  (even the optional UTXOs with negative effective value) with the target
  we'd like to reach, we could in some cases error and tell the user we
  don't have enough funds, while in fact we do! Since we are not required
  to spend any of the optional UTXOs, so we could just ignore the ones
  that *cost us* money to spend and excluding them could potentially
  allow us to reach the target.

There's a third issue that was present before and remains even with this
fix: when we report the "available" funds in the error, we are ignoring
UTXOs with negative effective value, so it may look like there are less
funds in the wallet than there actually are.

I don't know how to convey the right message the user: if we actually
consider them we just make the "needed" value larger and larger (which
may be confusing, because if the user asks BDK to send 10k satoshis, why
do we say that we actually need 100k?), while if we don't we could report
an incorrect "available" value.
2022-08-03 19:15:06 +02:00
Daniela Brozzoni
85bd126c6c Merge bitcoindevkit/bdk#686: doc: Document that list_transactions() might return unsorted txs
7fdacdbad4 doc: Document that list_transactions() might return unsorted txs, show how to sort them if needed (w0xlt)

Pull request description:

  This PR documents that `list_transactions()` might return unsorted transaction and shows how to sort them if needed.

  Closes #518.

ACKs for top commit:
  danielabrozzoni:
    re-ACK 7fdacdbad4

Tree-SHA512: 83bec98e1903d6dc6b8933e8994cb9d04aad059cee8a7b8e1e3a322cf52511364b36d0cd6be1c8cb1fd82c67f8be5a262bbd2c76e30b24eb4097c30f38aa8b10
2022-08-03 17:17:36 +02:00
w0xlt
7fdacdbad4 doc: Document that list_transactions() might return unsorted txs, show how to sort them if needed 2022-08-03 12:08:50 -03:00
Alekos Filini
9c0a769675 Merge bitcoindevkit/bdk#662: Consolidate fee_amount and amount_needed
e8df3d2d91 Consolidate `fee_amount` and `amount_needed` (Cesar Alvarez Vallero)

Pull request description:

  ### Description

  Before this commit `fee_amount` and `amount_needed` were passed as independent
  parameters. From the perspective of coin selection algorithms, they are always
  used jointly for the same purpose, to create a coin selection with a total
  effective value greater than it's summed values.

  This commit removes the abstraction that the use of the two parameter
  introduced by consolidating both into a single parameter, `target_amount`, who
  carries their values added up.

  Resolves: #641

  ### Notes to the reviewers

  I just updated old tests and didn't create new ones because almost all changes
  are renames and "logic changes" (like the addition of the selection fee) are
  tested in the modified 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

  #### 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`

  #### Bugfixes:

  * [x] This pull request breaks the existing API
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    re-ACK e8df3d2d91 - I tested with the fuzzer, run it for 13,000,000 iterations, couldn't find any crash :)

Tree-SHA512: 64b46473799352c06cc554659e4b159a33812b3d3793c9d436bd1e46b65edd085d71b219f6a0474f6836979ca608aa019a72bdc6915a2cc2d744a76e2a28b889
2022-08-03 12:32:44 +02:00
Alekos Filini
11865fddff Merge bitcoindevkit/bdk#681: Add electrsd/bitcoind_22_0 to example\rpcwallet target
a63c51f35d Add `electrsd/bitcoind_22_0` to `example\rpcwallet` target (w0xlt)

Pull request description:

  On master branch, `example\rpcwallet` fails.

  ```
  $ cargo run --features="keys-bip39 key-value-db rpc" --example rpcwallet
     Compiling bitcoin_hashes v0.9.7
     Compiling bip39 v1.0.1
     Compiling bdk v0.20.1-dev (/home/node01/Dev/wbdk)
      Finished dev [unoptimized + debuginfo] target(s) in 19.64s
       Running `target/debug/examples/rpcwallet`
  >> Setting up bitcoind
  thread 'main' panicked at 'We should always have downloaded path: Called a method requiring a feature to be set, but it's not', examples/rpcwallet.rs:56:51
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  ```

  This PR adds `electrsd/bitcoind_22_0` to `required-features`, making clear that this lib is needed to run this example..

  ```
  $ cargo run --features="keys-bip39 key-value-db rpc electrsd/bitcoind_22_0" --example rpcwallet
      Blocking waiting for file lock on package cache
     Compiling electrsd v0.19.1
     Compiling bdk v0.20.1-dev (/home/node01/Dev/wbdk)
      Finished dev [unoptimized + debuginfo] target(s) in 10.27s
       Running `target/debug/examples/rpcwallet`
  >> Setting up bitcoind
  >> bitcoind setup complete
  Available coins in Core wallet : 50.00000000 BTC

  >> Setting up BDK wallet
  >> BDK wallet setup complete.
  Available initial coins in BDK wallet : 0 sats

  >> Sending coins: Core --> BDK, 10 BTC
  >> Received coins in BDK wallet
  Available balance in BDK wallet: 1000000000 sats

  >> Sending coins: BDK --> Core, 5 BTC
  >> Coins sent to Core wallet
  Remaining BDK wallet balance: 499999859 sats

  Congrats!! you made your first test transaction with bdk and bitcoin core.
  ```

ACKs for top commit:
  afilini:
    reACK a63c51f35d

Tree-SHA512: ef13d5e001121c8b1ff6436f9e95b656737bee6692e9b18c4012846a2d2e9e9ad7e6b5cd87cebf4a873335e92a524694e684567a1268f5f0705156659fd9a916
2022-08-03 12:31:11 +02:00
Cesar Alvarez Vallero
e8df3d2d91 Consolidate fee_amount and amount_needed
Before this commit `fee_amount` and `amount_needed` were passed as independent
parameters. From the perspective of coin selection algorithms, they are always
used jointly for the same purpose, to create a coin selection with a total
effective value greater than it's summed values.

This commit removes the abstraction that the use of the two parameter
introduced by consolidating both into a single parameter, `target_amount`, who
carries their values added up.
2022-08-03 12:19:01 +02:00
w0xlt
a63c51f35d Add electrsd/bitcoind_22_0 to example\rpcwallet target 2022-08-03 12:13:56 +02:00
Alekos Filini
1730e0150f Merge bitcoindevkit/bdk#666: Various fixes to the fee_amount calculation in create_tx
419dc248b6 test: Document `test_bump_fee_add_input_change_dust` (Daniela Brozzoni)
632dabaa07 test: Check tx feerate with longer signatures (Daniela Brozzoni)
2756411ef7 test: Reproduce #660 conditions (Daniela Brozzoni)
50af51da5a test: Fix P2WPKH_FAKE_WITNESS_SIZE (Daniela Brozzoni)
ae919061e2 Take into account the segwit tx header when... ...selecting coins (Daniela Brozzoni)
7ac87b8f99 TXIN_BASE_WEIGHT shouldn't include the script len (Daniela Brozzoni)
ac051d7ae9 Calculate fee amount after output addition (Daniela Brozzoni)
00d426b885 test: Check that the feerate is never below... ...the requested one in assert_fee_rate (Daniela Brozzoni)
42fde6d457 test: Check fee_amount in assert_fee_rate (Daniela Brozzoni)

Pull request description:

  ### Description

  This PR mainly fixes two bugs:
  1. TXIN_BASE_WEIGHT wrongly included the `script_len` (Fixes #160)
  2. We wouldn't take into account the segwit header in the fee calculation, which could have resulted in a transaction with a lower feerate than the requested one
  3. In tests we used to push 108 bytes on the witness as a fake signature, but we should have pushed 106 instead

  I also add a test to reproduce the conditions of #660, to check if it's solved. Turns out it's been solved already in #630, but if you're curious about what the bug was, here it is: https://github.com/bitcoindevkit/bdk/issues/660#issuecomment-1196436776
  ### 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:
  afilini:
    ACK 419dc248b6

Tree-SHA512: c7b55342eac440a3607a16b94560cb9c08c4805c853432adfda8e21c5177f85d5a8afe0e7e61140e92c8f10934332459c6234fc5f1509ea699d97b1d04f030c6
2022-08-03 11:40:36 +02:00
Alekos Filini
5a415979af Merge bitcoindevkit/bdk#645: Allow signing only specific leaf hashes
a713a5a062 Better customize signing in taproot transactions (Daniela Brozzoni)

Pull request description:

  Fixes #616

  ### 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:
  afilini:
    ACK a713a5a062

Tree-SHA512: 1100d43cb394b429450fc34f49dd815a024701987c0e6dd163865bd5c4c6f7102127b1ea6e10ced5fdb319874be97baeeb0deea66b4138410871a1d68b4def10
2022-08-02 22:48:29 +02:00
Daniela Brozzoni
a713a5a062 Better customize signing in taproot transactions
We would previously always try to sign with the taproot internal
key, and try to sign all the script leaves hashes.
Instead, add the `sign_with_tap_internal_key` and `TapLeaveOptions`
parameters, to be able to specify if we should sign with the internal
key, and exactly which leaves we should sign.
Fixes #616
2022-08-02 12:20:08 +02:00
Daniela Brozzoni
419dc248b6 test: Document test_bump_fee_add_input_change_dust
Add a rationale for the feerate in the test
2022-08-02 12:09:42 +02:00
Daniela Brozzoni
632dabaa07 test: Check tx feerate with longer signatures
This commit also suppresses the `unused_mut` warning
in `assert_fee_rate`, which happens because we call it
without `add_signatures`.
2022-08-02 12:08:56 +02:00
Daniela Brozzoni
2756411ef7 test: Reproduce #660 conditions
Issue #660 has been fixed by 32ae95f463,
when we moved the change calculation inside the coin selection.
This commit just adds a test to make sure that the problem is fixed.
2022-08-02 12:08:55 +02:00
Daniela Brozzoni
50af51da5a test: Fix P2WPKH_FAKE_WITNESS_SIZE
We would previously push 108 bytes on a P2WPKH witness
to simulate signature + pubkey. This was wrong: we should push
106 bytes instead.
The max satisfaction size for a P2WPKH is 112 WU:
elements in witness (1 byte, 1WU) + OP_PUSH (1 byte, 1WU) +
pk (33 bytes, 33 WU) + OP_PUSH (1 byte, 1WU) + signature and sighash
(72 bytes, 72 WU) + scriptsig len (1 byte, 4WU)
We should push on the witness pk + signature and sighash. This is 105
WU. Since we push just once instead of twice, we add 1WU for the OP_PUSH
we are omitting.
2022-08-02 12:08:54 +02:00
Daniela Brozzoni
ae919061e2 Take into account the segwit tx header when...
...selecting coins

We take into account the larger segwit tx header for every
transaction, not just the segwit ones. The reason for this is that
we prefer to overestimate the fees for the transaction than
underestimating them - the former might create txs with a slightly
higher feerate than the requested one, while the latter might
create txs with a slightly lower one - or worse, invalid (<1 sat/vbyte)!
2022-08-02 12:08:53 +02:00
Daniela Brozzoni
7ac87b8f99 TXIN_BASE_WEIGHT shouldn't include the script len
We would before calculate the TXIN_BASE_WEIGHT as prev_txid (32 bytes) +
prev_vout (4 bytes) + sequence (4 bytes) + script_sig_len (1 bytes), but
that's wrong: the script_sig_len shouldn't be included, as miniscript
already includes it in the `max_satisfaction_size` calculation.
Fixes #160
2022-08-02 12:08:52 +02:00
Daniela Brozzoni
ac051d7ae9 Calculate fee amount after output addition
We would previously calculate the fee amount in two steps:
1. Add the weight of the empty transaction
2. Add the weight of each output

That's unnecessary: you can just use the weight of the transaction
*after* the output addition. This is clearer, but also avoids a
rare bug: if there are many outputs, adding them would cause the
"number of outputs" transaction parameter lenght to increase, and we
wouldn't notice it.
This might still happen when adding the drain output - this
commit also adds a comment as a reminder.
2022-08-02 12:08:51 +02:00
Daniela Brozzoni
00d426b885 test: Check that the feerate is never below...
...the requested one in assert_fee_rate
2022-08-02 12:08:26 +02:00
Daniela Brozzoni
42fde6d457 test: Check fee_amount in assert_fee_rate 2022-08-02 12:08:12 +02:00
Alekos Filini
8e0d00a3ea Merge bitcoindevkit/bdk#694: Add assertions in the FeeRate constructor
235011feef Add assertions in the FeeRate constructor (Alekos Filini)

Pull request description:

  ### Description

  Disallow negative, NaN, infinite or subnormal fee rate values.

  ### Notes to the reviewers

  This commit is technically an API break because it makes the `FeeRate::from_sat_per_vb` function non-const. I think it's worth it compared to the risk of having completely nonsensical fee rates (that can break the coin selection in interesting ways).

  EDIT: it's also a breaking change because our code can now panic in scenarios where it didn't before. Again, I think it's worth it.

  ### 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] This pull request breaks the existing API
  * [x] 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:
  danielabrozzoni:
    re-ACK 235011feef

Tree-SHA512: c9432956162fadfd255edf20b825635a487adb29c88d791e18f170da79a2aac6f8e745b5e5be09be3c211697d0b1f4bddc1da75c181e8f9fc4fddf566a7a3e5c
2022-08-02 11:26:36 +02:00
Alekos Filini
235011feef Add assertions in the FeeRate constructor
Disallow negative, NaN, infinite or subnormal fee rate values.
2022-08-02 11:02:11 +02:00
Daniela Brozzoni
a1477405d1 Merge bitcoindevkit/bdk#675: Use T: AsRef<Path> as param to SqliteDatabase::new
558e37afa7 Use T: AsRef<Path> as param to SqliteDatabase::new (Vladimir Fomene)

Pull request description:

  This PR fixes #674

  ### Description

  Currently SqliteDatabase::new takes a String as path,
  with this change, it now accepts any type that implements
  AsRef<Path>.

  ### Notes to the reviewers

  ### 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
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    utACK 558e37afa7

Tree-SHA512: c5ff5b60e5904a5b7ef492a1e40296864b6b7506e4c6a187cfab05ef8140d14ddd322d016b4eeb18c5cfca8d4b575370b4f13c6ea7d7374ab0372a3237a5ed94
2022-07-30 09:30:20 +02:00
Vladimir Fomene
558e37afa7 Use T: AsRef<Path> as param to SqliteDatabase::new
Currently SqliteDatabase::new takes a String as path,
with this change, it now accepts any type that implements
AsRef<Path>.
2022-07-29 17:39:12 +03:00
Alekos Filini
6bae52e6f2 Merge bitcoindevkit/bdk#630: Move change logic to coin_select
32ae95f463 Move change calculus to coin_select (Cesar Alvarez Vallero)

Pull request description:

  ### Description

  The former way to compute and create change was inside `create_tx`, just after
  performing coin selection.
  It blocked the opportunity to have an "ensemble" algorithm to decide between
  multiple coin selection algorithms based on a metric, like Waste.
  Now, change is not created inside `coin_select` but the change amount and the
  possibility to create change is decided inside the `coin_select` method. In
  this way, change is associated with the coin selection algorithm that generated
  it, and a method to decide between them can be implemented.

  Fixes #147.
  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  <!-- 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
  * [x] I've added docs for the new feature
  * [x] I've updated `CHANGELOG.md`

ACKs for top commit:
  afilini:
    ACK 32ae95f463

Tree-SHA512: 350adb86538949ff50f41151fc46c8d28d9f5fd659e9869882cc3cb30128d76d4b479512c74c721f8beebfdb5423363ad63368e30556efe65ced2b8c52c34ef6
2022-07-26 11:51:10 +02:00
Cesar Alvarez Vallero
32ae95f463 Move change calculus to coin_select
The former way to compute and create change was inside `create_tx`, just after
performing coin selection.
It blocked the opportunity to have an "ensemble" algorithm to decide between
multiple coin selection algorithms based on a metric, like Waste.

Now, change isn't created inside `coin_select` but the change amount and the
possibility to create change is decided inside the `coin_select` method. In
this way, change is associated with the coin selection algorithm that generated
it, and a method to decide between them can be implemented.
2022-07-23 15:40:59 -03:00
Alekos Filini
3644a452c1 Merge bitcoindevkit/bdk#672: Fix wallet sync not finding coins of addresses which are not cached
5c940c33cb Fix wallet sync not finding coins of addresses which are not cached (志宇)

Pull request description:

  Fixes #521
  Fixes #451

  ^ However, only for electrum-based `Blockchain` implementations. For RPC and Compact Block Filters, syncing works differently, and so are the bugs - I have created a separate ticket for this (#677).

  ### Description

  Previously, electrum-based blockchain implementations only synced for `scriptPubKey`s that are already cached in `Database`.

  This PR introduces a feedback mechanism, that uses `stop_gap` and the difference between "current index" and "last active index" to determine whether we need to cache more `scriptPubKeys`.

  The `WalletSync::wallet_setup` trait now may return an `Error::MissingCachedScripts` error which contains the number of extra `scriptPubKey`s to cache, in order to satisfy `stop_gap` for the next call.

  `Wallet::sync` now calls `WalletSync` in a loop, caching in-between subsequent calls (if needed).

  #### Notes to reviewers

  1. The caveat to this solution is that it is not the most efficient. Every call to `WalletSync::wallet_setup` starts polling the Electrum-based server for `scriptPubKey`s starting from index 0.

      However, I feel like this solution is the least "destructive" to the API of `Blockchain`. Also, once the `bdk_core` sync logic is integration, we can select specific ranges of `scriptPubKey`s to sync.

  2. Also note that this PR only fixes the issue for electrum-based `Blockchain` implementations (currently `blockchain::electrum` and `blockchain::esplora` only).

  3. Another thing to note is that, although `Database` assumes 1-2 keychains, the current `WalletSync` "feedback" only returns one number (which is interpreted as the larger "missing count" of the two keychains). This is done for simplicity, and because we are planning to only have one keychain per database in the future.

      f0c876e7bf/src/blockchain/mod.rs (L157-L161)

  4. Please have a read of https://github.com/bitcoindevkit/bdk/pull/672#issuecomment-1186929465 for additional context.

  ### 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] 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:
  afilini:
    ACK 5c940c33cb

Tree-SHA512: aee917ed4821438fc0675241432a7994603a09a77d5a72e96bad863e7cdd55a9bc6fbd931ce096fef1153905cf1b786e1d8d932dc19032d549480bcda7c75d1b
2022-07-21 18:05:57 +02:00
志宇
5c940c33cb Fix wallet sync not finding coins of addresses which are not cached
Previously, electrum-based blockchain implementations only synced for
`scriptPubKey`s that are already cached in `Database`.

This PR introduces a feedback mechanism, that uses `stop_gap` and the
difference between "current index" and "last active index" to determine
whether we need to cache more `scriptPubKeys`.

The `WalletSync::wallet_setup` trait now may return an
`Error::MissingCachedScripts` error which contains the number of extra
`scriptPubKey`s to cache, in order to satisfy `stop_gap` for the next call.

`Wallet::sync` now calls `WalletSync` in a loop, cacheing inbetween
subsequent calls (if needed).
2022-07-20 23:08:12 +08:00
Daniela Brozzoni
277e18f5cb Merge bitcoindevkit/bdk#661: Test: No address reuse for single descriptor
2c02a44586 Test: No address reuse for single descriptor (志宇)

Pull request description:

  ### Description

  Just a simple new test.

  This test is to ensure there are no regressions when we later change
  internal logic of `Wallet`. A single descriptor wallet should always get
  a new address with `AddressIndex::New` even if we alternate grabbing
  internal/external keychains.

  I thought of adding this during work on #647

  ### 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:
  danielabrozzoni:
    tACK 2c02a44586
  rajarshimaitra:
    tACK 2c02a44586

Tree-SHA512: d065ae0979dc3ef7c26d6dfc19c88498e4bf17cc908e4f5677dcbf62ee59162e666cb00eb87b96d4c2557310960e3677eec7b6d907a5a4860cb7d2d74dba07b0
2022-07-20 14:14:05 +02:00
Daniela Brozzoni
8d3b2a9581 Merge bitcoindevkit/bdk#659: Fix: Run README.md examples on the CI
9d2024434e Fix: Run README.md example on the CI (meryacine)

Pull request description:

  ### Description
  Seems like `doc(include = "../README.md")` doesn't include the readme file as doc for the dummy struct. This might be due to a difference in Rust edition used back then or something.

  Fixes #637
  ### 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
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    tACK 9d2024434e

Tree-SHA512: 5842f7cdc34d76045596a248ec80bbcf86591ec9abe32d92af8322672e7a5d08d3b4baf1a000b1556542b449271dc8c438e6269eaf0204bee815c67fcf1218a8
2022-07-20 11:53:33 +02:00
Alekos Filini
45a4ae5828 Merge bitcoindevkit/bdk#671: Introduce get_checksum_bytes method and improvements
6db5b4a094 Introduce `get_checksum_bytes` method and improvements (志宇)

Pull request description:

  ### Description

  `get_checksum_bytes()` returns a descriptor checksum as `[u8; 8]` instead of `String`, potentially improving performance and memory usage.

  In addition to this, since descriptors only use characters that fit within a UTF-8 8-bit code unit ([US-ASCII](https://www.charset.org/charsets/us-ascii)), there is no need to use the `char` type (which is 4 bytes). This can also potentially bring in some performance and memory-usage benefits.

  ### Notes to the reviewers

  This is useful because we will be using descriptor checksums for indexing operations in the near future (multi-descriptor wallets #486 ).

  Refer to comments by @afilini :
  * https://github.com/bitcoindevkit/bdk/pull/647#discussion_r921184366
  * https://github.com/bitcoindevkit/bdk/pull/647#discussion_r921914696
  * https://github.com/bitcoindevkit/bdk/pull/654#discussion_r921980876

  ### 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:
  afilini:
    ACK 6db5b4a094

Tree-SHA512: 1cecc3a1514a3ec3ac0a50775f6b3c4dd9785e3606390ceba57cc6248b8ff19c4023add0643c48dd9d84984341c506c036c4880fca4a4358ce1b54ccb4c56687
2022-07-20 09:13:21 +02:00
志宇
6db5b4a094 Introduce get_checksum_bytes method and improvements
`get_checksum_bytes` returns a descriptor checksum as `[u8; 8]` instead
of `String`, potentially improving performance and memory usage.

In addition to this, since descriptors only use charaters that fit
within a UTF-8 8-bit code unit, there is no need to use the `char` type
(which is 4 bytes). This can also potentially bring in some performance
and memory-usage benefits.
2022-07-19 22:02:49 +08:00
meryacine
9d2024434e Fix: Run README.md example on the CI
Seems like `doc(include = "../README.md")` doesn't include the readme file as docs for the dummy struct. This might be due to a difference in Rust edition used back then or something
2022-07-17 18:33:56 +02:00
Alekos Filini
9165faef95 Merge bitcoindevkit/bdk#657: Release 0.20.0
46c344feb0 Bump version to 0.20.1-dev (Steve Myers)
78d26f6eb3 Bump version to 0.20.0 (Steve Myers)
92b9597f8b Rename `set_current_height` to `current_height` (Alekos Filini)
b5a120c649 Missing newlines (Alekos Filini)
af6bde3997 Fix: Wallet sync may decrement address index (志宇)
45db468c9b Deprecate `AddressValidator` (志宇)
01141bed5a Update CHANGELOG and lib.rs docs version (Steve Myers)
87e8646743 Bump version to 0.20.0-rc.1 (Steve Myers)

Pull request description:

  Proposed tweet:

  📢 Release 0.20.0 is out! Highlights include bug fixes for the ElectrumBlockchain and descriptor templates, discourage fee sniping in tx building, and new tx signing options. A big thanks to our past and latest new contributors. For all changes see: https://github.com/bitcoindevkit/bdk/releases/tag/v0.20.0

ACKs for top commit:
  afilini:
    ACK 46c344feb0

Tree-SHA512: 7c36a85611f715d76a37d5a285bc72f1a06297fc06b85cca7e38c3350fcbc0a3e35d38ce617a82d191538776aa49362e523beef70bbe3b93b21d8d28d774b75f
2022-07-14 12:05:13 +02:00
Steve Myers
46c344feb0 Bump version to 0.20.1-dev 2022-07-13 11:41:57 -07:00
Steve Myers
78d26f6eb3 Bump version to 0.20.0 2022-07-13 10:55:57 -07:00
Alekos Filini
844856d39e Merge bitcoindevkit/bdk#667: Rename set_current_height to current_height
92b9597f8b Rename `set_current_height` to `current_height` (Alekos Filini)

Pull request description:

  ### Description

  Usually we don't have any prefix except for methods that can *add* to a list or replace the list entirely (e.g. `add_recipients` vs `set_recipients`)

  I missed this during review of #611

  ### 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:
  danielabrozzoni:
    utACK 92b9597f8b - I'm sorry I didn't notice it!

Tree-SHA512: 3391068b2761bcd04d740ef41f9e772039fca7bc0e0736afcbc582ec74b6c91eb155d9e09dd7a07462eec29e32ac86e41ba339d9a550af3f754164cab6bdbf61
2022-07-13 14:43:26 +02:00
Alekos Filini
b5a120c649 Missing newlines 2022-07-13 11:13:05 +02:00
Alekos Filini
92b9597f8b Rename set_current_height to current_height
Usually we don't have any prefix except for methods that can *add* to a
list or replace the list entirely (e.g. `add_recipients` vs
`set_recipients`)
2022-07-13 10:27:38 +02:00
Alekos Filini
556105780b Merge bitcoindevkit/bdk#653: Fix: Wallet sync may decrement address index
af6bde3997 Fix: Wallet sync may decrement address index (志宇)

Pull request description:

  ### Description

  Fixes #649

  It is critical to ensure `Wallet::get_address` with `AddressIndex::new` always returns a new and unused address.

  This bug seems to be Electrum-specific. The fix is to check address index updates to  ensure that newly suggested indexes are not smaller than indexes already in database.

  ### Notes to the reviewers

  I have written new tests in `/testutils/blockchain_tests.rs` that tests all `Blockchain` implementations.

  ### 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:
  afilini:
    ACK af6bde3997

Tree-SHA512: d714bebcf7c2836f8b98129b39b4939b0e36726acf0208e52d501f433be6cdb12f1abebc28bd7da0be8b780ccce6e1e42c8fdc6633dd486bf329bc6f88e1ce67
2022-07-11 16:58:35 +02:00
志宇
af6bde3997 Fix: Wallet sync may decrement address index
This bug seems to be Electrum-specific. The fix is to check the
proposed changes against the current state of the database. Ensure
newly suggested indexes are not smaller than indexes already in
database.

Changes:
* Check index updates before they are applied to database during
  Electrum Blockchain sync (Thank you @rajarshimaitra for providing
  an elegant solution).

Tests added:
* bdk_blockchain_tests!::test_sync_address_index_should_not_decrement
* bdk_blockchain_tests!::test_sync_address_index_should_increment

These tests ensure there will be no unexpected address reuse when
grabbing a new address via `Wallet::get_address` with `AddressIndex::New`.

Other changes:
* Tweak `rpc.rs` so that clippy is happy.
2022-07-11 17:52:36 +08:00
Alekos Filini
4bd1fd2441 Merge bitcoindevkit/bdk#664: Deprecate AddressValidator
45db468c9b Deprecate `AddressValidator` (志宇)

Pull request description:

  ### Description

  `AddressValidator` should be deprecated as noted by @afilini [on Discord](https://discord.com/channels/753336465005608961/753367451319926827/994899488957272064):

  > address validators are supposed to be used for a slightly different thing, which is when you ask the hardware wallet to independently generate the address for a derivation index and then you compare what you see on your computer/phone with what the hardware wallet is displaying
  > in the case of change addresses i agree that it's not as important (because as you said the device can just refuse to sign) but for consistency we implemented it for both external and internal addresses
  > more broadly, they can be thought of as a way to get a callback every time an address is generated, which may also be useful for other things (for example when i was working on a green-compatible client written in bdk i used that feature to ping the server every time a new address was generated, because that's required in their protocol)
  > that said, i think currently pretty much nobody uses them and i am myself moving away from the concept that "everything needs to happen inside bdk": currently my mindset is targeted more towards reducing complexity by breaking down individual parts and wrapping them or making them "extensible" in some way
  > that is to say: if you want to verify addresses in your hardware wallet you don't necessarily need bdk to do it for you (actually, you would still have to implement the callback manually), you can just call bdk to get a new addr and then ping the device yourself. and this would allow us to reduce complexity and delete some code
  > actually, here's an idea: unless somebody here is opposed to this, i can make a pr to deprecate address validators in the next (0.20) release. if after that again nobody complains we can completely remove them and point users towards different strategies to achieve the same goal

  ### Checklists

  * [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`

ACKs for top commit:
  afilini:
    ACK 45db468c9b

Tree-SHA512: 71071f4494537ece9153f5308cb4f576189016afa8ac87bc57bfdcda03ee94d5f7a3477d04f6dd37eeeea2fada6aaad42ad29c964df0971beeda7418ada65f6d
2022-07-11 11:49:17 +02:00
志宇
45db468c9b Deprecate AddressValidator 2022-07-11 17:31:59 +08:00
志宇
2c02a44586 Test: No address reuse for single descriptor
This test is to ensure there are no regressions when we later change
internal logic of `Wallet`. A single descriptor wallet should always get
a new address with `AddressIndex::New` even if we alternate grabbing
internal/external keychains.
2022-07-09 20:29:58 +08:00
Steve Myers
01141bed5a Update CHANGELOG and lib.rs docs version 2022-07-06 13:23:50 -07:00
Steve Myers
87e8646743 Bump version to 0.20.0-rc.1 2022-07-06 12:52:00 -07:00
Steve Myers
dd51380520 Merge bitcoindevkit/bdk#621: Add remove_partial_sigs and try_finalize to SignOptions
e3a17f67d9 add try_finalize to SignOptions (KaFai Choi)
c2e4ba8cbd add remove_partial_sigs to SignOptions (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 2 keys(`try_finalize` and `remove_partial_sigs`) in `SignOptions`. See this issue for detail https://github.com/bitcoindevkit/bdk/issues/612

  ### Notes to the reviewers

  ~I found the negative naming of these 2 new keys `do_not_finalize` and `do_not_remove_partial_sigs` are a bit confusing(like most negative named paremeter/variable). Should we actually change it back to positive naming(`do_finalize` and `do_remove_partial_sigs`)?~

  ### 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:
  notmandatory:
    ReACK e3a17f67d9

Tree-SHA512: 781b31d3ecf0bcd605206c0481fd5de3125f1c8ff18a463dbf4c821e5557847f7d70a3fe8618e100fb89f4f6899655ac0efa3593f77f915ad5bcb7e558bb2a7a
2022-07-06 10:46:30 -07:00
Steve Myers
73d4f6d3b1 Merge bitcoindevkit/bdk#634: Get block hash by its height
2af678aa84 Get block hash by its height (Vladimir Fomene)

Pull request description:

  ### Description
  This PR create a new trait `blockchain::GetBlockHash` with a `get_block_hash` method which returns a block hash given the block height. This has been implemented for all blockchain backends.
  Fixes #603

  ### Notes to the reviewers

  I haven't updated the `CHANGELOG.md` and docs. Am I suppose to update it for this change?

  ### 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
  * [ ] I've added docs for the new feature
  * [ ] I've updated `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK 2af678aa84

Tree-SHA512: 9c084a6665ecbf27ee8170fdb06e0dc8373d6a901ce29e5f5a1bec111d1507cb3bee6b03a653a55fd20e0fabe7a5eada3353e24a1e21f3a11f01bb9881ae99e5
2022-07-06 10:17:50 -07:00
Vladimir Fomene
2af678aa84 Get block hash by its height
Create blockchain::GetBlockHash trait
with a method to get block hash given
a block height. Then, implement this
trait for all backends (Electrum, RPC
, Esplora, CBF). Referenced in issue 603.
2022-07-06 18:03:20 +01:00
Alekos Filini
1c94108d7e Merge bitcoindevkit/bdk#648: test: BDK won't add unconf inputs when fee bumping
5d00f82388 test that BDK won't add unconf inputs when fee bumping (Daniela Brozzoni)
98748906f6 test: fix populate_test_db conf calculation (Daniela Brozzoni)
1d9fdd01fa Remove wrong TODO comment in build_fee_bump (Daniela Brozzoni)

Pull request description:

  ### Description

  Closes #144

  ### Notes to reviewers

  #144 is describing a bug that doesn't seem to happen in BDK master anymore (BDK not respecting BIP125 rule 2). This PR just adds a test to check that the bug is fixed.

  ### 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:
  afilini:
    ACK 5d00f82388

Tree-SHA512: 95833f3566f9716762884d65f3f656346482e45525a3e92efa86710b9f574fdd9af7d235f1f425e4298d6ff380db9af60d1d2008ccde2588d971757db2d136b8
2022-07-06 16:43:47 +02:00
Daniela Brozzoni
5d00f82388 test that BDK won't add unconf inputs when fee bumping
Fixes #144

Also removes a leftover dbg!() in a test
2022-07-06 12:48:19 +02:00
Daniela Brozzoni
98748906f6 test: fix populate_test_db conf calculation
populate_test_db would previously give back a transaction with N + 1
confirmations when you asked for N.

This commit also fixes test_spend_coinbase, which would improperly
ask for a transaction with 0 confirmations instead of 1.
2022-07-06 12:48:18 +02:00
Alekos Filini
dd832cb57a Merge bitcoindevkit/bdk#585: set coin type based on network
db9d43ed2f use network to set coin type (Esraa Jbara)

Pull request description:

  resolves #578

  * [x] 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:
    re-ACK db9d43ed2f
  afilini:
    re-ACK db9d43ed2f

Tree-SHA512: 0310a09ef21c6fc792688a9ccc19221b1cffaeceefd34f4c83f206e965abe963a78f9e4ca53db046b39e7bf1be118a101afe5c08c43f06ecf35ed9536102cd9b
2022-07-06 12:24:16 +02:00
KaFai Choi
e3a17f67d9 add try_finalize to SignOptions 2022-07-06 17:13:19 +07:00
KaFai Choi
c2e4ba8cbd add remove_partial_sigs to SignOptions 2022-07-06 17:10:36 +07:00
Daniela Brozzoni
1d9fdd01fa Remove wrong TODO comment in build_fee_bump
The proposed solution is bad for privacy as well.
Let's call the initial change output, which is normally shrink when you
fee bump, change#1, and the extra output aforementioned change#2 (as,
in this case, it's going to be a change output as well). If you add change#2
you might not revel change#1, but you're still revealing change#2.
You're not improving your privacy, and you're wasting money in fees.
2022-07-06 11:02:51 +02:00
Esraa Jbara
db9d43ed2f use network to set coin type
Signed-off-by: Esraa Jbara <jbesraa@gmail.com>
2022-07-06 09:08:24 +03:00
Alekos Filini
ec22fa2ad0 Merge bitcoindevkit/bdk#614: Avoid using immature coinbase inputs
e85aa247cb Avoid using immature coinbase inputs (Daniela Brozzoni)
0e0d5a0e95 populate_test_db accepts a `coinbase` param (Daniela Brozzoni)

Pull request description:

  ### Description

  With this PR we start considering how many confirmations a coinbase has. If it's not mature yet, we don't use it for building transactions.
  Fixes #413

  ### Notes to the reviewers

  This PR is based on #611, review that one before reviewing this 😄

  007c5a78335a3e9f6c9c28a077793c2ba34bbb4e adds a coinbase parameter to `populate_test_db`, to specify if you want the db to be populated with immature coins. This is useful for `test_spend_coinbase`, but that's probably going to be the only use case.
  I don't think it's a big deal to have a test function take an almost_always_useless parameter - it's not an exposed API, anyways. But, if you can come up with a different way of implementing `test_spend_coinbase` that doesn't require 007c5a78335a3e9f6c9c28a077793c2ba34bbb4e, even better! I looked for it for a while, but other than duplicating the whole `populate_test_db` code, which made the test way harder to comprehend, I didn't find any other 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

  #### 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:
  afilini:
    ACK e85aa24

Tree-SHA512: 30f470c33f9ffe928500a58f821f8ce445c653766459465eb005031ac523c6f143856fc9ca68a8e1f23a485c6543a9565bd889f9557c92bf5322e81291212a5f
2022-07-05 22:26:03 +02:00
Alekos Filini
0e92820af4 Merge bitcoindevkit/bdk#652: Fix: Hang when ElectrumBlockchainConfig::stop_gap == 0
612da165f8 `Blockchain` stop_gap testing improvements (志宇)
8a5f89e129 Fix hang when `ElectrumBlockchainConfig::stop_gap == 0` (志宇)

Pull request description:

  * Ensure `chunk_size` is > 0 during wallet sync.

  * Slight refactoring for better readability.

  * Add test: `test_electrum_blockchain_factory_sync_with_stop_gaps`

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

  ### Description

  `Wallet::sync` hangs indefinitely when syncing with Electrum with `stop_gap` set as 0.

  The culprit is having `chunk_size` set as `stop_gap`. A zero value results in syncing not being able to progress.

  Fixes #651

  ### 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:
  afilini:
    ACK 612da165f8

Tree-SHA512: 56f1bff788855facc21856209922594cff9f639c5c58ecd180a0493322a75a564b72ded330ab0b6d6c90007ce859d2b8a5d2870d619bae5ddf9a3d64837f3753
2022-07-05 12:30:30 +02:00
Daniela Brozzoni
e85aa247cb Avoid using immature coinbase inputs
Fixes #413
2022-07-05 12:11:48 +02:00
志宇
612da165f8 Blockchain stop_gap testing improvements
This is a continuation of the #651 fix. We should also check whether the
same bug affects esplora as noted by @afilini. To achieve this, I've
introduced a `ConfigurableBlockchainTester` trait that can test multiple
blockchain implementations.

* Introduce `ConfigurableBlockchainTester` trait.
* Use the aforementioned trait to also test esplora.
* Change the electrum test to also use the new trait.
* Fix some complaints by clippy in ureq.rs file (why is CI not seeing
  this?).
* Refactor some code.
2022-07-05 07:53:19 +08:00
Steve Myers
1fd62a7afc Merge bitcoindevkit/bdk#575: Remove database flush
5ff8320e3b add private function ivcec_to_u32 in keyvalue (KaFai Choi)
e68d3b9e63 remove Database::flush (KaFai Choi)

Pull request description:

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

  ### Description

  This PR is to remove Database::flush. See this issue for detail https://github.com/bitcoindevkit/bdk/issues/567

  ### Notes to the reviewers
  The 2nd commit is a small refactoring of adding a new private ivec_to_u32 to avoid too much code duplication. Please let me know if it's ok to include this in this PR or I should make it into a separate PR

  Currently existing test cases are shared across for all Databaes implementation so I am not sure if we should add  specific test cases for keyvalue(Tree) for this auto-flush behaviour?(and I feel like it's more a implementation detail). Please let me know how should I proceed for test case in this PR

  ### 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:
  danielabrozzoni:
    re-ACK 5ff8320e3b

Tree-SHA512: eb37de8217efeb89d3ae346da36d0fb55aa67554d591b4759500f793bcf6aa7601c3d717fd473136c88e76aa72dbb6008ecf62b1d4ccf5ba3cbd1598f758522a
2022-07-04 13:40:56 -07:00
志宇
8a5f89e129 Fix hang when ElectrumBlockchainConfig::stop_gap == 0
* Ensure chunk_size is > 0 during wallet sync.
* Slight refactoring for better readability.
* Add test: test_electrum_blockchain_factory_sync_with_stop_gaps
2022-07-04 21:53:26 +08:00
Alekos Filini
063d51fd75 Merge bitcoindevkit/bdk#625: Restrict drain_to usage
6a15036867 Restrict `drain_to` usage (Daniela Brozzoni)

Pull request description:

  ### Description
  Before this commit, you could create a transaction with `drain_to` set
  without specifying recipients, nor `drain_wallet`, nor `utxos`. What would
  happen is that BDK would pick one input from the wallet and send
  that one to `drain_to`, which is quite weird.
  This PR restricts the usage of `drain_to`: if you want to use it as a
  change output, you need to set recipients as well. If you want to send
  a specific utxo to the `drain_to` address, you specify it through
  `add_utxos`. If you want to drain the whole wallet, you set
  `drain_wallet`. In any other case, if `drain_to` is set, we return a
  `NoRecipients` error.

  Fixes #620

  ### 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] This pull request breaks the existing API - kinda?
  * [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:
  afilini:
    ACK 6a15036867

Tree-SHA512: 69076977df37fcaac92dd99d2f2c9c37098971817d5b0629fc7e3069390eb5789331199b3b7c5d0569d70473f4f37e683a5a0b30e2c6b4e2ec22a5ef1d0f2d77
2022-06-30 12:28:45 +02:00
Daniela Brozzoni
0e0d5a0e95 populate_test_db accepts a coinbase param
Allows user to ask for a test db populated with clean coins
from coinbases. This is useful for testing the wallet behaviour
when some inputs are coinbases.
2022-06-30 11:50:15 +02:00
Alekos Filini
bb55923a7d Merge bitcoindevkit/bdk#611: Discourage fee sniping with nLockTime
97bc9dc717 Discourage fee sniping with nLockTime (Daniela Brozzoni)

Pull request description:

  ### Description
  By default bdk sets the transaction's nLockTime to current_height
  to prevent fee sniping.
  current_height can be provided by the user through TxParams; if the user
  didn't provide it, we use the last sync height, or 0 if we never synced.

  Fixes https://github.com/bitcoindevkit/bdk/issues/533

  ### Notes to the reviewers:

  If you want to know more about fee sniping: https://bitcoinops.org/en/topics/fee-sniping/

  ### 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:
  afilini:
    ACK 97bc9dc717

Tree-SHA512: e92d1ae907687d9fee44d120d790f1ebdf14b698194979e1be8433310fd5636afa63808effed12fce6091f968ec6b76b727cfee6fed54068af0a7450239fdd26
2022-06-29 15:58:07 +02:00
Alekos Filini
f184557fa0 Merge bitcoindevkit/bdk#633: Additional comments for TransactionDetails.
77c7d0aae9 Additional comments for `TransactionDetails`. (志宇)

Pull request description:

  ### Description

  I'm not sure if this is needed or helpful, but this PR adds comments to describe how the `sent` and `received` fields of `TransactionDetails` are calculated.

  I wasn't sure how it was done until I looked deeper into the codebase (but maybe I am too much of a beginner and this is common sense for most).

  ### 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:
  danielabrozzoni:
    re-ACK 77c7d0aae9

Tree-SHA512: 8d29d249a70bc2d0631078b6772c5543bdc61ee43df3810ab666f5e97ca59b0d4cfc8acad14bbaf8674baba319f24fa2781a42740ca42bccd9688831aaedea72
2022-06-29 15:52:57 +02:00
志宇
77c7d0aae9 Additional comments for TransactionDetails.
Describe how fields `received` and `sent` are calculated for
`TransactionDetails`.
2022-06-29 19:38:12 +08:00
KaFai Choi
5ff8320e3b add private function ivcec_to_u32 in keyvalue 2022-06-29 12:39:51 +07:00
KaFai Choi
e68d3b9e63 remove Database::flush 2022-06-29 12:39:49 +07:00
Daniela Brozzoni
97bc9dc717 Discourage fee sniping with nLockTime
By default bdk sets the transaction's nLockTime to current_height
to discourage fee sniping.
current_height can be provided by the user through TxParams; if the user
didn't provide it, we use the last sync height, or 0 if we never synced.

Fixes #533
2022-06-28 10:35:03 +02:00
Daniela Brozzoni
6a15036867 Restrict drain_to usage
Before this commit, you could create a transaction with `drain_to` set
without specifying recipients, nor `drain_wallet`, nor `utxos`. What would
happen is that BDK would pick one input from the wallet and send
that one to `drain_to`, which is quite weird.
This PR restricts the usage of `drain_to`: if you want to use it as a
change output, you need to set recipients as well. If you want to send
a specific utxo to the `drain_to` address, you specify it through
`add_utxos`. If you want to drain the whole wallet, you set
`drain_wallet`. In any other case, if `drain_to` is set, we return a
`NoRecipients` error.

Fixes #620
2022-06-28 10:32:48 +02:00
Alekos Filini
17d0ae0f71 Merge bitcoindevkit/bdk#643: Fix README.md link to rust 1.56.1 blog post
d020dede37 Fix README.md link to rust 1.56.1 blog post (Steve Myers)

Pull request description:

  ### Description

  Fix link to rust blog for 1.56.1 as pointed out by ulrichard: https://github.com/weareseba/bdk-reserves/pull/5#issuecomment-1159212838

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits

Top commit has no ACKs.

Tree-SHA512: a5bd76fca97dd64c12617b43230dbc36b1178e47224ce324b67cd13999e5f92d2a05d6a9e909841e6d5c6904f2fa426b6bee1001e757d53cc91fb4fd3803f56b
2022-06-28 08:43:17 +02:00
Steve Myers
d020dede37 Fix README.md link to rust 1.56.1 blog post 2022-06-27 22:04:54 -07:00
Alekos Filini
5c566bb05e Merge bitcoindevkit/bdk#638: Fix CI, bump MSRV to 1.56.1
b289c4ec2d Bump MSRV from 1.56.0 to 1.56.1 (Daniela Brozzoni)

Pull request description:

  ### Description
  This PR fixes the CI, which is currently failing after a Github Actions update.
  The MSRV is bumped to 1.56.1 (from 1.56.0), since that's `reqwest`'s current MSRV.

  ### 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:
  afilini:
    ACK b289c4ec2d

Tree-SHA512: e3aa8638ffd374371037e71d1c9790dd4fb3aa29f0ade5655b47ef5184b26120ee5281cd1745612af2572beb8a6bb43f84da3d60a73b455f9652853fcf31b62d
2022-06-27 23:25:33 +02:00
Daniela Brozzoni
b289c4ec2d Bump MSRV from 1.56.0 to 1.56.1
In this way we can continue using reqwest v0.11, whose MSRV is now
1.56.1
The only difference between v1.56.0 and v1.56.1 is a bug fix for
CVE-2021-42574.
2022-06-27 12:29:10 +02:00
Alekos Filini
2283444f72 Merge bitcoindevkit/bdk#622: fix typo
a85ef62698 fix typo (Buck Perley)

Pull request description:

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

  ### Description

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->
  just a small typo fix

  ### 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

ACKs for top commit:
  rajarshimaitra:
    ACK a85ef62698
  afilini:
    ACK a85ef62698

Tree-SHA512: 089de23adae62492a0b39a27c9cb8cb8afc99e5634194118681b8a9a46ff0b073558f9cd515cd4db4c9c6e6f9c813bfa4b193d4e3f9558b34ad29cbd46cf028c
2022-06-13 11:44:46 +02:00
Steve Myers
a0e5820c32 Merge commit 'refs/pull/629/head' of github.com:bitcoindevkit/bdk 2022-06-10 10:46:27 -07:00
Steve Myers
04dc28d2b4 Bump version to 0.19.1-dev 2022-06-10 10:13:43 -07:00
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
Alekos Filini
688ff96c8e Merge bitcoindevkit/bdk#623: Bump versions
3283a200bc Bump rusqlite (Philipp Hoenisch)
3f9b4cdca9 Bump ahash (Philipp Hoenisch)

Pull request description:

  ### Description

  Not much to say besides that I bumped some version which helps us to resolve some dependency hell :)

  ### Notes to the reviewers

  `ahash` was previously fixed because of an incompatibility with the defined MSRV. The MSRV has been bumped to 1.56 so we can update the dependency.

  ### 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:
  rajarshimaitra:
    ACK 3283a200bc

Tree-SHA512: 4760890529bb4dd87fafdda04188fd06e707398abfa7d36d26077233525e76cc8c7d8888ad996c1cc4ac31ab708ea9a56a602b1d1578b97f9e44b610df3d969b
2022-06-08 12:59:27 +02: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
Philipp Hoenisch
3283a200bc Bump rusqlite
Signed-off-by: Philipp Hoenisch <philipp@hoenisch.at>
2022-06-07 08:48:58 +10:00
Philipp Hoenisch
3f9b4cdca9 Bump ahash
We can upgrade this now as MSRV was bumped to 1.56

Signed-off-by: Philipp Hoenisch <philipp@hoenisch.at>
2022-06-07 08:48:49 +10:00
Buck Perley
a85ef62698 fix typo 2022-06-05 14:19:37 -05: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
Lloyd Fournier
022256c91a Fix comment on peek_address
Co-authored-by: Raj <36541669+rajarshimaitra@users.noreply.github.com>
2022-02-23 09:28:13 +11:00
LLFourn
00f0901bac Add API for internal addresses
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.
2022-02-23 09:28:08 +11:00
Steve Myers
19f028714b Merge bitcoindevkit/bdk#502: Refactor verification logic
1999d97aeb Remove `verify` flag from `TransactionDetails` via new MIGRATIONS (Steve Myers)
0195bc0636 Update CHANGELOG (rajarshimaitra)
1d7ea89d8a Refactor sync time verification (rajarshimaitra)
b05ee78c73 Remove verifcation flag from compact_filters (rajarshimaitra)
53c30b0479 Add verification tests in CI (rajarshimaitra)
6a09075d1a Remove verify flag from sqlite DB (rajarshimaitra)
61a95d0d15 Update changelog (rajarshimaitra)
08f312a82f Remove `verify` flag from `TransactionDetails` (rajarshimaitra)
acbf0ae08e Add sync verification for `esplora` (rajarshimaitra)
4761155707 Add sync verification in `electrum` (rajarshimaitra)
98a3b3282a Remove sync verification (rajarshimaitra)

Pull request description:

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

  ### Description

  As discussed in https://github.com/bitcoindevkit/bdk/issues/498 and also in team call,
   - default verification from wallet sync is removed
   - `verify_tx` refactored as an wallet API
   - in `sync` verification added for electrum and esplora backends, gated by `verify` flag.
   - `verify` flag is removed from `TransactionDetails`.

  ### Notes to the reviewers

  I haven't looked into `comapct_filters` to see how verification can fit there, but that will probably be required in future.

  ### 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

  #### Wallet API change:

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

Top commit has no ACKs.

Tree-SHA512: 72e307008a137468d96d5c2a6ec804b18fa52363606f3c978208ae5dc22973a7f0aa37488e9bb98dde88409a12d59cc5f00c675d2d408e57e661bf6210bee67b
2022-02-21 19:23:49 -08:00
Steve Myers
ad65dd5c23 Merge branch 'master' into verifcation-refactor 2022-02-21 17:47:09 -08:00
Steve Myers
1999d97aeb Remove verify flag from TransactionDetails via new MIGRATIONS 2022-02-21 17:31:48 -08:00
rajarshimaitra
0195bc0636 Update CHANGELOG 2022-02-21 17:31:46 -08:00
Alekos Filini
760a6ca1a1 Merge bitcoindevkit/bdk#551: Fix sent calculation in the RPC backend
bfd0d13779 [blockchain] Fix `sent` calculation in the RPC backend (Daniela Brozzoni)
128c37595c [tests] Pass tx inputs to the testutils macro (Daniela Brozzoni)

Pull request description:

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

  ### Description

  <!-- 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

  #### Bugfixes:

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

ACKs for top commit:
  afilini:
    ACK bfd0d13779

Tree-SHA512: 1c214819c5dc1f1c30b1c6ef18a44a3013d587651890b26aecfc5203d128173fd91497337186bbee6934f77d3cfe1686e67b83ca6fe6e47b4c1d4b1dbcc656ee
2022-02-20 11:20:09 +01: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
Daniela Brozzoni
bfd0d13779 [blockchain] Fix sent calculation in the RPC backend
We used to consider a tx input as ours if we had the
tx that creates it in the database.
This commit actually checks if an input is ours before adding
its value to the `sent` field.
2022-02-18 12:54:51 +01:00
Daniela Brozzoni
128c37595c [tests] Pass tx inputs to the testutils macro 2022-02-18 12:54:51 +01:00
Steve Myers
5c5bb7833c Merge bitcoindevkit/bdk#550: Pin tokio version to ~1.14
b04bb590f3 Pin tokio version to ~1.14 (Steve Myers)

Pull request description:

  ### Description

  The `tokio` project recently changed their MSRV to `1.49.0`. This PR will pin the `tokio` dependency version to `~1.14` which is prior to their MSRV increase.

  ### Notes to the reviewers

  The LDK team took this approach for their `tokio` dev-dependency, see https://github.com/lightningdevkit/rust-lightning/pull/1315.

  As long as `tokio` backports bug fixes  to`1.14.x` releases this should be safe. If we need any new features we can revisit this decision.

  ### 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 updated `CHANGELOG.md`

ACKs for top commit:
  LLFourn:
    ACK b04bb590f3

Tree-SHA512: a44d61e0d644900837b5a99e0f2b9e5706cf9932f7430c3a2c8adbff37c82e4a6a545849fe031da748fcd8c8a70f5266d6974c5b6634b50af5eae148b9a26635
2022-02-17 18:09:58 -08:00
Steve Myers
b04bb590f3 Pin tokio version to ~1.14 2022-02-17 11:47:28 -08:00
Alekos Filini
0efbece41a Merge bitcoindevkit/bdk#542: Implement XKeyUtils on InnerXKey
b6fe01c466 Implement XKeyUtils on InnerXKey (Gianluca Acerbis)

Pull request description:

  Closes #395

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

  ### Description

  <!-- 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
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    ACK b6fe01c466 - the code looks good to me, I tested locally and all tests pass
  notmandatory:
    ACK b6fe01c466

Tree-SHA512: 00a3ed9532a0589ac4be55a7d0c6ac5251f03f716adb2086eb934d9a9b04bcb7fd95aaeba38b68c5c90876644ce53ac5e39a912a4096e789887342f8727ae434
2022-02-14 20:16:10 +01:00
Gianluca Acerbis
b6fe01c466 Implement XKeyUtils on InnerXKey
Closes #395
2022-02-12 17:14:07 +01:00
rajarshimaitra
1d7ea89d8a Refactor sync time verification
Instead of verifying txs at sync time in every backend, its moved to
script_sync to by default be available to any backend.
2022-02-09 16:59:53 +05:30
rajarshimaitra
b05ee78c73 Remove verifcation flag from compact_filters 2022-02-09 12:30:32 +05:30
rajarshimaitra
53c30b0479 Add verification tests in CI 2022-02-09 12:30:32 +05:30
rajarshimaitra
6a09075d1a Remove verify flag from sqlite DB 2022-02-09 12:30:32 +05:30
rajarshimaitra
61a95d0d15 Update changelog 2022-02-09 12:30:25 +05:30
rajarshimaitra
08f312a82f Remove verify flag from TransactionDetails 2022-02-09 12:29:47 +05:30
rajarshimaitra
acbf0ae08e Add sync verification for esplora 2022-02-09 12:29:47 +05:30
rajarshimaitra
4761155707 Add sync verification in electrum 2022-02-09 12:29:47 +05:30
rajarshimaitra
98a3b3282a Remove sync verification
The default sync verification is removed from wallet module.
By default sync time verification only makes sense for `electrum` and
`esplora` backend as they are usually untrusted 3rd party services.

script verification for transaction is costly, so removing default
script verification optimizes performance.
2022-02-09 12:29:46 +05:30
Alekos Filini
e745122bf5 Merge bitcoindevkit/bdk#539: [ci] Pin nightly docs workflow rust version to nightly-2022-01-25
07c270db03 [ci] Pin nightly docs workflow rust version to nightly-2022-01-25 (Steve Myers)

Pull request description:

  ### Description

  Pin nightly docs workflow rust version to `nightly-2022-01-25` to fix #538 .

  ### Notes to the reviewers

  The nightly docs should be changed to the `stable` version once it supports `feature(doc_cfg)`.

  ### 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)

  #### Bugfixes:

  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  afilini:
    ACK 07c270db03

Tree-SHA512: a8e5de051e1963c10d686d5cb5d1785e20f6cd322b9bfcf2d2f5c381d9124a9b8518671dfbedb11550bdfa66c8c64ad505c7e1604e2bfe23600de39cb20467dd
2022-02-07 11:01:48 +01:00
Steve Myers
07c270db03 [ci] Pin nightly docs workflow rust version to nightly-2022-01-25 2022-02-04 08:55:00 -06:00
Alekos Filini
375674ffff Merge bitcoindevkit/bdk#532: Release/0.16.0
fcf422752b Fix 0.16.0 changelog, include WIF fix (Steve Myers)
6fb42fdea1 Bump version to 0.16.1-dev (Steve Myers)
3f65e8c64b Bump version to 0.16.0 (Steve Myers)
3f0101d317 Bump version to 0.16.0-rc.1 (Steve Myers)

Pull request description:

  ### Description

  Merge the 0.16.0 release branch back into the master branch.

  ### 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:
  afilini:
    ACK fcf422752b

Tree-SHA512: 7a1e9297f4e93284e5e57124c7afe37d1fbef1da48375b310aad7fe8556be6bba00c3263e9d15540209fc99ba5e928d3fc751d0df6592c2af871599f00941a7b
2022-01-27 10:44:57 +01:00
Steve Myers
fcf422752b Fix 0.16.0 changelog, include WIF fix 2022-01-21 14:27:36 -06:00
Steve Myers
6fb42fdea1 Bump version to 0.16.1-dev 2022-01-21 14:27:02 -06:00
Steve Myers
3f65e8c64b Bump version to 0.16.0 2022-01-21 12:39:27 -06:00
Steve Myers
3f0101d317 Bump version to 0.16.0-rc.1 2022-01-12 18:01:32 +01:00
Steve Myers
b1346d4ccf Merge bitcoindevkit/bdk#505: Using dust value from rust-bitcoin in `is_dust`
5ac51dfe74 fix and test is_dust (James Taylor)
a0c140bb29 add doc comment for IsDust trait (James Taylor)
bf5994b14a fixed fee in test, removed unnecessary comment (James Taylor)
ca682819b3 using dust value from rust-bitcoin (James Taylor)

Pull request description:

  ### Description

  This PR aims to fix #472 . We can retrieve the dust value for a given ``bitcoin::blockdata::script::Script``, so I adjusted the ``is_dust`` function within the ``IsDust`` trait to receive such a ``&Script``. Thus, the ``is_dust`` function can make the proper comparison.
  Let me know if you think that there could be a better interface than this.

  Furthermore, because this new ``is_dust`` function provides a tighter upper bound on Bitcoin Core's ``GetDustThreshold()``, it actually invalidated a test. In the test, the drain output for a transaction was no longer considered dust and no longer included in the fee. Instead, the drain output was kicked back to the sender, invalidating the asserts in line 3436, 3437 and 3441 in ``src/wallet/mod.rs``. I increased the ``FeeRate`` in the test just enough that the drain output would be small enough to considered dust again and included in the total fee.

  ### 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:
  afilini:
    ACK 5ac51dfe74
  notmandatory:
    re ACK 5ac51dfe74

Tree-SHA512: addf38fe065de581ddfcd3b4e6db92cd35d5bfa8cac78bd08c01f7a01292724a203ef59b09f3f5cd8e0fa0bb6d89efe72afda36efc11ded0424fc8105326af3f
2022-01-12 17:49:16 +01:00
Steve Myers
5107ff80c1 Merge bitcoindevkit/bdk#495: Disable reqwest's default features
380a4f2588 Disable reqwest's default features (Thomas Eizinger)

Pull request description:

  ### Description

  By default, reqwest uses openssl for TLS. Any consumer wanting to use
  rustls will thus pull in unnecessary dependencies.

  ### 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 380a4f2588

Tree-SHA512: 17827fdd7656a1e97b4cc302bc3c4907a8493505c798fafd9b15fde12531a32cf60e7d63e878eb2001d6b3e95f7ae3da730e227eb85c73d9de55b56456cfb3a0
2022-01-12 09:16:24 +01:00
James Taylor
5ac51dfe74 fix and test is_dust 2022-01-11 18:21:35 -05:00
Steve Myers
04d58f7903 Merge commit 'refs/pull/508/head' of github.com:bitcoindevkit/bdk 2022-01-11 10:08:33 +01:00
Thomas Eizinger
380a4f2588 Disable reqwest's default features
By default, reqwest uses openssl for TLS. Any consumer wanting to use
rustls will thus pull in unnecessary dependencies. To make getting started
with bdk and reqwest easier, we add a `reqwest-default-tls` feature
that can be used to activate reqwest's `default-tls` feature. TLS is
necessary for the esplora integration. Adding this feature makes it possible
for people to use bdk with esplora without adding a reqwest dependency to
their manifest.
2022-01-10 13:57:22 +11:00
Alekos Filini
fdb272e039 Merge bitcoindevkit/bdk#511: Fix nightly_docs.yml publish_docs 'Commit' step
947a9c29db Fix nightly_docs.yml publish_docs 'Commit' step (Steve Myers)

Pull request description:

  ### Description

  I forgot to fix in #503 the `nightly_docs.yaml` `publish_docs` `Commit` step to add new files for the path `./docs/.vuepress/public/docs-rs`.

  ### 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)

ACKs for top commit:
  afilini:
    ACK 947a9c2

Tree-SHA512: d2bdbcb6cea46ec1949eba6f334acd5dbbe9b4b1323bb4713dc5d7f749666260ab05c29247c35f08c587b46d6bfb765c6a612c6522fd15211c84f7590f8c4748
2021-12-29 10:59:32 +01:00
Steve Myers
db6ffb90f0 Merge commit 'refs/pull/510/head' of github.com:bitcoindevkit/bdk 2021-12-23 10:34:58 -08:00
Steve Myers
947a9c29db Fix nightly_docs.yml publish_docs 'Commit' step 2021-12-23 10:23:11 -08:00
Alekos Filini
61ee2a9c1c Merge commit 'refs/pull/504/head' of github.com:bitcoindevkit/bdk 2021-12-23 12:24:52 +01:00
Alekos Filini
44e4c5dac5 Merge commit 'refs/pull/509/head' of github.com:bitcoindevkit/bdk 2021-12-23 12:22:09 +01:00
Alekos Filini
e09aaf055a Add a custom logo to our docs
As suggested in #497, add our logo to the docs as well
2021-12-23 11:37:41 +01:00
Alekos Filini
c40898ba08 Merge commit 'refs/pull/503/head' of github.com:bitcoindevkit/bdk 2021-12-23 11:34:34 +01:00
Steve Myers
2f98db8549 Add back old logo to static/bdk.svg
This is required so that old releases of bdk on crates.io won't show a
broken image link. Should be replaced with SVG version of new logo.
2021-12-22 21:38:54 -08:00
James Taylor
a0c140bb29 add doc comment for IsDust trait 2021-12-22 01:50:17 -05:00
James Taylor
bf5994b14a fixed fee in test, removed unnecessary comment 2021-12-19 18:37:05 -05:00
James Taylor
ca682819b3 using dust value from rust-bitcoin 2021-12-19 02:55:24 -05:00
mcroad
ee41d88f25 Test WIF from BIP39 words has correct network 2021-12-18 15:34:18 -06:00
mcroad
beb1e4114d Add fix to changelog 2021-12-18 15:13:09 -06:00
mcroad
af047f90db Set the correct inner private_key network 2021-12-18 15:10:25 -06:00
mcroad
d01ec6d259 Add test to ensure WIF uses the correct network 2021-12-18 15:08:16 -06:00
Alekos Filini
77bce06caf Update logo 2021-12-18 15:17:45 +01:00
Steve Myers
1a907f8a53 [ci] Fix publish_docs job 2021-12-17 21:28:39 -08:00
58 changed files with 7934 additions and 2310 deletions

89
.github/ISSUE_TEMPLATE/minor_release.md vendored Normal file
View File

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

67
.github/ISSUE_TEMPLATE/patch_release.md vendored Normal file
View File

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

View File

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

View File

@@ -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.1 # 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,13 +90,20 @@ jobs:
matrix:
blockchain:
- name: electrum
features: test-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
features: test-esplora,use-esplora-reqwest
testprefix: esplora
features: test-esplora,use-esplora-reqwest,verify
- name: esplora
features: test-esplora,use-esplora-ureq
testprefix: esplora
features: test-esplora,use-esplora-ureq,verify
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -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
@@ -138,7 +146,7 @@ jobs:
- run: sudo apt-get update || exit 1
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
- name: Set default toolchain
run: rustup default 1.56.0 # STABLE
run: rustup default 1.56.1 # STABLE
- name: Set profile
run: rustup set profile minimal
- name: Add target wasm32
@@ -164,3 +172,32 @@ jobs:
run: rustup update
- name: Check fmt
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
test_harware_wallet:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- version: 1.60.0 # STABLE
- version: 1.56.1 # MSRV
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build simulator image
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
- name: Run simulator image
run: docker run --name simulator --network=host hwi/ledger_emulator &
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install python dependencies
run: pip install hwi==2.1.1 protobuf==3.20.1
- name: Set default toolchain
run: rustup default ${{ matrix.rust.version }}
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features test-hardware-signer

View File

@@ -18,7 +18,7 @@ jobs:
target
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain
run: rustup default nightly
run: rustup default nightly-2022-01-25
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
@@ -44,18 +44,18 @@ jobs:
repository: bitcoindevkit/bitcoindevkit.org
ref: master
- name: Create directories
run: mkdir -p ./static/docs-rs/bdk/nightly
run: mkdir -p ./docs/.vuepress/public/docs-rs/bdk/nightly
- name: Remove old latest
run: rm -rf ./static/docs-rs/bdk/nightly/latest
run: rm -rf ./docs/.vuepress/public/docs-rs/bdk/nightly/latest
- name: Download built docs
uses: actions/download-artifact@v1
with:
name: built-docs
path: ./static/docs-rs/bdk/nightly/latest
path: ./docs/.vuepress/public/docs-rs/bdk/nightly/latest
- name: Configure git
run: git config user.email "github-actions@github.com" && git config user.name "github-actions"
- name: Commit
continue-on-error: true # If there's nothing to commit this step fails, but it's fine
run: git add ./static && git commit -m "Publish autogenerated nightly docs"
run: git add ./docs/.vuepress/public/docs-rs && git commit -m "Publish autogenerated nightly docs"
- name: Push
run: git push origin master

View File

@@ -1,10 +1,93 @@
# Changelog
All notable changes to this project will be documented in this file.
All notable changes to this project prior to release **0.22.0** are documented in this file. Future
changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk/releases)
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [v0.21.0] - [v0.20.0]
- Add `descriptor::checksum::get_checksum_bytes` method.
- Add `Excess` enum to handle remaining amount after coin selection.
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
- Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef<Path>
- Add the ability to specify which leaves to sign in a taproot transaction through `TapLeavesOptions` in `SignOptions`
- Add the ability to specify whether a taproot transaction should be signed using the internal key or not, using `sign_with_tap_internal_key` in `SignOptions`
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
- New `RpcBlockchain` implementation with various fixes.
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
## [v0.20.0] - [v0.19.0]
- New MSRV set to `1.56.1`
- Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced)
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
- Set coin type in BIP44, BIP49, and BIP84 templates
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
- Deprecate `AddressValidator`
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
## [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, i.e. 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`.
- remove `flush` method from the `Database` trait.
## [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]
- Disable `reqwest` default features.
- Added `reqwest-default-tls` feature: Use this to restore the TLS defaults of reqwest if you don't want to add a dependency to it in your own manifest.
- Use dust_value from rust-bitcoin
- Fixed generating WIF in the correct network format.
## [v0.15.0] - [v0.14.0]
@@ -387,7 +470,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
@@ -404,3 +486,10 @@ 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.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
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.15.1-dev"
version = "0.22.0-rc.1"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -14,18 +14,18 @@ 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 }
rusqlite = { version = "0.25.3", optional = true }
ahash = { version = "=0.7.4", optional = true }
reqwest = { version = "0.11", optional = true, features = ["json"] }
electrum-client = { version = "0.10", optional = true }
rusqlite = { version = "0.27.0", optional = true }
ahash = { version = "0.7.6", optional = true }
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
ureq = { version = "~2.2.0", features = ["json"], optional = true }
futures = { version = "0.3", optional = true }
async-trait = { version = "0.1", optional = true }
@@ -33,12 +33,13 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
cc = { version = ">=1.0.64", optional = true }
socks = { version = "0.3", optional = true }
lazy_static = { version = "1.4", optional = true }
hwi = { version = "0.2.2", 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]
@@ -55,11 +56,13 @@ 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"]
keys-bip39 = ["bip39"]
rpc = ["bitcoincore-rpc"]
hardware-signer = ["hwi"]
# We currently provide mulitple implementations of `Blockchain`, all are
# blocking except for the `EsploraBlockchain` which can be either async or
@@ -82,19 +85,23 @@ use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
# Typical configurations will not need to use `esplora` feature directly.
esplora = []
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
reqwest-default-tls = ["reqwest/default-tls"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
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"]
test-hardware-signer = ["hardware-signer"]
[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"
@@ -107,9 +114,14 @@ name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]
[[example]]
name = "rpcwallet"
path = "examples/rpcwallet.rs"
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
features = ["compiler", "electrum", "esplora", "use-esplora-ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
# defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -1,46 +1,16 @@
# Development Cycle
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
This project follows a regular releasing schedule similar to the one [used by the Rust language]. In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API changes in the changelog info attached to each release tag.
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
```
master: - - - - * - - - * - - - - - - * - - - * ...
| / | |
release/0.x.0: * - - # | |
| /
release/0.y.0: * - - #
```
To create a new release a release manager will create a new issue using the `Release` template and follow the template instructions.
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
## Making the Release
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
4. During the one week of feature freeze run additional tests on the release branch.
5. If a bug is found:
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
6. Update the changelog with the new release version.
7. Update `src/lib.rs` with the new version (line ~43)
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
11. Publish **all** the updated crates to crates.io.
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
13. Merge the release branch back into `master`.
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (ie. `bdk-macros = { path = "./macros"}`)
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
17. Announce the release on Twitter, Discord and Telegram.
18. Celebrate :tada:
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
[Semantic Versioning]: https://semver.org/

View File

@@ -1,7 +1,7 @@
<div align="center">
<h1>BDK</h1>
<img src="./static/bdk.svg" width="220" />
<img src="./static/bdk.png" width="220" />
<p>
<strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
@@ -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/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
</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,26 +88,26 @@ 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;
use bitcoin::base64;
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) = {
@@ -132,10 +132,11 @@ fn main() -> Result<(), bdk::Error> {
```rust,no_run
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::base64;
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,
@@ -155,7 +156,7 @@ fn main() -> Result<(), bdk::Error> {
### Unit testing
```
```bash
cargo test
```
@@ -163,11 +164,11 @@ cargo test
Integration testing require testing features, for example:
```
```bash
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

9
ci/Dockerfile.ledger Normal file
View File

@@ -0,0 +1,9 @@
# Taken from bitcoindevkit/rust-hwi
FROM ghcr.io/ledgerhq/speculos
RUN apt-get update
RUN apt-get install wget -y
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf
ADD automation.json /speculos/automation.json
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--display", "headless", "--vnc-port", "41000", "btc.elf"]

30
ci/automation.json Normal file
View File

@@ -0,0 +1,30 @@
{
"version": 1,
"rules": [
{
"regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount",
"actions": [
[ "button", 2, true ],
[ "button", 2, false ]
]
},
{
"text": "Sign",
"conditions": [
[ "seen", false ]
],
"actions": [
[ "button", 2, true ],
[ "button", 2, false ],
[ "setbool", "seen", true ]
]
},
{
"regexp": "Approve|Sign|Accept",
"actions": [
[ "button", 3, true ],
[ "button", 3, false ]
]
}
]
}

View File

@@ -14,6 +14,7 @@ use std::sync::Arc;
use bdk::bitcoin;
use bdk::database::MemoryDatabase;
use bdk::descriptor::HdKeyPaths;
#[allow(deprecated)]
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
use bdk::KeychainKind;
use bdk::Wallet;
@@ -25,6 +26,7 @@ use bitcoin::{Network, Script};
#[derive(Debug)]
struct DummyValidator;
#[allow(deprecated)]
impl AddressValidator for DummyValidator {
fn validate(
&self,
@@ -48,9 +50,9 @@ 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())?;
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(DummyValidator));
wallet.get_address(New)?;

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,
sync_params: 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,69 @@ 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 GetBlockHash for AnyBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
maybe_await!(impl_inner_method!(self, get_block_hash, height))
}
}
#[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 +222,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;
@@ -207,7 +216,6 @@ impl CompactFiltersBlockchain {
received: incoming,
sent: outgoing,
confirmation_time: BlockTime::new(height, timestamp),
verified: height.is_some(),
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
};
@@ -226,11 +234,48 @@ 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 GetBlockHash for CompactFiltersBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
self.headers
.get_block_hash(height as usize)?
.ok_or(Error::CompactFilters(
CompactFiltersError::BlockHashNotFound,
))
}
}
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];
@@ -431,27 +476,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
@@ -522,6 +546,8 @@ pub enum CompactFiltersError {
InvalidFilter,
/// The peer is missing a block in the valid chain
MissingBlock,
/// Block hash at specified height not found
BlockHashNotFound,
/// The data stored in the block filters storage are corrupted
DataCorruption,

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

@@ -25,6 +25,7 @@
//! ```
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
@@ -68,16 +69,65 @@ 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 Deref for ElectrumBlockchain {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
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 GetBlockHash for ElectrumBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = self.client.block_header(height as usize)?;
Ok(block_header.block_hash())
}
}
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();
let mut txid_to_height = HashMap::<Txid, u32>::new();
let mut tx_cache = TxCache::new(database, &self.client);
let chunk_size = self.stop_gap;
// Set chunk_size to the smallest value capable of finding a gap greater than stop_gap.
let chunk_size = self.stop_gap + 1;
// The electrum server has been inconsistent somehow in its responses during sync. For
// example, we do a batch request of transactions and the response contains less
// tranascations than in the request. This should never happen but we don't want to panic.
@@ -113,21 +163,12 @@ impl Blockchain for ElectrumBlockchain {
Request::Conftime(conftime_req) => {
// collect up to chunk_size heights to fetch from electrum
let needs_block_height = {
let mut needs_block_height_iter = conftime_req
.request()
.filter_map(|txid| txid_to_height.get(txid).cloned())
.filter(|height| block_times.get(height).is_none());
let mut needs_block_height = HashSet::new();
while needs_block_height.len() < chunk_size {
match needs_block_height_iter.next() {
Some(height) => needs_block_height.insert(height),
None => break,
};
}
needs_block_height
};
let needs_block_height = conftime_req
.request()
.filter_map(|txid| txid_to_height.get(txid).cloned())
.filter(|height| block_times.get(height).is_none())
.take(chunk_size)
.collect::<HashSet<u32>>();
let new_block_headers = self
.client
@@ -175,6 +216,7 @@ impl Blockchain for ElectrumBlockchain {
let full_details = full_transactions
.into_iter()
.map(|tx| {
let mut input_index = 0usize;
let prev_outputs = tx
.input
.iter()
@@ -189,6 +231,7 @@ impl Blockchain for ElectrumBlockchain {
.output
.get(input.previous_output.vout as usize)
.ok_or_else(electrum_goof)?;
input_index += 1;
Ok(Some(txout.clone()))
})
.collect::<Result<Vec<_>, Error>>()?;
@@ -205,29 +248,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> {
@@ -312,8 +332,93 @@ 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::testutils::configurable_blockchain_tests::ConfigurableBlockchainTester;
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().untrusted_pending, 50_000);
}
#[test]
fn test_electrum_with_variable_configs() {
struct ElectrumTester;
impl ConfigurableBlockchainTester<ElectrumBlockchain> for ElectrumTester {
const BLOCKCHAIN_NAME: &'static str = "Electrum";
fn config_with_stop_gap(
&self,
test_client: &mut TestClient,
stop_gap: usize,
) -> Option<ElectrumBlockchainConfig> {
Some(ElectrumBlockchainConfig {
url: test_client.electrsd.electrum_url.clone(),
socks5: None,
retry: 0,
timeout: None,
stop_gap: stop_gap,
})
}
}
ElectrumTester.run();
}
}

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

@@ -209,4 +209,38 @@ mod test {
"should inherit from value for 25"
);
}
#[test]
#[cfg(feature = "test-esplora")]
fn test_esplora_with_variable_configs() {
use crate::testutils::{
blockchain_tests::TestClient,
configurable_blockchain_tests::ConfigurableBlockchainTester,
};
struct EsploraTester;
impl ConfigurableBlockchainTester<EsploraBlockchain> for EsploraTester {
const BLOCKCHAIN_NAME: &'static str = "Esplora";
fn config_with_stop_gap(
&self,
test_client: &mut TestClient,
stop_gap: usize,
) -> Option<EsploraBlockchainConfig> {
Some(EsploraBlockchainConfig {
base_url: format!(
"http://{}",
test_client.electrsd.esplora_url.as_ref().unwrap()
),
proxy: None,
concurrency: None,
stop_gap: stop_gap,
timeout: None,
})
}
}
EsploraTester.run();
}
}

View File

@@ -12,6 +12,7 @@
//! Esplora by way of `reqwest` HTTP client.
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
@@ -31,8 +32,9 @@ use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
/// Structure encapsulates Esplora client
#[derive(Debug)]
struct UrlClient {
pub struct UrlClient {
url: String,
// We use the async client instead of the blocking one because it automatically uses `fetch`
// when the target platform is wasm32.
@@ -91,10 +93,54 @@ 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 Deref for EsploraBlockchain {
type Target = UrlClient;
fn deref(&self) -> &Self::Target {
&self.url_client
}
}
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 GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = await_or_block!(self.url_client._get_header(height as u32))?;
Ok(block_header.block_hash())
}
}
#[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)?;
@@ -167,9 +213,9 @@ impl Blockchain for EsploraBlockchain {
.request()
.map(|txid| {
let tx = tx_index.get(txid).expect("must be in index");
(tx.previous_outputs(), tx.to_tx())
Ok((tx.previous_outputs(), tx.to_tx()))
})
.collect();
.collect::<Result<_, Error>>()?;
tx_req.satisfy(full_txs)?
}
Request::Finish(batch_update) => break batch_update,
@@ -177,26 +223,8 @@ impl Blockchain for EsploraBlockchain {
};
database.commit_batch(batch_update)?;
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

@@ -14,6 +14,7 @@
use std::collections::{HashMap, HashSet};
use std::io;
use std::io::Read;
use std::ops::Deref;
use std::time::Duration;
#[allow(unused_imports)]
@@ -33,8 +34,9 @@ use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
/// Structure encapsulates ureq Esplora client
#[derive(Debug, Clone)]
struct UrlClient {
pub struct UrlClient {
url: String,
agent: Agent,
}
@@ -87,10 +89,51 @@ impl Blockchain for EsploraBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
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 Deref for EsploraBlockchain {
type Target = UrlClient;
fn deref(&self) -> &Self::Target {
&self.url_client
}
}
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 GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = self.url_client._get_header(height as u32)?;
Ok(block_header.block_hash())
}
}
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 +146,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 +172,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![];
@@ -166,9 +211,9 @@ impl Blockchain for EsploraBlockchain {
.request()
.map(|txid| {
let tx = tx_index.get(txid).expect("must be in index");
(tx.previous_outputs(), tx.to_tx())
Ok((tx.previous_outputs(), tx.to_tx()))
})
.collect();
.collect::<Result<_, Error>>()?;
tx_req.satisfy(full_txs)?
}
Request::Finish(batch_update) => break batch_update,
@@ -179,24 +224,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

@@ -21,11 +21,12 @@ use std::ops::Deref;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use bitcoin::{Transaction, Txid};
use bitcoin::{BlockHash, 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,57 @@ 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 + GetBlockHash {
/// 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>;
}
#[maybe_async]
/// Trait for getting block hash by block height
pub trait GetBlockHash {
/// fetch block hash given its height
fn get_block_hash(&self, height: u64) -> Result<BlockHash, 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 +154,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 +172,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<Balance, 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 +302,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 +317,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 +343,51 @@ 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: GetBlockHash> GetBlockHash for Arc<T> {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
maybe_await!(self.deref().get_block_hash(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))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
#![allow(dead_code)]
use crate::{
database::{BatchDatabase, BatchOperations, DatabaseUtils},
error::MissingCachedScripts,
wallet::time::Instant,
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
};
@@ -34,11 +35,12 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
let scripts_needed = db
.iter_script_pubkeys(Some(keychain))?
.into_iter()
.collect();
.collect::<VecDeque<_>>();
let state = State::new(db);
Ok(Request::Script(ScriptReq {
state,
initial_scripts_needed: scripts_needed.len(),
scripts_needed,
script_index: 0,
stop_gap,
@@ -50,6 +52,7 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
pub struct ScriptReq<'a, D: BatchDatabase> {
state: State<'a, D>,
script_index: usize,
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
scripts_needed: VecDeque<Script>,
stop_gap: usize,
keychain: KeychainKind,
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
self.script_index += 1;
}
for _ in txids {
self.scripts_needed.pop_front();
}
self.scripts_needed.drain(..txids.len());
let last_active_index = self
// last active index: 0 => No last active
let last = self
.state
.last_active_index
.get(&self.keychain)
.map(|x| x + 1)
.unwrap_or(0); // so no addresses active maps to 0
.map(|&l| l + 1)
.unwrap_or(0);
// remaining scripts left to check
let remaining = self.scripts_needed.len();
// difference between current index and last active index
let current_gap = self.script_index - last;
Ok(
if self.script_index > last_active_index + self.stop_gap
|| self.scripts_needed.is_empty()
{
debug!(
"finished scanning for transactions for keychain {:?} at index {}",
self.keychain, last_active_index
);
// we're done here -- check if we need to do the next keychain
if let Some(keychain) = self.next_keychains.pop() {
self.keychain = keychain;
self.script_index = 0;
self.scripts_needed = self
.state
.db
.iter_script_pubkeys(Some(keychain))?
.into_iter()
.collect();
Request::Script(self)
} else {
Request::Tx(TxReq { state: self.state })
}
} else {
Request::Script(self)
},
)
// this is a hack to check whether the scripts are coming from a derivable descriptor
// we assume for non-derivable descriptors, the initial script count is always 1
let is_derivable = self.initial_scripts_needed > 1;
debug!(
"sync: last={}, remaining={}, diff={}, stop_gap={}",
last, remaining, current_gap, self.stop_gap
);
if is_derivable {
if remaining > 0 {
// we still have scriptPubKeys to do requests for
return Ok(Request::Script(self));
}
if last > 0 && current_gap < self.stop_gap {
// current gap is not large enough to stop, but we are unable to keep checking since
// we have exhausted cached scriptPubKeys, so return error
let err = MissingCachedScripts {
last_count: self.script_index,
missing_count: self.stop_gap - current_gap,
};
return Err(Error::MissingCachedScripts(err));
}
// we have exhausted cached scriptPubKeys and found no txs, continue
}
debug!(
"finished scanning for txs of keychain {:?} at index {:?}",
self.keychain, last
);
if let Some(keychain) = self.next_keychains.pop() {
// we still have another keychain to request txs with
let scripts_needed = self
.state
.db
.iter_script_pubkeys(Some(keychain))?
.into_iter()
.collect::<VecDeque<_>>();
self.keychain = keychain;
self.script_index = 0;
self.initial_scripts_needed = scripts_needed.len();
self.scripts_needed = scripts_needed;
return Ok(Request::Script(self));
}
// We have finished requesting txids, let's get the actual txs.
Ok(Request::Tx(TxReq { state: self.state }))
}
}
@@ -178,7 +209,9 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
let mut inputs_sum: u64 = 0;
let mut outputs_sum: u64 = 0;
for (txout, input) in vout.into_iter().zip(tx.input.iter()) {
for (txout, (_input_index, input)) in
vout.into_iter().zip(tx.input.iter().enumerate())
{
let txout = match txout {
Some(txout) => txout,
None => {
@@ -190,7 +223,19 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
continue;
}
};
// Verify this input if requested via feature flag
#[cfg(feature = "verify")]
{
use crate::wallet::verify::VerifyError;
let serialized_tx = bitcoin::consensus::serialize(&tx);
bitcoinconsensus::verify(
txout.script_pubkey.to_bytes().as_ref(),
txout.value,
&serialized_tx,
_input_index,
)
.map_err(VerifyError::from)?;
}
inputs_sum += txout.value;
if self.state.db.is_mine(&txout.script_pubkey)? {
sent += txout.value;
@@ -214,7 +259,6 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
// we're going to fill this in later
confirmation_time: None,
fee: Some(fee),
verified: false,
})
})
.collect::<Result<Vec<_>, _>>()?;
@@ -281,6 +325,8 @@ struct State<'a, D> {
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
/// The start of the sync
start_time: Instant,
/// Missing number of scripts to cache per keychain
missing_script_counts: HashMap<KeychainKind, usize>,
}
impl<'a, D: BatchDatabase> State<'a, D> {
@@ -292,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
tx_needed: BTreeSet::default(),
tx_missing_conftime: BTreeMap::default(),
start_time: Instant::new(),
missing_script_counts: HashMap::default(),
}
}
fn into_db_update(self) -> Result<D::Batch, Error> {
@@ -301,6 +348,22 @@ impl<'a, D: BatchDatabase> State<'a, D> {
let finished_txs = make_txs_consistent(&self.finished_txs);
let observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
let txids_to_delete = existing_txids.difference(&observed_txids);
// Ensure `last_active_index` does not decrement database's current state.
let index_updates = self
.last_active_index
.iter()
.map(|(keychain, sync_index)| {
let sync_index = *sync_index as u32;
let index_res = match self.db.get_last_index(*keychain) {
Ok(Some(db_index)) => Ok(std::cmp::max(db_index, sync_index)),
Ok(None) => Ok(sync_index),
Err(err) => Err(err),
};
index_res.map(|index| (*keychain, index))
})
.collect::<Result<Vec<(KeychainKind, u32)>, _>>()?;
let mut batch = self.db.begin_batch();
// Delete old txs that no longer exist
@@ -319,7 +382,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
@@ -330,34 +409,28 @@ 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)?;
}
}
for (keychain, last_active_index) in self.last_active_index {
batch.set_last_index(keychain, last_active_index as u32)?;
// apply index updates
for (keychain, new_index) in index_updates {
debug!("updating index ({}, {})", keychain.as_byte(), new_index);
batch.set_last_index(keychain, new_index)?;
}
info!(

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")]
@@ -254,10 +255,6 @@ impl Database for AnyDatabase {
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
}
fn flush(&mut self) -> Result<(), Error> {
impl_inner_method!(AnyDatabase, self, flush)
}
}
impl BatchOperations for AnyBatch {

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, }))
}
}
}
@@ -164,16 +166,9 @@ macro_rules! impl_batch_operations {
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
let key = MapKey::LastIndex(keychain).as_map_key();
let res = self.remove(key);
let res = $process_delete!(res);
match res {
None => Ok(None),
Some(b) => {
let array: [u8; 4] = b.as_ref().try_into().map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
let val = u32::from_be_bytes(array);
Ok(Some(val))
}
}
$process_delete!(res)
.map(ivec_to_u32)
.transpose()
}
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
@@ -246,11 +241,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 +314,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()
@@ -345,16 +350,7 @@ impl Database for Tree {
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
let key = MapKey::LastIndex(keychain).as_map_key();
self.get(key)?
.map(|b| -> Result<_, Error> {
let array: [u8; 4] = b
.as_ref()
.try_into()
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
let val = u32::from_be_bytes(array);
Ok(val)
})
.transpose()
self.get(key)?.map(ivec_to_u32).transpose()
}
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
@@ -381,19 +377,17 @@ impl Database for Tree {
Some(new.to_be_bytes().to_vec())
})?
.map_or(Ok(0), |b| -> Result<_, Error> {
let array: [u8; 4] = b
.as_ref()
.try_into()
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
let val = u32::from_be_bytes(array);
Ok(val)
})
.map_or(Ok(0), ivec_to_u32)
}
}
fn flush(&mut self) -> Result<(), Error> {
Ok(Tree::flush(self).map(|_| ())?)
}
fn ivec_to_u32(b: sled::IVec) -> Result<u32, Error> {
let array: [u8; 4] = b
.as_ref()
.try_into()
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
let val = u32::from_be_bytes(array);
Ok(val)
}
impl BatchDatabase for Tree {

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,
}
}))
}
@@ -444,10 +449,6 @@ impl Database for MemoryDatabase {
Ok(*value)
}
fn flush(&mut self) -> Result<(), Error> {
Ok(())
}
}
impl BatchDatabase for MemoryDatabase {
@@ -481,15 +482,23 @@ impl ConfigurableDatabase for MemoryDatabase {
/// don't have `test` set.
macro_rules! populate_test_db {
($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{
$crate::populate_test_db!($db, $tx_meta, $current_height, (@coinbase false))
}};
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
use std::str::FromStr;
use $crate::database::BatchOperations;
use $crate::database::SyncTime;
use $crate::database::{BatchOperations, Database};
let mut db = $db;
let tx_meta = $tx_meta;
let current_height: Option<u32> = $current_height;
let mut input = vec![$crate::bitcoin::TxIn::default()];
if !$is_coinbase {
input[0].previous_output.vout = 0;
}
let tx = $crate::bitcoin::Transaction {
version: 1,
lock_time: 0,
input: vec![],
input,
output: tx_meta
.output
.iter()
@@ -503,10 +512,31 @@ macro_rules! populate_test_db {
};
let txid = tx.txid();
let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime {
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
timestamp: 0,
});
// Set Confirmation time only if current height is provided.
// panics if `tx_meta.min_confirmation` is Some, and current_height is None.
let confirmation_time = tx_meta
.min_confirmations
.and_then(|v| if v == 0 { None } else { Some(v) })
.map(|conf| $crate::BlockTime {
height: current_height.expect("Current height is needed for testing transaction with min-confirmation values").checked_sub(conf as u32).unwrap() + 1,
timestamp: 0,
});
// Set the database sync_time.
// Check if the current_height is less than already known sync height, apply the max
// If any of them is None, the other will be applied instead.
// If both are None, this will not be set.
if let Some(height) = db.get_sync_time().unwrap()
.map(|sync_time| sync_time.block_time.height)
.max(current_height) {
let sync_time = SyncTime {
block_time: BlockTime {
height,
timestamp: 0
}
};
db.set_sync_time(sync_time).unwrap();
}
let tx_details = $crate::TransactionDetails {
transaction: Some(tx.clone()),
@@ -515,7 +545,6 @@ macro_rules! populate_test_db {
received: 0,
sent: 0,
confirmation_time,
verified: current_height.is_some(),
};
db.set_tx(&tx_details).unwrap();
@@ -527,6 +556,7 @@ macro_rules! populate_test_db {
vout: vout as u32,
},
keychain: $crate::KeychainKind::External,
is_spent: false,
})
.unwrap();
}
@@ -555,7 +585,7 @@ macro_rules! doctest_wallet {
Some(100),
);
$crate::Wallet::new_offline(
$crate::Wallet::new(
&descriptors.0,
descriptors.1.as_ref(),
Network::Regtest,

View File

@@ -158,9 +158,6 @@ 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>;
/// Force changes to be written to disk
fn flush(&mut self) -> Result<(), Error>;
}
/// Trait for a database that supports batch operations
@@ -196,8 +193,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 +312,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));
}
@@ -348,7 +346,6 @@ pub mod test {
timestamp: 123456,
height: 1000,
}),
verified: true,
};
tree.set_tx(&tx_details).unwrap();
@@ -372,6 +369,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

@@ -8,6 +8,8 @@
// <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 std::path::Path;
use std::path::PathBuf;
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hash_types::Txid;
@@ -35,7 +37,22 @@ static MIGRATIONS: &[&str] = &[
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);",
"ALTER TABLE transaction_details RENAME TO transaction_details_old;",
"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
@@ -45,7 +62,7 @@ static MIGRATIONS: &[&str] = &[
#[derive(Debug)]
pub struct SqliteDatabase {
/// Path on the local filesystem to store the sqlite file
pub path: String,
pub path: PathBuf,
/// A rusqlite connection object to the sqlite database
pub connection: Connection,
}
@@ -53,9 +70,12 @@ pub struct SqliteDatabase {
impl SqliteDatabase {
/// Instantiate a new SqliteDatabase instance by creating a connection
/// to the database stored at path
pub fn new(path: String) -> Self {
pub fn new<T: AsRef<Path>>(path: T) -> Self {
let connection = get_connection(&path).unwrap();
SqliteDatabase { path, connection }
SqliteDatabase {
path: PathBuf::from(path.as_ref()),
connection,
}
}
fn insert_script_pubkey(
&self,
@@ -79,14 +99,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())
@@ -127,7 +149,7 @@ impl SqliteDatabase {
let txid: &[u8] = &transaction.txid;
let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height, verified) VALUES (:txid, :timestamp, :received, :sent, :fee, :height, :verified)")?;
let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height) VALUES (:txid, :timestamp, :received, :sent, :fee, :height)")?;
statement.execute(named_params! {
":txid": txid,
@@ -136,7 +158,6 @@ impl SqliteDatabase {
":sent": transaction.sent,
":fee": transaction.fee,
":height": height,
":verified": transaction.verified
})?;
Ok(self.connection.last_insert_rowid())
@@ -153,7 +174,7 @@ impl SqliteDatabase {
let txid: &[u8] = &transaction.txid;
let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height, verified=:verified WHERE txid=:txid")?;
let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height WHERE txid=:txid")?;
statement.execute(named_params! {
":txid": txid,
@@ -162,7 +183,6 @@ impl SqliteDatabase {
":sent": transaction.sent,
":fee": transaction.fee,
":height": height,
":verified": transaction.verified,
})?;
Ok(())
@@ -289,7 +309,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()? {
@@ -298,6 +318,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)?;
@@ -308,19 +329,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()? {
@@ -329,9 +347,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),
}
@@ -367,7 +394,7 @@ impl SqliteDatabase {
}
fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
let mut transaction_details: Vec<TransactionDetails> = vec![];
let mut rows = statement.query([])?;
while let Some(row) = rows.next()? {
@@ -378,8 +405,7 @@ impl SqliteDatabase {
let sent: u64 = row.get(3)?;
let fee: Option<u64> = row.get(4)?;
let height: Option<u32> = row.get(5)?;
let verified: bool = row.get(6)?;
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)?;
@@ -400,7 +426,6 @@ impl SqliteDatabase {
sent,
fee,
confirmation_time,
verified,
});
}
Ok(transaction_details)
@@ -408,7 +433,7 @@ impl SqliteDatabase {
fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached(
"SELECT txid, timestamp, received, sent, fee, height, verified FROM transaction_details",
"SELECT txid, timestamp, received, sent, fee, height FROM transaction_details",
)?;
let mut transaction_details: Vec<TransactionDetails> = vec![];
let mut rows = statement.query([])?;
@@ -420,7 +445,6 @@ impl SqliteDatabase {
let sent: u64 = row.get(3)?;
let fee: Option<u64> = row.get(4)?;
let height: Option<u32> = row.get(5)?;
let verified: bool = row.get(6)?;
let confirmation_time = match (height, timestamp) {
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
@@ -434,7 +458,6 @@ impl SqliteDatabase {
sent,
fee,
confirmation_time,
verified,
});
}
Ok(transaction_details)
@@ -444,7 +467,7 @@ impl SqliteDatabase {
&self,
txid: &[u8],
) -> Result<Option<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
let mut rows = statement.query(named_params! { ":txid": txid })?;
match rows.next()? {
@@ -454,9 +477,8 @@ impl SqliteDatabase {
let sent: u64 = row.get(2)?;
let fee: Option<u64> = row.get(3)?;
let height: Option<u32> = row.get(4)?;
let verified: bool = row.get(5)?;
let raw_tx: Option<Vec<u8>> = row.get(6)?;
let raw_tx: Option<Vec<u8>> = row.get(5)?;
let tx: Option<Transaction> = match raw_tx {
Some(raw_tx) => {
let tx: Transaction = deserialize(&raw_tx)?;
@@ -477,7 +499,6 @@ impl SqliteDatabase {
sent,
fee,
confirmation_time,
verified,
}))
}
None => Ok(None),
@@ -624,6 +645,7 @@ impl BatchOperations for SqliteDatabase {
utxo.outpoint.vout,
&utxo.outpoint.txid,
utxo.txout.script_pubkey.as_bytes(),
utxo.is_spent,
)?;
Ok(())
}
@@ -698,16 +720,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),
}
@@ -836,17 +851,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> {
@@ -891,10 +896,6 @@ impl Database for SqliteDatabase {
}
}
}
fn flush(&mut self) -> Result<(), Error> {
Ok(())
}
}
impl BatchDatabase for SqliteDatabase {
@@ -912,7 +913,7 @@ impl BatchDatabase for SqliteDatabase {
}
}
pub fn get_connection(path: &str) -> Result<Connection, Error> {
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
let connection = Connection::open(path)?;
migrate(&connection)?;
Ok(connection)
@@ -1030,4 +1031,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

@@ -14,12 +14,10 @@
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
//! checksum of a descriptor
use std::iter::FromIterator;
use crate::descriptor::DescriptorError;
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
fn poly_mod(mut c: u64, val: u64) -> u64 {
let c0 = c >> 35;
@@ -43,15 +41,17 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
c
}
/// Compute the checksum of a descriptor
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
/// Computes the checksum bytes of a descriptor
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
let mut c = 1;
let mut cls = 0;
let mut clscount = 0;
for ch in desc.chars() {
for ch in desc.as_bytes() {
let pos = INPUT_CHARSET
.find(ch)
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
.iter()
.position(|b| b == ch)
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
c = poly_mod(c, pos & 31);
cls = cls * 3 + (pos >> 5);
clscount += 1;
@@ -67,17 +67,18 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
(0..8).for_each(|_| c = poly_mod(c, 0));
c ^= 1;
let mut chars = Vec::with_capacity(8);
let mut checksum = [0_u8; 8];
for j in 0..8 {
chars.push(
CHECKSUM_CHARSET
.chars()
.nth(((c >> (5 * (7 - j))) & 31) as usize)
.unwrap(),
);
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
}
Ok(String::from_iter(chars))
Ok(checksum)
}
/// Compute the checksum of a descriptor
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
#[cfg(test)]
@@ -97,17 +98,12 @@ mod test {
#[test]
fn test_get_checksum_invalid_character() {
let sparkle_heart = vec![240, 159, 146, 150];
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
.unwrap()
.chars()
.next()
.unwrap();
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
assert!(matches!(
get_checksum(&invalid_desc).err(),
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
));
}
}

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
@@ -714,7 +839,7 @@ mod test {
}
}
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
// - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
// expected script for pk and bare manually created
@@ -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

@@ -28,8 +28,8 @@ pub enum Error {
/// Error while extracting and manipulating policies
Policy(crate::descriptor::policy::PolicyError),
/// Invalid character found in the descriptor checksum
InvalidDescriptorCharacter(char),
/// Invalid byte found in the descriptor checksum
InvalidDescriptorCharacter(u8),
/// BIP32 error
Bip32(bitcoin::util::bip32::Error),

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, ExtendedPrivKey, ExtendedPubKey, Fingerprint, KeySource,
};
use bitcoin::util::psbt;
use bitcoin::{Network, PublicKey, Script, TxOut};
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::util::{psbt, taproot};
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
use bitcoin::{Network, Script, TxOut};
use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey, Wildcard};
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
pub use miniscript::{
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
DescriptorPublicKey, 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 {
@@ -126,13 +134,13 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
let check_key = |pk: &DescriptorPublicKey| {
let (pk, _, networks) = if self.0.is_witness() {
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(secp)?
descriptor_key.extract(secp)?
} else {
let desciptor_key: DescriptorKey<miniscript::Legacy> =
let descriptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(secp)?
descriptor_key.extract(secp)?
};
if networks.contains(&network) {
@@ -267,41 +275,10 @@ pub(crate) trait XKeyUtils {
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
}
// FIXME: `InnerXKey` was made private in rust-miniscript, so we have to implement this manually on
// both `ExtendedPubKey` and `ExtendedPrivKey`.
//
// Revert back to using the trait once https://github.com/rust-bitcoin/rust-miniscript/pull/230 is
// released
impl XKeyUtils for DescriptorXKey<ExtendedPubKey> {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
let full_path = match self.origin {
Some((_, ref path)) => path
.into_iter()
.chain(self.derivation_path.into_iter())
.cloned()
.collect(),
None => self.derivation_path.clone(),
};
if self.wildcard != Wildcard::None {
full_path
.into_iter()
.chain(append.iter())
.cloned()
.collect()
} else {
full_path
}
}
fn root_fingerprint(&self, _: &SecpCtx) -> Fingerprint {
match self.origin {
Some((fingerprint, _)) => fingerprint,
None => self.xkey.fingerprint(),
}
}
}
impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
impl<T> XKeyUtils for DescriptorXKey<T>
where
T: InnerXKey,
{
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
let full_path = match self.origin {
Some((_, ref path)) => path
@@ -326,23 +303,35 @@ impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
match self.origin {
Some((fingerprint, _)) => fingerprint,
None => self.xkey.fingerprint(secp),
None => self.xkey.xkey_fingerprint(secp),
}
}
}
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,
@@ -359,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,
}
}
}
@@ -393,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();
@@ -407,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,
@@ -471,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;
@@ -479,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 =>
{
@@ -487,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)
@@ -497,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)
@@ -508,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() {
@@ -526,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
}
}
@@ -538,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;
@@ -801,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

@@ -40,18 +40,19 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// use bdk::keys::{IntoDescriptorKey, KeyError};
/// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
/// use bitcoin::Network;
///
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
///
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
/// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
/// Ok(bdk::descriptor!(pkh(self.0))?)
/// }
/// }
/// ```
pub trait DescriptorTemplate {
/// Build the complete descriptor
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError>;
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError>;
}
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
@@ -62,7 +63,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
self.build()?.into_wallet_descriptor(secp, network)
self.build(network)?.into_wallet_descriptor(secp, network)
}
}
@@ -79,7 +80,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,
@@ -95,7 +96,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
descriptor!(pkh(self.0))
}
}
@@ -113,7 +114,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,
@@ -130,7 +131,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
descriptor!(sh(wpkh(self.0)))
}
}
@@ -148,7 +149,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,
@@ -164,12 +165,12 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
descriptor!(wpkh(self.0))
}
}
/// BIP44 template. Expands to `pkh(key/44'/0'/0'/{0,1}/*)`
/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
///
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
///
@@ -186,28 +187,28 @@ 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,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
}
}
/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
///
/// This assumes that the key used has already been derived with `m/44'/0'/0'`.
/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet.
///
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
///
@@ -226,7 +227,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,
@@ -240,12 +241,12 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
}
}
/// BIP49 template. Expands to `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))`
///
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
///
@@ -262,22 +263,22 @@ 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,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
}
}
@@ -302,7 +303,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,
@@ -310,18 +311,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
}
}
/// BIP84 template. Expands to `wpkh(key/84'/0'/0'/{0,1}/*)`
/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)`
///
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
///
@@ -338,22 +339,22 @@ 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,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
}
}
@@ -378,7 +379,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,
@@ -392,8 +393,8 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
}
}
@@ -406,10 +407,19 @@ macro_rules! expand_make_bipxx {
bip: u32,
key: K,
keychain: KeychainKind,
network: Network,
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
let mut derivation_path = Vec::with_capacity(4);
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
match network {
Network::Bitcoin => {
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
}
_ => {
derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?);
}
}
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
match keychain {
@@ -466,6 +476,40 @@ mod test {
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
use miniscript::Descriptor;
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
#[test]
fn test_bip44_template_cointype() {
use bitcoin::util::bip32::ChildNumber::{self, Hardened};
let xprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
assert_eq!(Network::Bitcoin, xprvkey.network);
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
.build(Network::Bitcoin)
.unwrap();
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
let purpose = path.get(0).unwrap();
assert!(matches!(purpose, Hardened { index: 44 }));
let coin_type = path.get(1).unwrap();
assert!(matches!(coin_type, Hardened { index: 0 }));
}
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
assert_eq!(Network::Testnet, tprvkey.network);
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
.build(Network::Testnet)
.unwrap();
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
let purpose = path.get(0).unwrap();
assert!(matches!(purpose, Hardened { index: 44 }));
let coin_type = path.get(1).unwrap();
assert!(matches!(coin_type, Hardened { index: 1 }));
}
}
// verify template descriptor generates expected address(es)
fn check(
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
@@ -497,7 +541,7 @@ mod test {
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
.unwrap();
check(
P2Pkh(prvkey).build(),
P2Pkh(prvkey).build(Network::Bitcoin),
false,
true,
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
@@ -508,7 +552,7 @@ mod test {
)
.unwrap();
check(
P2Pkh(pubkey).build(),
P2Pkh(pubkey).build(Network::Bitcoin),
false,
true,
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
@@ -522,7 +566,7 @@ mod test {
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
.unwrap();
check(
P2Wpkh_P2Sh(prvkey).build(),
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
true,
true,
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
@@ -533,7 +577,7 @@ mod test {
)
.unwrap();
check(
P2Wpkh_P2Sh(pubkey).build(),
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
true,
true,
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
@@ -547,7 +591,7 @@ mod test {
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
.unwrap();
check(
P2Wpkh(prvkey).build(),
P2Wpkh(prvkey).build(Network::Bitcoin),
true,
true,
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
@@ -558,7 +602,7 @@ mod test {
)
.unwrap();
check(
P2Wpkh(pubkey).build(),
P2Wpkh(pubkey).build(Network::Bitcoin),
true,
true,
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
@@ -570,7 +614,7 @@ mod test {
fn test_bip44_template() {
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip44(prvkey, KeychainKind::External).build(),
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
false,
false,
&[
@@ -580,7 +624,7 @@ mod test {
],
);
check(
Bip44(prvkey, KeychainKind::Internal).build(),
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
false,
false,
&[
@@ -597,7 +641,7 @@ mod test {
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
false,
false,
&[
@@ -607,7 +651,7 @@ mod test {
],
);
check(
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
false,
false,
&[
@@ -623,7 +667,7 @@ mod test {
fn test_bip49_template() {
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip49(prvkey, KeychainKind::External).build(),
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
false,
&[
@@ -633,7 +677,7 @@ mod test {
],
);
check(
Bip49(prvkey, KeychainKind::Internal).build(),
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
&[
@@ -650,7 +694,7 @@ mod test {
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
true,
false,
&[
@@ -660,7 +704,7 @@ mod test {
],
);
check(
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
&[
@@ -676,7 +720,7 @@ mod test {
fn test_bip84_template() {
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
check(
Bip84(prvkey, KeychainKind::External).build(),
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
false,
&[
@@ -686,7 +730,7 @@ mod test {
],
);
check(
Bip84(prvkey, KeychainKind::Internal).build(),
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
&[
@@ -703,7 +747,7 @@ mod test {
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
check(
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
true,
false,
&[
@@ -713,7 +757,7 @@ mod test {
],
);
check(
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
&[

View File

@@ -9,6 +9,6 @@
// You may not use this file except in accordance with one or both of these
// licenses.
#[doc(include = "../README.md")]
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;

View File

@@ -13,7 +13,7 @@ use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator};
use bitcoin::OutPoint;
use bitcoin::{OutPoint, Txid};
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
#[derive(Debug)]
@@ -125,6 +125,10 @@ pub enum Error {
//DifferentDescriptorStructure,
//Uncapable(crate::blockchain::Capability),
//MissingCachedAddresses,
/// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which
/// are needed to satisfy `stop_gap`.
MissingCachedScripts(MissingCachedScripts),
#[cfg(feature = "electrum")]
/// Electrum client error
Electrum(electrum_client::Error),
@@ -145,6 +149,16 @@ pub enum Error {
Rusqlite(rusqlite::Error),
}
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
/// on cached `scriptPubKey`s.
#[derive(Debug)]
pub struct MissingCachedScripts {
/// Number of scripts in which txs were requested during last request.
pub last_count: usize,
/// Minimum number of scripts to cache more of in order to satisfy `stop_gap`.
pub missing_count: usize,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)

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(),
/// ))
@@ -333,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,
};
@@ -387,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 },
/// };
@@ -419,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 },
/// };
@@ -547,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>
@@ -687,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()))
@@ -769,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()?;
@@ -830,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()
@@ -931,4 +969,19 @@ pub mod test {
"L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch"
);
}
#[cfg(feature = "keys-bip39")]
#[test]
fn test_keys_wif_network_bip39() {
let xkey: ExtendedKey = bip39::Mnemonic::parse_in(
bip39::Language::English,
"jelly crash boy whisper mouse ecology tuna soccer memory million news short",
)
.unwrap()
.into_extended_key()
.unwrap();
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
assert_eq!(xprv.network, Network::Testnet);
}
}

View File

@@ -14,6 +14,10 @@
// only enables the `doc_cfg` feature when
// the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
docsrs,
doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
)]
//! A modern, lightweight, descriptor-based wallet library written in Rust.
//!
@@ -39,32 +43,29 @@
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
//! interact with the bitcoin P2P network.
//!
//! ```toml
//! bdk = "0.15.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()?);
@@ -83,7 +84,7 @@ fn main() -> Result<(), bdk::Error> {
//! 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,
@@ -102,11 +103,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;
@@ -119,10 +119,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) = {
@@ -146,7 +146,6 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ## Sign a transaction
//!
//! ### Example
//! ```no_run
//! use std::str::FromStr;
//!
@@ -156,7 +155,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,
@@ -188,7 +187,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.
@@ -247,6 +246,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;
@@ -268,16 +275,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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
use bitcoin::Network;
use crate::{
blockchain::ConfigurableBlockchain, database::MemoryDatabase, testutils, wallet::AddressIndex,
Wallet,
};
use super::blockchain_tests::TestClient;
/// Trait for testing [`ConfigurableBlockchain`] implementations.
pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
/// Blockchain name for logging.
const BLOCKCHAIN_NAME: &'static str;
/// Generates a blockchain config with a given stop_gap.
///
/// If this returns [`Option::None`], then the associated tests will not run.
fn config_with_stop_gap(
&self,
_test_client: &mut TestClient,
_stop_gap: usize,
) -> Option<B::Config> {
None
}
/// Runs all avaliable tests.
fn run(&self) {
let test_client = &mut TestClient::default();
if self.config_with_stop_gap(test_client, 0).is_some() {
test_wallet_sync_with_stop_gaps(test_client, self);
test_wallet_sync_fulfills_missing_script_cache(test_client, self);
test_wallet_sync_self_transfer_tx(test_client, self);
} else {
println!(
"{}: Skipped tests requiring config_with_stop_gap.",
Self::BLOCKCHAIN_NAME
);
}
}
}
/// Test whether blockchain implementation syncs with expected behaviour given different `stop_gap`
/// parameters.
///
/// For each test vector:
/// * Fill wallet's derived addresses with balances (as specified by test vector).
/// * [0..addrs_before] => 1000sats for each address
/// * [addrs_before..actual_gap] => empty addresses
/// * [actual_gap..addrs_after] => 1000sats for each address
/// * Then, perform wallet sync and obtain wallet balance
/// * Check balance is within expected range (we can compare `stop_gap` and `actual_gap` to
/// determine this).
fn test_wallet_sync_with_stop_gaps<T, B>(test_client: &mut TestClient, tester: &T)
where
T: ConfigurableBlockchainTester<B>,
B: ConfigurableBlockchain,
{
// Generates wallet descriptor
let descriptor_of_account = |account_index: usize| -> String {
format!("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/{account_index}/*)")
};
// Amount (in satoshis) provided to a single address (which expects to have a balance)
const AMOUNT_PER_TX: u64 = 1000;
// [stop_gap, actual_gap, addrs_before, addrs_after]
//
// [0] stop_gap: Passed to [`ElectrumBlockchainConfig`]
// [1] actual_gap: Range size of address indexes without a balance
// [2] addrs_before: Range size of address indexes (before gap) which contains a balance
// [3] addrs_after: Range size of address indexes (after gap) which contains a balance
let test_vectors: Vec<[u64; 4]> = vec![
[0, 0, 0, 5],
[0, 0, 5, 5],
[0, 1, 5, 5],
[0, 2, 5, 5],
[1, 0, 5, 5],
[1, 1, 5, 5],
[1, 2, 5, 5],
[2, 1, 5, 5],
[2, 2, 5, 5],
[2, 3, 5, 5],
];
for (account_index, vector) in test_vectors.into_iter().enumerate() {
let [stop_gap, actual_gap, addrs_before, addrs_after] = vector;
let descriptor = descriptor_of_account(account_index);
let blockchain = B::from_config(
&tester
.config_with_stop_gap(test_client, stop_gap as _)
.unwrap(),
)
.unwrap();
let wallet =
Wallet::new(&descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
// fill server-side with txs to specified address indexes
// return the max balance of the wallet (also the actual balance)
let max_balance = (0..addrs_before)
.chain(addrs_before + actual_gap..addrs_before + actual_gap + addrs_after)
.fold(0_u64, |sum, i| {
let address = wallet.get_address(AddressIndex::Peek(i as _)).unwrap();
test_client.receive(testutils! {
@tx ( (@addr address.address) => AMOUNT_PER_TX )
});
sum + AMOUNT_PER_TX
});
// minimum allowed balance of wallet (based on stop gap)
let min_balance = if actual_gap > stop_gap {
addrs_before * AMOUNT_PER_TX
} else {
max_balance
};
let details = format!(
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
stop_gap, actual_gap, addrs_before, addrs_after,
);
println!("{}", details);
// perform wallet sync
wallet.sync(&blockchain, Default::default()).unwrap();
let wallet_balance = wallet.get_balance().unwrap().get_total();
println!(
"max: {}, min: {}, actual: {}",
max_balance, min_balance, wallet_balance
);
assert!(
wallet_balance <= max_balance,
"wallet balance is greater than received amount: {}",
details
);
assert!(
wallet_balance >= min_balance,
"wallet balance is smaller than expected: {}",
details
);
// generate block to confirm new transactions
test_client.generate(1, None);
}
}
/// With a `stop_gap` of x and every x addresses having a balance of 1000 (for y addresses),
/// we expect `Wallet::sync` to correctly self-cache addresses, so that the resulting balance,
/// after sync, should be y * 1000.
fn test_wallet_sync_fulfills_missing_script_cache<T, B>(test_client: &mut TestClient, tester: &T)
where
T: ConfigurableBlockchainTester<B>,
B: ConfigurableBlockchain,
{
// wallet descriptor
let descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/200/*)";
// amount in sats per tx
const AMOUNT_PER_TX: u64 = 1000;
// addr constants
const ADDR_COUNT: usize = 6;
const ADDR_GAP: usize = 60;
let blockchain =
B::from_config(&tester.config_with_stop_gap(test_client, ADDR_GAP).unwrap()).unwrap();
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
let expected_balance = (0..ADDR_COUNT).fold(0_u64, |sum, i| {
let addr_i = i * ADDR_GAP;
let address = wallet.get_address(AddressIndex::Peek(addr_i as _)).unwrap();
println!(
"tx: {} sats => [{}] {}",
AMOUNT_PER_TX,
addr_i,
address.to_string()
);
test_client.receive(testutils! {
@tx ( (@addr address.address) => AMOUNT_PER_TX )
});
test_client.generate(1, None);
sum + AMOUNT_PER_TX
});
println!("expected balance: {}, syncing...", expected_balance);
// perform sync
wallet.sync(&blockchain, Default::default()).unwrap();
println!("sync done!");
let balance = wallet.get_balance().unwrap().get_total();
assert_eq!(balance, expected_balance);
}
/// Given a `stop_gap`, a wallet with a 2 transactions, one sending to `scriptPubKey` at derivation
/// index of `stop_gap`, and the other spending from the same `scriptPubKey` into another
/// `scriptPubKey` at derivation index of `stop_gap * 2`, we expect `Wallet::sync` to perform
/// correctly, so that we detect the total balance.
fn test_wallet_sync_self_transfer_tx<T, B>(test_client: &mut TestClient, tester: &T)
where
T: ConfigurableBlockchainTester<B>,
B: ConfigurableBlockchain,
{
const TRANSFER_AMOUNT: u64 = 10_000;
const STOP_GAP: usize = 75;
let descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/*)";
let blockchain =
B::from_config(&tester.config_with_stop_gap(test_client, STOP_GAP).unwrap()).unwrap();
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
let address1 = wallet
.get_address(AddressIndex::Peek(STOP_GAP as _))
.unwrap();
let address2 = wallet
.get_address(AddressIndex::Peek((STOP_GAP * 2) as _))
.unwrap();
test_client.receive(testutils! {
@tx ( (@addr address1.address) => TRANSFER_AMOUNT )
});
test_client.generate(1, None);
wallet.sync(&blockchain, Default::default()).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(address2.script_pubkey(), TRANSFER_AMOUNT / 2);
let (mut psbt, details) = builder.finish().unwrap();
assert!(wallet.sign(&mut psbt, Default::default()).unwrap());
blockchain.broadcast(&psbt.extract_tx()).unwrap();
test_client.generate(1, None);
// obtain what is expected
let fee = details.fee.unwrap();
let expected_balance = TRANSFER_AMOUNT - fee;
println!("fee={}, expected_balance={}", fee, expected_balance);
// actually test the wallet
wallet.sync(&blockchain, Default::default()).unwrap();
let balance = wallet.get_balance().unwrap().get_total();
assert_eq!(balance, expected_balance);
// now try with a fresh wallet
let fresh_wallet =
Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
fresh_wallet.sync(&blockchain, Default::default()).unwrap();
let fresh_balance = fresh_wallet.get_balance().unwrap().get_total();
assert_eq!(fresh_balance, expected_balance);
}

View File

@@ -14,11 +14,37 @@
#[cfg(feature = "test-blockchains")]
pub mod blockchain_tests;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, PublicKey};
#[cfg(test)]
#[cfg(feature = "test-blockchains")]
pub mod configurable_blockchain_tests;
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
use bitcoin::{Address, Txid};
#[derive(Clone, Debug)]
pub struct TestIncomingInput {
pub txid: Txid,
pub vout: u32,
pub sequence: Option<u32>,
}
impl TestIncomingInput {
pub fn new(txid: Txid, vout: u32, sequence: Option<u32>) -> Self {
Self {
txid,
vout,
sequence,
}
}
#[cfg(feature = "test-blockchains")]
pub fn into_raw_tx_input(self) -> bitcoincore_rpc::json::CreateRawTransactionInput {
bitcoincore_rpc::json::CreateRawTransactionInput {
txid: self.txid,
vout: self.vout,
sequence: self.sequence,
}
}
}
#[derive(Clone, Debug)]
pub struct TestIncomingOutput {
@@ -37,6 +63,7 @@ impl TestIncomingOutput {
#[derive(Clone, Debug)]
pub struct TestIncomingTx {
pub input: Vec<TestIncomingInput>,
pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>,
pub locktime: Option<i64>,
@@ -45,12 +72,14 @@ pub struct TestIncomingTx {
impl TestIncomingTx {
pub fn new(
input: Vec<TestIncomingInput>,
output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>,
locktime: Option<i64>,
replaceable: Option<bool>,
) -> Self {
Self {
input,
output,
min_confirmations,
locktime,
@@ -58,44 +87,15 @@ impl TestIncomingTx {
}
}
pub fn add_input(&mut self, input: TestIncomingInput) {
self.input.push(input);
}
pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output);
}
}
#[doc(hidden)]
pub trait TranslateDescriptor {
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey>;
}
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey> {
let translate = |key: &DescriptorPublicKey| -> PublicKey {
match key {
DescriptorPublicKey::XPub(xpub) => {
xpub.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("hardened derivation steps")
.public_key
}
DescriptorPublicKey::SinglePub(key) => key.key,
}
};
self.derive(index)
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! testutils {
@@ -103,36 +103,41 @@ 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) });
( @addr $addr:expr ) => ({ $addr });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @inputs $( ($txid:expr, $vout:expr) ),+ ) )? $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
let _ins: Vec<$crate::testutils::TestIncomingInput> = vec![];
$(
let _ins = vec![$( $crate::testutils::TestIncomingInput { txid: $txid, vout: $vout, sequence: None }),+];
)?
let locktime = None::<i64>$(.or(Some($locktime)))?;
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
$crate::testutils::TestIncomingTx::new(_ins, outs, min_confirmations, locktime, replaceable)
});
( @literal $key:expr ) => ({

View File

@@ -51,14 +51,44 @@ impl AsRef<[u8]> for KeychainKind {
pub struct FeeRate(f32);
impl FeeRate {
/// Create a new instance checking the value provided
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
fn new_checked(value: f32) -> Self {
assert!(value.is_normal() || value == 0.0);
assert!(value.is_sign_positive());
FeeRate(value)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
FeeRate(btc_per_kvb * 1e5)
FeeRate::new_checked(btc_per_kvb * 1e5)
}
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
pub const fn from_sat_per_vb(sat_per_vb: f32) -> Self {
FeeRate(sat_per_vb)
///
/// ## Panics
///
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
FeeRate::new_checked(sat_per_vb)
}
/// Create a new [`FeeRate`] with the default min relay fee value
@@ -78,7 +108,7 @@ impl FeeRate {
}
/// Return the value as satoshi/vbyte
pub fn as_sat_vb(&self) -> f32 {
pub fn as_sat_per_vb(&self) -> f32 {
self.0
}
@@ -89,7 +119,7 @@ impl FeeRate {
/// Calculate absolute fee in Satoshis using size in virtual bytes.
pub fn fee_vb(&self, vbytes: usize) -> u64 {
(self.as_sat_vb() * vbytes as f32).ceil() as u64
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
}
}
@@ -131,6 +161,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`.
@@ -200,10 +232,12 @@ pub struct TransactionDetails {
pub txid: Txid,
/// Received value (sats)
/// Sum of owned outputs of this transaction.
pub received: u64,
/// Sent value (sats)
/// Sum of owned inputs of this transaction.
pub sent: u64,
/// Fee value (sats) if available.
/// Fee value (sats) if confirmed.
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
/// funds while offline.
@@ -211,15 +245,6 @@ pub struct TransactionDetails {
/// If the transaction is confirmed, contains height and timestamp of the block containing the
/// transaction, unconfirmed transaction contains `None`.
pub confirmation_time: Option<BlockTime>,
/// Whether the tx has been verified against the consensus rules
///
/// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to
/// ensure an unstrusted [`Blockchain`](crate::blockchain::Blockchain) backend can't trick the
/// wallet into using an invalid tx as an RBF template.
///
/// The check is only performed when the `verify` feature is enabled.
#[serde(default = "bool::default")] // default to `false` if not specified
pub verified: bool,
}
/// Block height and timestamp of a block
@@ -247,13 +272,130 @@ impl BlockTime {
}
}
/// Balance differentiated in various categories
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct Balance {
/// All coinbase outputs not yet matured
pub immature: u64,
/// Unconfirmed UTXOs generated by a wallet tx
pub trusted_pending: u64,
/// Unconfirmed UTXOs received from an external wallet
pub untrusted_pending: u64,
/// Confirmed and immediately spendable balance
pub confirmed: u64,
}
impl Balance {
/// Get sum of trusted_pending and confirmed coins
pub fn get_spendable(&self) -> u64 {
self.confirmed + self.trusted_pending
}
/// Get the whole balance visible to the wallet
pub fn get_total(&self) -> u64 {
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
}
}
impl std::fmt::Display for Balance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
)
}
}
impl std::ops::Add for Balance {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
immature: self.immature + other.immature,
trusted_pending: self.trusted_pending + other.trusted_pending,
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
confirmed: self.confirmed + other.confirmed,
}
}
}
impl std::iter::Sum for Balance {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(
Balance {
..Default::default()
},
|a, b| a + b,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_store_feerate_in_const() {
const _MY_RATE: FeeRate = FeeRate::from_sat_per_vb(10.0);
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
}
#[test]
#[should_panic]
fn test_invalid_feerate_neg_zero() {
let _ = FeeRate::from_sat_per_vb(-0.0);
}
#[test]
#[should_panic]
fn test_invalid_feerate_neg_value() {
let _ = FeeRate::from_sat_per_vb(-5.0);
}
#[test]
#[should_panic]
fn test_invalid_feerate_nan() {
let _ = FeeRate::from_sat_per_vb(f32::NAN);
}
#[test]
#[should_panic]
fn test_invalid_feerate_inf() {
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
}
#[test]
fn test_valid_feerate_pos_zero() {
let _ = FeeRate::from_sat_per_vb(0.0);
}
#[test]
fn test_fee_from_btc_per_kvb() {
let fee = FeeRate::from_btc_per_kvb(1e-5);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_vbyte() {
let fee = FeeRate::from_sat_per_vb(1.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_default_min_relay_fee() {
let fee = FeeRate::default_min_relay_fee();
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_kvb() {
let fee = FeeRate::from_sat_per_kvb(1000.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_fee_from_sat_per_kwu() {
let fee = FeeRate::from_sat_per_kwu(250.0);
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
}
}

View File

@@ -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)?;
@@ -100,6 +100,7 @@ impl std::error::Error for AddressValidatorError {}
/// validator will be propagated up to the original caller that triggered the address generation.
///
/// For a usage example see [this module](crate::address_validator)'s documentation.
#[deprecated = "AddressValidator was rarely used. Address validation can occur outside of BDK"]
pub trait AddressValidator: Send + Sync + fmt::Debug {
/// Validate or inspect an address
fn validate(
@@ -120,6 +121,7 @@ mod test {
#[derive(Debug)]
struct TestValidator;
#[allow(deprecated)]
impl AddressValidator for TestValidator {
fn validate(
&self,
@@ -135,6 +137,7 @@ mod test {
#[should_panic(expected = "InvalidScript")]
fn test_address_validator_external() {
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(TestValidator));
wallet.get_address(New).unwrap();
@@ -144,6 +147,7 @@ mod test {
#[should_panic(expected = "InvalidScript")]
fn test_address_validator_internal() {
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(TestValidator));
let addr = crate::testutils!(@external descriptors, 10);

File diff suppressed because it is too large Load Diff

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");
}
@@ -230,7 +251,6 @@ mod test {
timestamp: 12345678,
height: 5000,
}),
verified: true,
})
.unwrap();
@@ -242,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()));
@@ -266,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]
@@ -280,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]
@@ -303,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()));
@@ -323,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\"}");
}
@@ -341,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()));

View File

@@ -0,0 +1,64 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! HWI Signer
//!
//! This module contains a simple implementation of a Custom signer for rust-hwi
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::util::bip32::Fingerprint;
use hwi::error::Error;
use hwi::types::{HWIChain, HWIDevice};
use hwi::HWIClient;
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
#[derive(Debug)]
/// Custom signer for Hardware Wallets
///
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
pub struct HWISigner {
fingerprint: Fingerprint,
client: HWIClient,
}
impl HWISigner {
/// Create a instance from the specified device and chain
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
let client = HWIClient::get_client(device, false, chain)?;
Ok(HWISigner {
fingerprint: device.fingerprint,
client,
})
}
}
impl SignerCommon for HWISigner {
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
SignerId::Fingerprint(self.fingerprint)
}
}
/// This implementation ignores `sign_options`
impl TransactionSigner for HWISigner {
fn sign_transaction(
&self,
psbt: &mut PartiallySignedTransaction,
_sign_options: &crate::SignOptions,
_secp: &crate::wallet::utils::SecpCtx,
) -> Result<(), SignerError> {
psbt.combine(self.client.sign_tx(psbt)?.psbt)
.expect("Failed to combine HW signed psbt with passed PSBT");
Ok(())
}
}

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,30 @@
//! }
//! }
//!
//! 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,
//! _sign_options: &SignOptions,
//! _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 +83,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 +155,26 @@ 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),
/// Error while signing using hardware wallets
#[cfg(feature = "hardware-signer")]
HWIError(hwi::error::Error),
}
#[cfg(feature = "hardware-signer")]
impl From<hwi::error::Error> for SignerError {
fn from(e: hwi::error::Error) -> Self {
SignerError::HWIError(e)
}
}
impl From<sighash::Error> for SignerError {
fn from(e: sighash::Error) -> Self {
SignerError::SighashError(e)
}
}
impl fmt::Display for SignerError {
@@ -163,27 +185,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 +241,69 @@ 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: Option<usize>,
input_index: usize,
sign_options: &SignOptions,
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,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError>;
}
impl<T: InputSigner> TransactionSigner for T {
fn sign_transaction(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError> {
for input_index in 0..psbt.inputs.len() {
self.sign_input(psbt, input_index, sign_options, 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,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError> {
let input_index = input_index.unwrap();
if input_index >= psbt.inputs.len() {
return Err(SignerError::InputIndexOutOfRange);
}
@@ -218,19 +314,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 +345,49 @@ 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, sign_options, 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,
sign_options: &SignOptions,
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 +397,136 @@ 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()
&& sign_options.sign_with_tap_internal_key
{
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| {
// Removing the leaves we shouldn't sign for
let should_sign = match &sign_options.tap_leaves_options {
TapLeavesOptions::All => true,
TapLeavesOptions::Include(v) => v.contains(lh),
TapLeavesOptions::Exclude(v) => !v.contains(lh),
TapLeavesOptions::None => false,
};
// Filtering out the leaves without our key
should_sign
&& !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 msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_ecdsa(msg, secret_key);
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
.expect("invalid or corrupted ecdsa signature");
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 msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_schnorr(msg, &keypair);
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair))
.expect("invalid or corrupted schnorr signature");
let 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 +561,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 +572,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 +623,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 +646,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()),
@@ -477,38 +695,99 @@ pub struct SignOptions {
///
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
pub allow_all_sighashes: bool,
/// Whether to remove partial_sigs from psbt inputs while finalizing psbt.
///
/// Defaults to `true` which will remove partial_sigs after finalizing.
pub remove_partial_sigs: bool,
/// Whether to try finalizing psbt input after the inputs are signed.
///
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
pub try_finalize: bool,
/// Specifies which Taproot script-spend leaves we should sign for. This option is
/// ignored if we're signing a non-taproot PSBT.
///
/// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
pub tap_leaves_options: TapLeavesOptions,
/// Whether we should try to sign a taproot transaction with the taproot internal key
/// or not. This option is ignored if we're signing a non-taproot PSBT.
///
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
pub sign_with_tap_internal_key: bool,
}
/// Customize which taproot script-path leaves the signer should sign.
#[derive(Debug, Clone, PartialEq)]
pub enum TapLeavesOptions {
/// The signer will sign all the leaves it has a key for.
All,
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
/// some of the specified leaves, if it doesn't have the right key to sign them.
Include(Vec<taproot::TapLeafHash>),
/// The signer won't sign the specified leaves.
Exclude(Vec<taproot::TapLeafHash>),
/// The signer won't sign any leaf.
None,
}
impl Default for TapLeavesOptions {
fn default() -> Self {
TapLeavesOptions::All
}
}
#[allow(clippy::derivable_impls)]
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
trust_witness_utxo: false,
assume_height: None,
allow_all_sighashes: false,
remove_partial_sigs: true,
try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true,
}
}
}
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 +805,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 +826,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 +889,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 +985,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 +1004,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 +1070,20 @@ 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,
_sign_options: &SignOptions,
_secp: &SecpCtx,
) -> Result<(), SignerError> {
Ok(())
}
}
@@ -756,7 +1098,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>,
@@ -150,6 +147,8 @@ pub(crate) struct TxParams {
pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>,
pub(crate) current_height: Option<u32>,
pub(crate) allow_dust: bool,
}
#[derive(Clone, Copy, Debug)]
@@ -170,7 +169,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 +181,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 +336,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 +412,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 +508,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,
@@ -517,7 +517,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
}
}
/// Finish the building the transaction.
/// Finish building the transaction.
///
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
///
@@ -545,9 +545,33 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
self.params.rbf = Some(RbfValue::Value(nsequence));
self
}
/// Set the current blockchain height.
///
/// This will be used to:
/// 1. Set the nLockTime for preventing fee sniping.
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
/// mature at `current_height`, we ignore them in the coin selection.
/// If you want to create a transaction that spends immature coinbase inputs, manually
/// add them using [`TxBuilder::add_utxos`].
///
/// In both cases, if you don't provide a current height, we use the last sync height.
pub fn current_height(&mut self, height: u32) -> &mut Self {
self.params.current_height = Some(height);
self
}
/// Set whether or not the dust limit is checked.
///
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
self.params.allow_dust = allow_dust;
self
}
}
impl<'a, 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;
@@ -576,6 +600,9 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should either provide the utxos that the
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
///
/// When bumping the fees of a transaction made with this option, you probably want to
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
///
@@ -606,6 +633,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
///
/// [`allow_shrinking`]: Self::allow_shrinking
/// [`add_recipient`]: Self::add_recipient
/// [`add_utxos`]: Self::add_utxos
/// [`drain_wallet`]: Self::drain_wallet
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
self.params.drain_to = Some(script_pubkey);
@@ -614,8 +642,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 +866,7 @@ mod test {
},
txout: Default::default(),
keychain: KeychainKind::External,
is_spent: false,
},
LocalUtxo {
outpoint: OutPoint {
@@ -846,6 +875,7 @@ mod test {
},
txout: Default::default(),
keychain: KeychainKind::Internal,
is_spent: false,
},
]
}

View File

@@ -9,13 +9,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use bitcoin::blockdata::script::Script;
use bitcoin::secp256k1::{All, Secp256k1};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
// De-facto standard "dust limit" (even though it should change based on the output type)
pub const DUST_LIMIT_SATOSHI: u64 = 546;
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
// spending using CSV in order to enforce CSV rules
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
@@ -28,18 +26,19 @@ pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
/// Trait to check if a value is below the dust limit
/// Trait to check if a value is below the dust limit.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate
// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
// instead of a <= etc. The constant value for the dust limit is not public on purpose, to
// encourage the usage of this trait.
// instead of a <= etc.
pub trait IsDust {
/// Check whether or not a value is below dust limit
fn is_dust(&self) -> bool;
fn is_dust(&self, script: &Script) -> bool;
}
impl IsDust for u64 {
fn is_dust(&self) -> bool {
*self <= DUST_LIMIT_SATOSHI
fn is_dust(&self, script: &Script) -> bool {
*self < script.dust_value().as_sat()
}
}
@@ -141,27 +140,27 @@ pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)]
mod test {
use super::{
check_nlocktime, check_nsequence_rbf, BLOCKS_TIMELOCK_THRESHOLD,
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
SEQUENCE_LOCKTIME_TYPE_FLAG,
};
use crate::types::FeeRate;
use crate::bitcoin::Address;
use std::str::FromStr;
#[test]
fn test_fee_from_btc_per_kb() {
let fee = FeeRate::from_btc_per_kvb(1e-5);
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
}
fn test_is_dust() {
let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe")
.unwrap()
.script_pubkey();
assert!(script_p2pkh.is_p2pkh());
assert!(545.is_dust(&script_p2pkh));
assert!(!546.is_dust(&script_p2pkh));
#[test]
fn test_fee_from_sats_vbyte() {
let fee = FeeRate::from_sat_per_vb(1.0);
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
}
#[test]
fn test_fee_default_min_relay_fee() {
let fee = FeeRate::default_min_relay_fee();
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8")
.unwrap()
.script_pubkey();
assert!(script_p2wpkh.is_v0_p2wpkh());
assert!(293.is_dust(&script_p2wpkh));
assert!(!294.is_dust(&script_p2wpkh));
}
#[test]

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]

BIN
static/bdk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB