Compare commits

...

215 Commits

Author SHA1 Message Date
Steve Myers
de46a51208 Bump version to 0.27.2 2023-03-15 11:35:10 -05:00
Steve Myers
e8acafce8e Merge bitcoindevkit/bdk#884: Update esplora client dependency to version 0.4
bb2b2d6dd8 Update esplora-client dependency to version 0.4 (Steve Myers)

Pull request description:

  ### Description

  Update the `esplora-client` to the new `0.4` release.

  ### Notes to the reviewers

  The `esplora-client` was updated in bitcoindevkit/rust-esplora-client#39 to not include default `rust-bitcoin` dependencies.

  ### Changelog notice

  - Update the `esplora-client` optional dependency to version `0.4`.

  ### 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:
  vladimirfomene:
    ACK bb2b2d6dd8
  rajarshimaitra:
    tACK bb2b2d6dd8

Tree-SHA512: 0f277106166ab4a8144a641ac9507f1a32d735f99c0b73a026ff6e0b82589110a71fe81f880caa01dab247572f3146b771cc28bdab5b22bcd87ff628e15b2e1a
2023-03-15 11:11:21 -05:00
Steve Myers
bb2b2d6dd8 Update esplora-client dependency to version 0.4 2023-03-14 23:14:28 -05:00
benthecarman
87c558c9cf Set default-features = false for rust-bitcoin 2023-03-07 20:45:53 -06:00
Steve Myers
b111f97c58 Set dev-dependency base64ct version to <1.6.0 2023-03-06 20:43:31 -06:00
Steve Myers
7a8e6609b1 Bump version to 0.27.1 2023-02-16 11:52:05 -06:00
Steve Myers
4ec6f3272e Update rusqlite from 0.27.0 to 0.28.0 2023-02-15 14:47:51 -06:00
Steve Myers
553df318ff Bump version to 0.27.0 2023-02-10 22:37:53 -06:00
Steve Myers
9e2e6411f2 Fix ci Dockerfile.ledger 2023-02-10 22:37:52 -06:00
Steve Myers
5d48e37926 Bump version to 0.27.0-rc.1 2023-02-03 16:06:57 -06:00
Steve Myers
c2a42493fd Merge bitcoindevkit/bdk#852: Bump version to 0.27.0
0c2570ae07 Pin dev-dependency zip to 0.6.3 (Steve Myers)
e83bb7c4dc Bump version to 0.27.0 (Steve Myers)

Pull request description:

  ### Description

  Bump version to 0.27.0.

  ### Notes to the reviewers

  Updating changelog file again.

  ### 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:
  danielabrozzoni:
    ACK 0c2570ae07

Tree-SHA512: 334e0a6933be18152f266e3622b877ccf67dafadd3a641eaf9ffa4e671babafbedcdaef431e2195aa7e11945f164b8da44bd8a208c8e7287ce87684fb2459787
2023-02-03 16:01:31 -06:00
Steve Myers
0c2570ae07 Pin dev-dependency zip to 0.6.3 2023-02-03 12:13:20 -06:00
Steve Myers
e83bb7c4dc Bump version to 0.27.0 2023-02-02 12:31:59 -06:00
Steve Myers
46273fe72f Merge bitcoindevkit/bdk#847: Update electrsd to version 0.22
147a4ed141 Update electrsd to version 0.22 (Steve Myers)

Pull request description:

  ### Description

  Update `electrsd` dev-dependency to version `0.22`.

  ### Notes to the reviewers

  We're able to do this now that esplora was updated in #830 and our MSRV was bumped to `1.57.0` in #842.

  ### Changelog

  None.

  ### 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:
  rajarshimaitra:
    ACK 147a4ed141

Tree-SHA512: 20e0aebb02d786a4d426a2d2e66b78225a803eee2c2f6092b0cf8f18e6fe91e37ef7a74e109e3ad4d734af8de4dc71598f7e80f50d40187442dad323000c6090
2023-02-02 11:33:11 -06:00
Steve Myers
0b26fa75dc Merge bitcoindevkit/bdk#844: Update rust-miniscript to 9.0
cf8cd2f2b4 Update rust-miniscript to version 9.0, hwi to version 0.5 (Steve Myers)

Pull request description:

  ### Description

  A new [`rust-miniscript` release 9.0](https://github.com/rust-bitcoin/rust-miniscript/blob/master/CHANGELOG.md#900---november-5-2022) came out on Nov 14, updating to it to pickup the bug fixes. Also updating dependency`hwi` to new `0.5` version which used the `9.0` version of `rust-miniscript`.

  ### Notes to the reviewers

  This new version of `rust-miniscript` uses the same version of `rust-bitcoin` we are on, 0.29.1.

  ### Changelog notice

  Update rust-miniscript dependency to latest bug fix release 9.0.

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

Tree-SHA512: 12473f67d2a4388e3d93b91988233e067328c344bb993981b014e1f7469db82f12d8f68eb1bf093feb25c4428d10451d8f361497c71c4f696d19939d4be9d858
2023-02-02 10:44:33 -06:00
Steve Myers
35bbe2beef Merge bitcoindevkit/bdk#838: Add small clarification to docs
f0cec015b5 Add small clarification to docs (thunderbiscuit)

Pull request description:

  ### Description
  Very small fixes to documentation:
  1. I got a DM last week from a user who thought we had a bug with our timestamps. It turns out he was using the milliseconds version of the Unix timestamp in his project and didn't realize we were giving out a standard Unix timestamp. The docs now mention this explicitly.
  2. I noticed some small inconsistencies in the documentation on the public templates while porting them over to Kotlin. This PR also fixes that so that all templates use a common documentation wording.

  ### 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:
    Re-ACK f0cec015b5

Tree-SHA512: ad42278126a0613fb1ba15f4e0ca92e05038389ac2e6b1015ea045f30ee8e92a40d6c089c35d0492bba0dc6d71e44b29879bd37a8bc491ff6367a89cab958db2
2023-02-01 20:06:44 -06:00
Steve Myers
9e7bad8afa Merge bitcoindevkit/bdk#846: Update CHANGELOG and release process
4ada11f358 Update CHANGELOG and release process (Steve Myers)

Pull request description:

  ### Description

  Per discussion on discord with @danielabrozzoni this PR updates our release process to go back to updating our CHANGELOG.md file for each release.

  ### Notes to the reviewers

  This adds one more step the person making releases has to do but it isn't much.

  ### 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:
  danielabrozzoni:
    ACK 4ada11f358

Tree-SHA512: 80c116c90b4a5c5d4091678dd7714c9eee881c0f181dd220e878011001ad458db2bb81dea7dc13f2d768b9169c3644191d7969c533b784a5e26f5aa6c9e66fc2
2023-02-01 11:21:49 -06:00
Steve Myers
4ada11f358 Update CHANGELOG and release process 2023-01-31 14:08:14 -06:00
Steve Myers
cf8cd2f2b4 Update rust-miniscript to version 9.0, hwi to version 0.5 2023-01-31 13:54:52 -06:00
Steve Myers
147a4ed141 Update electrsd to version 0.22 2023-01-31 12:04:03 -06:00
Steve Myers
97f8fe3fd1 Merge bitcoindevkit/bdk#834: Document return type of sign method.
ff72078095 Document return type of `sign` method. (Thibaut Le Guilly)

Pull request description:

  Small thing but I had to look up the code to see what the returned boolean was about, thought it might be good to have in the docs.

  ### Description

  Documents the meaning of the returned value on the wallet `sign` method.

  I've just edited from github so skipped all the checks if that's not ok maybe someone else can update it properly.

ACKs for top commit:
  w0xlt:
    ACK ff72078095
  notmandatory:
    ACK ff72078095

Tree-SHA512: e0dd52a0af663b88d3fe0a20cb2909b9ba00bcf4f224b4f87cfd53fee0a199a01679dc5cd74aeb440326dfc52d45fa887534138d25df6fa1c8a38678be23204e
2023-01-31 11:50:38 -06:00
thunderbiscuit
f0cec015b5 Add small clarification to docs 2023-01-30 21:35:35 -05:00
Thibaut Le Guilly
ff72078095 Document return type of sign method.
Small thing but I had to look up the code to see what the returned boolean was about, thought it might be good to have in the docs.
2023-01-31 10:13:46 +09:00
Steve Myers
e678aad3c7 Merge bitcoindevkit/bdk#837: Derive PartialEq, Eq on SyncTime
45d41416ed Derive `PartialEq`, `Eq` on `SyncTime` (Max Fang)

Pull request description:

  ### Description

  This enables e.g. `assert_eq!` comparisons of `SyncTime` in tests (we use this)

  ### Changelog notice

  Derived `PartialEq`, `Eq` on `SyncTime`

  #### 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 45d41416ed
  evanlinjin:
    ACK 45d41416ed

Tree-SHA512: eb2dc87762c33874a3a521c2f020250e83401c0f0e784911de6139bc8bf8c56f40c93ad2aa93cf20058ba796efd050979bddc5414acdd6856003e7a9f88c75c6
2023-01-30 13:11:59 -06:00
Steve Myers
41dc7f7d0d Merge bitcoindevkit/bdk#831: Don't default to use async/await on wasm32
32912eaa05 Don't default to use `async`/`await` on `wasm32` (Elias Rohrer)

Pull request description:

  ### Description

  We don't automatically want to make the interface `async` based on the used architecture, but now require the user to explicitly set the `async-interface` feature.

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

Tree-SHA512: 41a8f3ab29508a359a8c0d96994e4fa97e52c15e8b8003c1988bcfe036cafa81d6210d446ed825672ce52aff684ebb328dc61b8ae3d25cda6f51a3cae838de58
2023-01-30 13:10:06 -06:00
Steve Myers
6b92a169ab Merge bitcoindevkit/bdk#814: Improve display error formatting
9019793bd4 Improve display error formatting (Yuki Kishimoto)

Pull request description:

  ### Description

  Closes #555

  ### 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:
  notmandatory:
    ACK 9019793bd4

Tree-SHA512: 44f0c7e0374e255bf99cbbfb01f2af76746e52afe67ace1595d2830bd60be99f3c8726e6ad9744878978257796a79b3933e7ef21c30a5e4b99a16aaf9ac7ff64
2023-01-30 13:08:29 -06:00
Max Fang
45d41416ed Derive PartialEq, Eq on SyncTime
This enables e.g. `assert_eq!` comparisons of `SyncTime` in tests.
2023-01-29 12:26:54 -08:00
Yuki Kishimoto
9019793bd4 Improve display error formatting 2023-01-27 18:21:56 +01:00
Elias Rohrer
32912eaa05 Don't default to use async/await on wasm32
We don't automatically want to make the interface `async` based on the
used architecture, but now require the user to explicitly set the
`async-interface` feature.
2023-01-27 11:09:26 -06:00
Steve Myers
2e7a220e39 Merge bitcoindevkit/bdk#842: Bump project MSRV to 1.57.0
b02bfb347d Bump project MSRV to 1.57.0, update stable for all jobs to 1.65.0 (Steve Myers)

Pull request description:

  ### Description

  Bump project MSRV from 1.56.1 to 1.57.0.

  Also bumped the `check-wasm` and `test_hardware_wallet` jobs rust STABLE version to 1.65.0 to match other jobs.

  ### Notes to the reviewers

  The `rustls` crate changed their MSRV to 1.57 on 2023-01-12 with a patch release from 0.20.7 to 0.20.8, rustls/rustls#1152. This breaks our CI builds that use `explora-client` 0.3 because it depends on a version of `ureq` that uses the latest `rustls`.

  ```
  rustls v0.20.8
  └── ureq v2.6.2
      └── esplora-client v0.3.0
          └── bdk v0.26.0 (/Users/steve/git/notmandatory/bdk)
      [build-dependencies]
      └── electrsd v0.21.1
          [dev-dependencies]
          └── bdk v0.26.0 (/Users/steve/git/notmandatory/bdk)
  ```

  https://github.com/rustls/rustls/blob/main/README.md

  ### Changelog notice

  Project MSRV changed from 1.56.1 to 1.57.0.

  ### 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: 86b3ec2a7412a5ca1d1f620a3a16ccf315ce6afc363fdaa096187054de2edca09e4ce394df86174bfe45640b8226ed919bd4dc2d6e2149842355cb66e5c834be
2023-01-27 11:01:07 -06:00
Steve Myers
b02bfb347d Bump project MSRV to 1.57.0, update stable for all jobs to 1.65.0 2023-01-27 10:33:43 -06:00
Steve Myers
0cce1ce982 Merge bitcoindevkit/bdk#815: Bump CI Rust stable version to 1.65.0
fb76c9ed9a ci: bump rust stable version to 1.65.0 (Yuki Kishimoto)
3a782b3b0d Fix Clippy Rust 1.65 (Yuki Kishimoto)

Pull request description:

  ### Description

  * Fix clippy for Rust 1.65.0
  * Bump CI Rust stable version to 1.65.0

  ### 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:
    ReACK fb76c9ed9a

Tree-SHA512: 2f27915895cb203842831e525cc9efe0503269df07b7e58ab2a587e2991decf2990cb48abe8209c89cec5706a1d9c72387da532b70979ec100b5eed469051136
2023-01-24 12:08:51 -06:00
Yuki Kishimoto
fb76c9ed9a ci: bump rust stable version to 1.65.0 2023-01-07 15:08:56 +01:00
Yuki Kishimoto
3a782b3b0d Fix Clippy Rust 1.65 2023-01-07 15:08:37 +01:00
Steve Myers
eac739d395 Merge bitcoindevkit/bdk#830: Update esplora-client
6e5873ebba Update esplora-client (rajarshimaitra)

Pull request description:

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

  ### Description

  Update esplora-client and electrsd.

  Update: `electrsd v0.22` requires msrv at 1.57.. So removed from the PR for now..

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

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

  * [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: 7c126822f7a45a6d3053c81d78e20340d64b159e7433ea333dc4bf5611316938718e16a65fcb37c59ec5e7e5d74d514bc312e49eeb49167a22b0eb2269203488
2023-01-02 20:23:14 -06:00
rajarshimaitra
6e5873ebba Update esplora-client 2023-01-01 11:23:54 +05:30
Steve Myers
3205f0c16d Bump version to 0.26.0 2022-12-26 13:55:48 -08:00
Steve Myers
5f0870a741 Merge bitcoindevkit/bdk#821: [#344] Add assert_matches
14bc9c0e35 [#344] Add assert_matches Replace assert!(matches! with assert_matches! everywhere Convert assert! to assert_eq! in 2 places (Jeremy Mawson)

Pull request description:

  [#344] Add assert_matches

ACKs for top commit:
  notmandatory:
    ACK 14bc9c0e35
  danielabrozzoni:
    utACK 14bc9c0e35

Tree-SHA512: 730fed9c8c22b0725b1337140636def1a059ac78e4ae16f1abd4c7f379628d9329ccd3ed28e4cbab58e9ace5f349333cb5fa83ec43d507d7a7609601efebc9e1
2022-12-26 13:38:48 -08:00
Steve Myers
5a483472c1 Merge bitcoindevkit/bdk#825: Bump hwi to 0.4.0
4cad18bbca Bump hwi to 0.4.0 (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 -->

  ### Changelog notice

  - Bump hwi to 0.4.0

  ### 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:
    ACK 4cad18bbca

Tree-SHA512: 6c73c091da743734ed87e4792f3c2a838ac7aa252388309ad46e017cc7e9b338bfdb7ed70925c8041a7d22a4d12e2a984e902619b1b7735c645e6c1b6855aeb3
2022-12-26 13:37:05 -08:00
Steve Myers
8d4cc3920a Merge bitcoindevkit/bdk#805: electrum: add validate_domain to ElectrumBlockchainConfig
2451c00268 electrum: add validate_domain to ElectrumBlockchainConfig (Igor Cota)

Pull request description:

  ### Description

  The purpose of the PR is to be able to configure both `stop_gap` **and** `validate_domain`. Perhaps there are nicer ways.

  #### 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'm linking the issue being fixed by this PR

  Issue in https://github.com/bitcoindevkit/bdk/issues/804

ACKs for top commit:
  notmandatory:
    ACK 2451c00268

Tree-SHA512: e10e3a027c202c8e680e5181f2a98c9ca30fa7773660cc47bef93f557cdc94c09e46b154b0edee65e498a49bc997cc9172eb3782dc4310d5775c0763d8c2ca4a
2022-12-23 16:28:50 -08:00
Jeremy Mawson
14bc9c0e35 [#344] Add assert_matches
Replace assert!(matches! with assert_matches! everywhere
Convert assert! to assert_eq! in 2 places
2022-12-23 22:05:53 +10:00
Igor Cota
2451c00268 electrum: add validate_domain to ElectrumBlockchainConfig 2022-12-20 10:44:57 +01:00
Daniela Brozzoni
4cad18bbca Bump hwi to 0.4.0 2022-12-19 11:18:44 +01:00
Daniela Brozzoni
634a0575cb Merge bitcoindevkit/bdk#812: Implement ordering for TransactionDetails
d3d07564f2 Implement ordering for TransactionDetails (benthecarman)

Pull request description:

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

  ### Description

  Pulled from https://github.com/BitcoinDevShop/mutiny-web-poc/pull/189

  Wallets should be able to sort the transactions easily, this makes it so you can just all `sort` on a list of tx details instead of needing to implement the sort_by yourself

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

Tree-SHA512: d2b53dc959897b71d71794f3c919f86f8b7886e6ea5f6ac511cfaca0c19b2f78784a23491b3010380cf41da7ef69fd9ca1be75437c53eca1c60bd6651d1fec41
2022-12-19 09:43:26 +01:00
benthecarman
d3d07564f2 Implement ordering for TransactionDetails 2022-12-17 14:34:09 -06:00
Steve Myers
0b768d6f0b Merge bitcoindevkit/bdk#818: Fix wrong key origin path in public descriptor templates
d6e730f18a Fix wrong key origin path in public descriptor templates (Yuki Kishimoto)

Pull request description:

  ### Description

  Fixes #817

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

Top commit has no ACKs.

Tree-SHA512: df08007b27d44a889cf94fe3e2c608cb92f963737c937ca952288d0468636308b09124f04b5dbd793de81d6cb06cf076c89358356abfe8417775ab1ab5650698
2022-12-16 15:40:29 -06:00
Steve Myers
ec9aefac6b Merge bitcoindevkit/bdk#808: fix: make save_tx order independent
d72aa7ebc0 chore: make TxCache.save_txs can order independent (bodymindarts)

Pull request description:

  fulcrum doesn't return txs in the order they are requested in. This PR makes the `TxCache` insensitive to this behaviour.

Top commit has no ACKs.

Tree-SHA512: fe17345ba26dd047ee18d23eb7341849a80ada71a72826dc653616014ca1371b07bddcdc4983e842d4cbfd951b192de21849a5f00662d40fdc8033f13ac2bb75
2022-12-16 15:37:45 -06:00
bodymindarts
d72aa7ebc0 chore: make TxCache.save_txs can order independent 2022-12-16 15:09:44 -06:00
Daniela Brozzoni
99930af12e Merge bitcoindevkit/bdk#820: Make README.md code examples compile without errors
d1e5b87bfc Make README.md code examples compile without errors (Peter Todd)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    ACK d1e5b87bfc

Tree-SHA512: 343c62acfcfe9e2ea9af8a89d5a1653f9076c50a6fe2efcb23bc838d10d388395872080f2d98f21dd98fdd18a1b79457eb68dae3d73f02aa817f1b9045f19a25
2022-12-15 09:06:17 +01:00
Yuki Kishimoto
d6e730f18a Fix wrong key origin path in public descriptor templates 2022-12-14 21:31:22 +01:00
Peter Todd
d1e5b87bfc Make README.md code examples compile without errors 2022-12-14 15:26:18 -05:00
Daniela Brozzoni
c101dea460 Merge bitcoindevkit/bdk#822: Update ci nightly-docs workflow to use nightly-2022-12-14
9ddd502538 Update ci nightly-docs workflow to use nightly-2022-12-14 (Steve Myers)

Pull request description:

  ### Description

  The current nightly version we were using for the nightly-docs workflow is failing so I updated it to tonight's (2022-12-14)
  version.

  ### Notes to the reviewers

  I decided to select another hard-coded date nightly version so we don't run the risk of some random nightly release breaking this workflow.

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

Tree-SHA512: 650fc4880c1de37f3cd581ba1e2f416ac732d06e03b1dea2878b5051bf38ff809dd965baa0987c7ce47e5aa5e30932cedad3dce17fca6b787e6ace72d604c591
2022-12-14 20:15:47 +01:00
Steve Myers
9ddd502538 Update ci nightly-docs workflow to use nightly-2022-12-14 2022-12-14 11:53:54 -06:00
Daniela Brozzoni
a5d345fff2 Merge bitcoindevkit/bdk#811: Downgrade ubuntu to 20.04 for test_hardware_wallet CI job
11dcc14374 Downgrade ubuntu to 20.04 for test_hardware_wallet CI job (Steve Myers)

Pull request description:

  ### Description

  As suggested by bitcoindevkit/rust-hwi#61, downgrade ubuntu version to 20.04 (instead of using latest), to fix `test_hardware_wallet` CI job.

  ### 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:
    ACK 11dcc14374

Tree-SHA512: 26de7bb0e562065a9d1071fb292f8532894f921c3557c418e95b05dfff529d8e5bf1751d8b5136618d3b768f10d8af714882f7c453c91a08a9c5e9aafef976b5
2022-12-06 08:17:20 +01:00
Steve Myers
11dcc14374 Downgrade ubuntu to 20.04 for test_hardware_wallet CI job 2022-12-05 11:23:40 -06:00
Steve Myers
4c5ceaff14 Merge bitcoindevkit/bdk#806: Ensure there are no duplicated script_pubkeys in sqlite
b5fcddcf1a Add sqlite migration to drop duplicated script_pubkeys rows (Steve Myers)
21c96c9c81 Add test for issue #801 (Alekos Filini)
c51d544932 [wip] Ensure there are no duplicated script_pubkeys in sqlite (Alekos Filini)

Pull request description:

  ### Description

  Add a `UNIQUE` constraint on the script_pubkeys table so that it doesn't grow constantly when caching new addresses.

  Fixes #801

  ### Notes to the reviewers

  Adding it to the 0.25 milestone since it's just a bugfix.

  Still in draft because I need to add extra migration queries to clean up existing dbs.

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

Tree-SHA512: 7b10e453bb38af5c4f80f77692a56e37259680e50f9c2c9e554a0e5f04fb9cab897da6476c6c9930f1c501b455472984a1c92c4f137cff49acdc390d2e705107
2022-11-30 09:05:47 -08:00
Steve Myers
b5fcddcf1a Add sqlite migration to drop duplicated script_pubkeys rows 2022-11-29 07:20:49 -08:00
Alekos Filini
d570ff2c65 Merge bitcoindevkit/bdk#803: Bump version to 0.25.0
5e56c3b3c1 Bump version to 0.25.0 (Steve Myers)

Pull request description:

  ### Description

  Bump version to 0.25.0

ACKs for top commit:
  afilini:
    ACK 5e56c3b3c1

Tree-SHA512: b1fc49caed9676d4e3db2a625b6209fffa19694ada2e2ff4d3e5d5cbbd6816ff03924387d66bf3a8b8ec4a3b44bf5d00d95cc450427e5b669e88af60400b02c9
2022-11-28 10:55:16 +01:00
Alekos Filini
21c96c9c81 Add test for issue #801 2022-11-26 15:11:09 +01:00
Alekos Filini
c51d544932 [wip] Ensure there are no duplicated script_pubkeys in sqlite
Add a `UNIQUE` constraint on the script_pubkeys table so that it doesn't
grow constantly when caching new addresses.

Fixes #801
2022-11-26 15:04:34 +01:00
Steve Myers
5e56c3b3c1 Bump version to 0.25.0 2022-11-24 22:01:01 -08:00
Steve Myers
235961a934 Merge bitcoindevkit/bdk#746: Add mnemonic_to_descriptors example
df905a8d5e Add mnemonic to descriptors example. (Vladimir Fomene)

Pull request description:

  ### Description

  Using *bdk-cli* it is simple for a new user to generate a mnemonic phrase and descriptors. This might
  not be clear for new users when using *bdk* itself.

  ### Notes to the reviewers

  This was initially requested by one user but might be relevant for other users as well.

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

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

Tree-SHA512: ccaab775d664d1d5ad6f4cd4fb11f9552c4662fbac747ad0487a016fd4a0fa6f13fcb4dffdc6f0d2f14ee432fcb2a72a8b8837b0a977ded649ec7d3d84e1c0ed
2022-11-24 12:52:42 -08:00
Vladimir Fomene
df905a8d5e Add mnemonic to descriptors example.
This was initially requested by one user but might be
relevant for other users as well.
2022-11-24 21:18:13 +03:00
Steve Myers
8b68cf9546 Merge bitcoindevkit/bdk#800: fix: ensure the key network is updated in the KeyMap as well
150f4d6f41 fix: ensure the key network is updated in the KeyMap as well (Alekos Filini)

Pull request description:

  ### Description

  Otherwise we may have inconsistencies with keys in the descriptor that have a network and keys in the keymap that are different.

  ### Notes to the reviewers

  Adding it to the `0.25` milestone since it's just a quick fix

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

ACKs for top commit:
  notmandatory:
    ACK 150f4d6f41

Tree-SHA512: df87323cc29cf74f54a0867bfbe9fe24543de69dc6443656bd920458c9055d4b05614430c89bb470a8a6f7d10da023a0fb107b1cfb0fcc38e50f0579b6411a33
2022-11-24 09:23:10 -08:00
Alekos Filini
150f4d6f41 fix: ensure the key network is updated in the KeyMap as well
Otherwise we may have inconsistencies with keys in the descriptor that
have a network and keys in the keymap that are different.
2022-11-23 17:58:47 +01:00
Steve Myers
1c95ca33a8 Merge bitcoindevkit/bdk#526: Add code example for each supported backend
f99a6b9f43 add `esplora_backend` example. (w0xlt)
aedbc8c97d add `electrum_backend` example. (w0xlt)

Pull request description:

  This PR adds code example for connecting to Esplora, Electrum Server, Neutrino and Bitcoin Core.
  Also shows how to retrieve balance, sign and broadcast transactions.

  To test:
  ```
  cd examples/backend/
  cargo run electrum
  cargo run esplora
  cargo run neutrino
  cargo run rpc_core
  ```

ACKs for top commit:
  rajarshimaitra:
    tACK f99a6b9f43

Tree-SHA512: 1d99129f14d83d9a833cee1587fe0eb3e5da4c83ae9008fb3e510be96a874dc86f800f203f68f5da70648a911709107cf0f286c2623808dc97dd63b7addef16b
2022-11-22 08:13:16 -08:00
Steve Myers
108edc3a6b Merge bitcoindevkit/bdk#785: Fix wallet export rescan height
e9bbb8724f Fix wallet export rescan height (LLFourn)

Pull request description:

  It would return the latest transaction height rather than the earliest as the height to rescan from.

  Found by @evanlinjin  and I while implementing `bdk_core` stuff into bdk's wallet.

  ### Changelog notice

  - Fix wallet export transaction height

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing

ACKs for top commit:
  rajarshimaitra:
    tACK e9bbb8724f
  notmandatory:
    ACK e9bbb8724f

Tree-SHA512: 9b29ef0df39d26806f48b38fa5c3643bad32f58b993ffdcfc7811aca64a025bd8f163967321f874aa2ef3d29c3e7bc6e2f44d348306a37111f4def036d4c095e
2022-11-22 06:30:17 -08:00
w0xlt
f99a6b9f43 add esplora_backend example. 2022-11-01 19:59:55 -03:00
w0xlt
aedbc8c97d add electrum_backend example. 2022-11-01 19:59:46 -03:00
Steve Myers
5c42102c79 Bump version to 0.24.0 2022-10-26 23:27:30 -05:00
Steve Myers
5d5b2fb88c Merge bitcoindevkit/bdk#765: Fix how descriptor checksums are calculated
648282e602 Update docs and tests based on review comments (Steve Myers)
60057a7bf7 Deprecate backward compatible get_checksum_bytes, get_checksum functions (Steve Myers)
e2a4a5884b Ensure backward compatibility of the  "checksum inception" bug (志宇)
fd34956c29 `get_checksum_bytes` now checks input data for checksum (志宇)

Pull request description:

  ### Description

  Previously, the methods `get_checksum_bytes` and `get_checksum` do not check input data to see whether the input data already has a checksum.

  This PR does the following:

  * Introduce a `exclude_hash: bool` flag for `get_checksum_bytes`, that excludes the checksum portion of the original data when calculating the checksum. In addition to this, if the calculated checksum does not match the original checksum, an error is returned for extra safety.
  * Ensure `Wallet` is still backwards compatible with databases created with the "checksum inception" bug.

  ### Notes to the reviewers

  Thank you.

  ### Changelog notice

  Fix the "checksum inception" bug, where we may accidentally calculate the checksum of a descriptor that already has a checksum.

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

Top commit has no ACKs.

Tree-SHA512: 7ea2721dcd56459b6996e56a3ddfc3559a0c64869a08f5312a8f0f4fcb5dbef7ac7461a4ab017acde4a62fed02d8a620c402dd384323aba85736610514fcb7e1
2022-10-26 22:39:38 -05:00
Steve Myers
9cb6f70fc0 Merge branch 'master' into fix_wallet_checksum 2022-10-26 22:01:07 -05:00
Steve Myers
5720e38033 Merge bitcoindevkit/bdk#770: Upgrade to rust-bitcoin 0.29
c7a43d941f Remove unused code (Alekos Filini)
1ffd59d469 Upgrade to rust-bitcoin 0.29 (Alekos Filini)
ae4f4e5416 Upgrade `rand` to `0.8` (Alekos Filini)
9854fd34ea Remove deprecated address validators (Alekos Filini)

Pull request description:

  ### Description

  Upgrade BDK to rust-bitcoin 0.29

  Missing pieces:

  - [x] rust-miniscript `update_output_with_descriptor` - rust-bitcoin/rust-miniscript#465
  - [x] rust-miniscript 8.0.0 release - rust-bitcoin/rust-miniscript#462
  - [x] Upgrade rust-hwi to bitcoin 0.29 bitcoindevkit/rust-hwi#50
  - [x] Upgrade esplora-client to bitcoin 0.29 https://github.com/bitcoindevkit/rust-esplora-client/pull/20
  - [x] Upgrade rand to 0.8 like secp256k1 did

  ### Notes to the reviewers

  The commits still need to be reordered and cleaned up

  ### Changelog notice

  - Upgrade rust-bitcoin to 0.29
  - Remove deprecated "address validators"

  ### 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:
    ACK c7a43d941f

Tree-SHA512: 718a1baf3613b31ec1de39fe63467ebee38617963a4ce0670a617e20fe4f46a57c5786933cdde6cfad9fc76ce0af08843f58844fb4a89f5948cb42c697f802ef
2022-10-26 21:46:41 -05:00
LLFourn
e9bbb8724f Fix wallet export rescan height
It would return the latest transaction height rather than the earliest :S
2022-10-26 12:35:21 +08:00
Steve Myers
648282e602 Update docs and tests based on review comments 2022-10-25 11:20:22 -05:00
Alekos Filini
c7a43d941f Remove unused code 2022-10-25 12:14:36 +02:00
Alekos Filini
1ffd59d469 Upgrade to rust-bitcoin 0.29 2022-10-25 11:16:02 +02:00
Alekos Filini
ae4f4e5416 Upgrade rand to 0.8 2022-10-25 11:15:59 +02:00
Alekos Filini
9854fd34ea Remove deprecated address validators 2022-10-25 11:08:47 +02:00
Steve Myers
60057a7bf7 Deprecate backward compatible get_checksum_bytes, get_checksum functions
Rename replacement functions calc_checksum_bytes and calc_checksum
2022-10-24 14:24:54 -05:00
Alekos Filini
ea47d7a35b Merge bitcoindevkit/bdk#758: Add HWI example in docs
1437e1ecfe Add the hardware_signer example (Daniela Brozzoni)
1a71eb1f47 Update the hardwaresigner module documentation (Daniela Brozzoni)
0695e9fb3e Bump HWI to 0.2.3 (Daniela Brozzoni)
a4a43ea860 Re-export HWI if the hardware-signer feature is set (Daniela Brozzoni)

Pull request description:

  ### Description

  ### Notes to the reviewers

  ### Changelog notice

  - bdk re-exports the `hwi` create when the feature `hardware-signer` is on
  - Add `examples/hardware_signer.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)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  afilini:
    ACK 1437e1ecfe

Tree-SHA512: 181f4d14dce11e19497fbf30e0af8de21c2c210d37129d7d879ed5670ed09a25be1c8d371389c431e18df9e76870cf5e4afe7b29a6c05fe59b3e1816bc8cf673
2022-10-24 10:53:39 +02:00
Steve Myers
f2181f5467 Merge bitcoindevkit/bdk#782: Make psbt mod public and add required docs
34987d58ec Make psbt mod public and add required docs (Steve Myers)

Pull request description:

  ### Description

  Make psbt mod public and add required docs. The module needs to be public so `bdk-ffi` can expose the new PSBT `fee_amount()` and `fee_rate()` functions.

  ### Notes to the reviewers

  I should have done this as part of #728.

  ### Changelog notice

  Make psbt module public to expose PsbtUtils trait to downstream projects.

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

ACKs for top commit:
  rajarshimaitra:
    Concept + tACK 34987d58ec

Tree-SHA512: 99e91e948bccb7593a3da3ac5468232103d4ba90ad4e5888ef6aebb0d16511ad3a3286951779789c05587b4bb996bc359baa28b0f4c3c55e29b24bfc12a10073
2022-10-21 17:58:47 -05:00
Steve Myers
34987d58ec Make psbt mod public and add required docs 2022-10-18 15:26:12 -05:00
Daniela Brozzoni
1c76084db8 Merge bitcoindevkit/bdk#779: Add signature grinding for ECDSA signatures
68dd6d2031 Add signature grinding for ECDSA signatures (Vladimir Fomene)

Pull request description:

  ### Description

  This PR adds a new field called `allow_grinding`
  in the Signer's `SignOptions` struct that is used
  to determine whether or not to grind an ECDSA signature during the signing process.

  ### Changelog notice

  Breaking change: the BDK Signer now produces low-R signatures by default, saving one byte. If you want to preserve the original behavior, set `allow_grinding` in the `SignOptions` to `false`.

  ### Notes to the reviewers

  This PR resolves issue #695

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

  #### 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 68dd6d2031
  rajarshimaitra:
    ACK 68dd6d2031

Tree-SHA512: 6472338c611b4b32986cf66fcd313ef84f17f5b0ae9e7991ea7da47142641ab812f8b325d4d18314e1a58abe462683101160e62e2363a048fdab3f18aee4d699
2022-10-17 11:48:19 +01:00
Vladimir Fomene
68dd6d2031 Add signature grinding for ECDSA signatures
This PR adds a new field called `allow_grinding`
in the Signer's `SignOptions` struct that is used
to determine whether or not to grind an ECDSA signature
during the signing process.
2022-10-17 12:27:35 +03:00
Daniela Brozzoni
1437e1ecfe Add the hardware_signer example 2022-10-13 10:46:49 +01:00
Daniela Brozzoni
1a71eb1f47 Update the hardwaresigner module documentation
Add a little example on how to use the HWISigner, slightly improve
the module description
2022-10-13 10:46:47 +01:00
Daniela Brozzoni
0695e9fb3e Bump HWI to 0.2.3 2022-10-12 14:24:09 +01:00
Daniela Brozzoni
a4a43ea860 Re-export HWI if the hardware-signer feature is set 2022-10-12 14:23:42 +01:00
Daniela Brozzoni
b627455b8f Merge bitcoindevkit/bdk#780: Update psbt_signer example to use descriptor! macro
1331193800 Update psbt_signer example to use descriptor! macro (Steve Myers)

Pull request description:

  ### Description

  This is a small fix to the psbt_signer example to also use the `descriptor!` macro.

  ### Notes to the reviewers

  I also added more docs to at the beginning of the example.

  ### Changelog notice

  None

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

  * [x] I've added docs for the new example

ACKs for top commit:
  danielabrozzoni:
    ACK 1331193800

Tree-SHA512: 602fa317313dea77bc4804abce500db33d5834625704019c6590ae6b80cf339cbaddffef667eaef2696e8e769756a2c2405c84109409ef33816db60d3df4d53d
2022-10-12 11:07:29 +01:00
Steve Myers
1331193800 Update psbt_signer example to use descriptor! macro 2022-10-09 21:39:04 -05:00
Steve Myers
7de8be46c0 Add enhancement request github issue template 2022-10-01 10:14:37 -05:00
Alekos Filini
55145f57a1 Bump version to 0.23.0 2022-09-29 20:57:36 +02:00
Steve Myers
8e8fd49e04 Merge bitcoindevkit/bdk#764: Use the esplora client crate
d7bfe68e2d Fix broken nightly docs (Alekos Filini)
b11c86d074 Rename internal esplora modules, fix docs (Alekos Filini)
b5b92248c7 Rename esplora features to -async and -blocking (Alekos Filini)
cf2bc388f2 Re-export `esplora_client` (Elias Rohrer)
5baf46f84d Use the external esplora client library (Alekos Filini)

Pull request description:

  ### Description

  Use the external esplora client crate now that it's published

  ### Changelog notice

  - Start using the external esplora client crate
  - Deprecate the `use-esplora-reqwest` and `use-esplora-ureq` features in favor of `use-esplora-async` and `use-esplora-blocking`

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

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

Tree-SHA512: 23bd47536fe6f723602cbcc51d909eb9aed28376430f4453eea832e30a587be3d312cdca993d114391132bfb39c48637030f974ab1a742f7defe44f40a82ef8b
2022-09-29 11:46:05 -05:00
Alekos Filini
d7bfe68e2d Fix broken nightly docs 2022-09-29 12:00:11 +02:00
Alekos Filini
b11c86d074 Rename internal esplora modules, fix docs 2022-09-29 12:00:09 +02:00
志宇
e2a4a5884b Ensure backward compatibility of the "checksum inception" bug
`Wallet` stores the descriptors' checksum in the database for safety.
Previously, the checksum used was a checksum of a descriptor that
already had a checksum.

This PR allows for backward-compatibility of databases created with this
bug.
2022-09-29 14:45:24 +08:00
志宇
fd34956c29 get_checksum_bytes now checks input data for checksum
If `exclude_hash` is set, we split the input data, and if a checksum
already existed within the original data, we check the calculated
checksum against the original checksum.

Additionally, the implementation of `IntoWalletDescriptor` for `&str`
has been refactored for clarity.
2022-09-29 13:06:03 +08:00
Alekos Filini
b5b92248c7 Rename esplora features to -async and -blocking 2022-09-28 21:08:18 +02:00
Elias Rohrer
cf2bc388f2 Re-export esplora_client 2022-09-28 21:08:16 +02:00
Alekos Filini
5baf46f84d Use the external esplora client library 2022-09-28 21:08:14 +02:00
Daniela Brozzoni
a8cf34e809 Merge bitcoindevkit/bdk#763: Fix Wallet::descriptor_checksum to actually return the checksum
af0b3698c6 Fix `Wallet::descriptor_checksum` to actually return the checksum (志宇)

Pull request description:

  ### Description

  `Wallet::descriptor_checksum` should return the checksum, not the descriptor without the checksum.

  ### Notes to the reviewers

  Please merge.

  ### Changelog notice

  Fix `Wallet::descriptor_checksum` to actually return the descriptor checksum.

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

ACKs for top commit:
  danielabrozzoni:
    ACK af0b3698c6 - I run the test you added with the old code, and verified that the bug was there. I then run the test (with a few more dbg!() expressions) and manually verified that the problem is fixed.
  notmandatory:
    ACK af0b3698c6

Tree-SHA512: 64a5b1f4708db6f6dcff2f6ef5e0a4c7d206e764d7602ea803c8cc002410326eb59eee770d9c91694dfbf07193ee3ff6bfe163bcbb3506cd7b2a6b59814a3e5c
2022-09-28 15:26:22 +02:00
志宇
af0b3698c6 Fix Wallet::descriptor_checksum to actually return the checksum 2022-09-27 21:56:25 +08:00
志宇
92ad4876c4 Add vscode filter to gitignore 2022-09-27 20:40:14 +08:00
Steve Myers
b14e4ee3a0 Merge bitcoindevkit/bdk#756: Remove genesis_block lazy initialization
e6f2d029fa Remove genesis_block lazy initialization (Shobit Beltangdy)

Pull request description:

  ### Description

  This commit contains a change to address issue #752

  cargo test runs successfully.

  ### Notes to the reviewers

  Hi, newbie here learning Rust and BDK!  I've removed the lazy_static block in this commit, and when learning about lazy_static also came across something called [once_cell](https://doc.rust-lang.org/std/cell/struct.OnceCell.html), [soon to be available in stdlib](https://github.com/rust-lang/rust/issues/74465).  It's like lazy_static but faster and is not a macro.  Shall I keep the lazy_static and create an issue to switch to once_cell in the future, or remove the lazy_static (as I've done) and create an issue anyway?

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

Tree-SHA512: 528f5fdfb0d7d1f7a83869b7a0de1b25dfcfafae2671c9229cdb4e5d80d11e5578d9325b3d95555e59f5dfb4ed6304c0c112a01a56b6a596e50dd62e310c516e
2022-09-26 17:33:35 -05:00
Shobit Beltangdy
e6f2d029fa Remove genesis_block lazy initialization
This commit contains a change to address issue #752

cargo test runs successfully.
2022-09-26 10:54:12 -07:00
Steve Myers
bbf524b3f9 Merge bitcoindevkit/bdk#754: Fix the release process
e1fa0b6695 Fix the new release process (Alekos Filini)

Pull request description:

  ### Description

  A few things I noticed while doing the 0.22 release.

  Follow-up of #544

  ### 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:
  notmandatory:
    ACK e1fa0b6695

Tree-SHA512: 5e60e73ba1f820fc39f62b75583e1125f911e578ab7263a20636e2a995d0efa10ba1b3b66a1e03d8a2ed61e32c00b53de9d6bbdb31de94db37c79fa5acdbf483
2022-09-26 11:45:40 -05:00
Daniela Brozzoni
dbf6bf5fdf Merge bitcoindevkit/bdk#761: Remove redundant duplicated keys check
e2bf9734b1 Remove redundant duplicated keys check (Alekos Filini)

Pull request description:

  ### Description

  This check is redundant since it's already performed by miniscript (see https://docs.rs/miniscript/7.0.0/miniscript/miniscript/analyzable/enum.AnalysisError.html#variant.RepeatedPubkeys) and it was incorrectly failing on tr descriptors that contain duplicated keys across different taproot leaves

  Fixes #760

  ### Changelog notice

  ### 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:
  danielabrozzoni:
    Code review ACK e2bf9734b1 - the code looks good to me, but I didn't test manually

Tree-SHA512: 3c557d741e34abf6336c7e257867f2c6f91a4be0024317af834f08ba7c0c64557bd74b643005254c9f04e953350b104b0d4d287f0d0528134b357a4adf580f87
2022-09-26 14:45:58 +02:00
Alekos Filini
aff41d6e1c Merge bitcoindevkit/bdk#730: Update compiler example to a Policy example
97b6fb06aa Add a policy example (rajarshimaitra)
da7670801b Update compiler example. (rajarshimaitra)

Pull request description:

  ### Description

  Fixes #729.

  There is an "unmaintained" warning in a old version of clap, which triggered the issue.

  Ideally, we should not have clap in bdk's dependency. It was only used for the `compiler.rs` example, which was a very tiny clap app compiling miniscript policies, and it wasn't really an example for bdk.

  This PR rewrites the example as a `policy.rs` which demos the BDK's Policy module and policy structures.

   - Use a `wsh(multi(2, Privkey, Pubkey))` descriptor, which has only one part private and other part public.
   - use `into_wallet_descriptor()` to turn that into a `Descriptor` and `KeyMap`.
   - Use the `KeyMap` to create a custom signer.
   - Extract the descriptor `Policy` structure from the given keymap.

   I am not very sure on how much this example is helpful. I still find it hard to read the Policy structure visually. But if Policy is something we want the user to know about descriptors and bdk wallets, this shows how to extract it for a simple multisig condition.

  Note: There is no use of `bdk::wallet` in the example. BDK uses the Policy extraction internally while transaction creation. But all these are exposed publicly, so can be used independently too.

  ### Questions:

  - Should we still have a `minscript::policy::compile()` example?  Which IIUC is very different from `bdk::policy:Policy`.  I didn't include it in this PR, because I am not sure if it fits inside bdk example categories.

   - Should we expose `extract_policy` as an wallet API? All though its possible to get policy without creating a wallet, why not let the wallet also spit one out for itself, if its useful?

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

Tree-SHA512: 8e3719fdad308a347d22377050b2f29e02a884ff7d4e57a05a06d078de0709b5bf70bbcb1a696d1e1cdfe02cdb470e5af643da7c775b67fe318046bd6b80f440
2022-09-26 14:13:46 +02:00
Alekos Filini
e2bf9734b1 Remove redundant duplicated keys check
This check is redundant since it's already performed by miniscript (see
https://docs.rs/miniscript/7.0.0/miniscript/miniscript/analyzable/enum.AnalysisError.html#variant.RepeatedPubkeys)
and it was incorrectly failing on tr descriptors that contain duplicated
keys across different taproot leaves

Fixes #760
2022-09-24 15:42:42 +02:00
Alekos Filini
c3faf05be9 Merge bitcoindevkit/bdk#713: Add datatype for is_spent sqlite column
54d768412a Sqlite migrations should either succeed or fail (Vladimir Fomene)
369e17b801 Add datatype for is_spent sqlite column (Vladimir Fomene)

Pull request description:

  ### Description

  During table creation, Sqlite does not throw an error when a column datatype is not defined. In addition, the datatype provided during table creation does not put a constraint on the type of data that can be put in that column. So you can easily put a string value in an integer column. Despite this, I think it is important for us to add the datatype for clarity.

  ### Notes to the reviewers

  You can read more about how Sqlite dynamic typing [here](https://www.sqlite.org/faq.html). I have amended our `migrate` code with a new commit. The idea is to run migrations in a transaction so that they either succeed or fail. This prevents us from having the database in an inconsistent state at any point in time.

  ### 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:
  rajarshimaitra:
    tACK 54d768412a
  afilini:
    ACK 54d768412a

Tree-SHA512: bb6c0467f799ca917f8d45c6495b766352b3177fc81952fcdd678208abf092fdeae966686528a5dcb3f342d7171783274df6312a08cbef3580063e059f5f7254
2022-09-23 12:08:17 +02:00
Alekos Filini
aad5461ee1 Merge bitcoindevkit/bdk#757: Enable signing taproot transactions with only non_witness_utxos
5e9965fca7 Enable signing taproot transactions with only `non_witness_utxos` (Alekos Filini)

Pull request description:

  ### Description

  Some wallets may only specify the `non_witness_utxo` for a PSBT input. If that's the case, BDK should still be able to sign.

  This was pointed out in the discussion of #734

  ### Changelog notice

  - Enable signing taproot transactions that only specify the `non_witness_utxo`

  ### 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:
  danielabrozzoni:
    tACK 5e9965fca7 - the code looks good to me, I played around with the test you provided (inspecting the PSBT, adding/removing the witness and non-witness utxos, etc) and everything works as expected.

Tree-SHA512: 2f205286263bfee4c76de8e8c81ae1349b1c3b255b72045488f8d629c05cab64c6f775307e831674dc036e5a3a760f95d9cdc1beaf48afb4c475aee838131a33
2022-09-23 11:52:20 +02:00
Daniela Brozzoni
0a7a1f4ef2 Merge bitcoindevkit/bdk#745: Add tests to improve coverage
e65edbf53c Change parameter name of database in test funcs (Vladimir Fomene)
88307045b0 Add more test to the database module (Vladimir Fomene)
e06c3f945c Set tx field to none if `include_raw` is false (Vladimir Fomene)

Pull request description:

  ### Description

  This PR add more test to the database module and also fixes certain bugs discovered by the written test. I also amended the name used for the database parameter in the test functions.

  ### Notes to the reviewers

  This contributes to fixing #699

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

  * [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 e65edbf53c
  danielabrozzoni:
    Code review ACK e65edbf53c

Tree-SHA512: 1ac1475f7d63f25e94ef21342e6f6e243c34c8c9208d11a5492f224026055da2a96f20be83497c1ba361effff9861f4e68920f98feebaf4b201d205c7030c282
2022-09-22 13:02:55 +02:00
Alekos Filini
5e9965fca7 Enable signing taproot transactions with only non_witness_utxos
Some wallets may only specify the `non_witness_utxo` for a PSBT input.
If that's the case, BDK should still be able to sign.

This was pointed out in the discussion of #734
2022-09-19 10:54:55 +02:00
Vladimir Fomene
54d768412a Sqlite migrations should either succeed or fail
The current implementation of the `migrate` method for
Sqlite database does not rollback changes when there is
an error while running one of the migration scripts. This
can leave the database in an inconsistent state. This
change ensures that migrations either succeed completely
or fail.
2022-09-16 19:03:56 +03:00
rajarshimaitra
97b6fb06aa Add a policy example
Add a new policy example demonstrating the bdk's policy structure
and how to derive it for any descriptor without creating a bdk wallet.
2022-09-16 18:50:26 +05:30
rajarshimaitra
da7670801b Update compiler example.
Change the compiler clap app into a specific example. Add comment docs
and example description. Remove clap from dependency.
2022-09-16 18:50:26 +05:30
Alekos Filini
e1fa0b6695 Fix the new release process 2022-09-16 14:56:27 +02:00
Alekos Filini
dfeb08fa00 Merge bitcoindevkit/bdk#753: Improve docs regarding PSBT finalization
8963e8c9f4 Improve docs w.r.t. PSBT finalization (Elias Rohrer)

Pull request description:

  ### Description

  This PR includes just a few tiny changes to the docs trying to make it a bit more clear what PSBT finalization is and what to expect.

  ### Notes to the reviewers

  ### Changelog notice

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

Tree-SHA512: ea1a6c715c2832cdf3a428654fb8fad08a0549e46a808ca7a424590145fc2c40164ea08eb636ce5cd648dd6335480accd1d91f9e37e9397feaf0dac2015e8baa
2022-09-15 16:16:19 +02:00
Elias Rohrer
8963e8c9f4 Improve docs w.r.t. PSBT finalization 2022-09-15 14:45:47 +02:00
Alekos Filini
562cb81cad Merge bitcoindevkit/bdk#728: Add fee_amount() and fee_rate() functions to PsbtUtils trait
ab41679368 Add fee_amount() and fee_rate() functions to PsbtUtils trait (Steve Myers)

Pull request description:

  ### Description

  The purpose of the PR is to provide a more convenient way to calculate the transaction fee amount and fee rate for a PSBT. This PR adds `fee_amount` and `fee_rate` functions to the existing `PsbtUtils` trait and implements them for `PartiallySignedBitcoinTransaction`. The `fee_rate` value is only valid if the PSBT it is called on is fully signed and finalized.

  See related discussion: https://github.com/bitcoindevkit/bdk-ffi/issues/179

  ### Changelog

  Added
  - PsbtUtils.fee_amount(), calculates the PSBT total transaction fee amount in Sats.
  - PsbtUtils.fee_rate(), calculates the PSBT FeeRate, the value is only accurate AFTER the PSBT is finalized.

  ### Notes to the reviewers

  Ideally I'd like `fee_rate` to return an `Option` and return `None` if the PSBT isn't finalized. But I'm not quite sure how to determine if a PSBT is finalized without having a `Wallet` and running it through the finalize code first. Or there might be a way to fill in missing signatures with properly sized fake data prior to calculating the fee rate. For now I think it's enough to do this simple approach with usage warning in the rust docs.

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

ACKs for top commit:
  afilini:
    ACK ab41679368

Tree-SHA512: 5386109c9ffcf63160f18b4c51eb2c582f4121b669c7276aaba489d186cf9b97343d46f887469b1407ccd7a24448b5e7aee4c6832d86768239b21f4e68054a0f
2022-09-13 18:36:23 +02:00
Alekos Filini
b12dec3620 Merge bitcoindevkit/bdk#744: Add psbt_signer.rs example
fa998de4b1 Add psbt_signer.rs example (Steve Myers)

Pull request description:

  ### Description

  Adding a simple example of how to create a PSBT with a watch only wallet and then sign it with a signing wallet.

  ### Notes to the reviewers

  This example was inspired by a question from a user.

  ### Changelog notice

  none.

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

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

Tree-SHA512: 2e178ee59ce94eb1e9b5616a499e106e1d2843589036bdb6cff2c987e280588ad9989b026cdbf01290cc60f02eccbc410a3e1d1cd45eb4b8ff04353ae31b23ea
2022-09-13 18:24:38 +02:00
Vladimir Fomene
e65edbf53c Change parameter name of database in test funcs
Change parameter name in database test functions
from `tree` to `db`.
2022-09-13 16:36:31 +03:00
Vladimir Fomene
88307045b0 Add more test to the database module
This PR aims to add more test to database
code so that we can catch bugs as soon
as they occur. Contributing to fixing
issue #699.
2022-09-13 16:35:53 +03:00
Vladimir Fomene
e06c3f945c Set tx field to none if include_raw is false
`del_tx` pulls the TransactionDetails object using
`select_transaction_details_by_txid` method which gets the transaction
details' data with a non-None transaction field even if the
`include_raw` argument is `false`. So it becomes necessary to Set
the transaction field in transactiondetails to None in `del_tx`, when
we make a call to it with `include_raw=false`.
2022-09-13 11:08:03 +03:00
Steve Myers
ab41679368 Add fee_amount() and fee_rate() functions to PsbtUtils trait
PsbtUtils.fee_amount(), calculates the PSBT total transaction fee amount in Sats.
PsbtUtils.fee_rate(), calculates the PSBT FeeRate, the value is only accurate AFTER the PSBT is finalized.
2022-09-12 22:01:18 -05:00
Alekos Filini
7b12f35698 Merge bitcoindevkit/bdk#747: Run code coverage on every PR
aa0ea6aeff codecov: warn about missing features (Daniela Brozzoni)
c3a7bbb3ff codecov: slightly change the test features (Daniela Brozzoni)
1c4d47825b Run code coverage on every PR (Daniela Brozzoni)

Pull request description:

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

Tree-SHA512: c2d0d9ad1e956f1c1808b46394f810939154442534241f9a023f18173910339ac236a5b6c66f78207dd2cb90a8f18000fc057b960e3354e2882c627fe1ef2c9f
2022-09-12 16:39:27 +02:00
Daniela Brozzoni
aa0ea6aeff codecov: warn about missing features 2022-09-10 18:11:22 +02:00
Daniela Brozzoni
c3a7bbb3ff codecov: slightly change the test features
- Remove default and minimal, as they are redundant
- Use lexicographic order
2022-09-10 18:11:17 +02:00
Daniela Brozzoni
1c4d47825b Run code coverage on every PR
In this way we can check how much of a PR is covered by the tests,
making the review process slightly easier.
2022-09-09 22:34:58 +02:00
Steve Myers
fa998de4b1 Add psbt_signer.rs example 2022-09-07 20:33:39 -05:00
Alekos Filini
06310f1dd0 Merge bitcoindevkit/bdk#708: Change configs for source-base code coverage
0010ecd94a Add badge to README (wszdexdrf)
690411722e Change configs for source-base code coverage (wszdexdrf)

Pull request description:

  ### Description

  This also changes the code coverage front end to coveralls instead of codecov, which had some issues with other changes in the PR. This will provide better and more accurate code coverage reports.

  ### Notes to the reviewers

  The tests run before generating the report are not exhaustive (not exhaustive earlier too, but I added as many as I could), and hence the report won't be 100% accurate.

  ### 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:
    ACK 0010ecd94a

Tree-SHA512: 04a21b7481b80287cf8a31276238b0a5958871310664363f54d53779bb2dda6f49198baaf3b5471667fa6131443b022aabd86f2e3b1cfcf5d4aacadf137a166e
2022-09-02 16:57:21 +02:00
Alekos Filini
8dd02094df Merge bitcoindevkit/bdk#737: Update electrum-client to 0.11.0
d7163c3a97 Update electrum-client to 0.11.0 (Alekos Filini)

Pull request description:

  ### Description

  Update electrum-client to 0.11.0

  ### Changelog notice

  - Updated `electrum-client` to `0.11.0`

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

Tree-SHA512: 3be61935e2bebfc5e4b530d17b9900e3c1bd2ef377fdd18df4303de713b124d51555f48f7aff5f4c2579105e86f52480e6b3f2af9c7494cbdb1dd2a2c5b4e0da
2022-09-02 16:53:12 +02:00
wszdexdrf
0010ecd94a Add badge to README 2022-09-02 11:20:08 +05:30
wszdexdrf
690411722e Change configs for source-base code coverage
Also add cacheing to code coverage workflow
2022-09-02 11:20:04 +05:30
Alekos Filini
7001b14b4c Bump version to 0.22.0 2022-09-01 15:46:44 +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
d7163c3a97 Update electrum-client to 0.11.0 2022-08-31 15:34:20 +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
Vladimir Fomene
369e17b801 Add datatype for is_spent sqlite column
Although, Sqlite column accepts
values of any type, it is
important to annotate this column
to make it easy to reason about.
2022-08-29 13:35:33 +03: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
志宇
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
69 changed files with 5754 additions and 3050 deletions

View File

@@ -0,0 +1,17 @@
---
name: Enhancement request
about: Request a new feature or change to an existing feature
title: ''
labels: 'enhancement'
assignees: ''
---
**Describe the enhancement**
<!-- A clear and concise description of what you would like added or changed. -->
**Use case**
<!-- Tell us how you or others will use this new feature or change to an existing feature. -->
**Additional context**
<!-- Add any other context about the enhancement here. -->

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

@@ -0,0 +1,99 @@
---
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`.
- Update the `CHANGELOG.md` file.
- 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`.
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.1`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.1".
- [ ] 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.
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.x+1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.x+1`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.x+1".
- [ ] 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:
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
- [ ] 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/

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

@@ -0,0 +1,71 @@
---
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`.
- Update the `CHANGELOG.md` file.
- 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.
- [ ] Bump the `release/MAJOR.MINOR.PATCH+1` branch to `MAJOR.MINOR.PATCH+1` version.
- Change the `Cargo.toml` version value to `MAJOR.MINOR.MINOR.PATCH+1`.
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
- [ ] 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

@@ -1,37 +1,61 @@
on: [push]
on: [push, pull_request]
name: Code Coverage
jobs:
Codecov:
name: Code Coverage
runs-on: ubuntu-latest
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
RUSTFLAGS: "-Cinstrument-coverage"
RUSTDOCFLAGS: "-Cinstrument-coverage"
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install lcov tools
run: sudo apt-get install lcov -y
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Add llvm tools
run: rustup component add llvm-tools-preview
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
- id: coverage
name: Generate coverage
uses: actions-rs/grcov@v0.1.5
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
- name: Cache cargo
uses: actions/cache@v3
with:
file: ${{ steps.coverage.outputs.report }}
directory: ./coverage/reports/
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
- name: Test
# WARNING: this is not testing the following features: test-esplora, test-hardware-signer, async-interface
# This is because some of our features are mutually exclusive, and generating various reports and
# merging them doesn't seem to be working very well.
# For more info, see:
# - https://github.com/bitcoindevkit/bdk/issues/696
# - https://github.com/bitcoindevkit/bdk/pull/748#issuecomment-1242721040
run: cargo test --features all-keys,compact_filters,compiler,key-value-db,sqlite,sqlite-bundled,test-electrum,test-rpc,verify
- name: Run grcov
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
- name: Generate HTML coverage report
run: genhtml -o coverage-report.html ./coverage/lcov.info
- name: Coveralls upload
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: coverage-report
path: coverage-report.html

View File

@@ -10,23 +10,23 @@ jobs:
strategy:
matrix:
rust:
- version: 1.60.0 # STABLE
- version: 1.65.0 # STABLE
clippy: true
- version: 1.56.1 # MSRV
- version: 1.57.0 # MSRV
features:
- default
- minimal
- all-keys
- minimal,use-esplora-ureq
- minimal,use-esplora-blocking
- key-value-db
- electrum
- compact_filters
- esplora,ureq,key-value-db,electrum
- use-esplora-blocking,key-value-db,electrum
- compiler
- rpc
- verify
- async-interface
- use-esplora-reqwest
- use-esplora-async
- sqlite
- sqlite-bundled
steps:
@@ -100,10 +100,10 @@ jobs:
features: test-rpc-legacy
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-reqwest,verify
features: test-esplora,use-esplora-async,verify
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-ureq,verify
features: test-esplora,use-esplora-blocking,verify
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -146,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.1 # STABLE
run: rustup default 1.65.0 # STABLE
- name: Set profile
run: rustup set profile minimal
- name: Add target wasm32
@@ -154,7 +154,7 @@ jobs:
- name: Update toolchain
run: rustup update
- name: Check
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
run: cargo check --target wasm32-unknown-unknown --features async-interface,use-esplora-async,dev-getrandom-wasm --no-default-features
fmt:
name: Rust fmt
@@ -172,3 +172,32 @@ jobs:
run: rustup update
- name: Check fmt
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
test_hardware_wallet:
runs-on: ubuntu-20.04
strategy:
matrix:
rust:
- version: 1.65.0 # STABLE
- version: 1.57.0 # 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,13 +18,13 @@ jobs:
target
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain
run: rustup default nightly-2022-01-25
run: rustup default nightly-2022-12-14
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Build docs
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,use-esplora-blocking,compact_filters,rpc,key-value-db,sqlite,all-keys,verify,hardware-signer -- --cfg docsrs -Dwarnings
- name: Upload artifact
uses: actions/upload-artifact@v2
with:

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/target
Cargo.lock
/.vscode
*.swp
.idea

View File

@@ -1,31 +1,197 @@
# Changelog
All notable changes to this project will be documented in this file.
All notable changes to this project can be found here and in each release's git tag and can be viewed with `git tag -ln100 "v*"`. See also [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
Contributors do not need to change this file but do need to add changelog details in their PR descriptions. The person making the next release will collect changelog details from included PRs and edit this file prior to each release.
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.27.2]
## [v0.20.0] - [v0.19.0]
### Summary
Disable default-features for rust-bitcoin and rust-miniscript dependencies, and for rust-esplora-client optional dependency.
### Changed
- Set default-features = false for rust-bitcoin and rust-miniscript #882
- Update esplora client dependency to version 0.4 #884
## [v0.27.1]
### Summary
Fixes [RUSTSEC-2022-0090], this issue is only applicable if you are using the optional sqlite database feature.
[RUSTSEC-2022-0090]: https://rustsec.org/advisories/RUSTSEC-2022-0090
### Changed
- Update optional sqlite dependency from 0.27.0 to 0.28.0. #867
## [v0.27.0]
### Summary
A maintenance release with a bump in project MSRV to 1.57.0, updated dependence and a few developer oriented improvements. Improvements include better error formatting, don't default to async/await for wasm32 and adding derived PartialEq and Eq on SyncTime.
### Changed
- Improve display error formatting #814
- Don't default to use async/await on wasm32 #831
- Project MSRV changed from 1.56.1 to 1.57.0 #842
- Update rust-miniscript dependency to latest bug fix release 9.0 #844
### Added
- Derive PartialEq, Eq on SyncTime #837
## [v0.26.0]
### Summary
This release improves Fulcrum electrum server compatibility and fixes public descriptor template key origin paths. We also snuck in small enhancements to configure the electrum client to validate the domain using SSL and sort TransactionDetails by block height and timestamp.
### Fixed
- Make electrum blockchain client `save_tx` function order independent to work with Fulcrum servers. #808
- Fix wrong testnet key origin path in public descriptor templates. #818
- Make README.md code examples compile without errors. #820
### Changed
- Bump `hwi` dependency to `0.4.0`. #825
- Bump `esplora-client` dependency to `0.3` #830
### Added
- For electrum blockchain client, allow user to configure whether to validate the domain using SSL. #805
- Implement ordering for `TransactionDetails`. #812
## [v0.25.0]
### Summary
This release fixes slow sync time and big script_pubkeys table with SQLite, the wallet rescan height for the FullyNodedExport and setting the network for keys in the KeyMap when using descriptor templates. Also added are new blockchain and mnemonic examples.
### Fixed
- Slow sync time and big script_pubkeys table with SQLite.
- Wallet rescan height for the FullyNodedExport.
- Setting the network for keys in the KeyMap when using descriptor templates.
### Added
- Examples for connecting to Esplora, Electrum Server, Neutrino and Bitcoin Core.
- Example for using a mnemonic in a descriptors.
## [v0.24.0]
### Summary
This release contains important dependency updates for `rust-bitcoin` to `0.29` and `rust-miniscript` to `8.0`, plus related crates that also depend on the latest version of `rust-bitcoin`. The release also includes a breaking change to the BDK signer which now produces low-R signatures by default, saving one byte. A bug was found in the `get_checksum` and `get_checksum_bytes` functions, which are now deprecated in favor of fixed versions called `calc_checksum` and `calc_checksum_bytes`. And finally a new `hardware-signer` features was added that re-exports the `hwi` crate, along with a new `hardware_signers.rs` example file.
### Changed
- Updated dependency versions for `rust-bitcoin` to `0.29` and `rust-miniscript` to `8.0`, plus all related crates. @afilini #770
- BDK Signer now produces low-R signatures by default, saving one byte. If you want to preserve the original behavior, set allow_grinding in the SignOptions to false. @vladimirfomene #779
- Deprecated `get_checksum`and `get_checksum_bytes` due to bug where they calculates the checksum of a descriptor that already has a checksum. Use `calc_checksum` and `calc_checksum_bytes` instead. @evanlinjin #765
- Remove deprecated "address validators". @afilini #770
### Added
- New `calc_checksum` and `calc_checksum_bytes`, replace deprecated `get_checksum` and `get_checksum_bytes`. @evanlinjin #765
- Re-export the hwi crate when the feature hardware-signer is on. @danielabrozzoni #758
- New examples/hardware_signer.rs. @danielabrozzoni #758
- Make psbt module public to expose PsbtUtils trait to downstream projects. @notmandatory #782
## [v0.23.0]
### Summary
This release brings new utilities functions on PSBTs like `fee_amount()` and `fee_rate()` and migrates BDK to use our new external esplora client library.
As always many bug fixes, docs and tests improvement are also included.
### Changed
- Update electrum-client to 0.11.0 by @afilini in https://github.com/bitcoindevkit/bdk/pull/737
- Change configs for source-base code coverage by @wszdexdrf in https://github.com/bitcoindevkit/bdk/pull/708
- Improve docs regarding PSBT finalization by @tnull in https://github.com/bitcoindevkit/bdk/pull/753
- Update compiler example to a Policy example by @rajarshimaitra in https://github.com/bitcoindevkit/bdk/pull/730
- Fix the release process by @afilini in https://github.com/bitcoindevkit/bdk/pull/754
- Remove redundant duplicated keys check by @afilini in https://github.com/bitcoindevkit/bdk/pull/761
- Remove genesis_block lazy initialization by @shobitb in https://github.com/bitcoindevkit/bdk/pull/756
- Fix `Wallet::descriptor_checksum` to actually return the checksum by @evanlinjin in https://github.com/bitcoindevkit/bdk/pull/763
- Use the esplora client crate by @afilini in https://github.com/bitcoindevkit/bdk/pull/764
### Added
- Run code coverage on every PR by @danielabrozzoni in https://github.com/bitcoindevkit/bdk/pull/747
- Add psbt_signer.rs example by @notmandatory in https://github.com/bitcoindevkit/bdk/pull/744
- Add fee_amount() and fee_rate() functions to PsbtUtils trait by @notmandatory in https://github.com/bitcoindevkit/bdk/pull/728
- Add tests to improve coverage by @vladimirfomene in https://github.com/bitcoindevkit/bdk/pull/745
- Enable signing taproot transactions with only `non_witness_utxos` by @afilini in https://github.com/bitcoindevkit/bdk/pull/757
- Add datatype for is_spent sqlite column by @vladimirfomene in https://github.com/bitcoindevkit/bdk/pull/713
- Add vscode filter to gitignore by @evanlinjin in https://github.com/bitcoindevkit/bdk/pull/762
## [v0.22.0]
### Summary
This release brings support for hardware signers on desktop through the HWI library.
It also includes fixes and improvements which are part of our ongoing effort of integrating
BDK and LDK together.
### Changed
- FeeRate function name as_sat_vb to as_sat_per_vb. #678
- Verify signatures after signing. #718
- Dependency electrum-client to 0.11.0. #737
### Added
- Functions to create FeeRate from sats/kvbytes and sats/kwu. #678
- Custom hardware wallet signer HwiSigner in wallet::hardwaresigner module. #682
- Function allow_dust on TxBuilder. #689
- Implementation of Deref<Target=UrlClient> for EsploraBlockchain. #722
- Implementation of Deref<Target=Client> for ElectrumBlockchain #705
- Implementation of Deref<Target=Client> for RpcBlockchain. #731
## [v0.21.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]
- 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.
- 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]
## [v0.19.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
- 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
@@ -34,17 +200,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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]
## [v0.18.0]
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
- 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`.
- Bump `miniscript` dependency version to `^6.1`.
## [v0.17.0] - [v0.16.1]
## [v0.17.0]
- 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`.
@@ -65,45 +231,45 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
- 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]
## [v0.16.1]
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
## [v0.16.0] - [v0.15.0]
## [v0.16.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]
## [v0.15.0]
- Overhauled sync logic for electrum and esplora.
- Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter.
- Fixed esplora fee estimation.
## [v0.14.0] - [v0.13.0]
## [v0.14.0]
- BIP39 implementation dependency, in `keys::bip39` changed from tiny-bip39 to rust-bip39.
- Add new method on the `TxBuilder` to embed data in the transaction via `OP_RETURN`. To allow that a fix to check the dust only on spendable output has been introduced.
- Update the `Database` trait to store the last sync timestamp and block height
- Rename `ConfirmationTime` to `BlockTime`
## [v0.13.0] - [v0.12.0]
## [v0.13.0]
- Exposed `get_tx()` method from `Database` to `Wallet`.
## [v0.12.0] - [v0.11.0]
## [v0.12.0]
- Activate `miniscript/use-serde` feature to allow consumers of the library to access it via the re-exported `miniscript` crate.
- Add support for proxies in `EsploraBlockchain`
- Added `SqliteDatabase` that implements `Database` backed by a sqlite database using `rusqlite` crate.
## [v0.11.0] - [v0.10.0]
## [v0.11.0]
- Added `flush` method to the `Database` trait to explicitly flush to disk latest changes on the db.
## [v0.10.0] - [v0.9.0]
## [v0.10.0]
- Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`)
- Removed hard dependency on `tokio`.
@@ -117,21 +283,21 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
- Removed `stop_gap` from `Blockchain` trait and added it to only `ElectrumBlockchain` and `EsploraBlockchain` structs.
- Added a `ureq` backend for use when not using feature `async-interface` or target WASM. `ureq` is a blocking HTTP client.
## [v0.9.0] - [v0.8.0]
## [v0.9.0]
### Wallet
- Added Bitcoin core RPC added as blockchain backend
- Added a `verify` feature that can be enable to verify the unconfirmed txs we download against the consensus rules
## [v0.8.0] - [v0.7.0]
## [v0.8.0]
### Wallet
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
#### Changed
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
## [v0.7.0] - [v0.6.0]
## [v0.7.0]
### Policy
#### Changed
@@ -146,7 +312,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
## [v0.6.0] - [v0.5.1]
## [v0.6.0]
### Misc
#### Changed
@@ -170,13 +336,13 @@ Timelocks are considered (optionally) in building the `satisfaction` field
#### Fixed
- Fixed `coin_select` calculation for UTXOs where `value < fee` that caused over-/underflow errors.
## [v0.5.1] - [v0.5.0]
## [v0.5.1]
### Misc
#### Changed
- Pin `hyper` to `=0.14.4` to make it compile on Rust 1.45
## [v0.5.0] - [v0.4.0]
## [v0.5.0]
### Misc
#### Changed
@@ -186,7 +352,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
#### Changed
- `FeeRate` constructors `from_sat_per_vb` and `default_min_relay_fee` are now `const` functions
## [v0.4.0] - [v0.3.0]
## [v0.4.0]
### Keys
#### Changed
@@ -215,7 +381,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
- Removed unneeded `Result<(), PolicyError>` return type for `Satisfaction::finalize()`
- Removed the `TooManyItemsSelected` policy error (see commit message for more details)
## [v0.3.0] - [v0.2.0]
## [v0.3.0]
### Descriptor
#### Changed
@@ -252,7 +418,7 @@ final transaction is created by calling `finish` on the builder.
#### Changed
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository
## [v0.2.0] - [0.1.0-beta.1]
## [v0.2.0]
### Project
#### Added
@@ -479,4 +645,13 @@ final transaction is created by calling `finish` on the builder.
[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
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...HEAD
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
[v0.22.0]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...v0.22.0
[v0.23.0]: https://github.com/bitcoindevkit/bdk/compare/v0.22.0...v0.23.0
[v0.24.0]: https://github.com/bitcoindevkit/bdk/compare/v0.23.0...v0.24.0
[v0.25.0]: https://github.com/bitcoindevkit/bdk/compare/v0.24.0...v0.25.0
[v0.26.0]: https://github.com/bitcoindevkit/bdk/compare/v0.25.0...v0.26.0
[v0.27.0]: https://github.com/bitcoindevkit/bdk/compare/v0.26.0...v0.27.0
[v0.27.1]: https://github.com/bitcoindevkit/bdk/compare/v0.27.0...v0.27.1
[v0.27.2]: https://github.com/bitcoindevkit/bdk/compare/v0.27.1...v0.27.2
[Unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.27.2...HEAD

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.20.0"
version = "0.27.2"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -14,40 +14,39 @@ license = "MIT OR Apache-2.0"
[dependencies]
bdk-macros = "^0.6"
log = "^0.4"
miniscript = { version = "7.0", features = ["use-serde"] }
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
miniscript = { version = "9.0", default-features = false, features = ["serde"] }
bitcoin = { version = "0.29.2", default-features = false, features = ["serde", "base64", "rand"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.7"
rand = "^0.8"
# Optional dependencies
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.10", optional = true }
rusqlite = { version = "0.27.0", optional = true }
electrum-client = { version = "0.12", optional = true }
esplora-client = { version = "0.4", default-features = false, optional = true }
rusqlite = { version = "0.28.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 }
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
cc = { version = ">=1.0.64", optional = true }
socks = { version = "0.3", optional = true }
lazy_static = { version = "1.4", optional = true }
hwi = { version = "0.5", optional = true, features = ["use-miniscript"] }
bip39 = { version = "1.0.1", optional = true }
bitcoinconsensus = { version = "0.19.0-3", optional = true }
# Needed by bdk_blockchain_tests macro and the `rpc` feature
bitcoincore-rpc = { version = "0.15", optional = true }
bitcoincore-rpc = { version = "0.16", optional = true }
# Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] }
tokio = { version = "1", features = ["rt", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
async-trait = "0.1"
js-sys = "0.3"
rand = { version = "^0.7", features = ["wasm-bindgen"] }
[features]
minimal = []
@@ -56,11 +55,12 @@ verify = ["bitcoinconsensus"]
default = ["key-value-db", "electrum"]
sqlite = ["rusqlite", "ahash"]
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
compact_filters = ["rocksdb", "socks", "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
@@ -68,23 +68,26 @@ rpc = ["bitcoincore-rpc"]
#
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
# access to the asynchronous method implementations. Then, if Esplora is wanted,
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
# enable the `use-esplora-async` feature.
# - Users wanting blocking HTTP calls can use any of the other blockchain
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
# use Esplora should enable the `use-esplora-blocking` feature.
#
# WARNING: Please take care with the features below, various combinations will
# fail to build. We cannot currently build `bdk` with `--all-features`.
async-interface = ["async-trait"]
electrum = ["electrum-client"]
# MUST ALSO USE `--no-default-features`.
use-esplora-reqwest = ["esplora", "reqwest", "reqwest/socks", "futures"]
use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
use-esplora-async = ["esplora", "esplora-client/async", "futures"]
use-esplora-blocking = ["esplora", "esplora-client/blocking"]
# Deprecated aliases
use-esplora-reqwest = ["use-esplora-async"]
use-esplora-ureq = ["use-esplora-blocking"]
# 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"]
# Use below feature with `use-esplora-async` to enable reqwest default TLS support
reqwest-default-tls = ["esplora-client/async-https"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
@@ -93,15 +96,27 @@ test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-bl
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"]
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
dev-getrandom-wasm = ["getrandom/js"]
[dev-dependencies]
miniscript = { version = "9.0", features = ["std"] }
bitcoin = { version = "0.29.2", features = ["std"] }
lazy_static = "1.4"
env_logger = "0.7"
clap = "2.33"
electrsd = "0.19.1"
electrsd = "0.22"
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
base64 = "^0.13"
assert_matches = "1.5.0"
# zip versions after 0.6.3 don't work with our MSRV 1.57.0
zip = "=0.6.3"
# base64ct versions at 1.6.0 and higher have MSRV 1.60.0
base64ct = "<1.6.0"
[[example]]
name = "address_validator"
[[example]]
name = "compact_filters_balance"
required-features = ["compact_filters"]
@@ -111,14 +126,48 @@ name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]
[[example]]
name = "policy"
path = "examples/policy.rs"
[[example]]
name = "rpcwallet"
path = "examples/rpcwallet.rs"
required-features = ["keys-bip39", "key-value-db", "rpc"]
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
[[example]]
name = "psbt_signer"
path = "examples/psbt_signer.rs"
required-features = ["electrum"]
[[example]]
name = "hardware_signer"
path = "examples/hardware_signer.rs"
required-features = ["electrum", "hardware-signer"]
[[example]]
name = "electrum_backend"
path = "examples/electrum_backend.rs"
required-features = ["electrum"]
[[example]]
name = "esplora_backend_synchronous"
path = "examples/esplora_backend_synchronous.rs"
required-features = ["use-esplora-ureq"]
[[example]]
name = "esplora_backend_asynchronous"
path = "examples/esplora_backend_asynchronous.rs"
required-features = ["use-esplora-reqwest", "reqwest-default-tls", "async-interface"]
[[example]]
name = "mnemonic_to_descriptors"
path = "examples/mnemonic_to_descriptors.rs"
required-features = ["all-keys"]
[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-blocking", "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

@@ -11,9 +11,9 @@
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></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/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://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html"><img alt="Rustc Version 1.57.0+" src="https://img.shields.io/badge/rustc-1.57.0%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>
@@ -68,12 +68,13 @@ fn main() -> Result<(), bdk::Error> {
```rust
use bdk::{Wallet, database::MemoryDatabase};
use bdk::wallet::AddressIndex::New;
use bdk::bitcoin::Network;
fn main() -> Result<(), bdk::Error> {
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(),
)?;
@@ -95,14 +96,16 @@ use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New;
use bitcoin::consensus::serialize;
use base64;
use bdk::bitcoin::consensus::serialize;
use bdk::bitcoin::Network;
fn main() -> Result<(), bdk::Error> {
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(),
)?;
@@ -131,20 +134,22 @@ fn main() -> Result<(), bdk::Error> {
```rust,no_run
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::consensus::deserialize;
use base64;
use bdk::bitcoin::consensus::deserialize;
use bdk::bitcoin::Network;
fn main() -> Result<(), bdk::Error> {
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
bitcoin::Network::Testnet,
Network::Testnet,
MemoryDatabase::default(),
)?;
let psbt = "...";
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
let _finalized = wallet.sign(&mut psbt, SignOptions::default())?;
Ok(())
}
@@ -154,7 +159,7 @@ fn main() -> Result<(), bdk::Error> {
### Unit testing
```
```bash
cargo test
```
@@ -162,13 +167,24 @@ cargo test
Integration testing require testing features, for example:
```
```bash
cargo test --features test-electrum
```
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.
## Running under WASM
If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`:
```toml
[dependencies]
getrandom = { version = "0.2", features = ["js"] }
```
This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more.
## License
Licensed under either of

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", "--model", "nanos", "--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

@@ -1,13 +0,0 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
base: auto
informational: false
patch:
default:
target: auto
threshold: 100%
base: auto

View File

@@ -1,63 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
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;
use bdk::wallet::AddressIndex::New;
use bitcoin::hashes::hex::FromHex;
use bitcoin::util::bip32::Fingerprint;
use bitcoin::{Network, Script};
#[derive(Debug)]
struct DummyValidator;
#[allow(deprecated)]
impl AddressValidator for DummyValidator {
fn validate(
&self,
keychain: KeychainKind,
hd_keypaths: &HdKeyPaths,
script: &Script,
) -> Result<(), AddressValidatorError> {
let (_, path) = hd_keypaths
.values()
.find(|(fing, _)| fing == &Fingerprint::from_hex("bc123c3e").unwrap())
.ok_or(AddressValidatorError::InvalidScript)?;
println!(
"Validating `{:?}` {} address, script: {}",
keychain, path, script
);
Ok(())
}
}
fn main() -> Result<(), bdk::Error> {
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(DummyValidator));
wallet.get_address(New)?;
wallet.get_address(New)?;
wallet.get_address(New)?;
Ok(())
}

View File

@@ -11,7 +11,6 @@
extern crate bdk;
extern crate bitcoin;
extern crate clap;
extern crate log;
extern crate miniscript;
extern crate serde_json;
@@ -21,8 +20,6 @@ use std::str::FromStr;
use log::info;
use clap::{App, Arg};
use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
@@ -31,75 +28,49 @@ use bdk::database::memory::MemoryDatabase;
use bdk::wallet::AddressIndex::New;
use bdk::{KeychainKind, Wallet};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
/// can be derived from the policy.
///
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let matches = App::new("Miniscript Compiler")
.arg(
Arg::with_name("POLICY")
.help("Sets the spending policy to compile")
.required(true)
.index(1),
)
.arg(
Arg::with_name("TYPE")
.help("Sets the script type used to embed the compiled policy")
.required(true)
.index(2)
.possible_values(&["sh", "wsh", "sh-wsh"]),
)
.arg(
Arg::with_name("parsed_policy")
.long("parsed_policy")
.short("p")
.help("Also return the parsed spending policy in JSON format"),
)
.arg(
Arg::with_name("network")
.short("n")
.long("network")
.help("Sets the network")
.takes_value(true)
.default_value("testnet")
.possible_values(&["testnet", "regtest", "bitcoin", "signet"]),
)
.get_matches();
let policy_str = matches.value_of("POLICY").unwrap();
info!("Compiling policy: {}", policy_str);
// We start with a generic miniscript policy string
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
info!("Compiling policy: \n{}", policy_str);
// Parse the string as a [`Concrete`] type miniscript policy.
let policy = Concrete::<String>::from_str(policy_str)?;
let descriptor = match matches.value_of("TYPE").unwrap() {
"sh" => Descriptor::new_sh(policy.compile()?)?,
"wsh" => Descriptor::new_wsh(policy.compile()?)?,
"sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?)?,
_ => panic!("Invalid type"),
};
// Create a `wsh` type descriptor from the policy.
// `policy.compile()` returns the resulting miniscript from the policy.
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
info!("... Descriptor: {}", descriptor);
info!("Compiled into following Descriptor: \n{}", descriptor);
let database = MemoryDatabase::new();
let network = matches
.value_of("network")
.map(Network::from_str)
.transpose()
.unwrap()
.unwrap_or(Network::Testnet);
let wallet = Wallet::new(&format!("{}", descriptor), None, network, database)?;
// Create a new wallet from this descriptor
let wallet = Wallet::new(&format!("{}", descriptor), None, Network::Regtest, database)?;
info!("... First address: {}", wallet.get_address(New)?);
info!(
"First derived address from the descriptor: \n{}",
wallet.get_address(New)?
);
if matches.is_present("parsed_policy") {
let spending_policy = wallet.policies(KeychainKind::External)?;
info!(
"... Spending policy:\n{}",
serde_json::to_string_pretty(&spending_policy)?
);
}
// BDK also has it's own `Policy` structure to represent the spending condition in a more
// human readable json format.
let spending_policy = wallet.policies(KeychainKind::External)?;
info!(
"The BDK spending policy: \n{}",
serde_json::to_string_pretty(&spending_policy)?
);
Ok(())
}

View File

@@ -0,0 +1,87 @@
use std::str::FromStr;
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::Network;
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::template::Bip84;
use bdk::wallet::export::FullyNodedExport;
use bdk::{KeychainKind, SyncOptions, Wallet};
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex;
use bitcoin::util::bip32;
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Electrum server.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --example electrum_backend` in the root folder.
fn main() {
let network = Network::Testnet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let electrum_url = "ssl://electrum.blockstream.info:60002";
run(&network, electrum_url, xpriv);
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
fn run(network: &Network, electrum_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
// Apparently it works only with Electrs (not EletrumX)
let blockchain = ElectrumBlockchain::from(Client::new(electrum_url).unwrap());
let wallet = create_wallet(network, &xpriv);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 6500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 5359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
blockchain.broadcast(&tx).unwrap();
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

View File

@@ -0,0 +1,93 @@
use std::str::FromStr;
use bdk::blockchain::Blockchain;
use bdk::{
blockchain::esplora::EsploraBlockchain,
database::MemoryDatabase,
template::Bip84,
wallet::{export::FullyNodedExport, AddressIndex},
KeychainKind, SyncOptions, Wallet,
};
use bitcoin::{
util::bip32::{self, ExtendedPrivKey},
Network,
};
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
/// using non blocking asynchronous calls with `reqwest`.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --no-default-features --features="use-esplora-reqwest, reqwest-default-tls, async-interface" --example esplora_backend_asynchronous`
/// in the root folder.
#[tokio::main(flavor = "current_thread")]
async fn main() {
let network = Network::Signet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let esplora_url = "https://explorer.bc-2.jp/api";
run(&network, esplora_url, xpriv).await;
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
async fn run(network: &Network, esplora_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
let blockchain = EsploraBlockchain::new(esplora_url, 20);
let wallet = create_wallet(network, &xpriv);
wallet
.sync(&blockchain, SyncOptions::default())
.await
.unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 10500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 9359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
let _ = blockchain.broadcast(&tx);
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

View File

@@ -0,0 +1,89 @@
use std::str::FromStr;
use bdk::blockchain::Blockchain;
use bdk::{
blockchain::esplora::EsploraBlockchain,
database::MemoryDatabase,
template::Bip84,
wallet::{export::FullyNodedExport, AddressIndex},
KeychainKind, SyncOptions, Wallet,
};
use bitcoin::{
util::bip32::{self, ExtendedPrivKey},
Network,
};
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
/// using blocking calls with `ureq`.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --features=use-esplora-ureq --example esplora_backend_synchronous`
/// in the root folder.
fn main() {
let network = Network::Signet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let esplora_url = "https://explorer.bc-2.jp/api";
run(&network, esplora_url, xpriv);
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
fn run(network: &Network, esplora_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
let blockchain = EsploraBlockchain::new(esplora_url, 20);
let wallet = create_wallet(network, &xpriv);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 10500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 9359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
blockchain.broadcast(&tx).unwrap();
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

105
examples/hardware_signer.rs Normal file
View File

@@ -0,0 +1,105 @@
use bdk::bitcoin::{Address, Network};
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::hwi::{types::HWIChain, HWIClient};
use bdk::miniscript::{Descriptor, DescriptorPublicKey};
use bdk::signer::SignerOrdering;
use bdk::wallet::{hardwaresigner::HWISigner, AddressIndex};
use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
use electrum_client::Client;
use std::str::FromStr;
use std::sync::Arc;
// This example shows how to sync a wallet, create a transaction, sign it
// and broadcast it using an external hardware wallet.
// The hardware wallet must be connected to the computer and unlocked before
// running the example. Also, the `hwi` python package should be installed
// and available in the environment.
//
// To avoid loss of funds, consider using an hardware wallet simulator:
// * Coldcard: https://github.com/Coldcard/firmware
// * Ledger: https://github.com/LedgerHQ/speculos
// * Trezor: https://docs.trezor.io/trezor-firmware/core/emulator/index.html
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Hold tight, I'm connecting to your hardware wallet...");
// Listing all the available hardware wallet devices...
let mut devices = HWIClient::enumerate()?;
if devices.is_empty() {
panic!("No devices found. Either plug in a hardware wallet, or start a simulator.");
}
let first_device = devices.remove(0)?;
// ...and creating a client out of the first one
let client = HWIClient::get_client(&first_device, true, HWIChain::Test)?;
println!("Look what I found, a {}!", first_device.model);
// Getting the HW's public descriptors
let descriptors = client.get_descriptors::<Descriptor<DescriptorPublicKey>>(None)?;
println!(
"The hardware wallet's descriptor is: {}",
descriptors.receive[0]
);
// Creating a custom signer from the device
let custom_signer = HWISigner::from_device(&first_device, HWIChain::Test)?;
let mut wallet = Wallet::new(
descriptors.receive[0].clone(),
Some(descriptors.internal[0].clone()),
Network::Testnet,
MemoryDatabase::default(),
)?;
// Adding the hardware signer to the BDK wallet
wallet.add_signer(
KeychainKind::External,
SignerOrdering(200),
Arc::new(custom_signer),
);
// create client for Blockstream's testnet electrum server
let blockchain =
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
println!("Syncing the wallet...");
wallet.sync(&blockchain, SyncOptions::default())?;
// get deposit address
let deposit_address = wallet.get_address(AddressIndex::New)?;
let balance = wallet.get_balance()?;
println!("Wallet balances in SATs: {}", balance);
if balance.get_total() < 10000 {
println!(
"Send some sats from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
addr = deposit_address.address
);
return Ok(());
}
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
let (mut psbt, _details) = {
let mut builder = wallet.build_tx();
builder
.drain_wallet()
.drain_to(return_address.script_pubkey())
.enable_rbf()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
builder.finish()?
};
// `sign` will call the hardware wallet asking for a signature
assert!(
wallet.sign(&mut psbt, SignOptions::default())?,
"The hardware wallet couldn't finalize the transaction :("
);
println!("Let's broadcast your tx...");
let raw_transaction = psbt.extract_tx();
let txid = raw_transaction.txid();
blockchain.broadcast(&raw_transaction)?;
println!("Transaction broadcasted! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
Ok(())
}

View File

@@ -0,0 +1,60 @@
// 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::util::bip32::DerivationPath;
use bdk::bitcoin::Network;
use bdk::descriptor;
use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{GeneratableKey, GeneratedKey};
use bdk::miniscript::Tap;
use bdk::Error as BDK_Error;
use std::error::Error;
use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase
/// using BDK and use that to generate a descriptor string.
fn main() -> Result<(), Box<dyn Error>> {
let secp = Secp256k1::new();
// In this example we are generating a 12 words mnemonic phrase
// but it is also possible generate 15, 18, 21 and 24 words
// using their respective `WordCount` variant.
let mnemonic: GeneratedKey<_, Tap> =
Mnemonic::generate((WordCount::Words12, Language::English))
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
println!("Mnemonic phrase: {}", *mnemonic);
let mnemonic_with_passphrase = (mnemonic, None);
// define external and internal derivation key path
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
// generate external and internal descriptor from mnemonic
let (external_descriptor, ext_keymap) =
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
.into_wallet_descriptor(&secp, Network::Testnet)?;
let (internal_descriptor, int_keymap) =
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
.into_wallet_descriptor(&secp, Network::Testnet)?;
println!("tpub external descriptor: {}", external_descriptor);
println!("tpub internal descriptor: {}", internal_descriptor);
println!(
"tprv external descriptor: {}",
external_descriptor.to_string_with_secret(&ext_keymap)
);
println!(
"tprv internal descriptor: {}",
internal_descriptor.to_string_with_secret(&int_keymap)
);
Ok(())
}

66
examples/policy.rs Normal file
View File

@@ -0,0 +1,66 @@
// 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.
extern crate bdk;
extern crate env_logger;
extern crate log;
use std::error::Error;
use bdk::bitcoin::Network;
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk::wallet::signer::SignersContainer;
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
///
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
/// This is useful to express complex miniscript spending conditions into more human readable form.
/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
/// to spend from.
///
/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
/// one of the Extend Private key.
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let secp = bitcoin::secp256k1::Secp256k1::new();
// The descriptor used in the example
// The form is "wsh(multi(2, <privkey>, <pubkey>))"
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
// Use the descriptor string to derive the full descriptor and a keymap.
// The wallet descriptor can be used to create a new bdk::wallet.
// While the `keymap` can be used to create a `SignerContainer`.
//
// The `SignerContainer` can sign for `PSBT`s.
// a bdk::wallet internally uses these to handle transaction signing.
// But they can be used as independent tools also.
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
log::info!("Example Descriptor for policy analysis : {}", wallet_desc);
// Create the signer with the keymap and descriptor.
let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
// Extract the Policy from the given descriptor and signer.
// Note that Policy is a wallet specific structure. It depends on the the descriptor, and
// what the concerned wallet with a given signer can sign for.
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
.expect("We expect a policy");
log::info!("Derived Policy for the descriptor {:#?}", policy);
Ok(())
}

120
examples/psbt_signer.rs Normal file
View File

@@ -0,0 +1,120 @@
// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::wallet::AddressIndex;
use bdk::{descriptor, SyncOptions};
use bdk::{FeeRate, SignOptions, Wallet};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{Address, Network};
use electrum_client::Client;
use miniscript::descriptor::DescriptorSecretKey;
use std::error::Error;
use std::str::FromStr;
/// This example shows how to sign and broadcast the transaction for a PSBT (Partially Signed
/// Bitcoin Transaction) for a single key, witness public key hash (WPKH) based descriptor wallet.
/// The electrum protocol is used to sync blockchain data from the testnet bitcoin network and
/// wallet data is stored in an ephemeral in-memory database. The process steps are:
/// 1. Create a "signing" wallet and a "watch-only" wallet based on the same private keys.
/// 2. Deposit testnet funds into the watch only wallet.
/// 3. Sync the watch only wallet and create a spending transaction to return all funds to the testnet faucet.
/// 4. Sync the signing wallet and sign and finalize the PSBT created by the watch only wallet.
/// 5. Broadcast the transactions from the finalized PSBT.
fn main() -> Result<(), Box<dyn Error>> {
// test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
let internal_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*").unwrap();
let secp = Secp256k1::new();
let external_public_xkey = external_secret_xkey.to_public(&secp).unwrap();
let internal_public_xkey = internal_secret_xkey.to_public(&secp).unwrap();
let signing_external_descriptor = descriptor!(wpkh(external_secret_xkey)).unwrap();
let signing_internal_descriptor = descriptor!(wpkh(internal_secret_xkey)).unwrap();
let watch_only_external_descriptor = descriptor!(wpkh(external_public_xkey)).unwrap();
let watch_only_internal_descriptor = descriptor!(wpkh(internal_public_xkey)).unwrap();
// create client for Blockstream's testnet electrum server
let blockchain =
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
// create watch only wallet
let watch_only_wallet: Wallet<MemoryDatabase> = Wallet::new(
watch_only_external_descriptor,
Some(watch_only_internal_descriptor),
Network::Testnet,
MemoryDatabase::default(),
)?;
// create signing wallet
let signing_wallet: Wallet<MemoryDatabase> = Wallet::new(
signing_external_descriptor,
Some(signing_internal_descriptor),
Network::Testnet,
MemoryDatabase::default(),
)?;
println!("Syncing watch only wallet.");
watch_only_wallet.sync(&blockchain, SyncOptions::default())?;
// get deposit address
let deposit_address = watch_only_wallet.get_address(AddressIndex::New)?;
let balance = watch_only_wallet.get_balance()?;
println!("Watch only wallet balances in SATs: {}", balance);
if balance.get_total() < 10000 {
println!(
"Send at least 10000 SATs (0.0001 BTC) from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
addr = deposit_address.address
);
} else if balance.get_spendable() < 10000 {
println!(
"Wait for at least 10000 SATs of your wallet transactions to be confirmed...\nBe patient, this could take 10 mins or longer depending on how testnet is behaving."
);
for tx_details in watch_only_wallet
.list_transactions(false)?
.iter()
.filter(|txd| txd.received > 0 && txd.confirmation_time.is_none())
{
println!(
"See unconfirmed tx for {} SATs: https://mempool.space/testnet/tx/{}",
tx_details.received, tx_details.txid
);
}
} else {
println!("Creating a PSBT sending 9800 SATs plus fee to the u01.net testnet faucet return address 'tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt'.");
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
let mut builder = watch_only_wallet.build_tx();
builder
.add_recipient(return_address.script_pubkey(), 9_800)
.enable_rbf()
.fee_rate(FeeRate::from_sat_per_vb(1.0));
let (mut psbt, details) = builder.finish()?;
println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", psbt);
// Sign and finalize the PSBT with the signing wallet
let finalized = signing_wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized, "The PSBT was not finalized!");
println!("The PSBT has been signed and finalized.");
// Broadcast the transaction
let raw_transaction = psbt.extract_tx();
let txid = raw_transaction.txid();
blockchain.broadcast(&raw_transaction)?;
println!("Transaction broadcast! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
}
Ok(())
}

View File

@@ -103,7 +103,7 @@ fn main() -> Result<(), Box<dyn Error>> {
auth: bitcoind_auth,
network: Network::Regtest,
wallet_name,
skip_blocks: None,
sync_params: None,
};
// Use the above configuration to create a RPC blockchain backend

30
examples/utils/mod.rs Normal file
View File

@@ -0,0 +1,30 @@
pub(crate) mod tx {
use std::str::FromStr;
use bdk::{database::BatchDatabase, SignOptions, Wallet};
use bitcoin::{Address, Transaction};
pub fn build_signed_tx<D: BatchDatabase>(
wallet: &Wallet<D>,
recipient_address: &str,
amount: u64,
) -> Transaction {
// Create a transaction builder
let mut tx_builder = wallet.build_tx();
let to_address = Address::from_str(recipient_address).unwrap();
// Set recipient of the transaction
tx_builder.set_recipients(vec![(to_address.script_pubkey(), amount)]);
// Finalise the transaction and extract PSBT
let (mut psbt, _) = tx_builder.finish().unwrap();
// Sign the above psbt with signing option
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
// Extract the final transaction
psbt.extract_tx()
}
}

View File

@@ -19,7 +19,7 @@ use syn::{parse, ImplItemMethod, ItemImpl, ItemTrait, Token};
fn add_async_trait(mut parsed: ItemTrait) -> TokenStream {
let output = quote! {
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#[cfg(not(feature = "async-interface"))]
#parsed
};
@@ -32,7 +32,7 @@ fn add_async_trait(mut parsed: ItemTrait) -> TokenStream {
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
#[async_trait(?Send)]
#parsed
};
@@ -42,7 +42,7 @@ fn add_async_trait(mut parsed: ItemTrait) -> TokenStream {
fn add_async_method(mut parsed: ImplItemMethod) -> TokenStream {
let output = quote! {
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#[cfg(not(feature = "async-interface"))]
#parsed
};
@@ -51,7 +51,7 @@ fn add_async_method(mut parsed: ImplItemMethod) -> TokenStream {
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
#parsed
};
@@ -60,7 +60,7 @@ fn add_async_method(mut parsed: ImplItemMethod) -> TokenStream {
fn add_async_impl_trait(mut parsed: ItemImpl) -> TokenStream {
let output = quote! {
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#[cfg(not(feature = "async-interface"))]
#parsed
};
@@ -73,7 +73,7 @@ fn add_async_impl_trait(mut parsed: ItemImpl) -> TokenStream {
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
#[async_trait(?Send)]
#parsed
};
@@ -81,7 +81,7 @@ fn add_async_impl_trait(mut parsed: ItemImpl) -> TokenStream {
output.into()
}
/// Makes a method or every method of a trait "async" only if the target_arch is "wasm32"
/// Makes a method or every method of a trait `async`, if the `async-interface` feature is enabled.
///
/// Requires the `async-trait` crate as a dependency whenever this attribute is used on a trait
/// definition or trait implementation.
@@ -101,18 +101,18 @@ pub fn maybe_async(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
/// Awaits if target_arch is "wasm32", does nothing otherwise
/// Awaits, if the `async-interface` feature is enabled.
#[proc_macro]
pub fn maybe_await(expr: TokenStream) -> TokenStream {
let expr: proc_macro2::TokenStream = expr.into();
let quoted = quote! {
{
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#[cfg(not(feature = "async-interface"))]
{
#expr
}
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
{
#expr.await
}
@@ -122,20 +122,20 @@ pub fn maybe_await(expr: TokenStream) -> TokenStream {
quoted.into()
}
/// Awaits if target_arch is "wasm32", uses `tokio::Runtime::block_on()` otherwise
/// Awaits, if the `async-interface` feature is enabled, uses `tokio::Runtime::block_on()` otherwise
///
/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build on non-wasm32 platforms.
/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build.
#[proc_macro]
pub fn await_or_block(expr: TokenStream) -> TokenStream {
let expr: proc_macro2::TokenStream = expr.into();
let quoted = quote! {
{
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#[cfg(not(feature = "async-interface"))]
{
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(#expr)
}
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
{
#expr.await
}

View File

@@ -131,7 +131,7 @@ impl GetBlockHash for AnyBlockchain {
impl WalletSync for AnyBlockchain {
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
@@ -144,7 +144,7 @@ impl WalletSync for AnyBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
@@ -178,7 +178,8 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
/// "type" : "electrum",
/// "url" : "ssl://electrum.blockstream.info:50002",
/// "retry": 2,
/// "stop_gap": 20
/// "stop_gap": 20,
/// "validate_domain": true
/// }"#,
/// )
/// .unwrap();
@@ -190,11 +191,12 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
/// socks5: None,
/// timeout: None,
/// stop_gap: 20,
/// validate_domain: true,
/// })
/// );
/// # }
/// ```
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AnyBlockchainConfig {
#[cfg(feature = "electrum")]

View File

@@ -51,6 +51,7 @@
use std::collections::HashSet;
use std::fmt;
use std::ops::DerefMut;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
@@ -274,7 +275,7 @@ impl WalletSync for CompactFiltersBlockchain {
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let first_peer = &self.peers[0];
@@ -322,6 +323,9 @@ impl WalletSync for CompactFiltersBlockchain {
cf_sync.prepare_sync(Arc::clone(first_peer))?;
let mut database = database.borrow_mut();
let database = database.deref_mut();
let all_scripts = Arc::new(
database
.iter_script_pubkeys(None)?
@@ -479,7 +483,7 @@ impl WalletSync for CompactFiltersBlockchain {
}
/// Data to connect to a Bitcoin P2P peer
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct BitcoinPeerConfig {
/// Peer address such as 127.0.0.1:18333
pub address: String,
@@ -490,7 +494,7 @@ pub struct BitcoinPeerConfig {
}
/// Configuration for a [`CompactFiltersBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct CompactFiltersBlockchainConfig {
/// List of peers to try to connect to for asking headers and filters
pub peers: Vec<BitcoinPeerConfig>,
@@ -576,7 +580,27 @@ pub enum CompactFiltersError {
impl fmt::Display for CompactFiltersError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
match self {
Self::InvalidResponse => write!(f, "A peer sent an invalid or unexpected response"),
Self::InvalidHeaders => write!(f, "Invalid headers"),
Self::InvalidFilterHeader => write!(f, "Invalid filter header"),
Self::InvalidFilter => write!(f, "Invalid filters"),
Self::MissingBlock => write!(f, "The peer is missing a block in the valid chain"),
Self::BlockHashNotFound => write!(f, "Block hash not found"),
Self::DataCorruption => write!(
f,
"The data stored in the block filters storage are corrupted"
),
Self::NotConnected => write!(f, "A peer is not connected"),
Self::Timeout => write!(f, "A peer took too long to reply to one of our messages"),
Self::PeerBloomDisabled => write!(f, "Peer doesn't advertise the BLOOM service flag"),
Self::NoPeers => write!(f, "No peers have been specified"),
Self::Db(err) => write!(f, "Internal database error: {}", err),
Self::Io(err) => write!(f, "Internal I/O error: {}", err),
Self::Bip158(err) => write!(f, "Invalid BIP158 filter: {}", err),
Self::Time(err) => write!(f, "Invalid system time: {}", err),
Self::Global(err) => write!(f, "Generic error: {}", err),
}
}
}

View File

@@ -75,7 +75,10 @@ impl Mempool {
/// Look-up a transaction in the mempool given an [`Inventory`] request
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
let identifer = match inventory {
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None,
Inventory::Error
| Inventory::Block(_)
| Inventory::WitnessBlock(_)
| Inventory::CompactBlock(_) => return None,
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),

View File

@@ -13,7 +13,6 @@ use std::convert::TryInto;
use std::fmt;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::RwLock;
@@ -22,9 +21,9 @@ use rand::{thread_rng, Rng};
use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch, DB};
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::consensus::{deserialize, encode::VarInt, serialize, Decodable, Encodable};
use bitcoin::hash_types::{FilterHash, FilterHeader};
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::Hash;
use bitcoin::util::bip158::BlockFilter;
use bitcoin::util::uint::Uint256;
@@ -33,17 +32,8 @@ use bitcoin::BlockHash;
use bitcoin::BlockHeader;
use bitcoin::Network;
use lazy_static::lazy_static;
use super::CompactFiltersError;
lazy_static! {
static ref MAINNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A29AB5F49FFFF001D1DAC2B7C0101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref TESTNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF001D1AA4AE180101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref REGTEST_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4ADAE5494DFFFF7F20020000000101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
static ref SIGNET_GENESIS: Block = deserialize(&Vec::<u8>::from_hex("0100000000000000000000000000000000000000000000000000000000000000000000003BA3EDFD7A7B12B27AC72C3E67768F617FC81BC3888A51323A9FB8AA4B1E5E4A008F4D5FAE77031E8AD222030101000000010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF4D04FFFF001D0104455468652054696D65732030332F4A616E2F32303039204368616E63656C6C6F72206F6E206272696E6B206F66207365636F6E64206261696C6F757420666F722062616E6B73FFFFFFFF0100F2052A01000000434104678AFDB0FE5548271967F1A67130B7105CD6A828E03909A67962E0EA1F61DEB649F6BC3F4CEF38C4F35504E51EC112DE5C384DF7BA0B8D578A4C702B6BF11D5FAC00000000").unwrap()).unwrap();
}
pub trait StoreType: Default + fmt::Debug {}
#[derive(Default, Debug)]
@@ -113,42 +103,42 @@ where
}
impl Encodable for BundleStatus {
fn consensus_encode<W: Write>(&self, mut e: W) -> Result<usize, std::io::Error> {
fn consensus_encode<W: Write + ?Sized>(&self, e: &mut W) -> Result<usize, std::io::Error> {
let mut written = 0;
match self {
BundleStatus::Init => {
written += 0x00u8.consensus_encode(&mut e)?;
written += 0x00u8.consensus_encode(e)?;
}
BundleStatus::CfHeaders { cf_headers } => {
written += 0x01u8.consensus_encode(&mut e)?;
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
written += 0x01u8.consensus_encode(e)?;
written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
for header in cf_headers {
written += header.consensus_encode(&mut e)?;
written += header.consensus_encode(e)?;
}
}
BundleStatus::CFilters { cf_filters } => {
written += 0x02u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x02u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters {
written += filter.consensus_encode(&mut e)?;
written += filter.consensus_encode(e)?;
}
}
BundleStatus::Processed { cf_filters } => {
written += 0x03u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x03u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters {
written += filter.consensus_encode(&mut e)?;
written += filter.consensus_encode(e)?;
}
}
BundleStatus::Pruned => {
written += 0x04u8.consensus_encode(&mut e)?;
written += 0x04u8.consensus_encode(e)?;
}
BundleStatus::Tip { cf_filters } => {
written += 0x05u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x05u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters {
written += filter.consensus_encode(&mut e)?;
written += filter.consensus_encode(e)?;
}
}
}
@@ -158,51 +148,53 @@ impl Encodable for BundleStatus {
}
impl Decodable for BundleStatus {
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
let byte_type = u8::consensus_decode(&mut d)?;
fn consensus_decode<D: Read + ?Sized>(
d: &mut D,
) -> Result<Self, bitcoin::consensus::encode::Error> {
let byte_type = u8::consensus_decode(d)?;
match byte_type {
0x00 => Ok(BundleStatus::Init),
0x01 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_headers = Vec::with_capacity(num);
for _ in 0..num {
cf_headers.push(FilterHeader::consensus_decode(&mut d)?);
cf_headers.push(FilterHeader::consensus_decode(d)?);
}
Ok(BundleStatus::CfHeaders { cf_headers })
}
0x02 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
}
Ok(BundleStatus::CFilters { cf_filters })
}
0x03 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
}
Ok(BundleStatus::Processed { cf_filters })
}
0x04 => Ok(BundleStatus::Pruned),
0x05 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?);
cf_filters.push(Vec::<u8>::consensus_decode(d)?);
}
Ok(BundleStatus::Tip { cf_filters })
@@ -224,12 +216,7 @@ pub struct ChainStore<T: StoreType> {
impl ChainStore<Full> {
pub fn new(store: DB, network: Network) -> Result<Self, CompactFiltersError> {
let genesis = match network {
Network::Bitcoin => MAINNET_GENESIS.deref(),
Network::Testnet => TESTNET_GENESIS.deref(),
Network::Regtest => REGTEST_GENESIS.deref(),
Network::Signet => SIGNET_GENESIS.deref(),
};
let genesis = genesis_block(network);
let cf_name = "default".to_string();
let cf_handle = store.cf_handle(&cf_name).unwrap();
@@ -246,7 +233,7 @@ impl ChainStore<Full> {
batch.put_cf(
cf_handle,
StoreEntry::BlockHeaderIndex(Some(genesis.block_hash())).get_key(),
&0usize.to_be_bytes(),
0usize.to_be_bytes(),
);
store.write(batch)?;
}
@@ -291,7 +278,11 @@ impl ChainStore<Full> {
}
pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> {
let new_cf_name: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect();
let new_cf_name: String = thread_rng()
.sample_iter(&Alphanumeric)
.map(|byte| byte as char)
.take(16)
.collect();
let new_cf_name = format!("_headers:{}", new_cf_name);
let mut write_store = self.store.write().unwrap();
@@ -311,7 +302,7 @@ impl ChainStore<Full> {
batch.put_cf(
new_cf_handle,
StoreEntry::BlockHeaderIndex(Some(header.block_hash())).get_key(),
&from.to_be_bytes(),
from.to_be_bytes(),
);
batch.put_cf(
new_cf_handle,
@@ -593,7 +584,7 @@ impl<T: StoreType> ChainStore<T> {
batch.put_cf(
cf_handle,
StoreEntry::BlockHeaderIndex(Some(header.block_hash())).get_key(),
&(height).to_be_bytes(),
(height).to_be_bytes(),
);
batch.put_cf(
cf_handle,
@@ -647,14 +638,9 @@ impl CfStore {
filter_type,
};
let genesis = match headers_store.network {
Network::Bitcoin => MAINNET_GENESIS.deref(),
Network::Testnet => TESTNET_GENESIS.deref(),
Network::Regtest => REGTEST_GENESIS.deref(),
Network::Signet => SIGNET_GENESIS.deref(),
};
let genesis = genesis_block(headers_store.network);
let filter = BlockFilter::new_script_filter(genesis, |utxo| {
let filter = BlockFilter::new_script_filter(&genesis, |utxo| {
Err(bitcoin::util::bip158::Error::UtxoMissing(*utxo))
})?;
let first_key = StoreEntry::CFilterTable((filter_type, Some(0))).get_key();
@@ -667,7 +653,7 @@ impl CfStore {
&first_key,
(
BundleStatus::Init,
filter.filter_header(&FilterHeader::from_hash(Default::default())),
filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
)
.serialize(),
)?;

View File

@@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use bitcoin::hash_types::{BlockHash, FilterHeader};
use bitcoin::hashes::Hash;
use bitcoin::network::message::NetworkMessage;
use bitcoin::network::message_blockdata::GetHeadersMessage;
use bitcoin::util::bip158::BlockFilter;
@@ -135,7 +136,7 @@ impl CfSync {
let resp = peer.get_cf_headers(0x00, start_height as u32, stop_hash)?;
assert!(resp.previous_filter_header == checkpoint);
assert_eq!(resp.previous_filter_header, checkpoint);
status =
self.cf_store
.advance_to_cf_headers(index, checkpoint, resp.filter_hashes)?;
@@ -254,7 +255,7 @@ where
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
locators_vec,
Default::default(),
Hash::all_zeros(),
)))?;
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
@@ -276,7 +277,7 @@ where
while sync_height < peer.get_version().start_height as usize {
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
vec![last_hash],
Default::default(),
Hash::all_zeros(),
)))?;
if let NetworkMessage::Headers(headers) = peer
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?

View File

@@ -25,6 +25,7 @@
//! ```
use std::collections::{HashMap, HashSet};
use std::ops::{Deref, DerefMut};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
@@ -79,6 +80,14 @@ impl Blockchain for ElectrumBlockchain {
}
}
impl Deref for ElectrumBlockchain {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl StatelessBlockchain for ElectrumBlockchain {}
impl GetHeight for ElectrumBlockchain {
@@ -108,9 +117,11 @@ impl GetBlockHash for ElectrumBlockchain {
impl WalletSync for ElectrumBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut database = database.borrow_mut();
let database = database.deref_mut();
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();
@@ -272,9 +283,11 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
.client
.batch_transaction_get(need_fetch.clone())
.map_err(Error::Electrum)?;
for (tx, _txid) in txs.into_iter().zip(need_fetch) {
debug_assert_eq!(*_txid, tx.txid());
self.cache.insert(tx.txid(), tx);
let mut txs: HashMap<_, _> = txs.into_iter().map(|tx| (tx.txid(), tx)).collect();
for txid in need_fetch {
if let Some(tx) = txs.remove(txid) {
self.cache.insert(*txid, tx);
}
}
}
@@ -287,7 +300,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
}
/// Configuration for an [`ElectrumBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct ElectrumBlockchainConfig {
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
///
@@ -301,6 +314,8 @@ pub struct ElectrumBlockchainConfig {
pub timeout: Option<u8>,
/// Stop searching addresses for transactions after finding an unused gap of this length
pub stop_gap: usize,
/// Validate the domain when using SSL
pub validate_domain: bool,
}
impl ConfigurableBlockchain for ElectrumBlockchain {
@@ -312,6 +327,7 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
.retry(config.retry)
.timeout(config.timeout)?
.socks5(socks5)?
.validate_domain(config.validate_domain)
.build();
Ok(ElectrumBlockchain {
@@ -385,7 +401,7 @@ mod test {
.sync_wallet(&wallet, None, Default::default())
.unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
}
#[test]
@@ -406,6 +422,7 @@ mod test {
retry: 0,
timeout: None,
stop_gap: stop_gap,
validate_domain: true,
})
}
}

View File

@@ -1,117 +0,0 @@
//! structs from the esplora API
//!
//! see: <https://github.com/Blockstream/esplora/blob/master/API.md>
use crate::BlockTime;
use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness};
#[derive(serde::Deserialize, Clone, Debug)]
pub struct PrevOut {
pub value: u64,
pub scriptpubkey: Script,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Vin {
pub txid: Txid,
pub vout: u32,
// None if coinbase
pub prevout: Option<PrevOut>,
pub scriptsig: Script,
#[serde(deserialize_with = "deserialize_witness", default)]
pub witness: Vec<Vec<u8>>,
pub sequence: u32,
pub is_coinbase: bool,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Vout {
pub value: u64,
pub scriptpubkey: Script,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct TxStatus {
pub confirmed: bool,
pub block_height: Option<u32>,
pub block_time: Option<u64>,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct Tx {
pub txid: Txid,
pub version: i32,
pub locktime: u32,
pub vin: Vec<Vin>,
pub vout: Vec<Vout>,
pub status: TxStatus,
pub fee: u64,
}
impl Tx {
pub fn to_tx(&self) -> Transaction {
Transaction {
version: self.version,
lock_time: self.locktime,
input: self
.vin
.iter()
.cloned()
.map(|vin| TxIn {
previous_output: OutPoint {
txid: vin.txid,
vout: vin.vout,
},
script_sig: vin.scriptsig,
sequence: vin.sequence,
witness: Witness::from_vec(vin.witness),
})
.collect(),
output: self
.vout
.iter()
.cloned()
.map(|vout| TxOut {
value: vout.value,
script_pubkey: vout.scriptpubkey,
})
.collect(),
}
}
pub fn confirmation_time(&self) -> Option<BlockTime> {
match self.status {
TxStatus {
confirmed: true,
block_height: Some(height),
block_time: Some(timestamp),
} => Some(BlockTime { timestamp, height }),
_ => None,
}
}
pub fn previous_outputs(&self) -> Vec<Option<TxOut>> {
self.vin
.iter()
.cloned()
.map(|vin| {
vin.prevout.map(|po| TxOut {
script_pubkey: po.scriptpubkey,
value: po.value,
})
})
.collect()
}
}
fn deserialize_witness<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
use crate::serde::Deserialize;
use bitcoin::hashes::hex::FromHex;
let list = Vec::<String>::deserialize(d)?;
list.into_iter()
.map(|hex_str| Vec::<u8>::from_hex(&hex_str))
.collect::<Result<Vec<Vec<u8>>, _>>()
.map_err(serde::de::Error::custom)
}

View File

@@ -12,49 +12,38 @@
//! Esplora by way of `reqwest` HTTP client.
use std::collections::{HashMap, HashSet};
use std::ops::{Deref, DerefMut};
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::{BlockHeader, Script, Transaction, Txid};
use bitcoin::{Transaction, Txid};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use ::reqwest::{Client, StatusCode};
use esplora_client::{convert_fee_rate, AsyncClient, Builder, Tx};
use futures::stream::{FuturesOrdered, TryStreamExt};
use super::api::Tx;
use crate::blockchain::esplora::EsploraError;
use crate::blockchain::*;
use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
#[derive(Debug)]
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.
client: Client,
concurrency: u8,
}
/// Structure that implements the logic to sync with Esplora
///
/// ## Example
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
#[derive(Debug)]
pub struct EsploraBlockchain {
url_client: UrlClient,
url_client: AsyncClient,
stop_gap: usize,
concurrency: u8,
}
impl std::convert::From<UrlClient> for EsploraBlockchain {
fn from(url_client: UrlClient) -> Self {
impl std::convert::From<AsyncClient> for EsploraBlockchain {
fn from(url_client: AsyncClient) -> Self {
EsploraBlockchain {
url_client,
stop_gap: 20,
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
}
}
}
@@ -62,19 +51,25 @@ impl std::convert::From<UrlClient> for EsploraBlockchain {
impl EsploraBlockchain {
/// Create a new instance of the client from a base URL and `stop_gap`.
pub fn new(base_url: &str, stop_gap: usize) -> Self {
let url_client = Builder::new(base_url)
.build_async()
.expect("Should never fail with no proxy and timeout");
Self::from_client(url_client, stop_gap)
}
/// Build a new instance given a client
pub fn from_client(url_client: AsyncClient, stop_gap: usize) -> Self {
EsploraBlockchain {
url_client: UrlClient {
url: base_url.to_string(),
client: Client::new(),
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
},
url_client,
stop_gap,
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
}
}
/// Set the concurrency to use when doing batch queries against the Esplora instance.
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
self.url_client.concurrency = concurrency;
self.concurrency = concurrency;
self
}
}
@@ -92,12 +87,22 @@ impl Blockchain for EsploraBlockchain {
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?)
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)
let estimates = await_or_block!(self.url_client.get_fee_estimates())?;
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
target, estimates,
)?))
}
}
impl Deref for EsploraBlockchain {
type Target = AsyncClient;
fn deref(&self) -> &Self::Target {
&self.url_client
}
}
@@ -106,22 +111,23 @@ 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())?)
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))?)
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())
Ok(await_or_block!(self
.url_client
.get_block_hash(height as u32))?)
}
}
@@ -129,10 +135,12 @@ impl GetBlockHash for EsploraBlockchain {
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
use crate::blockchain::script_sync::Request;
let mut database = database.borrow_mut();
let database = database.deref_mut();
let mut request = script_sync::start(database, self.stop_gap)?;
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
@@ -141,10 +149,10 @@ impl WalletSync for EsploraBlockchain {
Request::Script(script_req) => {
let futures: FuturesOrdered<_> = script_req
.request()
.take(self.url_client.concurrency as usize)
.take(self.concurrency as usize)
.map(|script| async move {
let mut related_txs: Vec<Tx> =
self.url_client._scripthash_txs(script, None).await?;
self.url_client.scripthash_txs(script, None).await?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
@@ -154,7 +162,7 @@ impl WalletSync for EsploraBlockchain {
loop {
let new_related_txs: Vec<Tx> = self
.url_client
._scripthash_txs(
.scripthash_txs(
script,
Some(related_txs.last().unwrap().txid),
)
@@ -194,6 +202,7 @@ impl WalletSync for EsploraBlockchain {
.get(txid)
.expect("must be in index")
.confirmation_time()
.map(Into::into)
})
.collect();
conftime_req.satisfy(conftimes)?
@@ -213,137 +222,30 @@ impl WalletSync for EsploraBlockchain {
};
database.commit_batch(batch_update)?;
Ok(())
}
}
impl UrlClient {
async fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
let resp = self
.client
.get(&format!("{}/tx/{}/raw", self.url, txid))
.send()
.await?;
if let StatusCode::NOT_FOUND = resp.status() {
return Ok(None);
}
Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?))
}
async fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
match self._get_tx(txid).await {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
async fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
let resp = self
.client
.get(&format!("{}/block-height/{}", self.url, block_height))
.send()
.await?;
if let StatusCode::NOT_FOUND = resp.status() {
return Err(EsploraError::HeaderHeightNotFound(block_height));
}
let bytes = resp.bytes().await?;
let hash = std::str::from_utf8(&bytes)
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
let resp = self
.client
.get(&format!("{}/block/{}/header", self.url, hash))
.send()
.await?;
let header = deserialize(&Vec::from_hex(&resp.text().await?)?)?;
Ok(header)
}
async fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
self.client
.post(&format!("{}/tx", self.url))
.body(serialize(transaction).to_hex())
.send()
.await?
.error_for_status()?;
Ok(())
}
async fn _get_height(&self) -> Result<u32, EsploraError> {
let req = self
.client
.get(&format!("{}/blocks/tip/height", self.url))
.send()
.await?;
Ok(req.error_for_status()?.text().await?.parse()?)
}
async fn _scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<Tx>, EsploraError> {
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
let url = match last_seen {
Some(last_seen) => format!(
"{}/scripthash/{}/txs/chain/{}",
self.url, script_hash, last_seen
),
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
};
Ok(self
.client
.get(url)
.send()
.await?
.error_for_status()?
.json::<Vec<Tx>>()
.await?)
}
async fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
Ok(self
.client
.get(&format!("{}/fee-estimates", self.url,))
.send()
.await?
.error_for_status()?
.json::<HashMap<String, f64>>()
.await?)
}
}
impl ConfigurableBlockchain for EsploraBlockchain {
type Config = super::EsploraBlockchainConfig;
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let map_e = |e: reqwest::Error| Error::Esplora(Box::new(e.into()));
let mut builder = Builder::new(config.base_url.as_str());
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap);
if let Some(concurrency) = config.concurrency {
blockchain.url_client.concurrency = concurrency;
}
let mut builder = Client::builder();
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = &config.proxy {
builder = builder.proxy(reqwest::Proxy::all(proxy).map_err(map_e)?);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(timeout) = config.timeout {
builder = builder.timeout(core::time::Duration::from_secs(timeout));
builder = builder.timeout(timeout);
}
blockchain.url_client.client = builder.build().map_err(map_e)?;
if let Some(proxy) = &config.proxy {
builder = builder.proxy(proxy);
}
let mut blockchain =
EsploraBlockchain::from_client(builder.build_async()?, config.stop_gap);
if let Some(concurrency) = config.concurrency {
blockchain = blockchain.with_concurrency(concurrency);
}
Ok(blockchain)
}

View File

@@ -12,40 +12,27 @@
//! Esplora by way of `ureq` HTTP client.
use std::collections::{HashMap, HashSet};
use std::io;
use std::io::Read;
use std::time::Duration;
use std::ops::DerefMut;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use ureq::{Agent, Proxy, Response};
use bitcoin::{Transaction, Txid};
use bitcoin::consensus::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::{BlockHeader, Script, Transaction, Txid};
use esplora_client::{convert_fee_rate, BlockingClient, Builder, Tx};
use super::api::Tx;
use crate::blockchain::esplora::EsploraError;
use crate::blockchain::*;
use crate::database::BatchDatabase;
use crate::error::Error;
use crate::FeeRate;
#[derive(Debug, Clone)]
struct UrlClient {
url: String,
agent: Agent,
}
/// Structure that implements the logic to sync with Esplora
///
/// ## Example
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
#[derive(Debug)]
pub struct EsploraBlockchain {
url_client: UrlClient,
url_client: BlockingClient,
stop_gap: usize,
concurrency: u8,
}
@@ -53,22 +40,22 @@ pub struct EsploraBlockchain {
impl EsploraBlockchain {
/// Create a new instance of the client from a base URL and the `stop_gap`.
pub fn new(base_url: &str, stop_gap: usize) -> Self {
let url_client = Builder::new(base_url)
.build_blocking()
.expect("Should never fail with no proxy and timeout");
Self::from_client(url_client, stop_gap)
}
/// Build a new instance given a client
pub fn from_client(url_client: BlockingClient, stop_gap: usize) -> Self {
EsploraBlockchain {
url_client: UrlClient {
url: base_url.to_string(),
agent: Agent::new(),
},
url_client,
concurrency: super::DEFAULT_CONCURRENT_REQUESTS,
stop_gap,
}
}
/// Set the inner `ureq` agent.
pub fn with_agent(mut self, agent: Agent) -> Self {
self.url_client.agent = agent;
self
}
/// Set the number of parallel requests the client can make.
pub fn with_concurrency(mut self, concurrency: u8) -> Self {
self.concurrency = concurrency;
@@ -88,13 +75,23 @@ impl Blockchain for EsploraBlockchain {
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.url_client._broadcast(tx)?;
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)
let estimates = self.url_client.get_fee_estimates()?;
Ok(FeeRate::from_sat_per_vb(convert_fee_rate(
target, estimates,
)?))
}
}
impl Deref for EsploraBlockchain {
type Target = BlockingClient;
fn deref(&self) -> &Self::Target {
&self.url_client
}
}
@@ -102,30 +99,31 @@ impl StatelessBlockchain for EsploraBlockchain {}
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
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)?)
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())
Ok(self.url_client.get_block_hash(height as u32)?)
}
}
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
use crate::blockchain::script_sync::Request;
let mut database = database.borrow_mut();
let database = database.deref_mut();
let mut request = script_sync::start(database, self.stop_gap)?;
let mut tx_index: HashMap<Txid, Tx> = HashMap::new();
let batch_update = loop {
@@ -141,7 +139,7 @@ impl WalletSync for EsploraBlockchain {
let client = self.url_client.clone();
// make each request in its own thread.
handles.push(std::thread::spawn(move || {
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
let mut related_txs: Vec<Tx> = client.scripthash_txs(&script, None)?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
@@ -149,7 +147,7 @@ impl WalletSync for EsploraBlockchain {
// keep requesting to see if there's more.
if n_confirmed >= 25 {
loop {
let new_related_txs: Vec<Tx> = client._scripthash_txs(
let new_related_txs: Vec<Tx> = client.scripthash_txs(
&script,
Some(related_txs.last().unwrap().txid),
)?;
@@ -192,6 +190,7 @@ impl WalletSync for EsploraBlockchain {
.get(txid)
.expect("must be in index")
.confirmation_time()
.map(Into::into)
})
.collect();
conftime_req.satisfy(conftimes)?
@@ -216,159 +215,22 @@ impl WalletSync for EsploraBlockchain {
}
}
impl UrlClient {
fn _get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, EsploraError> {
let resp = self
.agent
.get(&format!("{}/tx/{}/raw", self.url, txid))
.call();
match resp {
Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)),
Err(ureq::Error::Status(code, _)) => {
if is_status_not_found(code) {
return Ok(None);
}
Err(EsploraError::HttpResponse(code))
}
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, EsploraError> {
match self._get_tx(txid) {
Ok(Some(tx)) => Ok(tx),
Ok(None) => Err(EsploraError::TransactionNotFound(*txid)),
Err(e) => Err(e),
}
}
fn _get_header(&self, block_height: u32) -> Result<BlockHeader, EsploraError> {
let resp = self
.agent
.get(&format!("{}/block-height/{}", self.url, block_height))
.call();
let bytes = match resp {
Ok(resp) => Ok(into_bytes(resp)?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}?;
let hash = std::str::from_utf8(&bytes)
.map_err(|_| EsploraError::HeaderHeightNotFound(block_height))?;
let resp = self
.agent
.get(&format!("{}/block/{}/header", self.url, hash))
.call();
match resp {
Ok(resp) => Ok(deserialize(&Vec::from_hex(&resp.into_string()?)?)?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
let resp = self
.agent
.post(&format!("{}/tx", self.url))
.send_string(&serialize(transaction).to_hex());
match resp {
Ok(_) => Ok(()), // We do not return the txid?
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_height(&self) -> Result<u32, EsploraError> {
let resp = self
.agent
.get(&format!("{}/blocks/tip/height", self.url))
.call();
match resp {
Ok(resp) => Ok(resp.into_string()?.parse()?),
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}
}
fn _get_fee_estimates(&self) -> Result<HashMap<String, f64>, EsploraError> {
let resp = self
.agent
.get(&format!("{}/fee-estimates", self.url,))
.call();
let map = match resp {
Ok(resp) => {
let map: HashMap<String, f64> = resp.into_json()?;
Ok(map)
}
Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)),
Err(e) => Err(EsploraError::Ureq(e)),
}?;
Ok(map)
}
fn _scripthash_txs(
&self,
script: &Script,
last_seen: Option<Txid>,
) -> Result<Vec<Tx>, EsploraError> {
let script_hash = sha256::Hash::hash(script.as_bytes()).into_inner().to_hex();
let url = match last_seen {
Some(last_seen) => format!(
"{}/scripthash/{}/txs/chain/{}",
self.url, script_hash, last_seen
),
None => format!("{}/scripthash/{}/txs", self.url, script_hash),
};
Ok(self.agent.get(&url).call()?.into_json()?)
}
}
fn is_status_not_found(status: u16) -> bool {
status == 404
}
fn into_bytes(resp: Response) -> Result<Vec<u8>, io::Error> {
const BYTES_LIMIT: usize = 10 * 1_024 * 1_024;
let mut buf: Vec<u8> = vec![];
resp.into_reader()
.take((BYTES_LIMIT + 1) as u64)
.read_to_end(&mut buf)?;
if buf.len() > BYTES_LIMIT {
return Err(io::Error::new(
io::ErrorKind::Other,
"response too big for into_bytes",
));
}
Ok(buf)
}
impl ConfigurableBlockchain for EsploraBlockchain {
type Config = super::EsploraBlockchainConfig;
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let mut agent_builder = ureq::AgentBuilder::new();
let mut builder = Builder::new(config.base_url.as_str());
if let Some(timeout) = config.timeout {
agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
builder = builder.timeout(timeout);
}
if let Some(proxy) = &config.proxy {
agent_builder = agent_builder
.proxy(Proxy::new(proxy).map_err(|e| Error::Esplora(Box::new(e.into())))?);
builder = builder.proxy(proxy);
}
let mut blockchain = EsploraBlockchain::new(config.base_url.as_str(), config.stop_gap)
.with_agent(agent_builder.build());
let mut blockchain =
EsploraBlockchain::from_client(builder.build_blocking()?, config.stop_gap);
if let Some(concurrency) = config.concurrency {
blockchain = blockchain.with_concurrency(concurrency);
@@ -377,12 +239,3 @@ impl ConfigurableBlockchain for EsploraBlockchain {
Ok(blockchain)
}
}
impl From<ureq::Error> for EsploraError {
fn from(e: ureq::Error) -> Self {
match e {
ureq::Error::Status(code, _) => EsploraError::HttpResponse(code),
e => EsploraError::Ureq(e),
}
}
}

View File

@@ -15,89 +15,25 @@
//! depending on your needs (blocking or async respectively).
//!
//! Please note, to configure the Esplora HTTP client correctly use one of:
//! Blocking: --features='esplora,ureq'
//! Async: --features='async-interface,esplora,reqwest' --no-default-features
use std::collections::HashMap;
use std::fmt;
use std::io;
//! Blocking: --features='use-esplora-blocking'
//! Async: --features='async-interface,use-esplora-async' --no-default-features
use bitcoin::consensus;
use bitcoin::{BlockHash, Txid};
pub use esplora_client::Error as EsploraError;
use crate::error::Error;
use crate::FeeRate;
#[cfg(feature = "use-esplora-async")]
mod r#async;
#[cfg(feature = "reqwest")]
mod reqwest;
#[cfg(feature = "use-esplora-async")]
pub use self::r#async::*;
#[cfg(feature = "reqwest")]
pub use self::reqwest::*;
#[cfg(feature = "use-esplora-blocking")]
mod blocking;
#[cfg(feature = "ureq")]
mod ureq;
#[cfg(feature = "ureq")]
pub use self::ureq::*;
mod api;
fn into_fee_rate(target: usize, estimates: HashMap<String, f64>) -> Result<FeeRate, Error> {
let fee_val = {
let mut pairs = estimates
.into_iter()
.filter_map(|(k, v)| Some((k.parse::<usize>().ok()?, v)))
.collect::<Vec<_>>();
pairs.sort_unstable_by_key(|(k, _)| std::cmp::Reverse(*k));
pairs
.into_iter()
.find(|(k, _)| k <= &target)
.map(|(_, v)| v)
.unwrap_or(1.0)
};
Ok(FeeRate::from_sat_per_vb(fee_val as f32))
}
/// Errors that can happen during a sync with [`EsploraBlockchain`]
#[derive(Debug)]
pub enum EsploraError {
/// Error during ureq HTTP request
#[cfg(feature = "ureq")]
Ureq(::ureq::Error),
/// Transport error during the ureq HTTP call
#[cfg(feature = "ureq")]
UreqTransport(::ureq::Transport),
/// Error during reqwest HTTP request
#[cfg(feature = "reqwest")]
Reqwest(::reqwest::Error),
/// HTTP response error
HttpResponse(u16),
/// IO error during ureq response read
Io(io::Error),
/// No header found in ureq response
NoHeader,
/// Invalid number returned
Parsing(std::num::ParseIntError),
/// Invalid Bitcoin data returned
BitcoinEncoding(bitcoin::consensus::encode::Error),
/// Invalid Hex data returned
Hex(bitcoin::hashes::hex::Error),
/// Transaction not found
TransactionNotFound(Txid),
/// Header height not found
HeaderHeightNotFound(u32),
/// Header hash not found
HeaderHashNotFound(BlockHash),
}
impl fmt::Display for EsploraError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[cfg(feature = "use-esplora-blocking")]
pub use self::blocking::*;
/// Configuration for an [`EsploraBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct EsploraBlockchainConfig {
/// Base URL of the esplora service
///
@@ -138,16 +74,11 @@ impl EsploraBlockchainConfig {
}
}
impl std::error::Error for EsploraError {}
#[cfg(feature = "ureq")]
impl_error!(::ureq::Transport, UreqTransport, EsploraError);
#[cfg(feature = "reqwest")]
impl_error!(::reqwest::Error, Reqwest, EsploraError);
impl_error!(io::Error, Io, EsploraError);
impl_error!(std::num::ParseIntError, Parsing, EsploraError);
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
impl From<esplora_client::BlockTime> for crate::BlockTime {
fn from(esplora_client::BlockTime { timestamp, height }: esplora_client::BlockTime) -> Self {
Self { timestamp, height }
}
}
#[cfg(test)]
#[cfg(feature = "test-esplora")]
@@ -161,58 +92,11 @@ const DEFAULT_CONCURRENT_REQUESTS: u8 = 4;
#[cfg(test)]
mod test {
use super::*;
#[test]
fn feerate_parsing() {
let esplora_fees = serde_json::from_str::<HashMap<String, f64>>(
r#"{
"25": 1.015,
"5": 2.3280000000000003,
"12": 2.0109999999999997,
"15": 1.018,
"17": 1.018,
"11": 2.0109999999999997,
"3": 3.01,
"2": 4.9830000000000005,
"6": 2.2359999999999998,
"21": 1.018,
"13": 1.081,
"7": 2.2359999999999998,
"8": 2.2359999999999998,
"16": 1.018,
"20": 1.018,
"22": 1.017,
"23": 1.017,
"504": 1,
"9": 2.2359999999999998,
"14": 1.018,
"10": 2.0109999999999997,
"24": 1.017,
"1008": 1,
"1": 4.9830000000000005,
"4": 2.3280000000000003,
"19": 1.018,
"144": 1,
"18": 1.018
}
"#,
)
.unwrap();
assert_eq!(
into_fee_rate(6, esplora_fees.clone()).unwrap(),
FeeRate::from_sat_per_vb(2.236)
);
assert_eq!(
into_fee_rate(26, esplora_fees).unwrap(),
FeeRate::from_sat_per_vb(1.015),
"should inherit from value for 25"
);
}
#[test]
#[cfg(feature = "test-esplora")]
fn test_esplora_with_variable_configs() {
use super::*;
use crate::testutils::{
blockchain_tests::TestClient,
configurable_blockchain_tests::ConfigurableBlockchainTester,

View File

@@ -16,6 +16,7 @@
//! [Compact Filters/Neutrino](crate::blockchain::compact_filters), along with a generalized trait
//! [`Blockchain`] that can be implemented to build customized backends.
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::mpsc::{channel, Receiver, Sender};
@@ -133,7 +134,7 @@ pub trait WalletSync {
/// Populate the internal database with transactions and UTXOs
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error>;
@@ -156,7 +157,7 @@ pub trait WalletSync {
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.wallet_setup(database, progress_update))
@@ -194,7 +195,7 @@ This example shows how to sync multiple walles and return the sum of their balan
# use bdk::database::*;
# use bdk::wallet::*;
# use bdk::*;
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
Ok(wallets
.iter()
.map(|w| -> Result<_, Error> {
@@ -248,11 +249,8 @@ pub trait BlockchainFactory {
/// 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"))))
)]
#[cfg(not(feature = "async-interface"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "async-interface"))))]
fn sync_wallet<D: BatchDatabase>(
&self,
wallet: &Wallet<D>,
@@ -377,7 +375,7 @@ impl<T: GetBlockHash> GetBlockHash for Arc<T> {
impl<T: WalletSync> WalletSync for Arc<T> {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<D>,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.deref().wallet_setup(database, progress_update))
@@ -385,7 +383,7 @@ impl<T: WalletSync> WalletSync for Arc<T> {
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
database: &RefCell<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 }))
}
}
@@ -294,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> {
@@ -305,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> {

View File

@@ -492,4 +492,44 @@ mod test {
fn test_sync_time() {
crate::database::test::test_sync_time(get_tree());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_tree());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_tree());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_tree());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_tree());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_tree());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_tree());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_tree());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_tree());
}
}

View File

@@ -486,18 +486,18 @@ macro_rules! populate_test_db {
}};
($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 input = if $is_coinbase {
vec![$crate::bitcoin::TxIn::default()]
} else {
vec![]
};
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,
lock_time: bitcoin::PackedLockTime(0),
input,
output: tx_meta
.output
@@ -512,14 +512,32 @@ macro_rules! populate_test_db {
};
let txid = tx.txid();
// 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.unwrap().checked_sub(conf as u32).unwrap() + 1,
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()),
txid,
@@ -629,4 +647,44 @@ mod test {
fn test_sync_time() {
crate::database::test::test_sync_time(get_tree());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_tree());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_tree());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_tree());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_tree());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_tree());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_tree());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_tree());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_tree());
}
}

View File

@@ -49,7 +49,7 @@ pub use memory::MemoryDatabase;
/// Blockchain state at the time of syncing
///
/// Contains only the block time and height at the moment
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncTime {
/// Block timestamp and height at the time of sync
pub block_time: BlockTime,
@@ -217,32 +217,33 @@ pub mod test {
use std::str::FromStr;
use bitcoin::consensus::encode::deserialize;
use bitcoin::consensus::serialize;
use bitcoin::hashes::hex::*;
use bitcoin::*;
use super::*;
pub fn test_script_pubkey<D: Database>(mut tree: D) {
pub fn test_script_pubkey<D: Database>(mut db: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
tree.set_script_pubkey(&script, keychain, path).unwrap();
db.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
db.get_script_pubkey_from_path(keychain, path).unwrap(),
Some(script.clone())
);
assert_eq!(
tree.get_path_from_script_pubkey(&script).unwrap(),
db.get_path_from_script_pubkey(&script).unwrap(),
Some((keychain, path))
);
}
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut tree: D) {
let mut batch = tree.begin_batch();
pub fn test_batch_script_pubkey<D: BatchDatabase>(mut db: D) {
let mut batch = db.begin_batch();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
@@ -253,50 +254,50 @@ pub mod test {
batch.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
db.get_script_pubkey_from_path(keychain, path).unwrap(),
None
);
assert_eq!(tree.get_path_from_script_pubkey(&script).unwrap(), None);
assert_eq!(db.get_path_from_script_pubkey(&script).unwrap(), None);
tree.commit_batch(batch).unwrap();
db.commit_batch(batch).unwrap();
assert_eq!(
tree.get_script_pubkey_from_path(keychain, path).unwrap(),
db.get_script_pubkey_from_path(keychain, path).unwrap(),
Some(script.clone())
);
assert_eq!(
tree.get_path_from_script_pubkey(&script).unwrap(),
db.get_path_from_script_pubkey(&script).unwrap(),
Some((keychain, path))
);
}
pub fn test_iter_script_pubkey<D: Database>(mut tree: D) {
pub fn test_iter_script_pubkey<D: Database>(mut db: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
tree.set_script_pubkey(&script, keychain, path).unwrap();
db.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
}
pub fn test_del_script_pubkey<D: Database>(mut tree: D) {
pub fn test_del_script_pubkey<D: Database>(mut db: D) {
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
tree.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
db.set_script_pubkey(&script, keychain, path).unwrap();
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 1);
tree.del_script_pubkey_from_path(keychain, path).unwrap();
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0);
db.del_script_pubkey_from_path(keychain, path).unwrap();
assert_eq!(db.iter_script_pubkeys(None).unwrap().len(), 0);
}
pub fn test_utxo<D: Database>(mut tree: D) {
pub fn test_utxo<D: Database>(mut db: D) {
let outpoint = OutPoint::from_str(
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
)
@@ -315,24 +316,40 @@ pub mod test {
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));
db.set_utxo(&utxo).unwrap();
db.set_utxo(&utxo).unwrap();
assert_eq!(db.iter_utxos().unwrap().len(), 1);
assert_eq!(db.get_utxo(&outpoint).unwrap(), Some(utxo));
}
pub fn test_raw_tx<D: Database>(mut tree: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
pub fn test_raw_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let mut tx: Transaction = deserialize(&hex_tx).unwrap();
tree.set_raw_tx(&tx).unwrap();
db.set_raw_tx(&tx).unwrap();
let txid = tx.txid();
assert_eq!(tree.get_raw_tx(&txid).unwrap(), Some(tx));
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx.clone()));
// mutate transaction's witnesses
for tx_in in tx.input.iter_mut() {
tx_in.witness = Witness::new();
}
let updated_hex_tx = serialize(&tx);
// verify that mutation was successful
assert_ne!(hex_tx, updated_hex_tx);
db.set_raw_tx(&tx).unwrap();
let txid = tx.txid();
assert_eq!(db.get_raw_tx(&txid).unwrap(), Some(tx));
}
pub fn test_tx<D: Database>(mut tree: D) {
pub fn test_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
@@ -348,28 +365,28 @@ pub mod test {
}),
};
tree.set_tx(&tx_details).unwrap();
db.set_tx(&tx_details).unwrap();
// get with raw tx too
assert_eq!(
tree.get_tx(&tx_details.txid, true).unwrap(),
db.get_tx(&tx_details.txid, true).unwrap(),
Some(tx_details.clone())
);
// get only raw_tx
assert_eq!(
tree.get_raw_tx(&tx_details.txid).unwrap(),
db.get_raw_tx(&tx_details.txid).unwrap(),
tx_details.transaction
);
// now get without raw_tx
tx_details.transaction = None;
assert_eq!(
tree.get_tx(&tx_details.txid, false).unwrap(),
db.get_tx(&tx_details.txid, false).unwrap(),
Some(tx_details)
);
}
pub fn test_list_transaction<D: Database>(mut tree: D) {
pub fn test_list_transaction<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let txid = tx.txid();
@@ -385,46 +402,43 @@ pub mod test {
}),
};
tree.set_tx(&tx_details).unwrap();
db.set_tx(&tx_details).unwrap();
// get raw tx
assert_eq!(tree.iter_txs(true).unwrap(), vec![tx_details.clone()]);
assert_eq!(db.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()]);
assert_eq!(db.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();
pub fn test_last_index<D: Database>(mut db: D) {
db.set_last_index(KeychainKind::External, 1337).unwrap();
assert_eq!(
tree.get_last_index(KeychainKind::External).unwrap(),
db.get_last_index(KeychainKind::External).unwrap(),
Some(1337)
);
assert_eq!(tree.get_last_index(KeychainKind::Internal).unwrap(), None);
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), None);
let res = tree.increment_last_index(KeychainKind::External).unwrap();
let res = db.increment_last_index(KeychainKind::External).unwrap();
assert_eq!(res, 1338);
let res = tree.increment_last_index(KeychainKind::Internal).unwrap();
let res = db.increment_last_index(KeychainKind::Internal).unwrap();
assert_eq!(res, 0);
assert_eq!(
tree.get_last_index(KeychainKind::External).unwrap(),
db.get_last_index(KeychainKind::External).unwrap(),
Some(1338)
);
assert_eq!(
tree.get_last_index(KeychainKind::Internal).unwrap(),
Some(0)
);
assert_eq!(db.get_last_index(KeychainKind::Internal).unwrap(), Some(0));
}
pub fn test_sync_time<D: Database>(mut tree: D) {
assert!(tree.get_sync_time().unwrap().is_none());
pub fn test_sync_time<D: Database>(mut db: D) {
assert!(db.get_sync_time().unwrap().is_none());
tree.set_sync_time(SyncTime {
db.set_sync_time(SyncTime {
block_time: BlockTime {
height: 100,
timestamp: 1000,
@@ -432,13 +446,211 @@ pub mod test {
})
.unwrap();
let extracted = tree.get_sync_time().unwrap();
let extracted = db.get_sync_time().unwrap();
assert!(extracted.is_some());
assert_eq!(extracted.as_ref().unwrap().block_time.height, 100);
assert_eq!(extracted.as_ref().unwrap().block_time.timestamp, 1000);
tree.del_sync_time().unwrap();
assert!(tree.get_sync_time().unwrap().is_none());
db.del_sync_time().unwrap();
assert!(db.get_sync_time().unwrap().is_none());
}
pub fn test_iter_raw_txs<D: Database>(mut db: D) {
let txs = db.iter_raw_txs().unwrap();
assert!(txs.is_empty());
let hex_tx = Vec::<u8>::from_hex("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000").unwrap();
let first_tx: Transaction = deserialize(&hex_tx).unwrap();
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let second_tx: Transaction = deserialize(&hex_tx).unwrap();
db.set_raw_tx(&first_tx).unwrap();
db.set_raw_tx(&second_tx).unwrap();
let txs = db.iter_raw_txs().unwrap();
assert!(txs.contains(&first_tx));
assert!(txs.contains(&second_tx));
assert_eq!(txs.len(), 2);
}
pub fn test_del_path_from_script_pubkey<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let res = db.del_path_from_script_pubkey(&script).unwrap();
assert!(res.is_none());
let _res = db.set_script_pubkey(&script, keychain, path);
let (chain, child) = db.del_path_from_script_pubkey(&script).unwrap().unwrap();
assert_eq!(chain, keychain);
assert_eq!(child, path);
let res = db.get_path_from_script_pubkey(&script).unwrap();
assert!(res.is_none());
}
pub fn test_iter_script_pubkeys<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
assert!(scripts.is_empty());
let first_script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
db.set_script_pubkey(&first_script, keychain, path).unwrap();
let second_script = Script::from(
Vec::<u8>::from_hex("00145c9a1816d38db5cbdd4b067b689dc19eb7d930e2").unwrap(),
);
let path = 57;
db.set_script_pubkey(&second_script, keychain, path)
.unwrap();
let scripts = db.iter_script_pubkeys(Some(keychain)).unwrap();
assert!(scripts.contains(&first_script));
assert!(scripts.contains(&second_script));
assert_eq!(scripts.len(), 2);
}
pub fn test_del_utxo<D: Database>(mut db: D) {
let outpoint = OutPoint::from_str(
"5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:0",
)
.unwrap();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let txout = TxOut {
value: 133742,
script_pubkey: script,
};
let utxo = LocalUtxo {
txout,
outpoint,
keychain: KeychainKind::External,
is_spent: true,
};
let res = db.del_utxo(&outpoint).unwrap();
assert!(res.is_none());
db.set_utxo(&utxo).unwrap();
let res = db.del_utxo(&outpoint).unwrap();
assert_eq!(res.unwrap(), utxo);
let res = db.get_utxo(&outpoint).unwrap();
assert!(res.is_none());
}
pub fn test_del_raw_tx<D: Database>(mut db: D) {
let hex_tx = Vec::<u8>::from_hex("02000000000101f58c18a90d7a76b30c7e47d4e817adfdd79a6a589a615ef36e360f913adce2cd0000000000feffffff0210270000000000001600145c9a1816d38db5cbdd4b067b689dc19eb7d930e2cf70aa2b080000001600140f48b63160043047f4f60f7f8f551f80458f693f024730440220413f42b7bc979945489a38f5221e5527d4b8e3aa63eae2099e01945896ad6c10022024ceec492d685c31d8adb64e935a06933877c5ae0e21f32efe029850914c5bad012102361caae96f0e9f3a453d354bb37a5c3244422fb22819bf0166c0647a38de39f21fca2300").unwrap();
let tx: Transaction = deserialize(&hex_tx).unwrap();
let res = db.del_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
db.set_raw_tx(&tx).unwrap();
let res = db.del_raw_tx(&tx.txid()).unwrap();
assert_eq!(res.unwrap(), tx);
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
}
pub fn test_del_tx<D: Database>(mut db: 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.clone()),
txid,
received: 1337,
sent: 420420,
fee: Some(140),
confirmation_time: Some(BlockTime {
timestamp: 123456,
height: 1000,
}),
};
let res = db.del_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
db.set_tx(&tx_details).unwrap();
let res = db.del_tx(&tx.txid(), false).unwrap();
tx_details.transaction = None;
assert_eq!(res.unwrap(), tx_details);
let res = db.get_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert_eq!(res.unwrap(), tx);
db.set_tx(&tx_details).unwrap();
let res = db.del_tx(&tx.txid(), true).unwrap();
tx_details.transaction = Some(tx.clone());
assert_eq!(res.unwrap(), tx_details);
let res = db.get_tx(&tx.txid(), true).unwrap();
assert!(res.is_none());
let res = db.get_raw_tx(&tx.txid()).unwrap();
assert!(res.is_none());
}
pub fn test_del_last_index<D: Database>(mut db: D) {
let keychain = KeychainKind::External;
let _res = db.increment_last_index(keychain);
let res = db.get_last_index(keychain).unwrap().unwrap();
assert_eq!(res, 0);
let _res = db.increment_last_index(keychain);
let res = db.del_last_index(keychain).unwrap().unwrap();
assert_eq!(res, 1);
let res = db.get_last_index(keychain).unwrap();
assert!(res.is_none());
}
pub fn test_check_descriptor_checksum<D: Database>(mut db: D) {
// insert checksum associated to keychain
let checksum = "1cead456".as_bytes();
let keychain = KeychainKind::External;
let _res = db.check_descriptor_checksum(keychain, checksum);
// check if `check_descriptor_checksum` throws
// `Error::ChecksumMismatch` error if the
// function is passed a checksum that does
// not match the one initially inserted
let checksum = "1cead454".as_bytes();
let keychain = KeychainKind::External;
let res = db.check_descriptor_checksum(keychain, checksum);
assert!(res.is_err());
}
// TODO: more tests...

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;
@@ -50,7 +52,22 @@ static MIGRATIONS: &[&str] = &[
"DELETE FROM transactions;",
"DELETE FROM utxos;",
"DROP INDEX idx_txid_vout;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
"ALTER TABLE utxos RENAME TO utxos_old;",
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
"DROP TABLE utxos_old;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
// Fix issue https://github.com/bitcoindevkit/bdk/issues/801: drop duplicated script_pubkeys
"ALTER TABLE script_pubkeys RENAME TO script_pubkeys_old;",
"DROP INDEX idx_keychain_child;",
"DROP INDEX idx_script;",
"CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
"CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
"CREATE INDEX idx_script ON script_pubkeys(script);",
"CREATE UNIQUE INDEX idx_script_pks_unique ON script_pubkeys(keychain, child);",
"INSERT OR REPLACE INTO script_pubkeys SELECT keychain, child, script FROM script_pubkeys_old;",
"DROP TABLE script_pubkeys_old;"
];
/// Sqlite database stored on filesystem
@@ -60,7 +77,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,
}
@@ -68,9 +85,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,
@@ -78,7 +98,7 @@ impl SqliteDatabase {
child: u32,
script: &[u8],
) -> Result<i64, Error> {
let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
let mut statement = self.connection.prepare_cached("INSERT OR REPLACE INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
statement.execute(named_params! {
":keychain": keychain,
":child": child,
@@ -739,11 +759,13 @@ impl BatchOperations for SqliteDatabase {
include_raw: bool,
) -> Result<Option<TransactionDetails>, Error> {
match self.select_transaction_details_by_txid(txid)? {
Some(transaction_details) => {
Some(mut transaction_details) => {
self.delete_transaction_details_by_txid(txid)?;
if include_raw {
self.delete_transaction_by_txid(txid)?;
} else {
transaction_details.transaction = None;
}
Ok(Some(transaction_details))
}
@@ -908,9 +930,9 @@ impl BatchDatabase for SqliteDatabase {
}
}
pub fn get_connection(path: &str) -> Result<Connection, Error> {
let connection = Connection::open(path)?;
migrate(&connection)?;
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
let mut connection = Connection::open(path)?;
migrate(&mut connection)?;
Ok(connection)
}
@@ -945,28 +967,41 @@ pub fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<u
)
}
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
pub fn migrate(conn: &mut Connection) -> Result<(), Error> {
let version = get_schema_version(conn)?;
let stmts = &MIGRATIONS[(version as usize)..];
let mut i: i32 = version;
if version == MIGRATIONS.len() as i32 {
// begin transaction, all migration statements and new schema version commit or rollback
let tx = conn.transaction()?;
// execute every statement and return `Some` new schema version
// if execution fails, return `Error::Rusqlite`
// if no statements executed returns `None`
let new_version = stmts
.iter()
.enumerate()
.map(|version_stmt| {
log::info!(
"executing db migration {}: `{}`",
version + version_stmt.0 as i32 + 1,
version_stmt.1
);
tx.execute(version_stmt.1, [])
// map result value to next migration version
.map(|_| version_stmt.0 as i32 + version + 1)
})
.last()
.transpose()?;
// if `Some` new statement version, set new schema version
if let Some(version) = new_version {
set_schema_version(&tx, version)?;
} else {
log::info!("db up to date, no migration needed");
return Ok(());
}
for stmt in stmts {
let res = conn.execute(stmt, []);
if res.is_err() {
println!("migration failed on:\n{}\n{:?}", stmt, res);
break;
}
i += 1;
}
set_schema_version(conn, i)?;
// commit transaction
tx.commit()?;
Ok(())
}
@@ -1031,4 +1066,82 @@ pub mod test {
fn test_txs() {
crate::database::test::test_list_transaction(get_database());
}
#[test]
fn test_iter_raw_txs() {
crate::database::test::test_iter_raw_txs(get_database());
}
#[test]
fn test_del_path_from_script_pubkey() {
crate::database::test::test_del_path_from_script_pubkey(get_database());
}
#[test]
fn test_iter_script_pubkeys() {
crate::database::test::test_iter_script_pubkeys(get_database());
}
#[test]
fn test_del_utxo() {
crate::database::test::test_del_utxo(get_database());
}
#[test]
fn test_del_raw_tx() {
crate::database::test::test_del_raw_tx(get_database());
}
#[test]
fn test_del_tx() {
crate::database::test::test_del_tx(get_database());
}
#[test]
fn test_del_last_index() {
crate::database::test::test_del_last_index(get_database());
}
#[test]
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_database());
}
// Issue 801: https://github.com/bitcoindevkit/bdk/issues/801
#[test]
fn test_unique_spks() {
use crate::bitcoin::hashes::hex::FromHex;
use crate::database::*;
let mut db = get_database();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
for _ in 0..100 {
db.set_script_pubkey(&script, keychain, path).unwrap();
}
let mut statement = db
.connection
.prepare_cached(
"select keychain,child,count(child) from script_pubkeys group by keychain,child;",
)
.unwrap();
let mut rows = statement.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let keychain: String = row.get(0).unwrap();
let child: u32 = row.get(1).unwrap();
let count: usize = row.get(2).unwrap();
assert_eq!(
count, 1,
"keychain={}, child={}, count={}",
keychain, child, count
);
}
}
}

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,29 @@ 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.
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
pub(crate) fn calc_checksum_bytes_internal(
mut desc: &str,
exclude_hash: bool,
) -> Result<[u8; 8], DescriptorError> {
let mut c = 1;
let mut cls = 0;
let mut clscount = 0;
for ch in desc.chars() {
let mut original_checksum = None;
if exclude_hash {
if let Some(split) = desc.split_once('#') {
desc = split.0;
original_checksum = Some(split.1);
}
}
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,47 +79,103 @@ 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))
// if input data already had a checksum, check calculated checksum against original checksum
if let Some(original_checksum) = original_checksum {
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
}
Ok(checksum)
}
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, true)
}
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
calc_checksum_bytes_internal(desc, true)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
/// Compute the checksum bytes of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, false)
}
/// Compute the checksum of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
calc_checksum_bytes_internal(desc, false)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
#[cfg(test)]
mod test {
use super::*;
use crate::descriptor::get_checksum;
use crate::descriptor::calc_checksum;
use assert_matches::assert_matches;
// test get_checksum() function; it should return the same value as Bitcoin Core
// test calc_checksum() function; it should return the same value as Bitcoin Core
#[test]
fn test_get_checksum() {
fn test_calc_checksum() {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62");
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
assert_eq!(get_checksum(desc).unwrap(), "lasegmfs");
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
}
// test calc_checksum() function; it should return the same value as Bitcoin Core even if the
// descriptor string includes a checksum hash
#[test]
fn test_calc_checksum_with_checksum_hash() {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
assert_matches!(
calc_checksum(desc),
Err(DescriptorError::InvalidDescriptorChecksum)
);
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
assert_matches!(
calc_checksum(desc),
Err(DescriptorError::InvalidDescriptorChecksum)
);
}
#[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();
fn test_calc_checksum_invalid_character() {
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
));
assert_matches!(
calc_checksum(&invalid_desc),
Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
);
}
}

View File

@@ -1,210 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! 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;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use bitcoin::hashes::hash160;
use bitcoin::{PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard};
use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk};
use crate::wallet::utils::SecpCtx;
/// Extended [`DescriptorPublicKey`] that has been derived
///
/// Derived keys are guaranteed to never contain wildcards of any kind
#[derive(Debug, Clone)]
pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx);
impl<'s> DerivedDescriptorKey<'s> {
/// Construct a new derived key
///
/// Panics if the key is wildcard
pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> {
if let DescriptorPublicKey::XPub(xpub) = &key {
assert!(xpub.wildcard == Wildcard::None)
}
DerivedDescriptorKey(key, secp)
}
}
impl<'s> Deref for DerivedDescriptorKey<'s> {
type Target = DescriptorPublicKey;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'s> PartialEq for DerivedDescriptorKey<'s> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'s> Eq for DerivedDescriptorKey<'s> {}
impl<'s> PartialOrd for DerivedDescriptorKey<'s> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.0.partial_cmp(&other.0)
}
}
impl<'s> Ord for DerivedDescriptorKey<'s> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl<'s> fmt::Display for DerivedDescriptorKey<'s> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<'s> Hash for DerivedDescriptorKey<'s> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
type Hash = Self;
fn to_pubkeyhash(&self) -> Self::Hash {
DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1)
}
fn is_uncompressed(&self) -> bool {
self.0.is_uncompressed()
}
}
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
fn to_public_key(&self) -> PublicKey {
match &self.0 {
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,
),
}
}
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,
),
}
}
fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash {
hash.to_public_key().to_pubkeyhash()
}
}
/// 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
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
}
impl AsDerived for Descriptor<DescriptorPublicKey> {
fn as_derived<'s>(
&self,
index: u32,
secp: &'s SecpCtx,
) -> Descriptor<DerivedDescriptorKey<'s>> {
self.derive(index).translate_pk_infallible(
|key| DerivedDescriptorKey::new(key.clone(), secp),
|key| DerivedDescriptorKey::new(key.clone(), secp),
)
}
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>> {
assert!(!self.is_deriveable());
self.as_derived(0, secp)
}
}

View File

@@ -700,10 +700,10 @@ macro_rules! fragment {
$crate::keys::make_pkh($key, &secp)
});
( after ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(After, $value)
$crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302
});
( older ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Older, $value)
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
});
( sha256 ( $hash:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Sha256, $hash)
@@ -795,7 +795,7 @@ macro_rules! fragment {
mod test {
use bitcoin::hashes::hex::ToHex;
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::{Descriptor, Legacy, Segwitv0};
use std::str::FromStr;
@@ -806,8 +806,6 @@ mod test {
use bitcoin::util::bip32;
use bitcoin::PrivateKey;
use crate::descriptor::derived::AsDerived;
// test the descriptor!() macro
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
@@ -817,17 +815,15 @@ mod test {
is_fixed: bool,
expected: &[&str],
) {
let secp = Secp256k1::new();
let (desc, _key_map, _networks) = desc.unwrap();
assert_eq!(desc.is_witness(), is_witness);
assert_eq!(!desc.is_deriveable(), is_fixed);
assert_eq!(!desc.has_wildcard(), is_fixed);
for i in 0..expected.len() {
let index = i as u32;
let child_desc = if !desc.is_deriveable() {
desc.as_derived_fixed(&secp)
let child_desc = if !desc.has_wildcard() {
desc.at_derivation_index(0)
} else {
desc.as_derived(index, &secp)
desc.at_derivation_index(index)
};
let address = child_desc.address(Regtest);
if let Ok(address) = address {
@@ -839,7 +835,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

View File

@@ -20,16 +20,14 @@ pub enum Error {
InvalidDescriptorChecksum,
/// The descriptor contains hardened derivation steps on public extended keys
HardenedDerivationXpub,
/// The descriptor contains multiple keys with the same BIP32 fingerprint
DuplicatedKeys,
/// Error thrown while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),
/// 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),
@@ -55,7 +53,26 @@ impl From<crate::keys::KeyError> for Error {
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
match self {
Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"),
Self::InvalidDescriptorChecksum => {
write!(f, "The provided descriptor doesn't match its checksum")
}
Self::HardenedDerivationXpub => write!(
f,
"The descriptor contains hardened derivation steps on public extended keys"
),
Self::Key(err) => write!(f, "Key error: {}", err),
Self::Policy(err) => write!(f, "Policy error: {}", err),
Self::InvalidDescriptorCharacter(char) => {
write!(f, "Invalid descriptor character: {}", char)
}
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
Self::Base58(err) => write!(f, "Base58 error: {}", err),
Self::Pk(err) => write!(f, "Key-related error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
}
}
}

View File

@@ -14,33 +14,33 @@
//! This module contains generic utilities to work with descriptors, plus some re-exported types
//! from [`miniscript`].
use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use std::collections::BTreeMap;
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 bitcoin::{Network, TxOut};
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
use miniscript::descriptor::{
DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey,
};
pub use miniscript::{
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum;
pub mod derived;
#[doc(hidden)]
pub mod dsl;
pub mod error;
pub mod policy;
pub mod template;
pub use self::checksum::get_checksum;
pub use self::derived::{AsDerived, DerivedDescriptorKey};
pub use self::checksum::calc_checksum;
use self::checksum::calc_checksum_bytes;
pub use self::error::Error as DescriptorError;
pub use self::policy::Policy;
use self::template::DescriptorTemplateOut;
@@ -52,7 +52,7 @@ use crate::wallet::utils::SecpCtx;
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
/// Alias for a [`Descriptor`] that contains extended **derived** keys
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
/// [`psbt::Output`]
@@ -84,19 +84,15 @@ impl IntoWalletDescriptor for &str {
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let descriptor = if self.contains('#') {
let parts: Vec<&str> = self.splitn(2, '#').collect();
if !get_checksum(parts[0])
.ok()
.map(|computed| computed == parts[1])
.unwrap_or(false)
{
return Err(DescriptorError::InvalidDescriptorChecksum);
let descriptor = match self.split_once('#') {
Some((desc, original_checksum)) => {
let checksum = calc_checksum_bytes(desc)?;
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
desc
}
parts[0]
} else {
self
None => self,
};
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
@@ -132,28 +128,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
use crate::keys::DescriptorKey;
let check_key = |pk: &DescriptorPublicKey| {
let (pk, _, networks) = if self.0.is_witness() {
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
} else {
let descriptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
};
struct Translator<'s, 'd> {
secp: &'s SecpCtx,
descriptor: &'d ExtendedDescriptor,
network: Network,
}
if networks.contains(&network) {
Ok(pk)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
impl<'s, 'd>
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
for Translator<'s, 'd>
{
fn pk(
&mut self,
pk: &DescriptorPublicKey,
) -> Result<miniscript::DummyKey, DescriptorError> {
let secp = &self.secp;
let (_, _, networks) = if self.descriptor.is_taproot() {
let descriptor_key: DescriptorKey<miniscript::Tap> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
} else if self.descriptor.is_witness() {
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
} else {
let descriptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
};
if networks.contains(&self.network) {
Ok(miniscript::DummyKey)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
}
}
};
fn sha256(
&mut self,
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
) -> Result<miniscript::DummySha256Hash, DescriptorError> {
Ok(Default::default())
}
fn hash256(
&mut self,
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
) -> Result<miniscript::DummyHash256Hash, DescriptorError> {
Ok(Default::default())
}
fn ripemd160(
&mut self,
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
) -> Result<miniscript::DummyRipemd160Hash, DescriptorError> {
Ok(Default::default())
}
fn hash160(
&mut self,
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
) -> Result<miniscript::DummyHash160Hash, DescriptorError> {
Ok(Default::default())
}
}
// check the network for the keys
let translated = self.0.translate_pk(check_key, check_key)?;
self.0.translate_pk(&mut Translator {
secp,
network,
descriptor: &self.0,
})?;
Ok((translated, self.1))
Ok(self)
}
}
@@ -163,10 +207,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
_secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let valid_networks = &self.2;
struct Translator {
network: Network,
}
let fix_key = |pk: &DescriptorPublicKey| {
if valid_networks.contains(&network) {
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
for Translator
{
fn pk(
&mut self,
pk: &DescriptorPublicKey,
) -> Result<DescriptorPublicKey, DescriptorError> {
// workaround for xpubs generated by other key types, like bip39: since when the
// conversion is made one network has to be chosen, what we generally choose
// "mainnet", but then override the set of valid networks to specify that all of
@@ -175,7 +226,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
let pk = match pk {
DescriptorPublicKey::XPub(ref xpub) => {
let mut xpub = xpub.clone();
xpub.xkey.network = network;
xpub.xkey.network = self.network;
DescriptorPublicKey::XPub(xpub)
}
@@ -183,15 +234,42 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
};
Ok(pk)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
}
};
miniscript::translate_hash_clone!(
DescriptorPublicKey,
DescriptorPublicKey,
DescriptorError
);
}
// fixup the network for keys that need it
let translated = self.0.translate_pk(fix_key, fix_key)?;
let (desc, keymap, networks) = self;
Ok((translated, self.1))
if !networks.contains(&network) {
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
}
// fixup the network for keys that need it in the descriptor
let translated = desc.translate_pk(&mut Translator { network })?;
// ...and in the key map
let fixed_keymap = keymap
.into_iter()
.map(|(mut k, mut v)| {
match (&mut k, &mut v) {
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
xpub.xkey.network = network;
xprv.xkey.network = network;
}
(_, DescriptorSecretKey::Single(key)) => {
key.key.network = network;
}
_ => {}
}
(k, v)
})
.collect();
Ok((translated, fixed_keymap))
}
}
@@ -210,7 +288,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
derivation_path,
wildcard,
..
}) = k.as_key()
}) = k
{
return *wildcard == Wildcard::Hardened
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
@@ -222,23 +300,9 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
return Err(DescriptorError::HardenedDerivationXpub);
}
// Ensure that there are no duplicated keys
let mut found_keys = HashSet::new();
let descriptor_contains_duplicated_keys = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(xkey) = k.as_key() {
let fingerprint = xkey.root_fingerprint(secp);
if found_keys.contains(&fingerprint) {
return true;
}
found_keys.insert(fingerprint);
}
false
});
if descriptor_contains_duplicated_keys {
return Err(DescriptorError::DuplicatedKeys);
}
// Run miniscript's sanity check, which will look for duplicated keys and other potential
// issues
descriptor.sanity_check()?;
Ok((descriptor, keymap))
}
@@ -271,7 +335,6 @@ pub trait ExtractPolicy {
}
pub(crate) trait XKeyUtils {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
}
@@ -279,27 +342,6 @@ 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
.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, secp: &SecpCtx) -> Fingerprint {
match self.origin {
Some((fingerprint, _)) => fingerprint,
@@ -308,11 +350,6 @@ where
}
}
pub(crate) trait DerivedDescriptorMeta {
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;
@@ -321,63 +358,23 @@ pub(crate) trait DescriptorMeta {
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_tap_key_origins<'s>(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_key_origins<'s>(
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_input<'s>(
&self,
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
}
pub(crate) trait DescriptorScripts {
fn psbt_redeem_script(&self) -> Option<Script>;
fn psbt_witness_script(&self) -> Option<Script>;
}
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
fn psbt_redeem_script(&self) -> Option<Script> {
match self.desc_type() {
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().unwrap()),
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
Some(self.explicit_script().unwrap())
}
DescriptorType::Bare
| DescriptorType::Sh
| DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::ShSortedMulti
| DescriptorType::Tr
| DescriptorType::ShWpkh => None,
}
}
) -> Option<DerivedDescriptor>;
}
impl DescriptorMeta for ExtendedDescriptor {
@@ -401,7 +398,7 @@ impl DescriptorMeta for ExtendedDescriptor {
let mut answer = Vec::new();
self.for_each_key(|pk| {
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
if let DescriptorPublicKey::XPub(xpub) = pk {
answer.push(xpub.clone());
}
@@ -415,7 +412,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// Ensure that deriving `xpub` with `path` yields `expected`
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
path: &DerivationPath,
@@ -437,7 +434,7 @@ impl DescriptorMeta for ExtendedDescriptor {
// 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() {
if let DescriptorPublicKey::XPub(xpub) = key {
// 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 `key_origins` and save it in `derive_path`. We expect this to be a derivation
@@ -495,14 +492,14 @@ impl DescriptorMeta for ExtendedDescriptor {
false
});
path_found.map(|path| self.as_derived(path, secp))
path_found.map(|path| self.at_derivation_index(path))
}
fn derive_from_hd_keypaths<'s>(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
let key_origins = hd_keypaths
.iter()
@@ -520,7 +517,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
let key_origins = tap_key_origins
.iter()
@@ -534,19 +531,19 @@ impl DescriptorMeta for ExtendedDescriptor {
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
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() {
if self.has_wildcard() {
// We can't try to bruteforce the derivation index, exit here
return None;
}
let descriptor = self.as_derived_fixed(secp);
let descriptor = self.at_derivation_index(0);
match descriptor.desc_type() {
// TODO: add pk() here
DescriptorType::Pkh
@@ -580,90 +577,11 @@ impl DescriptorMeta for ExtendedDescriptor {
}
}
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
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() {
let derived_pubkey = xpub
.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("Derivation can't fail");
answer.insert(
derived_pubkey.public_key,
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
);
}
true
});
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
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use assert_matches::assert_matches;
use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;
use bitcoin::secp256k1::Secp256k1;
@@ -787,23 +705,40 @@ mod test {
let secp = Secp256k1::new();
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
// we are using an "xpub"
let key = (xpub, path).into_descriptor_key().unwrap();
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
// override it with any. this happens in some key conversions, like bip39
let key = key.override_valid_networks(any_network());
// make a descriptor out of it
let desc = crate::descriptor!(wpkh(key)).unwrap();
// this should convert the key that supports "any_network" to the right network (testnet)
let (wallet_desc, _) = desc
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
let mut xprv_testnet = xprv;
xprv_testnet.network = Network::Testnet;
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
xkey: xpub_testnet,
origin: None,
derivation_path: path,
wildcard: Wildcard::Unhardened,
});
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
assert_eq!(
keymap
.get(&desc_pubkey)
.map(|key| key.to_public(&secp).unwrap()),
Some(desc_pubkey)
);
}
// test IntoWalletDescriptor trait from &str with and without checksum appended
@@ -829,17 +764,11 @@ mod test {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
.into_wallet_descriptor(&secp, Network::Testnet);
assert!(matches!(
desc.err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
.into_wallet_descriptor(&secp, Network::Testnet);
assert!(matches!(
desc.err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
}
// test IntoWalletDescriptor trait from &str with keys from right and wrong network
@@ -873,17 +802,11 @@ mod test {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
.into_wallet_descriptor(&secp, Network::Bitcoin);
assert!(matches!(
desc.err(),
Some(DescriptorError::Key(KeyError::InvalidNetwork))
));
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
.into_wallet_descriptor(&secp, Network::Bitcoin);
assert!(matches!(
desc.err(),
Some(DescriptorError::Key(KeyError::InvalidNetwork))
));
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
}
// test IntoWalletDescriptor trait from the output of the descriptor!() macro
@@ -917,25 +840,17 @@ mod test {
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DescriptorError::HardenedDerivationXpub
));
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DescriptorError::DuplicatedKeys
));
}
#[test]
fn test_sh_wsh_sortedmulti_redeemscript() {
use super::{AsDerived, DescriptorScripts};
use miniscript::psbt::PsbtInputExt;
let secp = Secp256k1::new();
@@ -943,11 +858,16 @@ mod test {
let (descriptor, _) =
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
let descriptor = descriptor.as_derived(0, &secp);
let descriptor = descriptor.at_derivation_index(0);
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));
let mut psbt_input = psbt::Input::default();
psbt_input
.update_with_descriptor_unchecked(&descriptor)
.unwrap();
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
assert_eq!(psbt_input.witness_script, Some(script));
}
}

View File

@@ -43,14 +43,17 @@ use std::fmt;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use bitcoin::hashes::*;
use bitcoin::hashes::{hash160, ripemd160, sha256};
use bitcoin::util::bip32::Fingerprint;
use bitcoin::{PublicKey, XOnlyPublicKey};
use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner,
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
};
use miniscript::hash256;
use miniscript::{
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
};
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
@@ -58,9 +61,9 @@ 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 crate::wallet::utils::{After, Older, SecpCtx};
use super::checksum::get_checksum;
use super::checksum::calc_checksum;
use super::error::Error;
use super::XKeyUtils;
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
@@ -81,11 +84,11 @@ pub enum PkOrF {
impl PkOrF {
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
match k {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => PkOrF::Pubkey(*pk),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => PkOrF::XOnlyPubkey(*pk),
@@ -111,7 +114,7 @@ pub enum SatisfiableItem {
/// Double SHA256 preimage hash
Hash256Preimage {
/// The digest value
hash: sha256d::Hash,
hash: hash256::Hash,
},
/// RIPEMD160 preimage hash
Ripemd160Preimage {
@@ -125,13 +128,13 @@ pub enum SatisfiableItem {
},
/// Absolute timeclock timestamp
AbsoluteTimelock {
/// The timestamp value
value: u32,
/// The timelock value
value: LockTime,
},
/// Relative timelock locktime
RelativeTimelock {
/// The locktime value
value: u32,
/// The timelock value
value: Sequence,
},
/// Multi-signature public keys with threshold count
Multisig {
@@ -165,7 +168,7 @@ impl SatisfiableItem {
/// Returns a unique id for the [`SatisfiableItem`]
pub fn id(&self) -> String {
get_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
.expect("Failed to compute a SatisfiableItem id")
}
}
@@ -438,32 +441,30 @@ pub struct Policy {
}
/// An extra condition that must be satisfied but that is out of control of the user
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)]
/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
pub struct Condition {
/// Optional CheckSequenceVerify condition
#[serde(skip_serializing_if = "Option::is_none")]
pub csv: Option<u32>,
pub csv: Option<Sequence>,
/// Optional timelock condition
#[serde(skip_serializing_if = "Option::is_none")]
pub timelock: Option<u32>,
pub timelock: Option<LockTime>,
}
impl Condition {
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> {
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) {
fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
if !a.is_same_unit(b) {
Err(PolicyError::MixedTimelockUnits)
} else if a > b {
Ok(a)
} else {
Ok(max(a, b))
Ok(b)
}
}
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> {
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK;
let a = a & mask;
let b = b & mask;
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
if a.is_time_locked() != b.is_time_locked() {
Err(PolicyError::MixedTimelockUnits)
} else {
Ok(max(a, b))
@@ -511,7 +512,14 @@ pub enum PolicyError {
impl fmt::Display for PolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
match self {
Self::NotEnoughItemsSelected(err) => write!(f, "Not enought items selected: {}", err),
Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index),
Self::AddOnLeaf => write!(f, "Add on leaf"),
Self::AddOnPartialComplete => write!(f, "Add on partial complete"),
Self::MixedTimelockUnits => write!(f, "Mixed timelock units"),
Self::IncompatibleConditions => write!(f, "Incompatible conditions"),
}
}
}
@@ -720,15 +728,18 @@ impl From<SatisfiableItem> for Policy {
}
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
// For consistency we always compute the key hash in "ecdsa" form (with the leading sign
// prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
// for a key, so it doesn't really matter how the identifier is computed.
match key {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => pk.to_pubkeyhash().into(),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => pk.to_pubkeyhash().into(),
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
}
}
@@ -779,7 +790,7 @@ fn generic_sig_in_psbt<
) -> bool {
//TODO check signature validity
psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::XPub(xpub) => {
//TODO check actual derivation matches
match extract(input, xpub.root_fingerprint(secp)) {
@@ -891,10 +902,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
}
Terminal::After(value) => {
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
value: value.into(),
}
.into();
policy.contribution = Satisfaction::Complete {
condition: Condition {
timelock: Some(*value),
timelock: Some(value.into()),
csv: None,
},
};
@@ -905,9 +919,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
} = build_sat
{
let after = After::new(Some(current_height), false);
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
let after_sat =
Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
Satisfier::<bitcoin::PublicKey>::check_after(&sat, value.into())
});
if after_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
@@ -999,6 +1015,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Policy::make_thresh(mapped, threshold)?
}
// Unsupported
Terminal::RawPkH(_) => None,
})
}
}
@@ -1124,14 +1143,13 @@ mod test {
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use crate::wallet::signer::SignersContainer;
use assert_matches::assert_matches;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::bip32;
use bitcoin::Network;
use miniscript::DescriptorTrait;
use std::str::FromStr;
use std::sync::Arc;
@@ -1172,8 +1190,8 @@ mod test {
.unwrap()
.unwrap();
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(matches!(&policy.contribution, Satisfaction::None));
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
@@ -1185,10 +1203,8 @@ mod test {
.unwrap()
.unwrap();
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)
);
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
}
// 2 pub keys descriptor, required 2 prv keys
@@ -1207,19 +1223,16 @@ mod test {
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
&& 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?
assert!(
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
&& m == &2usize
&& items.is_empty()
&& conditions.is_empty()
)
);
}
@@ -1238,18 +1251,15 @@ mod test {
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
);
assert!(
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
&& m == &2usize
&& items.len() == 1
&& conditions.contains_key(&0)
)
);
}
@@ -1271,18 +1281,15 @@ mod test {
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
&& m == &1
&& items.len() == 2
&& conditions.contains_key(&vec![0])
&& conditions.contains_key(&vec![1])
)
);
}
@@ -1303,18 +1310,15 @@ mod test {
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
&& m == &2
&& items.len() == 2
&& conditions.contains_key(&vec![0,1])
)
);
}
@@ -1329,31 +1333,27 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
assert!(matches!(&policy.contribution, Satisfaction::None));
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 single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
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)
);
assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint);
assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
}
// single key, 1 prv and 1 pub key descriptor, required 1 prv keys
@@ -1368,25 +1368,21 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key
let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
&& m == &1
assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
&& m == 1
&& items.len() == 2
&& conditions.contains_key(&vec![0])
&& conditions.contains_key(&vec![1])
)
);
}
@@ -1418,18 +1414,14 @@ mod test {
.unwrap()
.unwrap();
assert!(
matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2)
);
assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
&& m == &2
&& items.len() == 3
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
)
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
);
}
@@ -1574,7 +1566,7 @@ mod test {
.unwrap();
let addr = wallet_desc
.as_derived(0, &secp)
.at_derivation_index(0)
.address(Network::Testnet)
.unwrap();
assert_eq!(
@@ -1592,11 +1584,9 @@ mod test {
.unwrap();
//println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
assert!(
matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![0]
)
);
let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
@@ -1606,11 +1596,9 @@ mod test {
.unwrap();
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
assert!(
matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![1]
)
);
let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
@@ -1618,11 +1606,9 @@ mod test {
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
&& m == &2
&& items == &vec![0, 1]
)
);
}
@@ -1646,7 +1632,7 @@ mod test {
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let addr = wallet_desc
.as_derived(0, &secp)
.at_derivation_index(0)
.address(Network::Testnet)
.unwrap();
assert_eq!(
@@ -1666,11 +1652,9 @@ mod test {
.extract_policy(&signers_container, build_sat, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items.is_empty()
)
);
//println!("{}", serde_json::to_string(&policy).unwrap());
@@ -1684,11 +1668,9 @@ mod test {
.extract_policy(&signers_container, build_sat_expired, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0]
)
);
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
@@ -1704,11 +1686,9 @@ mod test {
.extract_policy(&signers_container, build_sat_expired_signed, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0, 1]
)
);
//println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
}
@@ -1783,12 +1763,8 @@ mod test {
.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])
);
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));
@@ -1880,19 +1856,11 @@ mod test {
.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_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])
);
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,

View File

@@ -235,14 +235,17 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
/// )?;
///
/// 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.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
P2Pkh(legacy::make_bipxx_public(
44, self.0, self.1, self.2, network,
)?)
.build(network)
}
}
@@ -284,7 +287,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))`
///
/// This assumes that the key used has already been derived with `m/49'/0'/0'`.
/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet.
///
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
///
@@ -311,14 +314,17 @@ 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'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
/// # 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, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(
49, self.0, self.1, self.2, network,
)?)
.build(network)
}
}
@@ -360,7 +366,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)`
///
/// This assumes that the key used has already been derived with `m/84'/0'/0'`.
/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet.
///
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
///
@@ -387,14 +393,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// )?;
///
/// 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.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
P2Wpkh(segwit_v0::make_bipxx_public(
84, self.0, self.1, self.2, network,
)?)
.build(network)
}
}
@@ -440,6 +449,7 @@ macro_rules! expand_make_bipxx {
key: K,
parent_fingerprint: bip32::Fingerprint,
keychain: KeychainKind,
network: Network,
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
let derivation_path: bip32::DerivationPath = match keychain {
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
@@ -448,7 +458,10 @@ macro_rules! expand_make_bipxx {
let source_path = bip32::DerivationPath::from(vec![
bip32::ChildNumber::from_hardened_idx(bip)?,
bip32::ChildNumber::from_hardened_idx(0)?,
match network {
Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?,
_ => bip32::ChildNumber::from_hardened_idx(1)?,
},
bip32::ChildNumber::from_hardened_idx(0)?,
]);
@@ -468,12 +481,11 @@ mod test {
use std::str::FromStr;
use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::ValidNetworks;
use assert_matches::assert_matches;
use bitcoin::network::constants::Network::Regtest;
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::Descriptor;
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
@@ -490,9 +502,9 @@ mod test {
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 }));
assert_matches!(purpose, Hardened { index: 44 });
let coin_type = path.get(1).unwrap();
assert!(matches!(coin_type, Hardened { index: 0 }));
assert_matches!(coin_type, Hardened { index: 0 });
}
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
@@ -504,9 +516,9 @@ mod test {
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 }));
assert_matches!(purpose, Hardened { index: 44 });
let coin_type = path.get(1).unwrap();
assert!(matches!(coin_type, Hardened { index: 1 }));
assert_matches!(coin_type, Hardened { index: 1 });
}
}
@@ -517,17 +529,15 @@ mod test {
is_fixed: bool,
expected: &[&str],
) {
let secp = Secp256k1::new();
let (desc, _key_map, _networks) = desc.unwrap();
assert_eq!(desc.is_witness(), is_witness);
assert_eq!(!desc.is_deriveable(), is_fixed);
assert_eq!(!desc.has_wildcard(), is_fixed);
for i in 0..expected.len() {
let index = i as u32;
let child_desc = if !desc.is_deriveable() {
desc.as_derived_fixed(&secp)
let child_desc = if !desc.has_wildcard() {
desc.at_derivation_index(0)
} else {
desc.as_derived(index, &secp)
desc.at_derivation_index(index)
};
let address = child_desc.address(Regtest).unwrap();
assert_eq!(address.to_string(), *expected.get(i).unwrap());

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

@@ -12,8 +12,8 @@
use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator};
use bitcoin::OutPoint;
use crate::{descriptor, wallet};
use bitcoin::{OutPoint, Txid};
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
#[derive(Debug)]
@@ -99,15 +99,15 @@ pub enum Error {
/// Error related to the parsing and usage of descriptors
Descriptor(crate::descriptor::error::Error),
/// Error that can be returned to fail the validation of an address
AddressValidator(crate::wallet::address_validator::AddressValidatorError),
/// Encoding error
Encode(bitcoin::consensus::encode::Error),
/// Miniscript error
Miniscript(miniscript::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// BIP32 error
Bip32(bitcoin::util::bip32::Error),
/// An ECDSA error
/// A secp256k1 error
Secp256k1(bitcoin::secp256k1::Error),
/// Error serializing or deserializing JSON data
Json(serde_json::Error),
@@ -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,9 +149,125 @@ pub enum Error {
Rusqlite(rusqlite::Error),
}
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
Conversion(miniscript::descriptor::ConversionError),
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
}
}
}
impl std::error::Error for MiniscriptPsbtError {}
/// 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)
match self {
Self::InvalidU32Bytes(_) => write!(
f,
"Wrong number of bytes found when trying to convert to u32"
),
Self::Generic(err) => write!(f, "Generic error: {}", err),
Self::ScriptDoesntHaveAddressForm => write!(f, "Script doesn't have address form"),
Self::NoRecipients => write!(f, "Cannot build tx without recipients"),
Self::NoUtxosSelected => write!(f, "No UTXO selected"),
Self::OutputBelowDustLimit(limit) => {
write!(f, "Output below the dust limit: {}", limit)
}
Self::InsufficientFunds { needed, available } => write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
),
Self::BnBTotalTriesExceeded => {
write!(f, "Branch and bound coin selection: total tries exceeded")
}
Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
Self::UnknownUtxo => write!(f, "UTXO not found in the internal database"),
Self::TransactionNotFound => {
write!(f, "Transaction not found in the internal database")
}
Self::TransactionConfirmed => write!(f, "Transaction already confirmed"),
Self::IrreplaceableTransaction => write!(f, "Transaction can't be replaced"),
Self::FeeRateTooLow { required } => write!(
f,
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
),
Self::FeeTooLow { required } => write!(f, "Fee to low: required {} sat", required),
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
Self::MissingKeyOrigin(err) => write!(f, "Missing key origin: {}", err),
Self::Key(err) => write!(f, "Key error: {}", err),
Self::ChecksumMismatch => write!(f, "Descriptor checksum mismatch"),
Self::SpendingPolicyRequired(keychain_kind) => {
write!(f, "Spending policy required: {:?}", keychain_kind)
}
Self::InvalidPolicyPathError(err) => write!(f, "Invalid policy path: {}", err),
Self::Signer(err) => write!(f, "Signer error: {}", err),
Self::InvalidNetwork { requested, found } => write!(
f,
"Invalid network: requested {} but found {}",
requested, found
),
#[cfg(feature = "verify")]
Self::Verification(err) => write!(f, "Transaction verification error: {}", err),
Self::InvalidProgressValue(progress) => {
write!(f, "Invalid progress value: {}", progress)
}
Self::ProgressUpdateError => write!(
f,
"Progress update error (maybe the channel has been closed)"
),
Self::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist in the tx: {}",
outpoint
),
Self::Descriptor(err) => write!(f, "Descriptor error: {}", err),
Self::Encode(err) => write!(f, "Encoding error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
Self::Secp256k1(err) => write!(f, "Secp256k1 error: {}", err),
Self::Json(err) => write!(f, "Serialize/Deserialize JSON error: {}", err),
Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
Self::Psbt(err) => write!(f, "PSBT error: {}", err),
Self::PsbtParse(err) => write!(f, "Impossible to parse PSBT: {}", err),
Self::MissingCachedScripts(missing_cached_scripts) => {
write!(f, "Missing cached scripts: {:?}", missing_cached_scripts)
}
#[cfg(feature = "electrum")]
Self::Electrum(err) => write!(f, "Electrum client error: {}", err),
#[cfg(feature = "esplora")]
Self::Esplora(err) => write!(f, "Esplora client error: {}", err),
#[cfg(feature = "compact_filters")]
Self::CompactFilters(err) => write!(f, "Compact filters client error: {}", err),
#[cfg(feature = "key-value-db")]
Self::Sled(err) => write!(f, "Sled database error: {}", err),
#[cfg(feature = "rpc")]
Self::Rpc(err) => write!(f, "RPC client error: {}", err),
#[cfg(feature = "sqlite")]
Self::Rusqlite(err) => write!(f, "SQLite error: {}", err),
}
}
}
@@ -167,7 +287,6 @@ macro_rules! impl_error {
}
impl_error!(descriptor::error::Error, Descriptor);
impl_error!(address_validator::AddressValidatorError, AddressValidator);
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
impl_error!(wallet::signer::SignerError, Signer);
@@ -184,6 +303,7 @@ impl From<crate::keys::KeyError> for Error {
impl_error!(bitcoin::consensus::encode::Error, Encode);
impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::util::bip32::Error, Bip32);
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
impl_error!(serde_json::Error, Json);

View File

@@ -24,8 +24,8 @@ use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
pub use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
SinglePubKey, SortedMultiVec,
DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
SortedMultiVec,
};
pub use miniscript::ScriptContext;
use miniscript::{Miniscript, Terminal};
@@ -40,7 +40,7 @@ pub mod bip39;
/// Set of valid networks for a key
pub type ValidNetworks = HashSet<Network>;
/// Create a set containing mainnet, testnet and regtest
/// Create a set containing mainnet, testnet, signet, and regtest
pub fn any_network() -> ValidNetworks {
vec![
Network::Bitcoin,
@@ -95,7 +95,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
}
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
// public because it is effectively called by external crates, once the macros are expanded,
// public because it is effectively called by external crates once the macros are expanded,
// but since it is not meant to be part of the public api we hide it from the docs.
#[doc(hidden)]
pub fn extract(
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
let mut key_map = KeyMap::with_capacity(1);
let public = secret
.as_public(secp)
.to_public(secp)
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
key_map.insert(public.clone(), secret);
@@ -224,8 +224,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey;
///
/// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
/// ScriptContext, SinglePub, SinglePubKey,
/// };
///
/// pub struct MyKeyType {
@@ -235,7 +235,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
/// Ok(DescriptorKey::from_public(
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
/// DescriptorPublicKey::Single(SinglePub {
/// origin: None,
/// key: SinglePubKey::FullKey(self.pubkey),
/// }),
@@ -375,7 +375,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// `(DerivableKey, KeySource, DerivationPath)` tuples.
///
/// For key types that don't encode any indication about the path to use (like bip39), it's
/// generally recommended to implemented this trait instead of [`IntoDescriptorKey`]. The same
/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same
/// rules regarding script context and valid networks apply.
///
/// ## Examples
@@ -842,7 +842,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match self {
DescriptorPublicKey::SinglePub(_) => any_network(),
DescriptorPublicKey::Single(_) => any_network(),
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
if xkey.network == Network::Bitcoin =>
{
@@ -857,7 +857,7 @@ 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 {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(self),
origin: None,
})
@@ -867,7 +867,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(self),
origin: None,
})
@@ -878,7 +878,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match &self {
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
mainnet_network()
}
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
@@ -903,7 +903,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
DescriptorSecretKey::Single(SinglePriv {
key: self,
origin: None,
})
@@ -935,7 +935,14 @@ impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError);
impl std::fmt::Display for KeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
match self {
Self::InvalidScriptContext => write!(f, "Invalid script context"),
Self::InvalidNetwork => write!(f, "Invalid network"),
Self::InvalidChecksum => write!(f, "Invalid checksum"),
Self::Message(err) => write!(f, "{}", err),
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
}
}
}

View File

@@ -43,10 +43,6 @@
//! blockchain data and an [electrum](https://docs.rs/electrum-client/) blockchain client to
//! interact with the bitcoin P2P network.
//!
//! ```toml
//! bdk = "0.20.0"
//! ```
//!
//! # Examples
#![cfg_attr(
feature = "electrum",
@@ -81,6 +77,7 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ## Generate a few addresses
//!
//! ### Example
//! ```
//! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase;
@@ -206,6 +203,8 @@ pub extern crate miniscript;
extern crate serde;
#[macro_use]
extern crate serde_json;
#[cfg(feature = "hardware-signer")]
pub extern crate hwi;
#[cfg(all(feature = "reqwest", feature = "ureq"))]
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
@@ -228,21 +227,21 @@ compile_error!(
#[cfg(feature = "keys-bip39")]
extern crate bip39;
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[cfg(feature = "async-interface")]
#[macro_use]
extern crate async_trait;
#[macro_use]
extern crate bdk_macros;
#[cfg(feature = "compact_filters")]
extern crate lazy_static;
#[cfg(feature = "rpc")]
pub extern crate bitcoincore_rpc;
#[cfg(feature = "electrum")]
pub extern crate electrum_client;
#[cfg(feature = "esplora")]
pub extern crate esplora_client;
#[cfg(feature = "key-value-db")]
pub extern crate sled;
@@ -257,6 +256,9 @@ pub extern crate rusqlite;
#[macro_use]
pub mod testutils;
#[cfg(test)]
extern crate assert_matches;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
@@ -266,7 +268,7 @@ pub mod descriptor;
#[cfg(feature = "test-md-docs")]
mod doctest;
pub mod keys;
pub(crate) mod psbt;
pub mod psbt;
pub(crate) mod types;
pub mod wallet;
@@ -274,7 +276,6 @@ pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use error::Error;
pub use types::*;
pub use wallet::address_validator;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;

View File

@@ -9,11 +9,28 @@
// You may not use this file except in accordance with one or both of these
// licenses.
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
use crate::FeeRate;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut;
// TODO upstream the functions here to `rust-bitcoin`?
/// Trait to add functions to extract utxos and calculate fees.
pub trait PsbtUtils {
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_amount(&self) -> Option<u64>;
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
/// transaction.
/// If the PSBT is missing a TxOut for an input returns None.
fn fee_rate(&self) -> Option<FeeRate>;
}
impl PsbtUtils for Psbt {
@@ -37,6 +54,27 @@ impl PsbtUtils for Psbt {
None
}
}
fn fee_amount(&self) -> Option<u64> {
let tx = &self.unsigned_tx;
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
utxos.map(|inputs| {
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
input_amount
.checked_sub(output_amount)
.expect("input amount must be greater than output amount")
})
}
fn fee_rate(&self) -> Option<FeeRate> {
let fee_amount = self.fee_amount();
fee_amount.map(|fee| {
let weight = self.clone().extract_tx().weight();
FeeRate::from_wu(fee, weight)
})
}
}
#[cfg(test)]
@@ -44,8 +82,9 @@ mod test {
use crate::bitcoin::TxIn;
use crate::psbt::Psbt;
use crate::wallet::AddressIndex;
use crate::wallet::AddressIndex::New;
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
use crate::SignOptions;
use crate::{psbt, FeeRate, SignOptions};
use std::str::FromStr;
// from bip 174
@@ -118,4 +157,83 @@ mod test {
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
}
#[test]
fn test_psbt_fee_rate_with_witness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut psbt, _) = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_nonwitness_utxo() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut psbt, _) = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let finalized_fee_rate = psbt.fee_rate().unwrap();
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
}
#[test]
fn test_psbt_fee_rate_with_missing_txout() {
use psbt::PsbtUtils;
let expected_fee_rate = 1.2345;
let (wpkh_wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wpkh_wallet.get_address(New).unwrap();
let mut builder = wpkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut wpkh_psbt, _) = builder.finish().unwrap();
wpkh_psbt.inputs[0].witness_utxo = None;
wpkh_psbt.inputs[0].non_witness_utxo = None;
assert!(wpkh_psbt.fee_amount().is_none());
assert!(wpkh_psbt.fee_rate().is_none());
let (pkh_wallet, _, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = pkh_wallet.get_address(New).unwrap();
let mut builder = pkh_wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
let (mut pkh_psbt, _) = builder.finish().unwrap();
pkh_psbt.inputs[0].non_witness_utxo = None;
assert!(pkh_psbt.fee_amount().is_none());
assert!(pkh_psbt.fee_rate().is_none());
}
}

View File

@@ -1,8 +1,18 @@
// Bitcoin Dev Kit
//
// 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 crate::testutils::TestIncomingTx;
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::sha256d;
use bitcoin::{Address, Amount, Script, Transaction, Txid, Witness};
use bitcoin::{Address, Amount, PackedLockTime, Script, Sequence, Transaction, Txid, Witness};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr;
@@ -110,7 +120,7 @@ impl TestClient {
if let Some(true) = meta_tx.replaceable {
// for some reason core doesn't set this field right
for input in &mut tx.input {
input.sequence = 0xFFFFFFFD;
input.sequence = Sequence(0xFFFFFFFD);
}
}
@@ -164,6 +174,7 @@ impl TestClient {
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
use bitcoin::hashes::Hash;
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
@@ -176,7 +187,7 @@ impl TestClient {
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
merkle_root: TxMerkleNode::all_zeros(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
@@ -184,15 +195,15 @@ impl TestClient {
debug!("header: {:#?}", header);
let height = block_template["height"].as_u64().unwrap() as i64;
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into();
let witness_reserved_value: Vec<u8> = sha256d::Hash::all_zeros().as_ref().into();
// burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
sequence: Sequence(0xFFFFFFFF),
witness: Witness::from_vec(vec![witness_reserved_value]),
}],
output: vec![],
@@ -454,7 +465,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
@@ -477,7 +488,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
}
@@ -486,7 +497,7 @@ macro_rules! bdk_blockchain_tests {
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(wallet.get_balance().unwrap().get_total(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@@ -494,8 +505,16 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap().confirmed, 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
}
#[test]
@@ -508,7 +527,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 105_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
@@ -532,7 +551,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
}
@@ -546,14 +565,14 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
}
#[test]
@@ -566,7 +585,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
@@ -580,7 +599,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after bump");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
@@ -603,8 +622,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
@@ -617,7 +635,7 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after invalidate");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
@@ -635,7 +653,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
@@ -646,7 +664,12 @@ macro_rules! bdk_blockchain_tests {
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().trusted_pending, details.received, "incorrect balance after send");
test_client.generate(1, Some(node_addr));
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap().confirmed, details.received, "incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
@@ -720,7 +743,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
let tx1 = {
@@ -743,9 +766,8 @@ macro_rules! bdk_blockchain_tests {
blockchain.broadcast(&tx1).expect("broadcasting first");
blockchain.broadcast(&tx2).expect("broadcasting replacement");
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
assert_eq!(receiver_wallet.get_balance().expect("balance").untrusted_pending, 49_000, "should have received coins once and only once");
}
#[test]
@@ -771,7 +793,8 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
let balance = wallet.get_balance().unwrap();
assert_eq!(balance.untrusted_pending + balance.get_spendable(), 100_000);
}
#[test]
@@ -785,7 +808,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let details = tx_map.get(&received_txid).unwrap();
@@ -809,7 +832,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
@@ -821,7 +844,7 @@ macro_rules! bdk_blockchain_tests {
blockchain.broadcast(&sent_tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance after receive");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
@@ -852,7 +875,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
let mut total_sent = 0;
for _ in 0..5 {
@@ -869,7 +892,7 @@ macro_rules! bdk_blockchain_tests {
}
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance after chain");
// empty wallet
@@ -879,7 +902,7 @@ macro_rules! bdk_blockchain_tests {
test_client.generate(1, Some(node_addr));
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance empty wallet");
}
@@ -893,7 +916,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
@@ -902,8 +925,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance from received");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
@@ -912,8 +935,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance from received after bump");
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
}
@@ -928,7 +951,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -937,8 +960,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
@@ -947,7 +970,7 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after change removal");
assert_eq!(new_details.received, 0, "incorrect received after change removal");
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
@@ -963,7 +986,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -972,7 +995,7 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
@@ -983,7 +1006,7 @@ macro_rules! bdk_blockchain_tests {
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance after add input");
}
#[test]
@@ -996,7 +1019,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -1005,7 +1028,7 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
@@ -1018,7 +1041,7 @@ macro_rules! bdk_blockchain_tests {
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after add input");
assert_eq!(new_details.received, 0, "incorrect received after add input");
}
@@ -1032,7 +1055,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
let data = [42u8;80];
@@ -1047,7 +1070,7 @@ macro_rules! bdk_blockchain_tests {
blockchain.broadcast(&tx).unwrap();
test_client.generate(1, Some(node_addr));
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let _ = tx_map.get(&tx.txid()).unwrap();
@@ -1058,22 +1081,24 @@ macro_rules! bdk_blockchain_tests {
let (wallet, blockchain, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
println!("wallet addr: {}", wallet_addr);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
assert_eq!(wallet.get_balance().unwrap().immature, 0, "incorrect balance");
test_client.generate(1, Some(wallet_addr));
#[cfg(feature = "rpc")]
{
// rpc consider coinbase only when mature (100 blocks)
let node_addr = test_client.get_node_address(None);
test_client.generate(100, Some(node_addr));
}
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
assert!(wallet.get_balance().unwrap().immature > 0, "incorrect balance after receiving coinbase");
// make coinbase mature (100 blocks)
let node_addr = test_client.get_node_address(None);
test_client.generate(100, Some(node_addr));
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.get_balance().unwrap().confirmed > 0, "incorrect balance after maturing coinbase");
}
#[test]
@@ -1150,7 +1175,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "wallet has incorrect balance");
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
@@ -1162,7 +1187,7 @@ macro_rules! bdk_blockchain_tests {
let tx = psbt.extract_tx();
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "wallet has incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
test_client.generate(1, None);
@@ -1170,7 +1195,7 @@ macro_rules! bdk_blockchain_tests {
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
assert_eq!(taproot_balance.to_sat(), 25_000, "node has incorrect taproot wallet balance");
}
#[test]
@@ -1268,12 +1293,12 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
let tx = {
let mut builder = wallet.build_tx();
@@ -1296,7 +1321,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
let tx = {
let mut builder = wallet.build_tx();
@@ -1317,7 +1342,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000);
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();

View File

@@ -23,12 +23,14 @@ pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
None
}
/// Runs all avaliable tests.
/// Runs all available 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.",
@@ -113,16 +115,21 @@ where
} else {
max_balance
};
// perform wallet sync
wallet.sync(&blockchain, Default::default()).unwrap();
let wallet_balance = wallet.get_balance().unwrap();
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: {}",
@@ -138,3 +145,113 @@ where
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

@@ -101,25 +101,21 @@ impl TestIncomingTx {
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::descriptor::AsDerived;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.as_derived($child, &secp).address(bitcoin::Network::Regtest).expect("No address form")
parsed.at_derivation_index($child).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::descriptor::AsDerived;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
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.as_derived($child, &secp).address($crate::bitcoin::Network::Regtest).expect("No address form")
parsed.at_derivation_index($child).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) });
@@ -186,49 +182,50 @@ macro_rules! testutils {
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use std::convert::Infallible;
use $crate::miniscript::descriptor::Descriptor;
use $crate::miniscript::TranslatePk;
struct Translator {
keys: HashMap<&'static str, (String, Option<String>, Option<String>)>,
is_internal: bool,
}
impl $crate::miniscript::Translator<String, String, Infallible> for Translator {
fn pk(&mut self, pk: &String) -> Result<String, Infallible> {
match self.keys.get(pk.as_str()) {
Some((key, ext_path, int_path)) => {
let path = if self.is_internal { int_path } else { ext_path };
Ok(format!("{}{}", key, path.clone().unwrap_or_default()))
}
None => Ok(pk.clone()),
}
}
fn sha256(&mut self, sha256: &String) -> Result<String, Infallible> { Ok(sha256.clone()) }
fn hash256(&mut self, hash256: &String) -> Result<String, Infallible> { Ok(hash256.clone()) }
fn ripemd160(&mut self, ripemd160: &String) -> Result<String, Infallible> { Ok(ripemd160.clone()) }
fn hash160(&mut self, hash160: &String) -> Result<String, Infallible> { Ok(hash160.clone()) }
}
#[allow(unused_assignments, unused_mut)]
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
let mut keys = HashMap::new();
$(
keys = testutils!{ @keys $( $keys )* };
)*
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
} else {
k.clone()
}
}, |kh| {
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
} else {
kh.clone()
}
let mut translator = Translator { keys, is_internal: false };
});
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
let external = external.translate_pk(&mut translator).expect("Infallible conversion");
let external = external.to_string();
let internal = None::<String>$(.or({
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
translator.is_internal = true;
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
} else {
k.clone()
}
}, |kh| {
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
} else {
kh.clone()
}
});
Some(string_internal.to_string())
let internal = None::<String>$(.or({
let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
Some(internal.to_string())
}))?;
(external, internal)

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
}
}
@@ -136,7 +166,7 @@ pub struct LocalUtxo {
}
/// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo {
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
/// properly maintain the feerate when adding this input to a transaction during coin selection.
@@ -147,7 +177,7 @@ pub struct WeightedUtxo {
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
/// An unspent transaction output (UTXO).
pub enum Utxo {
/// A UTXO owned by the local wallet.
@@ -194,29 +224,42 @@ impl Utxo {
}
/// A wallet transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct TransactionDetails {
/// Optional transaction
pub transaction: Option<Transaction>,
/// Transaction id
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.
pub fee: Option<u64>,
/// If the transaction is confirmed, contains height and timestamp of the block containing the
/// If the transaction is confirmed, contains height and Unix timestamp of the block containing the
/// transaction, unconfirmed transaction contains `None`.
pub confirmation_time: Option<BlockTime>,
}
impl PartialOrd for TransactionDetails {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TransactionDetails {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.confirmation_time
.cmp(&other.confirmation_time)
.then_with(|| self.txid.cmp(&other.txid))
}
}
/// Block height and timestamp of a block
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct BlockTime {
@@ -226,6 +269,20 @@ pub struct BlockTime {
pub timestamp: u64,
}
impl PartialOrd for BlockTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BlockTime {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height
.cmp(&other.height)
.then_with(|| self.timestamp.cmp(&other.timestamp))
}
}
/// **DEPRECATED**: Confirmation time of a transaction
///
/// The structure has been renamed to `BlockTime`
@@ -242,13 +299,219 @@ 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::*;
use bitcoin::hashes::Hash;
#[test]
fn sort_block_time() {
let block_time_a = BlockTime {
height: 100,
timestamp: 100,
};
let block_time_b = BlockTime {
height: 100,
timestamp: 110,
};
let block_time_c = BlockTime {
height: 0,
timestamp: 0,
};
let mut vec = vec![
block_time_a.clone(),
block_time_b.clone(),
block_time_c.clone(),
];
vec.sort();
let expected = vec![block_time_c, block_time_a, block_time_b];
assert_eq!(vec, expected)
}
#[test]
fn sort_tx_details() {
let block_time_a = BlockTime {
height: 100,
timestamp: 100,
};
let block_time_b = BlockTime {
height: 0,
timestamp: 0,
};
let tx_details_a = TransactionDetails {
transaction: None,
txid: Txid::from_inner([0; 32]),
received: 0,
sent: 0,
fee: None,
confirmation_time: None,
};
let tx_details_b = TransactionDetails {
transaction: None,
txid: Txid::from_inner([0; 32]),
received: 0,
sent: 0,
fee: None,
confirmation_time: Some(block_time_a),
};
let tx_details_c = TransactionDetails {
transaction: None,
txid: Txid::from_inner([0; 32]),
received: 0,
sent: 0,
fee: None,
confirmation_time: Some(block_time_b.clone()),
};
let tx_details_d = TransactionDetails {
transaction: None,
txid: Txid::from_inner([1; 32]),
received: 0,
sent: 0,
fee: None,
confirmation_time: Some(block_time_b),
};
let mut vec = vec![
tx_details_a.clone(),
tx_details_b.clone(),
tx_details_c.clone(),
tx_details_d.clone(),
];
vec.sort();
let expected = vec![tx_details_a, tx_details_c, tx_details_d, tx_details_b];
assert_eq!(vec, expected)
}
#[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

@@ -1,158 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! Address validation callbacks
//!
//! The typical usage of those callbacks is for displaying the newly-generated address on a
//! hardware wallet, so that the user can cross-check its correctness.
//!
//! More generally speaking though, these callbacks can also be used to "do something" every time
//! an address is generated, without necessarily checking or validating it.
//!
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
//! whenever a new address is generated (either explicitly by the user with
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
//! address) all the attached validators will be polled, in sequence. All of them must complete
//! successfully to continue.
//!
//! ## Example
//!
//! ```
//! # use std::sync::Arc;
//! # use bitcoin::*;
//! # use bdk::address_validator::*;
//! # use bdk::database::*;
//! # use bdk::*;
//! # use bdk::wallet::AddressIndex::New;
//! #[derive(Debug)]
//! struct PrintAddressAndContinue;
//!
//! impl AddressValidator for PrintAddressAndContinue {
//! fn validate(
//! &self,
//! keychain: KeychainKind,
//! hd_keypaths: &HdKeyPaths,
//! script: &Script
//! ) -> Result<(), AddressValidatorError> {
//! let address = Address::from_script(script, Network::Testnet)
//! .as_ref()
//! .map(Address::to_string)
//! .unwrap_or(script.to_string());
//! println!("New address of type {:?}: {}", keychain, address);
//! println!("HD keypaths: {:#?}", hd_keypaths);
//!
//! Ok(())
//! }
//! }
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
//!
//! let address = wallet.get_address(New)?;
//! println!("Address: {}", address);
//! # Ok::<(), bdk::Error>(())
//! ```
use std::fmt;
use bitcoin::Script;
use crate::descriptor::HdKeyPaths;
use crate::types::KeychainKind;
/// Errors that can be returned to fail the validation of an address
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AddressValidatorError {
/// User rejected the address
UserRejected,
/// Network connection error
ConnectionError,
/// Network request timeout error
TimeoutError,
/// Invalid script
InvalidScript,
/// A custom error message
Message(String),
}
impl fmt::Display for AddressValidatorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for AddressValidatorError {}
/// Trait to build address validators
///
/// All the address validators attached to a wallet with [`Wallet::add_address_validator`](super::Wallet::add_address_validator) will be polled
/// every time an address (external or internal) is generated by the wallet. Errors returned in the
/// 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(
&self,
keychain: KeychainKind,
hd_keypaths: &HdKeyPaths,
script: &Script,
) -> Result<(), AddressValidatorError>;
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use super::*;
use crate::wallet::AddressIndex::New;
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
#[derive(Debug)]
struct TestValidator;
#[allow(deprecated)]
impl AddressValidator for TestValidator {
fn validate(
&self,
_keychain: KeychainKind,
_hd_keypaths: &HdKeyPaths,
_script: &bitcoin::Script,
) -> Result<(), AddressValidatorError> {
Err(AddressValidatorError::InvalidScript)
}
}
#[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();
}
#[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);
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
builder.finish().unwrap();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -134,15 +134,11 @@ impl FullyNodedExport {
let blockheight = match wallet.database.borrow().iter_txs(false) {
_ if !include_blockheight => 0,
Err(_) => 0,
Ok(txs) => {
let mut heights = txs
.into_iter()
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
.collect::<Vec<_>>();
heights.sort_unstable();
*heights.last().unwrap_or(&0)
}
Ok(txs) => txs
.into_iter()
.filter_map(|tx| tx.confirmation_time.map(|c| c.height))
.min()
.unwrap_or(0),
};
let export = FullyNodedExport {
@@ -249,6 +245,22 @@ mod test {
fee: Some(500),
confirmation_time: Some(BlockTime {
timestamp: 12345678,
height: 5001,
}),
})
.unwrap();
db.set_tx(&TransactionDetails {
transaction: None,
txid: Txid::from_str(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap(),
received: 25_000,
sent: 0,
fee: Some(300),
confirmation_time: Some(BlockTime {
timestamp: 12345677,
height: 5000,
}),
})

View File

@@ -0,0 +1,100 @@
// 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 HWISigner, an implementation of a [TransactionSigner] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk::bitcoin::Network;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::signer::SignerOrdering;
//! # use bdk::wallet::hardwaresigner::HWISigner;
//! # use bdk::wallet::AddressIndex::New;
//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
//! # use hwi::{types::HWIChain, HWIClient};
//! # use std::sync::Arc;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut devices = HWIClient::enumerate()?;
//! if devices.is_empty() {
//! panic!("No devices found!");
//! }
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, HWIChain::Test)?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # None,
//! # Network::Testnet,
//! # MemoryDatabase::default(),
//! # )?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
//! Arc::new(custom_signer),
//! );
//!
//! # Ok(())
//! # }
//! ```
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

@@ -58,6 +58,7 @@
//! &self,
//! psbt: &mut psbt::PartiallySignedTransaction,
//! input_index: usize,
//! _sign_options: &SignOptions,
//! _secp: &Secp256k1<All>,
//! ) -> Result<(), SignerError> {
//! self.device.hsm_sign_input(psbt, input_index)?;
@@ -95,13 +96,14 @@ use bitcoin::{secp256k1, XOnlyPublicKey};
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
use miniscript::descriptor::{
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
KeyMap, SinglePubKey,
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
SinglePubKey,
};
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils};
use crate::psbt::PsbtUtils;
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
/// multiple of them
@@ -158,6 +160,16 @@ pub enum SignerError {
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 {
@@ -168,7 +180,22 @@ impl From<sighash::Error> for SignerError {
impl fmt::Display for SignerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
match self {
Self::MissingKey => write!(f, "Missing private key"),
Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"),
Self::UserCanceled => write!(f, "The user canceled the operation"),
Self::InputIndexOutOfRange => write!(f, "Input index out of range"),
Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"),
Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"),
Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"),
Self::MissingWitnessScript => write!(f, "Missing witness script"),
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
#[cfg(feature = "hardware-signer")]
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
}
}
}
@@ -241,6 +268,7 @@ pub trait InputSigner: SignerCommon {
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError>;
}
@@ -254,6 +282,7 @@ pub trait TransactionSigner: SignerCommon {
fn sign_transaction(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError>;
}
@@ -262,10 +291,11 @@ 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, secp)?;
self.sign_input(psbt, input_index, sign_options, secp)?;
}
Ok(())
@@ -287,6 +317,7 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError> {
if input_index >= psbt.inputs.len() {
@@ -346,18 +377,18 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
inner: derived_key.private_key,
};
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp)
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
}
}
}
impl SignerCommon for SignerWrapper<PrivateKey> {
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.public_key(secp).to_pubkeyhash())
SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
Some(DescriptorSecretKey::Single(SinglePriv {
key: self.signer,
origin: None,
}))
@@ -369,6 +400,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
&self,
psbt: &mut psbt::PartiallySignedTransaction,
input_index: usize,
sign_options: &SignOptions,
secp: &SecpCtx,
) -> Result<(), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
@@ -385,7 +417,10 @@ impl InputSigner for SignerWrapper<PrivateKey> {
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() {
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,
@@ -404,9 +439,18 @@ impl InputSigner for SignerWrapper<PrivateKey> {
let leaf_hashes = leaf_hashes
.iter()
.filter(|lh| {
!psbt.inputs[input_index]
.tap_script_sigs
.contains_key(&(x_only_pubkey, **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<_>>();
@@ -443,6 +487,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
hash,
hash_ty,
secp,
sign_options.allow_grinding,
);
Ok(())
@@ -456,11 +501,16 @@ fn sign_psbt_ecdsa(
hash: bitcoin::Sighash,
hash_ty: EcdsaSighashType,
secp: &SecpCtx,
allow_grinding: bool,
) {
let sig = secp.sign_ecdsa(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
secret_key,
);
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = if allow_grinding {
secp.sign_ecdsa_low_r(msg, secret_key)
} else {
secp.sign_ecdsa(msg, secret_key)
};
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
.expect("invalid or corrupted ecdsa signature");
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
psbt_input.partial_sigs.insert(pubkey, final_signature);
@@ -482,14 +532,14 @@ fn sign_psbt_schnorr(
let keypair = match leaf_hash {
None => keypair
.tap_tweak(secp, psbt_input.tap_merkle_root)
.into_inner(),
.to_inner(),
Some(_) => keypair, // no tweak for script spend
};
let sig = secp.sign_schnorr(
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
&keypair,
);
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).0)
.expect("invalid or corrupted schnorr signature");
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
@@ -541,7 +591,7 @@ impl SignersContainer {
self.0
.values()
.filter_map(|signer| signer.descriptor_secret_key())
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
.filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
.collect()
}
@@ -566,8 +616,13 @@ impl SignersContainer {
};
match secret {
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
DescriptorSecretKey::Single(private_key) => container.add_external(
SignerId::from(
private_key
.key
.public_key(secp)
.to_pubkeyhash(SigType::Ecdsa),
),
SignerOrdering::default(),
Arc::new(SignerWrapper::new(private_key.key, ctx)),
),
@@ -668,15 +723,52 @@ 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.
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
///
/// Defaults to `true` which will remove partial_sigs after finalizing.
/// Defaults to `true` which will remove partial signatures during finalization.
pub remove_partial_sigs: bool,
/// Whether to try finalizing psbt input after the inputs are signed.
/// Whether to try finalizing the PSBT after the inputs are signed.
///
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
/// Defaults to `true` which will try finalizing 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,
/// Whether we should grind ECDSA signature to ensure signing with low r
/// or not.
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
pub allow_grinding: bool,
}
/// Customize which taproot script-path leaves the signer should sign.
#[derive(Debug, Clone, PartialEq, Eq)]
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)]
@@ -688,6 +780,9 @@ impl Default for SignOptions {
allow_all_sighashes: false,
remove_partial_sigs: true,
try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true,
allow_grinding: true,
}
}
}
@@ -859,11 +954,8 @@ impl ComputeSighash for Tap {
.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)
let witness_utxos = (0..psbt.inputs.len())
.map(|i| psbt.get_utxo_for(i))
.collect::<Vec<_>>();
let mut all_witness_utxos = vec![];
@@ -921,6 +1013,7 @@ mod signers_container_tests {
use crate::descriptor;
use crate::descriptor::IntoWalletDescriptor;
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use assert_matches::assert_matches;
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::util::bip32;
use bitcoin::Network;
@@ -990,17 +1083,17 @@ mod signers_container_tests {
signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
assert!(matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)));
assert!(matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)));
assert!(matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)));
assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1));
assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2));
assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3));
// The `signer4` has the same ID as `signer3` but lower ordering.
// It should be found by `id3` instead of `signer3`.
signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
assert!(matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)));
assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4));
// Can't find anything with ID that doesn't exist
assert!(matches!(signers.find(id_nonexistent), None));
assert_matches!(signers.find(id_nonexistent), None);
}
#[derive(Debug, Clone, Copy)]
@@ -1018,6 +1111,7 @@ mod signers_container_tests {
fn sign_transaction(
&self,
_psbt: &mut psbt::PartiallySignedTransaction,
_sign_options: &SignOptions,
_secp: &SecpCtx,
) -> Result<(), SignerError> {
Ok(())

View File

@@ -42,9 +42,7 @@ use std::default::Default;
use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, Transaction};
use miniscript::descriptor::DescriptorTrait;
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
@@ -139,7 +137,7 @@ pub(crate) struct TxParams {
pub(crate) manually_selected_only: bool,
pub(crate) sighash: Option<psbt::PsbtSighashType>,
pub(crate) ordering: TxOrdering,
pub(crate) locktime: Option<u32>,
pub(crate) locktime: Option<LockTime>,
pub(crate) rbf: Option<RbfValue>,
pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy,
@@ -147,7 +145,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) current_height: Option<LockTime>,
pub(crate) allow_dust: bool,
}
#[derive(Clone, Copy, Debug)]
@@ -425,7 +424,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// Use a specific nLockTime while creating the transaction
///
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self {
pub fn nlocktime(&mut self, locktime: LockTime) -> &mut Self {
self.params.locktime = Some(locktime);
self
}
@@ -516,7 +515,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
}
}
/// Finish the building the transaction.
/// Finish building the transaction.
///
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
///
@@ -540,7 +539,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
///
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
/// be a valid nSequence to signal RBF.
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
self.params.rbf = Some(RbfValue::Value(nsequence));
self
}
@@ -557,7 +556,15 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
///
/// 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.params.current_height = Some(LockTime::from_height(height).expect("Invalid 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
}
}
@@ -694,7 +701,7 @@ impl TxOrdering {
#[cfg(not(test))]
let mut rng = rand::thread_rng();
#[cfg(test)]
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
tx.output.shuffle(&mut rng);
}
@@ -727,13 +734,13 @@ impl Default for Version {
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub(crate) enum RbfValue {
Default,
Value(u32),
Value(Sequence),
}
impl RbfValue {
pub(crate) fn get_value(&self) -> u32 {
pub(crate) fn get_value(&self) -> Sequence {
match self {
RbfValue::Default => 0xFFFFFFFD,
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
RbfValue::Value(v) => *v,
}
}
@@ -849,10 +856,12 @@ mod test {
}
fn get_test_utxos() -> Vec<LocalUtxo> {
use bitcoin::hashes::Hash;
vec![
LocalUtxo {
outpoint: OutPoint {
txid: Default::default(),
txid: bitcoin::Txid::from_inner([0; 32]),
vout: 0,
},
txout: Default::default(),
@@ -861,7 +870,7 @@ mod test {
},
LocalUtxo {
outpoint: OutPoint {
txid: Default::default(),
txid: bitcoin::Txid::from_inner([0; 32]),
vout: 1,
},
txout: Default::default(),

View File

@@ -9,23 +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 bitcoin::{LockTime, Script, Sequence};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
// 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;
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
// otherwise it's time-based
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
// Mask for the bits used to express the timelock
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.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate
@@ -38,7 +26,7 @@ pub trait IsDust {
impl IsDust for u64 {
fn is_dust(&self, script: &Script) -> bool {
*self < script.dust_value().as_sat()
*self < script.dust_value().to_sat()
}
}
@@ -56,19 +44,15 @@ impl After {
}
}
pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
// This flag cannot be set in the nSequence when spending using OP_CSV
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
// The RBF value must enable relative timelocks
if !rbf.is_relative_lock_time() {
return false;
}
let mask = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK;
let rbf = rbf & mask;
let csv = csv & mask;
// Both values should be represented in the same unit (either time-based or
// block-height based)
if (rbf < SEQUENCE_LOCKTIME_TYPE_FLAG) != (csv < SEQUENCE_LOCKTIME_TYPE_FLAG) {
if rbf.is_time_locked() != csv.is_time_locked() {
return false;
}
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
true
}
pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
// Both values should be expressed in the same unit
if (nlocktime < BLOCKS_TIMELOCK_THRESHOLD) != (required < BLOCKS_TIMELOCK_THRESHOLD) {
return false;
}
// The value should be at least `required`
if nlocktime < required {
return false;
}
true
}
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
fn check_after(&self, n: u32) -> bool {
fn check_after(&self, n: LockTime) -> bool {
if let Some(current_height) = self.current_height {
current_height >= n
current_height >= n.to_consensus_u32()
} else {
self.assume_height_reached
}
@@ -125,10 +95,15 @@ impl Older {
}
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
fn check_older(&self, n: u32) -> bool {
fn check_older(&self, n: Sequence) -> bool {
if let Some(current_height) = self.current_height {
// TODO: test >= / >
current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64
current_height
>= self
.create_height
.unwrap_or(0)
.checked_add(n.to_consensus_u32())
.expect("Overflowing addition")
} else {
self.assume_height_reached
}
@@ -139,12 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)]
mod test {
use super::{
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
SEQUENCE_LOCKTIME_TYPE_FLAG,
};
use crate::bitcoin::Address;
use crate::types::FeeRate;
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
// otherwise it's time-based
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
use super::{check_nsequence_rbf, IsDust};
use crate::bitcoin::{Address, Sequence};
use std::str::FromStr;
#[test]
@@ -164,86 +139,42 @@ mod test {
assert!(!294.is_dust(&script_p2wpkh));
}
#[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);
}
#[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);
}
#[test]
fn test_check_nsequence_rbf_msb_set() {
let result = check_nsequence_rbf(0x80000000, 5000);
let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_lt_csv() {
let result = check_nsequence_rbf(4000, 5000);
let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_different_unit() {
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
let result =
check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
assert!(!result);
}
#[test]
fn test_check_nsequence_rbf_mask() {
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
assert!(result);
}
#[test]
fn test_check_nsequence_rbf_same_unit_blocks() {
let result = check_nsequence_rbf(10_000, 5000);
let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
assert!(result);
}
#[test]
fn test_check_nsequence_rbf_same_unit_time() {
let result = check_nsequence_rbf(
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
);
assert!(result);
}
#[test]
fn test_check_nlocktime_lt_cltv() {
let result = check_nlocktime(4000, 5000);
assert!(!result);
}
#[test]
fn test_check_nlocktime_different_unit() {
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
assert!(!result);
}
#[test]
fn test_check_nlocktime_same_unit_blocks() {
let result = check_nlocktime(10_000, 5000);
assert!(result);
}
#[test]
fn test_check_nlocktime_same_unit_time() {
let result = check_nlocktime(
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
BLOCKS_TIMELOCK_THRESHOLD + 5000,
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
);
assert!(result);
}

View File

@@ -29,6 +29,8 @@ 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.
///
/// [`Blockchain`]: crate::blockchain::Blockchain
pub fn verify_tx<D: Database, B: GetTx>(
tx: &Transaction,
database: &D,
@@ -89,7 +91,12 @@ pub enum VerifyError {
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
match self {
Self::MissingInputTx(txid) => write!(f, "The transaction being spent is not available in the database or the blockchain client: {}", txid),
Self::InvalidInput(outpoint) => write!(f, "The transaction being spent doesn't have the requested output: {}", outpoint),
Self::Consensus(err) => write!(f, "Consensus error: {:?}", err),
Self::Global(err) => write!(f, "Generic error: {}", err),
}
}
}
@@ -106,6 +113,7 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
mod test {
use super::*;
use crate::database::{BatchOperations, MemoryDatabase};
use assert_matches::assert_matches;
use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;
use bitcoin::{Transaction, Txid};
@@ -135,9 +143,7 @@ mod test {
}
let result = verify_tx(&signed_tx, &database, &blockchain);
assert!(result.is_err(), "Should fail with missing input tx");
assert!(
matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid()),
assert_matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid(),
"Error should be a `MissingInputTx` error"
);
@@ -145,9 +151,9 @@ mod test {
database.set_raw_tx(&prev_tx).unwrap();
let result = verify_tx(&unsigned_tx, &database, &blockchain);
assert!(result.is_err(), "Should fail since the TX is unsigned");
assert!(
matches!(result, Err(VerifyError::Consensus(_))),
assert_matches!(
result,
Err(VerifyError::Consensus(_)),
"Error should be a `Consensus` error"
);