Compare commits

...

248 Commits

Author SHA1 Message Date
Steve Myers
f4d2a76661 Bump version to 1.0.0-alpha.1 2023-07-04 12:28:06 -05:00
Steve Myers
81c7613391 [ci] fix docsrs error for bdk crate 2023-07-04 09:02:39 -05:00
志宇
26ade11726 Fix cargo features
* `bdk_chain/std` should also enable `miniscript/std`
* use the version of `hashbrown` that `bitcoin` and `miniscript` is
  using
2023-06-26 11:02:06 +08:00
Steve Myers
5d1f922b3b Merge bitcoindevkit/bdk#894: Better no-std support
7ab84be9c7 Better no-std support (Steve Myers)

Pull request description:

  This replaces #893

  ### Description

  Carrying over relevant maintenance changes from release 0.27.2 to bdk `master`.

  - Use `default-features = false` for `miniscript` and `bitcoin`
  - Introduce `std`  features for `bdk`, `bdk_chain` and `bdk_esplora`

  ### Notes to the reviewers

  The `default-features = false`for `bitcoin` and `miniscript` is to let `bdk` be unbiased for things like no-std.

  ### Changelog notice

  - Set default-features = false for rust-bitcoin and rust-miniscript #894
  - Introduce `std`  features for `bdk`, `bdk_chain` and `bdk_esplora`

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

Tree-SHA512: 217e0ebc04cd59898d4de1c4f8cc6c87de2561659e277cc6e0d969c8bc61142ca62c8bda86643d55676dce39da25891b516a901005a03a31404dae118a5fce76
2023-06-20 16:36:54 -05:00
Steve Myers
7ab84be9c7 Better no-std support
- Use `default-features = false` for `miniscript`,`bitcoin`,and `bdk_chain`
- Introduce `bdk_chain/std` feature
- Add GitHub workflow `check-no-std` (not yet completly working)
- Update GitHub workflow `check-wasm` to disable default `std` features
2023-06-20 15:53:48 -05:00
Steve Myers
b184e351e5 Merge bitcoindevkit/bdk#840: create taproot descriptor template
e30919ba3a Create taproot descriptor template (Vladimir Fomene)

Pull request description:

  ### Description

  This PR solves #836. This PR adds a P2TR
  descriptor template and a BIP86 taproot
  descriptor template. With this, users
  can now create a taproot descriptor with templates.

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

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

Tree-SHA512: ad171d29e1b22613a9fb5e6ed148d1d3002c76152df1b5aa94e24be134d08d7c90dda9560b7501a8df28e5a7076339745da627bbdde5ed6fa11eab3199c34a99
2023-06-19 15:44:01 -05:00
Daniela Brozzoni
352f5b29ab Merge bitcoindevkit/bdk#1009: Pin log dependency to 0.4.18 to keep the MSRV to 1.57.0
fa54a2e3a5 Pin log dependency to 0.4.18 to keep the MSRV to 1.57.0 (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:
  evanlinjin:
    ACK fa54a2e3a5

Tree-SHA512: 5fc19dd8485fcfe88c2720ff183dac7bfb6cacefa7be0c23aa1e7b345d81d1e41ac0903d84c5606b00adf2314911ee9d25972bfd4f0764e70456c8d8ffb3ab33
2023-06-19 14:12:01 +02:00
Daniela Brozzoni
fa54a2e3a5 Pin log dependency to 0.4.18 to keep the MSRV to 1.57.0 2023-06-19 13:50:59 +02:00
Daniela Brozzoni
97d542cf1c Merge bitcoindevkit/bdk#976: Reimplement Wallet, ElectrumExt and Esplora{Async}Ext with redesigned structures.
75f8b81d58 Update documentation (志宇)
cff92111d5 [wallet_redesign] Clean up and document address methods (志宇)
a7668a2f3e [wallet_redesign] Modified `insert_tx` to use lowest checkpoint (志宇)
ac80829caa Rename fields of `tx_graph::Additions` (Shourya742)
1c3cbefa4d [chain_redesign] Remove old structures (志宇)
5860704b2d Implement redesigned versions of `EsploraExt` and `EsploraAsyncExt` (志宇)
2952341e52 Update the `wallet_electrum` example (志宇)
78a7920ba3 `bdk_electrum` API improvements and simplifications (志宇)
92709d03ce Various tweaks to code arrangement and documentation (志宇)
50425e979b Introduce `keychain::LocalChangeSet` (志宇)
a78967e51b [example-cli] simplify new address logic (LLFourn)
6a1ac7f80a [examples_redesign] Implemented `example_electrum` (志宇)
f55974a64b [examples_redesign] Introduce `example_cli` package (志宇)
2e3cee4bd0 [electrum_redesign] Introduce redesigned `ElectrumExt` (志宇)
7261669c09 Add `last_seen` to the the `ConfirmationTime::Unconfirmed` variant (志宇)
aba88130d9 [wallet_redesign] Move the majority of `Update` to `bdk_chain` (志宇)
e69fccb15f [wallet_redesign] Update `Wallet` with redesigned structures (志宇)

Pull request description:

  ### Description

  Closes #938

  * Update `Wallet` to use redesigned structures.
  * Update `bdk_electrum::ElectrumExt` to produce updates for redesigned structures.
  * Update `bdk_esplora::EsploraExt` and `bdk_esplora::EsploraAsyncExt` to produce updates for redesigned structures.
  * Added `example-crates/example_cli` library for implementing examples with redesigned structures.
  * Added `example-crate/example_electrum` which is an electrum CLI wallet using the redesigned structures.
  * Updated `example-crate/{wallet_electrum|wallet_esplora|wallet_esplora_async}` examples to use redesigned structures.
  * Remove all old structures.

  ### Notes to the reviewers

  ~These changes bump our `all-features` MSRV to `1.60.0` because of the introduction of `bdk_esplora`. As long as the `bdk_chain` and `bdk_wallet` crates hit a MSRV of `1.48.0`, it will be fine (this work is done in #987).~ No longer needed due to #993

  ~I had to comment out the examples that use `Wallet` with our chain sources. Once we update the helper-packages for those chain sources, we can also update the examples.~

  Possible future improvements for `ElectrumExt`:

  * Remove requirement to retry obtaining ALL data after reorg is detected. Transactions can be anchored to a lower block (not block tip), and an `assume_final_depth` value can be used.

  * The logic to finalize an update with confirmation time can be improved during reorgs to not require returning an error.

  * Use the subscription model of electrum, as intended by the API.

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

  #### New Features:

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

ACKs for top commit:
  LLFourn:
    ACK 75f8b81d58
  danielabrozzoni:
    Partial ACK 75f8b81d58 - the Wallet code looks good to me, I don't have a good enough understanding of the esplora/electrum code to confidently ACK it.

Tree-SHA512: d1d2b79e3c28fbe826044a8b5ef9b122c2dcfc0d371f24cc4aac7f286500b587c2dc3b06ca6461c8721adbc29f56ca41e7566eace560b0a9c541604e6a225c61
2023-06-19 13:38:39 +02:00
志宇
75f8b81d58 Update documentation
* Add `warn(missing_docs)` for `bdk_wallet` and `bdk_chain`.
* Add missing documentation.
* Remove `LocalChain::heights` method.
* Remove old TODOs.
2023-06-04 03:32:19 +08:00
志宇
cff92111d5 [wallet_redesign] Clean up and document address methods 2023-06-04 03:32:19 +08:00
志宇
a7668a2f3e [wallet_redesign] Modified insert_tx to use lowest checkpoint
Also updated the documentation.
2023-06-04 03:32:19 +08:00
Shourya742
ac80829caa Rename fields of tx_graph::Additions
* Changed `tx` to `txs`
* Changed `txout` to `txouts`
2023-06-04 03:32:18 +08:00
志宇
1c3cbefa4d [chain_redesign] Remove old structures
Other changes:

* The `async-https` feature of `bdk_esplora` is no longer default.
* Rename `ObservedAs` to `ChainPosition`.
* Set temporary MSRV to 1.60.0 to compile all workspace members will all
  features.
2023-06-04 03:32:18 +08:00
志宇
5860704b2d Implement redesigned versions of EsploraExt and EsploraAsyncExt
All associated examples are also updated.
2023-06-04 03:32:18 +08:00
志宇
2952341e52 Update the wallet_electrum example 2023-06-04 03:32:18 +08:00
志宇
78a7920ba3 bdk_electrum API improvements and simplifications
* `ElectrumUpdate::missing_full_txs` now returns a `Vec<Txid>` so we
  don't keep a reference to the passed-in `graph`.

* `ElectrumUpdate::finalize*` methods now takes in `missing` txids
  instead of `full_txs`. `Client::batch_transaction_get` is called
within the methods.

Other changes:

* `wallet::ChangeSet` is now made public externally. This is required as
  a wallet db should implement `PersistBackend<wallet::ChangeSet>`.
2023-06-04 03:32:18 +08:00
志宇
92709d03ce Various tweaks to code arrangement and documentation
As per reviews by @danielabrozzoni and @LLFourn
2023-06-04 03:32:18 +08:00
志宇
50425e979b Introduce keychain::LocalChangeSet
This corresponds to `keychain::KeychainChangeSet` but for the redesigned
structures with `LocalChain`.

This structure is now used in `Wallet` as well as the examples.
2023-06-04 03:32:18 +08:00
LLFourn
a78967e51b [example-cli] simplify new address logic 2023-06-04 03:32:18 +08:00
志宇
6a1ac7f80a [examples_redesign] Implemented example_electrum
This is a version of `keychain_tracker_electrum` that uses the
redesigned structures instead.
2023-06-04 03:32:17 +08:00
志宇
f55974a64b [examples_redesign] Introduce example_cli package
This is the equivalent of `keychain_tracker_example_cli` that works with
the redesigned structures.
2023-06-04 03:32:17 +08:00
志宇
2e3cee4bd0 [electrum_redesign] Introduce redesigned ElectrumExt
There are a number of improvements that can be done, but it is in a
decent state to be usable.

Possible improvements:

* Remove requirement to retry obtaining ALL data after reorg is
  detected. Transactions can be anchored to a lower block (not block
  tip), and an `assume_final_depth` value can be used.

* The logic to finalize an update with confirmation time can be improved
  during reorgs to not require returning an error.
2023-06-04 03:32:17 +08:00
志宇
7261669c09 Add last_seen to the the ConfirmationTime::Unconfirmed variant
This allows us to skip adding an extra input to `Wallet::insert_tx`.

Also remove redundant logic.
2023-06-04 03:32:17 +08:00
志宇
aba88130d9 [wallet_redesign] Move the majority of Update to bdk_chain
This is to make it easier for chain source crates to formulate updates.
2023-06-04 03:32:17 +08:00
志宇
e69fccb15f [wallet_redesign] Update Wallet with redesigned structures 2023-06-04 03:32:14 +08:00
Daniela Brozzoni
8641847e6c Merge bitcoindevkit/bdk#1001: Rename "keychanins" to keychains
22b8a48842 Rename "keychanins" to keychains (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 22b8a48842

Tree-SHA512: 13ebbd597771a9d91c317fedc4dc506a275a641b764f24650cd13217cf8ac0d06c42a935a8c9e1e7f4761d2ade473c7f8381e90875e867b7a61381aa33cd8581
2023-06-02 00:45:03 +02:00
Daniela Brozzoni
22b8a48842 Rename "keychanins" to keychains 2023-06-01 19:29:02 +02:00
Daniela Brozzoni
9bc7fe855d Merge bitcoindevkit/bdk#957: Documentation regarding absolute_fee and fee_rate updated
10b4b6c665 Documentations regarding absolute_fee and fee_rate updated (TATHAGATA ROY)

Pull request description:

  ### Description
  This pr solves this issue #856
  Updated the documentation for absolute fee and fee rate in BDK docs

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)

  #### Bugfixes:

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

ACKs for top commit:
  danielabrozzoni:
    ACK 10b4b6c665

Tree-SHA512: 5b5f360b742313443368dc0b75275c38888cb358545661f7d9c78f3290789c50ffa9830e18473055d8e3da544c7d9527b023a1acdb28eaf862b24c892b23e9c4
2023-05-28 15:05:12 +02:00
TATHAGATA ROY
10b4b6c665 Documentations regarding absolute_fee and fee_rate updated
Update tx_builder.rs
2023-05-25 22:00:46 +00:00
Daniela Brozzoni
796f433f6c Merge bitcoindevkit/bdk#993: Unpin base64
ac3759254a Unpin base64 (Daniela Brozzoni)

Pull request description:

  base64 lowered the MSRV to 1.57.0 in version 0.21.2

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

Tree-SHA512: de66f4be7c7fd608f5a74e51c5666f4fe6b962c5a8345c08c7b1da2551f81b9adfb28c58f0991ac6c985fa5c31f3eb7d17f99edf136b04945cefa24b28eaee19
2023-05-25 17:24:44 +02:00
Daniela Brozzoni
ac3759254a Unpin base64
base64 lowered the MSRV to 1.57.0 in version 0.21.2
2023-05-25 15:37:16 +02:00
Vladimir Fomene
e30919ba3a Create taproot descriptor template
This PR solves #836. This PR adds a P2TR
descriptor template and a BIP86 taproot
descriptor template. With this, users
can now create a taproot descriptor with templates.
2023-05-25 13:42:53 +03:00
Daniela Brozzoni
1d55943fa1 Merge bitcoindevkit/bdk#990: Pin base64 to 0.21.0 to keep the MSRV to 1.57.0
df74b23f31 Pin base64 to 0.21.0 to keep the MSRV to 1.57.0 (Daniela Brozzoni)

Pull request description:

  Fixes #986

  ### Changelog notice

  - Pin base64 to 0.21.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 df74b23f31

Tree-SHA512: fb11db0771542b074b7605e4af9ec655c22106b1d3451e27103d2f81686270efb44d8687f8312b5b7ab724f03e8f24c349a9f4f3b6e0c89ed9550b121f526ce7
2023-05-24 17:49:59 +02:00
Daniela Brozzoni
df74b23f31 Pin base64 to 0.21.0 to keep the MSRV to 1.57.0 2023-05-24 16:12:34 +02:00
Daniela Brozzoni
725eee8c92 Merge bitcoindevkit/bdk#981: Fixed typos in CONTRIBUTING.md
c7a045fa54 Fixed typos in CONTRIBUTING.md (Jon Marrs)

Pull request description:

  ### Description

  Fixed typos in CONTRIBUTING.md

  ### 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:
  thunderbiscuit:
    ACK c7a045fa54.

Tree-SHA512: de66f4be7c7fd608f5a74e51c5666f4fe6b962c5a8345c08c7b1da2551f81b9adfb28c58f0991ac6c985fa5c31f3eb7d17f99edf136b04945cefa24b28eaee19
2023-05-18 12:36:31 +02:00
Jon Marrs
c7a045fa54 Fixed typos in CONTRIBUTING.md 2023-05-16 21:39:18 -07:00
志宇
34d60870ac Merge bitcoindevkit/bdk#975: Improve txout listing and balance APIs for redesigned structures
ed89de752c Improve txout filter/listing method docs for `TxGraph` (志宇)
fb75aa94a9 Clarify `TxGraph::try_filter_chain_unspents` logic (志宇)
96b1075132 Fix and improve `Persist::commit` method (志宇)
e01d17d59b Improve `txout` listing and balance APIs for redesigned structures (志宇)

Pull request description:

  ### Description

  As noted in https://github.com/bitcoindevkit/bdk/issues/971#issuecomment-1542408941.

  Instead of relying on a `OwnedIndexer` trait to filter for relevant txouts, we move the listing and balance methods from `IndexedTxGraph` to `TxGraph` and add an additional input (list of relevant outpoints) to these methods.

  This provides a simpler implementation and a more flexible API.

  #### Other Fixes

  The `Persist::commit` method is fixed in 96b1075132.

  Previously, regardless of whether writing to persistence backend is successful or not, the logic always cleared `self.staged`. This is changed to only clear `self.staged` after successful write.

  Additionally, the written changeset (if any) is returned, and `PersistBackend::write_changes` will not be called if `self.staged` is empty.

  ### Notes to the reviewers

  Yes, slightly more boilerplate to do the same things, but less code to maintain and a much more flexible API. Very worth it IMO.

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

  #### New Features:

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

ACKs for top commit:
  LLFourn:
    ACK ed89de752c

Tree-SHA512: efae18c13ee74eff801febca61cb16bf4d8f3203ced96172b9ef555d8d2a749a382f87d024e8c92aacbab7eb4e50a8337de798e10524291ad80c6b35c4dc0161
2023-05-11 21:02:16 +08:00
志宇
ed89de752c Improve txout filter/listing method docs for TxGraph
Rename the `S` trait bound to `OI` (outpoint index) to emphasize it's
not only for spk indexing.
2023-05-11 12:43:49 +08:00
志宇
fb75aa94a9 Clarify TxGraph::try_filter_chain_unspents logic 2023-05-11 11:59:57 +08:00
志宇
96b1075132 Fix and improve Persist::commit method
Previously, regardless of whether writing to persistence backend is
successful or not, the logic always cleared `self.staged`. This is
changed to only clear `self.staged` after successful write.

Additionally, the written changeset (if any) is returned, and
`PersistBackend::write_changes` will not be called if `self.staged` is
empty.
2023-05-11 11:49:33 +08:00
志宇
e01d17d59b Improve txout listing and balance APIs for redesigned structures
Instead of relying on a `OwnedIndexer` trait to filter for relevant
txouts, we move the listing and balance methods from `IndexedTxGraph` to
`TxGraph` and add an additional input (list of relevant outpoints) to
these methods.

This provides a simpler implementation and a more flexible API.
2023-05-10 23:57:24 +08:00
志宇
05d353c0ad Merge bitcoindevkit/bdk#965: Implement persistence with the new structures
4963240599 Add more `impl`s for `Append` and docs for file store `magic` (志宇)
2aa08a5898 [persist_redesign] Introduce redesigned `persist` types (志宇)

Pull request description:

  ### Description

  This is part of #895 and #971

  * Introduce a more generic version of the `keychain::persist::*` structures that only needs a single generic for the changeset type.

  Additional changes:

  * The `Append` trait has a new method `is_empty`.
  * Introduce `Store` structure for `bdk_file_store` (which implements `PersistBackend`).

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

  #### New Features:

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

Top commit has no ACKs.

Tree-SHA512: 0211fbe7d7e27805d3ed3a80b42f184cdff1cebb32fd559aa9838e4a7f7c7e47b6c366b6ef68e299f876bafed549b8d1d8b8cc0366bf5b61db079504a565b9b4
2023-05-10 23:25:15 +08:00
志宇
4963240599 Add more impls for Append and docs for file store magic 2023-05-10 15:07:19 +08:00
志宇
2aa08a5898 [persist_redesign] Introduce redesigned persist types
This is a more generic version of `keychain::persist::*` structures.

Additional changes:

* The `Append` trait has a new method `is_empty`.
* Introduce `Store` structure for `bdk_file_store`.
2023-05-09 12:55:44 +08:00
志宇
e3c137043f Merge pull request #963 from evanlinjin/chain_redesign_tweaks
Various tweaks to redesigned structures
2023-05-05 20:11:11 +08:00
志宇
065c64a675 [chain_redesign] Rename LocalChain::inner() to blocks()
Also, we can get rid of `LocalChain::get_blockhash`, since we can
already expose the internal map.

Additionally, tests and docs are improved.
2023-05-05 19:49:30 +08:00
志宇
a56d289eef [chain_redesign] Various LocalChain improvements
* Introduce `LocalChain::inner` method to get the inner map of block
  height to hash.
* Replace `LocalChain::get_block` (which outputted `BlockId`, thus able
  to return invalid representation) with `get_blockhash` that just
returns a `BlockHash`.
* Remove `TODO` comments that should be github tickets.
2023-05-05 16:55:21 +08:00
志宇
2ccc116eda [chain_redesign] BlockId should not implement Anchor
If `BlockId` implements `Anchor`, the meaning is ambiguous. We cannot
tell whether it means the tx is anchors at the block, or whether it also
means the tx is confirmed at that block.

Instead, `ConfirmationHeightAnchor` and `ConfirmationTimeAnchor` structs
are introduced as non-ambiguous `Anchor` implementations.

Additionally, `TxGraph::relevant_heights` is removed because it is also
ambiguous. What heights are deemed relevant? A simpler and more flexible
method `TxGraph::all_anchors` is introduced instead.
2023-05-05 16:38:29 +08:00
志宇
4ae727a1fb [chain_redesign] Relax generic constraints 2023-05-05 16:35:39 +08:00
志宇
085bf9413d [chain_redesign] Add LocalChain::insert_block 2023-05-05 16:35:35 +08:00
志宇
e413d3e424 [chain_redesign] Change behavior of try_get_chain_position
`TxGraph::try_get_chain_position` used to always exclude unconfirmed
transactions with last_seen value of 0. However, what is the point of
including a transaction in the graph if it cannot be part of the chain
history? Additionally, maybe sometimes we don't wish to use the
last_seen field at all.

The new behavior will consider unconfirmed transactions with last_seen
of 0.
2023-05-03 11:43:16 +08:00
志宇
c61995ca97 Merge pull request #927 from LagginTimes/custom_spk_iterator
Custom spk iterator
2023-05-02 17:36:51 +08:00
Wei
10fe32e6f1 Implement SpkIterator
SpkIterator was created with its own nth() and next() implementations
and its own new() and new_with_range() constructors.

Co-authored-by: 志宇 <hello@evanlinjin.me>
2023-05-02 17:31:40 +08:00
Steve Myers
6bc5f33ded Merge commit 'refs/pull/960/head' of github.com:bitcoindevkit/bdk
Reenable code coverage upload to Coveralls, only for /crates repos
2023-05-01 15:41:34 -05:00
志宇
702fe7ac5e Merge pull request #959 from rajarshimaitra/test-graph-2
Cleanup IndexedTxGraph test.
2023-05-01 14:53:30 +08:00
Lloyd Fournier
ce5ae3eac4 Merge pull request #953 from benthecarman/kwu
Add sat_per_kwu to FeeRate
2023-05-01 13:22:44 +10:00
Steve Myers
c1cffe9333 Filter code coverage report results 2023-04-29 15:47:08 -05:00
Steve Myers
5be7c1c50d Reenable code coverage upload to Coveralls, only for ./crates packages 2023-04-29 09:49:03 -05:00
rajarshimaitra
29055658a6 Test cleanup and added documentation.
- Explainer doc added for the test.
- Test code simplified.
- One more edge cases added.
2023-04-29 17:27:12 +05:30
Daniela Brozzoni
139e3d3802 Merge bitcoindevkit/bdk#926: Introduce redesigned bdk_chain structures
b799a5728b [bdk_chain_redesign] Add tests for `IndexedTxGraph` with `LocalChain` (rajarshimaitra)
8cd0328eec [bdk_chain_redesign] Implement `OwnedIndexer` for indexers (rajarshimaitra)
911af34f50 [bdk_chain_redesign] Fix calculation bugs. (rajarshimaitra)
e536307e5c [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic (志宇)
f101dde09b [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic (志宇)
1b152647c5 [bdk_chain_redesign] Change `insert_relevant_txs` method (志宇)
ecc74ce4cd [bdk_chain_redesign] Docs for `is_mature` and `is_confirmed_and_spendable` (志宇)
ac336aa32f [bdk_chain_redesign] Make `insert_relevant_txs` topologically-agnostic (志宇)
165b874dfe [bdk_chain_redesign] Add test for `insert_relevant_txs` (志宇)
f3e7b67bf1 [bdk_chain_redesign] Various tweaks and fixes (志宇)
03c128311a [bdk_chain_redesign] Revert changes to `SparseChain` (志宇)
34a7bf5afe [bdk_chain_redesign] Rm unnecessary code and premature optimisation (志宇)
6c49570742 [bdk_chain_redesign] Rm `HashSet` from `TxGraph::relevant_heights` (志宇)
1003fe2ee6 [bdk_chain_redesign] Test `LocalChain` (志宇)
7175a82c04 [bdk_chain_redesign] Add tests for `TxGraph::relevant_heights` (志宇)
8e36a2e5f6 [bdk_chain_redesign] Remove incomplete logic (志宇)
81436fcd72 [bdk_chain_redesign] Fix `Anchor` definition + docs (志宇)
001efdd1cb Include tests for new updates of TxGraph (rajarshimaitra)
10ab77c549 [bdk_chain_redesign] MOVE `TxIndex` into `indexed_chain_graph.rs` (志宇)
7d92337b93 [bdk_chain_redesign] Remove `IndexedTxGraph::last_height` (志宇)
a7fbe0ac67 [bdk_chain_redesign] Documentation improvements (志宇)
ee1060f2ff [bdk_chain_redesign] Simplify `LocalChain` (志宇)
611d2e3ea2 [bdk_chain_redesign] Consistent `ChainOracle` (志宇)
bff80ec378 [bdk_chain_redesign] Improve `BlockAnchor` docs (志宇)
24cd8c5cc7 [bdk_chain_redesign] More tweaks and renamings (志宇)
ddd5e951f5 [bdk_chain_redesign] Modify signature of `TxIndex` (志宇)
da4cef044d [bdk_chain_redesign] Introduce `Append` trait for additions (志宇)
89cfa4d78e [bdk_chain_redesign] Better names, comments and generic bounds (志宇)
6e59dce10b [bdk_chain_redesign] `chain_oracle::Cache` (志宇)
a7eaebbb77 [bdk_chain_redesign] Add serde support for `IndexedAdditions` (志宇)
c09cd2afce [bdk_chain_redesign] Added methods to `LocalChain` (志宇)
7810059ed0 [bdk_chain_redesign] `TxGraph` tweaks (志宇)
a63ffe9739 [bdk_chain_redesign] Simplify `TxIndex` (志宇)
a1172def7d [bdk_chain_redesign] Revert some API changes (志宇)
8c906170c9 [bdk_chain_redesign] Make default anchor for `TxGraph` as `()` (志宇)
468701a129 [bdk_chain_redesign] Initial work on `LocalChain`. (志宇)
34d0277e44 [bdk_chain_redesign] Rm anchor type param for structs that don't use it (志宇)
3440a05711 [bdk_chain_redesign] Add docs (志宇)
236c50fa7b [bdk_chain_redesign] `IndexedTxGraph` keeps track of the last synced height (志宇)
e902c10295 [bdk_chain_redesign] Fix `apply_additions` logic for `IndexedTxGraph`. (志宇)
313965d8c8 [bdk_chain_redesign] `mut_index` should be `index_mut` (志宇)
db7883d813 [bdk_chain_redesign] Add balance methods to `IndexedTxGraph` (志宇)
d0a2aa83be [bdk_chain_redesign] Add `apply_additions` to `IndexedTxGraph` (志宇)
6cbb18d409 [bdk_chain_redesign] MOVE: `IndexedTxGraph` into submodule (志宇)
784cd34e3d [bdk_chain_redesign] List chain data methods can be try/non-try (志宇)
43b648fee0 [bdk_chain_redesign] Add `..in_chain` methods (志宇)
61a8606fbc [bdk_chain_redesign] Introduce `ChainOracle` and `TxIndex` traits (志宇)
5ae5fe30eb [bdk_chain_redesign] Introduce `BlockAnchor` trait (志宇)

Pull request description:

  ### Description

  This is part of #895

  The initial `bdk_chain` structures allowed updating to be done without blocking IO (alongside many other benefits). However, the requirement to have transactions "perfectly positioned" in our `SparseChain` increased complexity of the syncing API. Updates needed to be meticulously crafted to properly connect with the original `SparseChain`. Additionally, it forced us to keep a local copy of the "best chain" data (which may not always be needed depending on the chain source).

  The redesigned structures, as introduced by this PR, fixes these shortcomings.

  1. Instead of `SparseChain`, we introduce the ability to `Anchor` a transaction to multiple blocks that may or may not be in the same chain history. We expand `TxGraph` to records these anchors (while still maintaining the *monotone* nature of `TxGraph`). When updating our new `TxGraph`, we don't need to complicated *are-we-connected* checks that we need for `SparseChain`.
  2. The chain source, in combination with our new`TxGraph` is all that we need to determine the "chain position" of a transaction. The chain source only needs to implement a single trait `ChainOracle`. This typically does not need to be updated (as it is remote), although we can have a special `ChainOracle` implementation that is stored locally. This only needs block height and hash information, reducing the scope of *non-monotine* structures that need to be updated.

  **What is done:**

  * `TxGraph` includes anchors (ids of blocks that the tx is seen in) and last-seem unix timestamp (for determining which non-confirmed tx we should consider as part of "best chain" if there are conflicts). This structure continues to be fully "monotone"; we can introduce data in any manner and not risk resulting in an inconsistent state.
  * `LocalChain` includes the "checkpoint" logic of `SparseChain` but removes the `txid` data. `LocalChain` implements the `ChainOracle` trait. Any blockchain-source can also implement the `ChainOracle` trait.
  * `IndexedTxGraph` is introduced and contains two fields; an internal `TxGraph` struct and a `TxIndex` implementation. These two fields will be updated atomically and can replace the functionality of `keychain::Tracker`.

  **What is in-progress:**

  * ~@evanlinjin: The `TxIndex` trait should not have `is_{}_relevant` methods as we cannot guarantee this across all transaction indexes. We should introduce extension traits for these (https://github.com/bitcoindevkit/bdk/pull/926#discussion_r1159286393).~
  * ~@evanlinjin: `BlockAnchor` should be defined as "if this block is in chain, then this tx must be in chain". Therefore, the anchor does not provide us with the exact confirmation height of the tx. We need to introduce an extension trait `ExactConfirmationHeightAnchor` for certain operations (https://github.com/bitcoindevkit/bdk/pull/926#discussion_r1155480352).~

  **What will be done external to this PR:**

  * @rajarshimaitra: Persisting `indexed_tx_graph::Additions` (#937).
  * @notmandatory: Update examples to use redesigned structures.
  * Update `bdk::Wallet` to use the redesigned structures.

  ### Changelog notice

  * Initial implementation of the `bdk_chain` redesign (as mentioned in #895). Currently, only the `bdk_chain` core structures are implemented. Persistence and updates to the current examples and `bdk::Wallet` will be done in separate PRs.

  ### 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:
  rajarshimaitra:
    ACK b799a5728b
  danielabrozzoni:
    Partial ACK b799a5728b - good job! I haven't looked at the tests or at the methods implementations in depth, but I have reviewed the architecture and it looks good. I have a few non-blocking questions.

Tree-SHA512: 8c386354cbd02f0701b5134991b65d206a54d19a2e78ab204e6ff1fa78a18f16299051bc0bf4ff4d2f5a0adab9b15658fa53cd0de2ca16969f4bf6a485225082
2023-04-28 18:49:36 +02:00
rajarshimaitra
b799a5728b [bdk_chain_redesign] Add tests for IndexedTxGraph with LocalChain
These tests cover list_txout, list_utxo and balance methods.
2023-04-28 19:18:50 +08:00
rajarshimaitra
8cd0328eec [bdk_chain_redesign] Implement OwnedIndexer for indexers
`SpkTxOutIndex` and `KeychainTxOutIndex` now both implement
`OwnedIndexer`.
2023-04-28 19:09:52 +08:00
rajarshimaitra
911af34f50 [bdk_chain_redesign] Fix calculation bugs.
* `IndexedTxGraph::try_balance` should include "confirmed and spendable"
  into `confirmed` balance.
* `TxGraph::try_list_chain_unspents` filter logic should be reversed.
2023-04-28 19:07:12 +08:00
志宇
e536307e5c [bdk_chain_redesign] Fix tx_graph::Additions::append logic
* `Additions` now implements `Append` and uses `Append` to implement
  `append()`.
* `append()` logic enforces that `last_seen` values should only
  increase.
* Test written for `append()` with `last_seen` behaviour.
2023-04-28 18:54:36 +08:00
benthecarman
217ea3321a Add sat_per_kwu to FeeRate 2023-04-27 09:31:32 -05:00
志宇
f101dde09b [bdk_chain_redesign] Fix tx_graph::Additions::append logic 2023-04-26 01:27:47 +08:00
志宇
1b152647c5 [bdk_chain_redesign] Change insert_relevant_txs method
Instead of forcing all transactions inserted to use the same anchors, we
change the API to have unique anchors per transaction.

This allows for more flexibility in general. For example, use `Anchor`
implementations that contain the position in a block of a transaction.
2023-04-23 00:21:31 +08:00
志宇
ecc74ce4cd [bdk_chain_redesign] Docs for is_mature and is_confirmed_and_spendable
Docs are updated to explain why `is_mature` and
`is_confirmed_and_spendable` may return false-negatives.
2023-04-22 23:39:49 +08:00
志宇
ac336aa32f [bdk_chain_redesign] Make insert_relevant_txs topologically-agnostic
The `insert_relevant_txs` test has also been changed to used
`KeychainTxOutIndex` so that index additions can be checked
(`SpkTxOutIndex` has no additions).

Additionally, generic bounds of some `IndexedTxGraph` list methods have
been fixed.
2023-04-22 23:09:39 +08:00
志宇
165b874dfe [bdk_chain_redesign] Add test for insert_relevant_txs
Ensure `insert_relevant_txs` does not require transactions to be in
topological order.

Other changes: Rm `try_list_owned_txs` as it is useless
2023-04-21 14:55:30 +08:00
志宇
f3e7b67bf1 [bdk_chain_redesign] Various tweaks and fixes
The `ConfirmationHeight` trait has been removed in favour of a second
method on the `Anchor` trait: `confirmation_height_upper_bound()`.

Methods `try_balance_at()` and `balance_at()` of `IndexedTxGraph` have
been removed as they do not provide additional functionality.

`IndexedTxGraph::insert_relevant_txs` now uses two loops, the first loop
indexes all transactions first. This is done as some indexes require
ancestor transactions to be indexed first and we cannot guarantee that
the input transactions are in topological order.
2023-04-21 13:29:44 +08:00
志宇
03c128311a [bdk_chain_redesign] Revert changes to SparseChain 2023-04-21 12:33:03 +08:00
志宇
34a7bf5afe [bdk_chain_redesign] Rm unnecessary code and premature optimisation
* Remove `chain_oracle::CacheBackend` for now as it is not used.
* `SparseChain` does not need to implement `ChainOracle`.
* Remove filter predicate for `list..` methods of `TxGraph` and
  `IndexedTxGraph` as this is premature optimisation.
* `Append` can be implemented for all `BTreeMap`s and `BTreeSet`s,
  instead of only `local_chain::ChangeSet`.
2023-04-20 18:07:26 +08:00
志宇
6c49570742 [bdk_chain_redesign] Rm HashSet from TxGraph::relevant_heights
The `HashSet` was used for iterating without duplicate items. However,
since `anchors` is a `BTreeSet`, heights are in order. So a single
variable tracking last height will be sufficient.
2023-04-20 15:56:28 +08:00
志宇
1003fe2ee6 [bdk_chain_redesign] Test LocalChain
This is mostly copying over the relevant tests from `SparseChain`.
Changes are made to `local_chain::ChangeSet` to re-add the ability to
remove blocks.
2023-04-20 15:29:20 +08:00
志宇
7175a82c04 [bdk_chain_redesign] Add tests for TxGraph::relevant_heights 2023-04-19 16:14:52 +08:00
志宇
8e36a2e5f6 [bdk_chain_redesign] Remove incomplete logic
`ObservedAs::ConfirmedImplicit` is incomplete, remove for now.

`local_chain::ChangeSet` does not need to be a single-element tuple
struct.
2023-04-19 12:21:39 +08:00
志宇
81436fcd72 [bdk_chain_redesign] Fix Anchor definition + docs
Previously, I have misunderstood the definition of anchor. If a tx is
anchored in a block, it does not necessarily mean it is confirmed in
that block. The tx can be confirmed in an ancestor block of the anchor
block.

With this new definition, we need a new trait `ConfirmationHeight` that
has one method `confirmation_height`. This trait can be used to extend
`Anchor` for those implementations that can give us the exact
conirmation height of a tx (which is useful in most cases).

Another change is to add another variant to the `ObservedAs` enum;
`ObservedAs::ConfirmedImplicit(A)`. If a tx does not have an anchor, but
another tx that spends it has an anchor that in in the best chain, we
can assume that tx is also in the best chain. The logic of
`TxGraph::try_get_chain_position` is also changed to reflect this.

Some methods from `IndexedTxGraph` have been moved to `TxGraph` as they
do not require the `Indexer`. Some `TxGraph` methods have been renamed
for clarity and consistency.

Also more docs are added.
2023-04-18 00:02:14 +08:00
Steve Myers
5e026cfd03 Merge bitcoindevkit/bdk#932: Fix policy condition calculation
ebd6103e65 Fix policy condition calculation (Alekos Filini)

Pull request description:

  ### Description

  When constructing the `Condition` struct we recursively call `get_condition` on all the items in a threshold and short-circuit if there's an error somewhere (for example, because the policy-path hasn't been provided for a specific threshold).

  This can cause issues when the user doesn't care about a subtree, because we still try to call `get_condition` on all the items and fail if something is missing, even if the specific subtree isn't selected and won't be used later on.

  This commit changes the logic so that we first filter only the `selected` items, and then unwrap the error using the question mark. If errors happened somewhere else they will be ignored, as they should.

  ### Notes to the reviewers

  I think it makes sense to backport this to `0.27`: even though it's not a critical issue (and there's a workaround[^1] for the bug) it may be a while before the new `1.0` is released. I wouldn't do a release just for this, but I would just leave it there and maybe in a few weeks if there are other fixes to be backported to pre-1.0 they could all be released.

  ### Changelog notice

  - Fixed a bug in the policy condition calculation

  ### 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
  * [x] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

  [^1]: The workaround is to simply set the items in the policy tree even if they won't be used. For example, if the item causing troubles is a `thresh(1, ...)` just set `[0]` in the policy path for that id.

ACKs for top commit:
  notmandatory:
    ACK ebd6103e65

Tree-SHA512: 1e0c6140e47ead3a518656cab660932b40f9e67425ce144f072267c30fd5cae5923ba943d9682af050c8f314756c522260abb68b104b9eeaae597319612984db
2023-04-14 22:47:04 -05:00
rajarshimaitra
001efdd1cb Include tests for new updates of TxGraph 2023-04-12 13:21:20 +08:00
志宇
10ab77c549 [bdk_chain_redesign] MOVE TxIndex into indexed_chain_graph.rs
`tx_graph.rs` is rearranged as well.
2023-04-12 11:24:05 +08:00
志宇
7d92337b93 [bdk_chain_redesign] Remove IndexedTxGraph::last_height
It is better to have this external to this structure.
2023-04-10 16:51:16 +08:00
志宇
a7fbe0ac67 [bdk_chain_redesign] Documentation improvements 2023-04-10 16:23:10 +08:00
志宇
ee1060f2ff [bdk_chain_redesign] Simplify LocalChain
Remove the requirement that evicted blocks should have in-best-chain
counterparts in the update.
2023-04-10 15:04:20 +08:00
志宇
611d2e3ea2 [bdk_chain_redesign] Consistent ChainOracle
The problem with the previous `ChainOracle` interface is that it had no
guarantee for consistency. For example, a block deemed to be part of the
"best chain" can be reorged out. So when `ChainOracle` is called
multiple times for an operation (such as getting the UTXO set), the
returned result may be inconsistent.

This PR changes `ChainOracle::is_block_in_chain` to take in another
input `static_block`, ensuring `block` is an ancestor of `static_block`.
Thus, if `static_block` is consistent across the operation, the result
will be consistent also.

`is_block_in_chain` now returns `Option<bool>`. The `None` case means
that the oracle implementation cannot determine whether block is an
ancestor of static block. `IndexedTxGraph::list_chain_txouts` handles
this case by checking child spends that are in chain, and if so, the
parent tx must be in chain too.
2023-04-10 13:03:51 +08:00
志宇
bff80ec378 [bdk_chain_redesign] Improve BlockAnchor docs 2023-04-07 09:23:00 +08:00
志宇
24cd8c5cc7 [bdk_chain_redesign] More tweaks and renamings 2023-04-05 19:13:42 +08:00
志宇
ddd5e951f5 [bdk_chain_redesign] Modify signature of TxIndex
This makes the API of `TxIndex` more consistent between scanning in data
and checking whether certain data is relevant.
2023-04-05 18:17:08 +08:00
志宇
da4cef044d [bdk_chain_redesign] Introduce Append trait for additions
Before, we were using `core::ops::AddAsign` but it was not the most
appropriate.
2023-04-05 17:29:20 +08:00
志宇
89cfa4d78e [bdk_chain_redesign] Better names, comments and generic bounds
* Instead of implementing `ChainPosition` for `ObservedIn<BlockId>` to
  use `FullTxOut` methods (`is_spendable_at` and `is_mature`), we create
  alternative versions of those methods that require bounds with `Anchor`.
  This removes all `ObservedIn<A>: ChainPosition` bounds for methods of
  `IndexedTxGraph`.

* Various improvements to comments and names.
2023-04-05 16:39:54 +08:00
志宇
6e59dce10b [bdk_chain_redesign] chain_oracle::Cache
Introduce `chain_oracle::Cache` which is a cache for requests to the
chain oracle. `ChainOracle` has also been moved to the `chain_oracle`
module.

Introduce `get_tip_in_best_chain` method to the `ChainOracle` trait.
This allows for guaranteeing that chain state can be consistent across
operations with `IndexedTxGraph`.
2023-04-05 10:57:26 +08:00
Alekos Filini
ebd6103e65 Fix policy condition calculation
When constructing the `Condition` struct we recursively call
`get_condition` on all the items in a threshold and short-circuit if
there's an error somewhere (for example, because the policy-path hasn't
been provided for a specific threshold).

This can cause issues when the user doesn't care about a subtree, because
we still try to call `get_condition` on all the items and fail if
something is missing, even if the specific subtree isn't selected and
won't be used later on.

This commit changes the logic so that we first filter only the `selected`
items, and then unwrap the error using the question mark. If errors
happened somewhere else they will be ignored, as it should.
2023-04-01 12:57:49 +02:00
志宇
a7eaebbb77 [bdk_chain_redesign] Add serde support for IndexedAdditions 2023-03-31 22:55:57 +08:00
志宇
c09cd2afce [bdk_chain_redesign] Added methods to LocalChain
Also made the `IndexedTxGraph::index` field public (`index()` and
`index_mut()` methods are no longer needed).
2023-03-31 22:42:47 +08:00
志宇
7810059ed0 [bdk_chain_redesign] TxGraph tweaks
* Rename `TxNode::last_seen` to `last_seen_unconfirmed` and improve docs
* Improve `try_get_chain_position` logic and tweak comments
2023-03-31 14:15:34 +08:00
志宇
a63ffe9739 [bdk_chain_redesign] Simplify TxIndex 2023-03-31 13:45:15 +08:00
志宇
a1172def7d [bdk_chain_redesign] Revert some API changes
Methods of old structures that return transaction(s) no longer return
`TxNode`, but `Transaction` as done previously.

`TxInGraph` is renamed to `TxNode`, while the internal `TxNode` is
renamed to `TxNodeInternal`.
2023-03-31 11:55:11 +08:00
志宇
8c906170c9 [bdk_chain_redesign] Make default anchor for TxGraph as ()
This allows us to use the old API with minimal changes. `TxGraph`
methods have also been rearranged to allow for it.
2023-03-30 18:14:44 +08:00
志宇
468701a129 [bdk_chain_redesign] Initial work on LocalChain. 2023-03-30 13:07:13 +08:00
志宇
34d0277e44 [bdk_chain_redesign] Rm anchor type param for structs that don't use it 2023-03-28 14:58:59 +08:00
志宇
3440a05711 [bdk_chain_redesign] Add docs 2023-03-28 10:58:23 +08:00
志宇
236c50fa7b [bdk_chain_redesign] IndexedTxGraph keeps track of the last synced height
This is important as a `ChainOracle` implementation is updated
separately to an `IndexedTxGraph`.
2023-03-27 22:42:39 +08:00
志宇
e902c10295 [bdk_chain_redesign] Fix apply_additions logic for IndexedTxGraph. 2023-03-27 21:51:11 +08:00
志宇
313965d8c8 [bdk_chain_redesign] mut_index should be index_mut 2023-03-27 20:56:42 +08:00
志宇
db7883d813 [bdk_chain_redesign] Add balance methods to IndexedTxGraph 2023-03-27 19:55:57 +08:00
志宇
d0a2aa83be [bdk_chain_redesign] Add apply_additions to IndexedTxGraph
* Get mutable index from `IndexedChainGraph`.
* Also add `apply_additions` method to `TxIndex` trait.
2023-03-27 16:02:21 +08:00
志宇
6cbb18d409 [bdk_chain_redesign] MOVE: IndexedTxGraph into submodule 2023-03-27 14:21:10 +08:00
志宇
784cd34e3d [bdk_chain_redesign] List chain data methods can be try/non-try
Methods that list chain data have try and non-try versions. Both of
these versions now return an `Iterator`.

* Try versions return `Iterator<Item = Result>`.
* Non-try versions require the `ChainOracle` implementation to be
  `ChainOracle<Error = Infallible>`.
2023-03-27 13:59:51 +08:00
志宇
43b648fee0 [bdk_chain_redesign] Add ..in_chain methods
Add methods to `TxGraph` and `IndexedTxGraph` that gets in-best-chain
data (such as transactions, txouts, unspent txouts).
2023-03-27 12:42:59 +08:00
志宇
61a8606fbc [bdk_chain_redesign] Introduce ChainOracle and TxIndex traits
The chain oracle keeps track of the best chain, while the transaction
index indexes transaction data in relation to script pubkeys.

This commit also includes initial work on `IndexedTxGraph`.
2023-03-26 11:03:35 +08:00
志宇
5ae5fe30eb [bdk_chain_redesign] Introduce BlockAnchor trait
* Introduce `GraphedTx` struct to access transaction data of graphed
  transactions.
* Ability to insert/access anchors and "seen at" values for graphed
  transactions.
* `Additions` now records changes to anchors and last_seen_at.
2023-03-24 12:11:41 +08:00
Steve Myers
5a090fac90 Add description to file_store cargo metadata 2023-03-20 12:48:25 -05:00
Steve Myers
f99eb32ac5 Remove keyword from file_store cargo metadata 2023-03-20 12:48:21 -05:00
Steve Myers
30c11904a7 Add bdk_chain README 2023-03-20 12:12:54 -05:00
Steve Myers
82f9caddab Bump bdk version to 1.0.0-alpha.0
chain to 0.4.0
electrum to 0.2.0
esplora to 0.2.0
file_store to 0.1.0
2023-03-19 23:03:48 -05:00
Steve Myers
3b68a7bcc0 For examples and nursery remove workspace dependency versions 2023-03-19 23:03:45 -05:00
志宇
919e74aa8d Merge pull request #890 from evanlinjin/simplify-reveal-to-target-logic
Simplify `reveal_to_target` logic
2023-03-18 10:28:57 +08:00
志宇
72b1e2a485 Simplify reveal_to_target logic
- Remove unnecessary check
- Better comments
- Better variable names
- Add test for scanning txouts with lookahead
2023-03-18 09:18:09 +08:00
志宇
2ae69ca10b Merge pull request #889 from evanlinjin/add-async-syncing-esplora
Add docs for `EsploraAsyncExt` and make doctests runnable
2023-03-17 17:20:08 +08:00
志宇
877b658787 Add docs for EsploraAsyncExt and make doctests runnable 2023-03-16 13:08:42 +08:00
志宇
82f5d9c81e Merge pull request #888 from rajarshimaitra/greamarly-fixes
Nit fixes on documentation
2023-03-15 16:07:05 +11:00
rajarshimaitra
24df03afd6 Add documentation fixes 2023-03-15 13:00:45 +08:00
Steve Myers
cd4945af3a Merge bitcoindevkit/bdk#793: bdk v1.0.0-alpha.0
bc3e05c6c6 Doc fixes (Daniela Brozzoni)
352f95f558 [ci] misc fixes (Daniela Brozzoni)
2fcf9c4adb Make async esplora futures Send (LLFourn)
5dd4ce74cf More documentation improvements (志宇)
ae9b19d84c `bdk` README improvements (志宇)
def0c9ed39 Add `wallet_esplora_async` example and various fixes (志宇)
26ab2e2d6c Implement EsploraExt for Async client (Vladimir Fomene)
ab9242d10d Replace current caching solution with Rust Cache (Vladimir Fomene)
0aaf420f6d examples: exit if balance < SEND_AMOUNT (Daniela Brozzoni)
47faa881fb Fix cargo clippy (Daniela Brozzoni)
9d26121dbc Improve stdout output for wallet esplora/electrum examples (志宇)
eddd748870 Add `wallet_electrum` example (志宇)
0505cd7242 Remove transaction-based type parameters and traits (志宇)
de9457fce6 Changed `inflate_update` logic to not depend on `Cow` (志宇)
69cf6d7924 Use os-specific temp dir for `wallet_esplora` example (志宇)
b3836cb308 Use hardcoded send amount for `wallet_esplora` example (志宇)
b082932268 Add the wallet_esplora example (Daniela Brozzoni)
d267517dbd NewError implements StdError (fix typo) (Daniela Brozzoni)
0c7a0abb19 Disable code coverage (Daniela Brozzoni)
dfcbafd6b1 Use action-rs/toolchain in CI (Daniela Brozzoni)
0ba41c5751 Make bdk_esplora wasm compatible again (Daniela Brozzoni)
a38f63359d Make bdk_file_store use bincode v1 (LLFourn)
38ef170ed1 Make bdk and bdk_chain work under 1.57.0 (Steve Myers)
3a5d727899 Update workspace dependencies to be relative paths (Steve Myers)
96d932c830 Add clippy.toml (Daniela Brozzoni)
5708bf0c8c Fix docs (Daniela Brozzoni)
5acee82496 Update rust stable in CI (1.67.0) (Daniela Brozzoni)
8c9bcebc71 Fix the "repository" field in Cargo.toml (Daniela Brozzoni)
c61b3604e1 Fix cargo clippy warnings (Daniela Brozzoni)
1805bd35c0 Fix clippy (Daniela Brozzoni)
3f5a78ae3b Disable test-hardware-signer (Daniela Brozzoni)
303a1703c9 Rust fmt (Daniela Brozzoni)
b5559767db Rename the stub wallet examples (LLFourn)
2e82cd8c04 Use tempfile for file_store tests (LLFourn)
c069b0fb41 Move everything else over 🎉 (LLFourn)
949608ab1f Move bdk_electrum into the bdk repo 🎉 (Daniela Brozzoni)
03deafb553 Move bdk_file_store into the bdk repo 🎉 (Daniela Brozzoni)
37dfa77d9d Move bdk_chain into the bdk repo 🎉 (Daniela Brozzoni)
f2188f9dcd Make lib.rs's docs be the README.md (LLFourn)
1c970a9295 Fix code coverage CI (Daniela Brozzoni)
94a084aafd Fix doc links (LLFourn)
9edbdf54c9 [ci] Fix feature flags (LLFourn)
20e45b7af0 Add back test-hardware-signer feature (LLFourn)
6d05598407 Remove test-readme-examples (LLFourn)
b60820a7b5 Remove authors.workspace because older cargo don't like (LLFourn)
22bec6d363 Delete unused errors (LLFourn)
8a6de3aa2d Convert to workspace (LLFourn)
fdfc9b9ede Delete unused things (LLFourn)
e1eb0253cf Make doctests work (LLFourn)
3baf9721ec Use bdk_chain Balance (LLFourn)
b310a7afdd Add Wallet::cancel_tx (LLFourn)
5985706c1a Add wallet persistence (LLFourn)
57538e53e4 Move tests to /tests (LLFourn)
a40da9ba6c Make bdk no_std (LLFourn)
aab2b12f7a bdk_core integration initial commit 🔥 (LLFourn)
544c397a38 Modified build, clippy and test steps (志宇)

Pull request description:

  We prepare the BDK repo for a major restructuring 🔥. This PR maintains the existing wallet API as much as possible and adds very little.

  ## Things Done

  - database modules removed
  - blockchain gutted but new esplora syncing code added (this will be gone soon hopefully).
  - minimal API changes.
  - Many macros removed.
  - no longer applicable examples removed.
  - Much conditional compilation removed. Can compile with `--all-features` now.
  - All wallet tests passing
  - TestClient moved into its own repo
  - Example using `esplora`

  ## APIs changed

  - wallet no longer has a `sync` method. This is replaced with `apply_wallet_scan`.
  - address "caching" is gone. You can just change the derivation index with `ensure_derived_up_to` which sets your derivation to at least the argument. Unlike `ensure_addresses_cached` used to do this will alter what getting a new address gives you.
  - `AddressIndex::Reset` is gone. This thing didn't make much sense and is hard to do with the more sane internals we've established. Changing the derivation index changes what script pubkeys the wallet will search so this is dangerous. We plan to add method like `trim_unused` which lowers the derivation index to the highest unused index. Applications must handle giving out old addresses manually now (which I think is good).

  ## Unfinished work

  - [x] esplora example doesn't work for mempool transactions yet (seems like our esplora in testclient doesn't index mempool??).
  - [x] we need to figure out a way to retrieve and store transaction timestamps (we're currently just setting them to `u64::MAX`). In `bdk_core` we never got around to doing this but it needs to be done.
  - [x] A few insights we got from doing this PR should be applied to bdk_core first.
  - [x] doctests not working.

  ### Notes to the reviewers

  Try not to review the actual changes. This PR will be forced pushed a bit so it will be likely wasted.
  I think I did a faithful job of translating the tests. A bit of review here would be helpful.

  I *do* think it would be good to merge this PR soon into the v1 branch so we have something to work off once unfinished work is done.
  Checking out the branch and poke around and give feedback would be the most helpful thing.

  Run the (sort of) working example:

  ```
  cargo run --example esplora --features="bdk_test_client/bitcoind_22_0 bdk_test_client/esplora esplora"
  ```
  ### 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
  * [ ] I'm linking the issue being fixed by this PR (there's too many!)

ACKs for top commit:
  danielabrozzoni:
    ACK bc3e05c6c6

Tree-SHA512: b5fbfd2263bb2a7cbf7b486530e639683775209bea944b266efd9c0447fb632295f7f5ddbd2e8d79796338415eaca26f1876617c2b66523485167b22e75e449e
2023-03-14 12:48:51 -05:00
Daniela Brozzoni
bc3e05c6c6 Doc fixes 2023-03-13 22:23:40 +01:00
Daniela Brozzoni
352f95f558 [ci] misc fixes
LLFourn is squashing these to get them all signed:

Remove useless clippy allow

ci: use clippy action

[ci] remove check for features=default
2023-03-13 14:32:13 +11:00
LLFourn
2fcf9c4adb Make async esplora futures Send 2023-03-10 12:19:54 +11:00
志宇
5dd4ce74cf More documentation improvements 2023-03-10 13:40:27 +13:00
志宇
ae9b19d84c bdk README improvements 2023-03-10 10:29:12 +13:00
志宇
def0c9ed39 Add wallet_esplora_async example and various fixes
Fixes include:
* Allow `bdk_esplora` to use async with tls
* Reorganize `bdk_esplora` crate to have separate files for
  async vs blocking
* Use optional dependencies for `bdk_esplora` async
2023-03-09 11:07:45 +13:00
Vladimir Fomene
26ab2e2d6c Implement EsploraExt for Async client
Creates a separate async EsploraAsyncExt trait for the
async client using async-trait crate. It has thesame
methods as the EsploraExt trait for the blocking client.
This trait is implemented on the AsyncClient of the
rust-esplora-client crate.
2023-03-09 09:25:54 +13:00
Vladimir Fomene
ab9242d10d Replace current caching solution with Rust Cache 2023-03-08 15:42:45 +01:00
Daniela Brozzoni
0aaf420f6d examples: exit if balance < SEND_AMOUNT 2023-03-08 15:41:49 +01:00
Daniela Brozzoni
47faa881fb Fix cargo clippy 2023-03-08 15:20:20 +01:00
志宇
9d26121dbc Improve stdout output for wallet esplora/electrum examples 2023-03-08 15:28:56 +13:00
志宇
eddd748870 Add wallet_electrum example 2023-03-08 12:01:01 +13:00
志宇
0505cd7242 Remove transaction-based type parameters and traits 2023-03-08 11:39:25 +13:00
志宇
de9457fce6 Changed inflate_update logic to not depend on Cow
As mentioned by @LLFourn:

1. We have a "sparse chain" from which there is a subset of txids M that are missing from graph.
2. There is also another subset C that are in the graph but their positions have changed.
3. We used the Cow to avoid copying/duplicating in memory transactions in subset C and M

Instead in inflate_update we could remove transactions in subset M and just clone data in subset C (which is usually tiny).
2023-03-08 01:53:09 +13:00
志宇
69cf6d7924 Use os-specific temp dir for wallet_esplora example 2023-03-07 10:59:13 +13:00
志宇
b3836cb308 Use hardcoded send amount for wallet_esplora example 2023-03-07 10:47:13 +13:00
Daniela Brozzoni
b082932268 Add the wallet_esplora example 2023-03-03 17:55:23 +01:00
Daniela Brozzoni
d267517dbd NewError implements StdError (fix typo) 2023-03-03 17:55:23 +01:00
Daniela Brozzoni
0c7a0abb19 Disable code coverage
To be enabled again soon ™️
2023-03-03 17:55:22 +01:00
Daniela Brozzoni
dfcbafd6b1 Use action-rs/toolchain in CI 2023-03-03 17:44:11 +01:00
Daniela Brozzoni
0ba41c5751 Make bdk_esplora wasm compatible again
There's a huge todo!() for the AsyncClient
2023-03-03 17:44:10 +01:00
LLFourn
a38f63359d Make bdk_file_store use bincode v1 2023-03-03 17:44:10 +01:00
Steve Myers
38ef170ed1 Make bdk and bdk_chain work under 1.57.0
- rewrite some parts of the code to deal with older borrow checker
- downgraded hashbrown
2023-03-03 17:44:09 +01:00
Steve Myers
3a5d727899 Update workspace dependencies to be relative paths 2023-03-03 17:44:08 +01:00
Daniela Brozzoni
96d932c830 Add clippy.toml
Explicitly sets the project MSRV so that clippy won't give warnings
for feature added after our MSRV
2023-03-03 17:44:07 +01:00
Daniela Brozzoni
5708bf0c8c Fix docs 2023-03-03 17:44:06 +01:00
Daniela Brozzoni
5acee82496 Update rust stable in CI (1.67.0) 2023-03-03 17:44:05 +01:00
Daniela Brozzoni
8c9bcebc71 Fix the "repository" field in Cargo.toml 2023-03-03 17:44:04 +01:00
Daniela Brozzoni
c61b3604e1 Fix cargo clippy warnings
Disabled warnings for nursery/tmp_plan as it's going to be replaced
anyways
2023-03-03 17:44:03 +01:00
Daniela Brozzoni
1805bd35c0 Fix clippy 2023-03-03 17:44:02 +01:00
Daniela Brozzoni
3f5a78ae3b Disable test-hardware-signer
I suspect the latest version of HWI just broke everything
2023-03-03 17:44:01 +01:00
Daniela Brozzoni
303a1703c9 Rust fmt 2023-03-02 12:27:52 +01:00
LLFourn
b5559767db Rename the stub wallet examples 2023-03-02 10:56:38 +01:00
LLFourn
2e82cd8c04 Use tempfile for file_store tests 2023-03-02 10:56:37 +01:00
LLFourn
c069b0fb41 Move everything else over 🎉
This completes the move of things from https://github.com/LLFourn/bdk_core_staging
2023-03-02 10:56:36 +01:00
Daniela Brozzoni
949608ab1f Move bdk_electrum into the bdk repo 🎉
Original repository: 250b4f1dcc/bdk_electrum

Co-authored-by: 志宇 <hello@evanlinjin.me>
Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
2023-03-02 10:56:35 +01:00
Daniela Brozzoni
03deafb553 Move bdk_file_store into the bdk repo 🎉
Original repository: 250b4f1dcc/bdk_file_store

Co-authored-by: 志宇 <hello@evanlinjin.me>
Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
2023-03-02 10:56:34 +01:00
Daniela Brozzoni
37dfa77d9d Move bdk_chain into the bdk repo 🎉
Original repository: 250b4f1dcc/bdk_chain

Co-authored-by: Steve Myers <steve@notmandatory.org>
Co-authored-by: 志宇 <hello@evanlinjin.me>
Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
Co-authored-by: rajarshimaitra <rajarshi149@gmail.com>
Co-authored-by: LagginTimes <wzc110@gmail.com>
Co-authored-by: Steve Myers <steve@notmandatory.org>
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
2023-03-02 10:56:33 +01:00
LLFourn
f2188f9dcd Make lib.rs's docs be the README.md
Also flesh out README a bit
2023-03-02 10:56:32 +01:00
Daniela Brozzoni
1c970a9295 Fix code coverage CI
If you want to have tests on the hardware-signer you need to install
the python libraries and start the emulator
2023-03-02 10:56:31 +01:00
LLFourn
94a084aafd Fix doc links 2023-03-02 10:56:30 +01:00
LLFourn
9edbdf54c9 [ci] Fix feature flags 2023-03-02 10:56:29 +01:00
LLFourn
20e45b7af0 Add back test-hardware-signer feature 2023-03-02 10:56:28 +01:00
LLFourn
6d05598407 Remove test-readme-examples
we shouldn't have a feature flag for this it should always be done.
2023-03-02 10:56:27 +01:00
LLFourn
b60820a7b5 Remove authors.workspace because older cargo don't like 2023-03-02 10:56:26 +01:00
LLFourn
22bec6d363 Delete unused errors 2023-03-02 10:56:25 +01:00
LLFourn
8a6de3aa2d Convert to workspace 2023-03-02 10:56:22 +01:00
LLFourn
fdfc9b9ede Delete unused things 2023-03-02 10:55:15 +01:00
LLFourn
e1eb0253cf Make doctests work 2023-03-02 10:55:14 +01:00
LLFourn
3baf9721ec Use bdk_chain Balance
instead of bdk's one
2023-03-02 10:55:13 +01:00
LLFourn
b310a7afdd Add Wallet::cancel_tx
To allow you to re-use change addresses from transactions that get cancelled.
2023-03-02 10:55:12 +01:00
LLFourn
5985706c1a Add wallet persistence 2023-03-02 10:55:11 +01:00
LLFourn
57538e53e4 Move tests to /tests
To stop my rust-anlayzer from killing me
2023-03-02 10:55:10 +01:00
LLFourn
a40da9ba6c Make bdk no_std 2023-03-02 10:55:09 +01:00
LLFourn
aab2b12f7a bdk_core integration initial commit 🔥
We prepare the BDK repo for a major restructuring.

- database modules removed
- blockchain module removed
- minimal API changes.
- Many macros removed.
- no longer applicable examples removed.
- Much conditional compilation removed. Can compile with --all-features.
- delete verify module
2023-03-02 10:55:07 +01:00
志宇
544c397a38 Modified build, clippy and test steps
Co-authored-by: Daniela Brozzoni <danielabrozzoni@protonmail.com>
2023-03-02 10:53:51 +01:00
Daniela Brozzoni
ced2d05e64 Merge bitcoindevkit/bdk#857: Check results in the DB tests
a1a70a5011 Check results in the DB tests (Silvestrs Timofejevs)

Pull request description:

  ### Description

  Checksum test was giving a false positive due to ignoring the return value.

  Other tests were giving assertion fail later in the test, rather than an error on failed method call, thus making it more challenging to debug the issue.

  These have been discovered, when implementing PostgreSQL backend in a downstream fork.

  ### Checklists

  #### All Submissions:

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

ACKs for top commit:
  danielabrozzoni:
    ACK a1a70a5011

Tree-SHA512: d3542cdd9f5f76f8318910261dbeb9cdcc9d4f26e9852e7aad7419c4abdf269d030b72bb21ee34a9dd314d2fa8433f1a6a9f1338a62699e5d8b6764c03067904
2023-02-24 11:22:59 +01:00
Steve Myers
843807b08f Merge bitcoindevkit/bdk#869: Bump version to 0.27.1
231a1fba61 Bump version to 0.27.1 (Steve Myers)

Pull request description:

  ### Description

  Bump dev version to 0.27.1.

  ### Notes to the reviewers

  This is in preparation for making a patch release and will be cherry picked to the release/0.27 branch. See #868.

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

Top commit has no ACKs.

Tree-SHA512: 7c791d221d95fb800c19b85c110a47f462b1c9ea7991dc1cf9bc5bb17d12b2dea9d1823d7f928ba06f09aaac36b1140610a29454857257f918124e0e0893fc4e
2023-02-16 11:49:58 -06:00
Steve Myers
231a1fba61 Bump version to 0.27.1 2023-02-16 11:22:20 -06:00
Steve Myers
74119e70c3 Merge bitcoindevkit/bdk#867: Update rusqlite from 0.27.0 to 0.28.0
8b2943c49b Update rusqlite from 0.27.0 to 0.28.0 (Steve Myers)

Pull request description:

  ### Description

  Fix #866 by updating `rusqlite` dependency version from `0.27.0` to `0.28.0` to fix [RUSTSEC-2022-0090](https://rustsec.org/advisories/RUSTSEC-2022-0090).

  ### Notes to the reviewers

  This will also need to be cherry-picked to the `release/0.27` branch to create a new `0.27.1` release.

  ### Changelog notice

  Changed

  * Update rusqlite version from 0.27.0 to 0.28.0 to fix [RUSTSEC-2022-0090](https://rustsec.org/advisories/RUSTSEC-2022-0090).

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

Tree-SHA512: 0633e0523805895efb0e4c21671e6018d440b93f0b8119e8d553a2fac1f089b36addb2bf82fdf370674887e1b3efc09dc4c461d85043c469aeb41077488384a5
2023-02-15 14:43:23 -06:00
Steve Myers
8b2943c49b Update rusqlite from 0.27.0 to 0.28.0 2023-02-14 19:05:06 -06:00
Silvestrs Timofejevs
a1a70a5011 Check results in the DB tests
Checksum test was giving a false positive due to ignoring the return
value.

Other tests were giving assertion fail later in the test, rather than
an error on failed method call, thus making it more challenging to
debug the issue.

These have been discovered, when implementing PostgreSQL backend in
a downstream fork.
2023-02-14 07:17:52 +00:00
Daniela Brozzoni
2d173a17f7 Merge bitcoindevkit/bdk#863: Fix ci Dockerfile.ledger
5b9e0e392a Fix ci Dockerfile.ledger (Steve Myers)

Pull request description:

  ### Description

  Ledger emulator used to default to `-model nanos` but in latest release we need to add it to the command line. This change fixes `test_hardware_wallet` CI tests.

  ### Notes to the reviewers

  I originally made this fix in the `release/0.27` branch, this PR cherry-picks the change back to the `master` branch. I should have done a `master` branch PR first but it's only a CI change and I wanted to get the `0.27.0` release out.

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

ACKs for top commit:
  danielabrozzoni:
    tACK 5b9e0e392a

Tree-SHA512: 552586dc614d11c4084b73ad865078c9e28a1bdd3eda9addc8055df6408bba5f110f8aa7b3da730949f41cfd48f87b12cda0ba2df2b98e40e1590aa99b54a7ee
2023-02-13 23:23:00 +01:00
Steve Myers
5b9e0e392a Fix ci Dockerfile.ledger 2023-02-13 11:38:26 -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
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
137 changed files with 19505 additions and 19300 deletions

View File

@@ -34,6 +34,7 @@ Change the `master` branch to the next MINOR+1 version:
- [ ] 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".

View File

@@ -34,6 +34,7 @@ Change the `master` branch to the new PATCH+1 version:
- [ ] 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".

View File

@@ -9,47 +9,42 @@ jobs:
env:
RUSTFLAGS: "-Cinstrument-coverage"
RUSTDOCFLAGS: "-Cinstrument-coverage"
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
LLVM_PROFILE_FILE: "./target/coverage/%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: Cache cargo
uses: actions/cache@v3
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
toolchain: "1.65.0"
override: true
profile: minimal
components: llvm-tools-preview
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
- 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: 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
run: cargo test --all-features
- name: Make coverage directory
run: mkdir coverage
- name: Run grcov
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
run: grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --keep-only '**/crates/**' --ignore '**/tests/**' --ignore '**/examples/**' -o ./coverage/lcov.info
- name: Generate HTML coverage report
run: genhtml -o coverage-report.html ./coverage/lcov.info
run: genhtml -o coverage-report.html --ignore-errors source ./coverage/lcov.info
- name: Coveralls upload
uses: coverallsapp/github-action@master
with:

View File

@@ -10,118 +10,55 @@ jobs:
strategy:
matrix:
rust:
- version: 1.60.0 # STABLE
- version: stable
clippy: true
- version: 1.56.1 # MSRV
- version: 1.57.0 # MSRV
features:
- default
- minimal
- all-keys
- minimal,use-esplora-blocking
- key-value-db
- electrum
- compact_filters
- use-esplora-blocking,key-value-db,electrum
- compiler
- rpc
- verify
- async-interface
- use-esplora-async
- sqlite
- sqlite-bundled
- --no-default-features
- --all-features
steps:
- name: checkout
uses: actions/checkout@v2
- name: Generate cache key
run: echo "${{ matrix.rust.version }} ${{ matrix.features }}" | tee .cache_key
- name: cache
uses: actions/cache@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain
run: rustup default ${{ matrix.rust.version }}
- name: Set profile
run: rustup set profile minimal
- name: Add clippy
if: ${{ matrix.rust.clippy }}
run: rustup component add clippy
- name: Update toolchain
run: rustup update
toolchain: ${{ matrix.rust.version }}
override: true
profile: minimal
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Build
run: cargo build --features ${{ matrix.features }} --no-default-features
- name: Clippy
if: ${{ matrix.rust.clippy }}
run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
run: cargo build ${{ matrix.features }}
- name: Test
run: cargo test --features ${{ matrix.features }} --no-default-features
run: cargo test ${{ matrix.features }}
test-readme-examples:
name: Test README.md examples
check-no-std:
name: Check no_std
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-md-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
test-blockchains:
name: Blockchain ${{ matrix.blockchain.features }}
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
blockchain:
- name: electrum
testprefix: blockchain::electrum::test
features: test-electrum,verify
- name: rpc
testprefix: blockchain::rpc::test
features: test-rpc
- name: rpc-legacy
testprefix: blockchain::rpc::test
features: test-rpc-legacy
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-async,verify
- name: esplora
testprefix: esplora
features: test-esplora,use-esplora-blocking,verify
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Setup rust toolchain
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Test
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.testprefix }}::bdk_blockchain_tests
profile: minimal
# target: "thumbv6m-none-eabi"
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk_chain
working-directory: ./crates/chain
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown
- name: Check bdk
working-directory: ./crates/bdk
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
- name: Check esplora
working-directory: ./crates/esplora
# TODO "--target thumbv6m-none-eabi" should work but currently does not
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
check-wasm:
name: Check WASM
@@ -132,29 +69,26 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
# Install a recent version of clang that supports wasm32
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
- run: sudo apt-add-repository "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-10 main" || exit 1
- 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
- name: Set profile
run: rustup set profile minimal
- name: Add target wasm32
run: rustup target add wasm32-unknown-unknown
- name: Update toolchain
run: rustup update
- name: Check
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
target: "wasm32-unknown-unknown"
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk
working-directory: ./crates/bdk
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
- name: Check esplora
working-directory: ./crates/esplora
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,async
fmt:
name: Rust fmt
@@ -162,42 +96,30 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set default toolchain
run: rustup default nightly
- name: Set profile
run: rustup set profile minimal
- name: Add rustfmt
run: rustup component add rustfmt
- name: Update toolchain
run: rustup update
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
components: rustfmt
- name: Check fmt
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
test_harware_wallet:
clippy_check:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- version: 1.60.0 # STABLE
- version: 1.56.1 # MSRV
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build simulator image
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
- name: Run simulator image
run: docker run --name simulator --network=host hwi/ledger_emulator &
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install python dependencies
run: pip install hwi==2.1.1 protobuf==3.20.1
- name: Set default toolchain
run: rustup default ${{ matrix.rust.version }}
- name: Set profile
run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features test-hardware-signer
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
# we pin clippy instead of using "stable" so that our CI doesn't break
# at each new cargo release
toolchain: "1.67.0"
components: clippy
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --all-targets -- -D warnings

View File

@@ -9,22 +9,18 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
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: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Build docs
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
run: cargo doc --no-deps
env:
RUSTDOCFLAGS: '--cfg docsrs -Dwarnings'
- name: Upload artifact
uses: actions/upload-artifact@v2
with:

View File

@@ -1,13 +1,155 @@
# Changelog
All notable changes to this project prior to release **0.22.0** are documented in this file. Future
changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk/releases)
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
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).
## [v0.21.0] - [v0.20.0]
## [Unreleased]
## [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.
@@ -20,7 +162,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New `RpcBlockchain` implementation with various fixes.
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
## [v0.20.0] - [v0.19.0]
## [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)
@@ -31,7 +173,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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`
@@ -47,7 +189,7 @@ 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, i.e. for mobile platforms.
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
@@ -57,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
- 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`.
@@ -78,45 +220,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`.
@@ -130,21 +272,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
@@ -159,7 +301,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
@@ -183,13 +325,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
@@ -199,7 +341,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
@@ -228,7 +370,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
@@ -265,7 +407,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
@@ -493,3 +635,11 @@ final transaction is created by calling `finish` on the builder.
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
[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
[Unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.27.1...HEAD

View File

@@ -28,7 +28,7 @@ The codebase is maintained using the "contributor workflow" where everyone
without exception contributes patch proposals using "pull requests". This
facilitates social contribution, easy testing and peer review.
To contribute a patch, the worflow is a as follows:
To contribute a patch, the workflow is as follows:
1. Fork Repository
2. Create topic branch

View File

@@ -1,146 +1,18 @@
[package]
name = "bdk"
version = "0.24.0"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library"
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
readme = "README.md"
license = "MIT OR Apache-2.0"
[dependencies]
bdk-macros = "^0.6"
log = "^0.4"
miniscript = { version = "8.0", features = ["serde"] }
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.8"
# Optional dependencies
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.12", optional = true }
esplora-client = { version = "0.2", default-features = false, optional = true }
rusqlite = { version = "0.27.0", optional = true }
ahash = { version = "0.7.6", 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 }
hwi = { version = "0.3.0", optional = true }
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.16", optional = true }
# Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
async-trait = "0.1"
js-sys = "0.3"
[features]
minimal = []
compiler = ["miniscript/compiler"]
verify = ["bitcoinconsensus"]
default = ["key-value-db", "electrum"]
sqlite = ["rusqlite", "ahash"]
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
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
# blocking, depending on the HTTP client in use.
#
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
# access to the asynchronous method implementations. Then, if Esplora is wanted,
# enable 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 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-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-async` to enable reqwest default TLS support
reqwest-default-tls = ["esplora-client/async-https"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
test-md-docs = ["electrum"]
test-hardware-signer = ["hardware-signer"]
# 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]
lazy_static = "1.4"
env_logger = "0.7"
electrsd = "0.21"
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
base64 = "^0.13"
[[example]]
name = "compact_filters_balance"
required-features = ["compact_filters"]
[[example]]
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", "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"]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]
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"]
members = [
"crates/bdk",
"crates/chain",
"crates/file_store",
"crates/electrum",
"crates/esplora",
"example-crates/example_cli",
"example-crates/example_electrum",
"example-crates/wallet_electrum",
"example-crates/wallet_esplora",
"example-crates/wallet_esplora_async",
"nursery/tmp_plan",
"nursery/coin_select"
]
[workspace.package]
authors = ["Bitcoin Dev Kit Developers"]

188
README.md
View File

@@ -1,3 +1,5 @@
# The Bitcoin Dev Kit
<div align="center">
<h1>BDK</h1>
@@ -13,7 +15,7 @@
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
<a href="https://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>
@@ -26,175 +28,27 @@
## About
The `bdk` library aims to be the core building block for Bitcoin wallets of any kind.
The `bdk` libraries aims to provide well engineered and reviewed components for Bitcoin based applications.
It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
* It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build
single-sig wallets, multisigs, timelocked contracts and more.
* It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects.
* It's built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly.
* It's very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library.
> ⚠ The Bitcoin Dev Kit developers are in the process of releasing a `v1.0` which is a fundamental re-write of how the library works.
> See for some background on this project: https://bitcoindevkit.org/blog/road-to-bdk-1/ (ignore the timeline 😁)
> For a release timeline see the [`bdk_core_staging`] repo where a lot of the component work is being done. The plan is that everything in the `bdk_core_staging` repo will be moved into the `crates` directory here.
## Examples
## Architecture
### Sync the balance of a descriptor
The project is split up into several crates in the `/crates` directory:
```rust,no_run
use bdk::Wallet;
use bdk::database::MemoryDatabase;
use bdk::blockchain::ElectrumBlockchain;
use bdk::SyncOptions;
use bdk::electrum_client::Client;
use bdk::bitcoin::Network;
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
- [`chain`](./crates/chain): Tools for storing and indexing chain data
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
- [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume.
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
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/*)"),
Network::Testnet,
MemoryDatabase::default(),
)?;
Fully working examples of how to use these components are in `/example-crates`
wallet.sync(&blockchain, SyncOptions::default())?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
Ok(())
}
```
### Generate a few addresses
```rust
use bdk::{Wallet, database::MemoryDatabase};
use bdk::wallet::AddressIndex::New;
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,
MemoryDatabase::default(),
)?;
println!("Address #0: {}", wallet.get_address(New)?);
println!("Address #1: {}", wallet.get_address(New)?);
println!("Address #2: {}", wallet.get_address(New)?);
Ok(())
}
```
### Create a transaction
```rust,no_run
use bdk::{FeeRate, Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New;
use base64;
use bitcoin::consensus::serialize;
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,
MemoryDatabase::default(),
)?;
wallet.sync(&blockchain, SyncOptions::default())?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
let mut builder = wallet.build_tx();
builder
.add_recipient(send_to.script_pubkey(), 50_000)
.enable_rbf()
.do_not_spend_change()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
builder.finish()?
};
println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
Ok(())
}
```
### Sign a transaction
```rust,no_run
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use base64;
use bitcoin::consensus::deserialize;
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,
MemoryDatabase::default(),
)?;
let psbt = "...";
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
Ok(())
}
```
## Testing
### Unit testing
```bash
cargo test
```
### Integration testing
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
* Apache License, Version 2.0
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
[`bdk_core_staging`]: https://github.com/LLFourn/bdk_core_staging
[`rust-miniscript`]: https://github.com/rust-bitcoin/rust-miniscript
[`rust-bitcoin`]: https://github.com/rust-bitcoin/rust-bitcoin
[`esplora-client`]: https://docs.rs/esplora-client/0.3.0/esplora_client/
[`electrum-client`]: https://docs.rs/electrum-client/0.13.0/electrum_client/

View File

@@ -6,4 +6,4 @@ RUN apt-get install wget -y
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf
ADD automation.json /speculos/automation.json
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanos", "--display", "headless", "--vnc-port", "41000", "btc.elf"]

1
clippy.toml Normal file
View File

@@ -0,0 +1 @@
msrv="1.57.0"

65
crates/bdk/Cargo.toml Normal file
View File

@@ -0,0 +1,65 @@
[package]
name = "bdk"
homepage = "https://bitcoindevkit.org"
version = "1.0.0-alpha.1"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library"
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
readme = "README.md"
license = "MIT OR Apache-2.0"
authors = ["Bitcoin Dev Kit Developers"]
edition = "2021"
rust-version = "1.57"
[dependencies]
log = "=0.4.18"
rand = "^0.8"
miniscript = { version = "9", features = ["serde"], default-features = false }
bitcoin = { version = "0.29", features = ["serde", "base64", "rand"], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.5.0", features = ["miniscript", "serde"], default-features = false }
# Optional dependencies
hwi = { version = "0.5", optional = true, features = [ "use-miniscript"] }
bip39 = { version = "1.0.1", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
js-sys = "0.3"
[features]
default = ["std"]
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
hardware-signer = ["hwi"]
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]
lazy_static = "1.4"
env_logger = "0.7"
# 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"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "mnemonic_to_descriptors"
path = "examples/mnemonic_to_descriptors.rs"
required-features = ["all-keys"]
[[example]]
name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]

227
crates/bdk/README.md Normal file
View File

@@ -0,0 +1,227 @@
<div align="center">
<h1>BDK</h1>
<img src="https://raw.githubusercontent.com/bitcoindevkit/bdk/master/static/bdk.png" width="220" />
<p>
<strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
</p>
<p>
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://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://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/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>
<h4>
<a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span>
<a href="https://docs.rs/bdk">Documentation</a>
</h4>
</div>
## `bdk`
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
for many simple applications as well as a good demonstration of how to use the other mechanisms to
construct a wallet. It has two keychains (external and internal) which are defined by
[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
can create and sign transactions.
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
### Blockchain data
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
This can be created manually or from blockchain-scanning crates.
**Blockchain Data Sources**
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
**Examples**
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
### Persistence
To persist the `Wallet` on disk, `Wallet` needs to be constructed with a
[`Persist`](https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainPersist.html) implementation.
**Implementations**
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
**Example**
```rust
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
fn main() {
// a type that implements `Persist`
let db = ();
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
// get a new address (this increments revealed derivation index)
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
println!("staged changes: {:?}", wallet.staged());
// persist changes
wallet.commit().expect("must save");
}
```
<!-- ### Sync the balance of a descriptor -->
<!-- ```rust,no_run -->
<!-- use bdk::Wallet; -->
<!-- use bdk::blockchain::ElectrumBlockchain; -->
<!-- use bdk::SyncOptions; -->
<!-- use bdk::electrum_client::Client; -->
<!-- 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/*)"), -->
<!-- Network::Testnet, -->
<!-- )?; -->
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
<!-- Ok(()) -->
<!-- } -->
<!-- ``` -->
<!-- ### Generate a few addresses -->
<!-- ```rust -->
<!-- use bdk::Wallet; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
<!-- Network::Testnet, -->
<!-- )?; -->
<!-- println!("Address #0: {}", wallet.get_address(New)); -->
<!-- println!("Address #1: {}", wallet.get_address(New)); -->
<!-- println!("Address #2: {}", wallet.get_address(New)); -->
<!-- Ok(()) -->
<!-- } -->
<!-- ``` -->
<!-- ### Create a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
<!-- use bdk::blockchain::ElectrumBlockchain; -->
<!-- use bdk::electrum_client::Client; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- 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_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
<!-- Network::Testnet, -->
<!-- )?; -->
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
<!-- let send_to = wallet.get_address(New); -->
<!-- let (psbt, details) = { -->
<!-- let mut builder = wallet.build_tx(); -->
<!-- builder -->
<!-- .add_recipient(send_to.script_pubkey(), 50_000) -->
<!-- .enable_rbf() -->
<!-- .do_not_spend_change() -->
<!-- .fee_rate(FeeRate::from_sat_per_vb(5.0)); -->
<!-- builder.finish()? -->
<!-- }; -->
<!-- println!("Transaction details: {:#?}", details); -->
<!-- println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); -->
<!-- Ok(()) -->
<!-- } -->
<!-- ``` -->
<!-- ### Sign a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk::{Wallet, SignOptions}; -->
<!-- use base64; -->
<!-- use bdk::bitcoin::consensus::deserialize; -->
<!-- use bdk::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
<!-- Network::Testnet, -->
<!-- )?; -->
<!-- let psbt = "..."; -->
<!-- let mut psbt = deserialize(&base64::decode(psbt).unwrap())?; -->
<!-- let _finalized = wallet.sign(&mut psbt, SignOptions::default())?; -->
<!-- Ok(()) -->
<!-- } -->
<!-- ``` -->
## Testing
### Unit testing
```bash
cargo test
```
## License
Licensed under either of
* Apache License, Version 2.0
([LICENSE-APACHE](LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
* MIT license
([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
at your option.
## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html

View File

@@ -24,12 +24,11 @@ use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
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-miscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
/// 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.
@@ -54,14 +53,12 @@ fn main() -> Result<(), Box<dyn Error>> {
info!("Compiled into following Descriptor: \n{}", descriptor);
let database = MemoryDatabase::new();
// Create a new wallet from this descriptor
let wallet = Wallet::new(&format!("{}", descriptor), None, Network::Regtest, database)?;
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
info!(
"First derived address from the descriptor: \n{}",
wallet.get_address(New)?
wallet.get_address(New)
);
// BDK also has it's own `Policy` structure to represent the spending condition in a more

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(())
}

View File

@@ -15,6 +15,7 @@
//! checksum of a descriptor
use crate::descriptor::DescriptorError;
use alloc::string::String;
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
@@ -133,6 +134,7 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
mod test {
use super::*;
use crate::descriptor::calc_checksum;
use assert_matches::assert_matches;
// test calc_checksum() function; it should return the same value as Bitcoin Core
#[test]
@@ -155,26 +157,26 @@ mod test {
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
assert!(matches!(
calc_checksum(desc).err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
assert_matches!(
calc_checksum(desc),
Err(DescriptorError::InvalidDescriptorChecksum)
);
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
assert!(matches!(
calc_checksum(desc).err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
assert_matches!(
calc_checksum(desc),
Err(DescriptorError::InvalidDescriptorChecksum)
);
}
#[test]
fn test_calc_checksum_invalid_character() {
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
assert!(matches!(
calc_checksum(&invalid_desc).err(),
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
));
assert_matches!(
calc_checksum(&invalid_desc),
Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
);
}
}

View File

@@ -23,7 +23,7 @@ macro_rules! impl_top_level_sh {
};
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{
use std::marker::PhantomData;
use core::marker::PhantomData;
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
use $crate::miniscript::$ctx;
@@ -35,7 +35,7 @@ macro_rules! impl_top_level_sh {
$crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*)
}};
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{
use std::marker::PhantomData;
use core::marker::PhantomData;
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
use $crate::miniscript::$ctx;
@@ -203,8 +203,8 @@ macro_rules! impl_node_opcode_two {
a_keymap.extend(b_keymap.into_iter());
let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
std::sync::Arc::new(a_minisc),
std::sync::Arc::new(b_minisc),
$crate::alloc::sync::Arc::new(a_minisc),
$crate::alloc::sync::Arc::new(b_minisc),
))?;
minisc.check_miniscript()?;
@@ -234,9 +234,9 @@ macro_rules! impl_node_opcode_three {
let networks = $crate::keys::merge_networks(&networks, &c_networks);
let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
std::sync::Arc::new(a_minisc),
std::sync::Arc::new(b_minisc),
std::sync::Arc::new(c_minisc),
$crate::alloc::sync::Arc::new(a_minisc),
$crate::alloc::sync::Arc::new(b_minisc),
$crate::alloc::sync::Arc::new(c_minisc),
))?;
minisc.check_miniscript()?;
@@ -263,7 +263,7 @@ macro_rules! impl_sortedmulti {
)*
];
keys.into_iter().collect::<Result<Vec<_>, _>>()
keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
.and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp))
});
@@ -274,7 +274,7 @@ macro_rules! impl_sortedmulti {
#[macro_export]
macro_rules! parse_tap_tree {
( @merge $tree_a:expr, $tree_b:expr) => {{
use std::sync::Arc;
use $crate::alloc::sync::Arc;
use $crate::miniscript::descriptor::TapTree;
$tree_a
@@ -318,7 +318,7 @@ macro_rules! parse_tap_tree {
// Single leaf
( $op:ident ( $( $minisc:tt )* ) ) => {{
use std::sync::Arc;
use $crate::alloc::sync::Arc;
use $crate::miniscript::descriptor::TapTree;
$crate::fragment!( $op ( $( $minisc )* ) )
@@ -337,7 +337,7 @@ macro_rules! apply_modifier {
.and_then(|(minisc, keymap, networks)| {
let minisc = $crate::miniscript::Miniscript::from_ast(
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
std::sync::Arc::new(minisc),
$crate::alloc::sync::Arc::new(minisc),
),
)?;
@@ -374,8 +374,8 @@ macro_rules! apply_modifier {
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
$crate::impl_leaf_opcode_value_two!(
AndV,
std::sync::Arc::new(a_minisc),
std::sync::Arc::new($crate::fragment!(true).unwrap().0)
$crate::alloc::sync::Arc::new(a_minisc),
$crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0)
)
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
})
@@ -384,8 +384,8 @@ macro_rules! apply_modifier {
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
$crate::impl_leaf_opcode_value_two!(
OrI,
std::sync::Arc::new($crate::fragment!(false).unwrap().0),
std::sync::Arc::new(a_minisc)
$crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0),
$crate::alloc::sync::Arc::new(a_minisc)
)
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
})
@@ -394,8 +394,8 @@ macro_rules! apply_modifier {
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
$crate::impl_leaf_opcode_value_two!(
OrI,
std::sync::Arc::new(a_minisc),
std::sync::Arc::new($crate::fragment!(false).unwrap().0)
$crate::alloc::sync::Arc::new(a_minisc),
$crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0)
)
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
})
@@ -495,6 +495,8 @@ macro_rules! apply_modifier {
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [`Vec`]: alloc::vec::Vec
#[macro_export]
macro_rules! descriptor {
( bare ( $( $minisc:tt )* ) ) => ({
@@ -599,7 +601,7 @@ macro_rules! group_multi_keys {
)*
];
keys.into_iter().collect::<Result<Vec<_>, _>>()
keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
.map_err($crate::descriptor::DescriptorError::Key)
}};
}
@@ -744,8 +746,8 @@ macro_rules! fragment {
( thresh_vec ( $thresh:expr, $items:expr ) ) => ({
use $crate::miniscript::descriptor::KeyMap;
let (items, key_maps_networks): (Vec<_>, Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
let items = items.into_iter().map(std::sync::Arc::new).collect();
let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect();
let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| {
keys_acc.extend(key.into_iter());
@@ -760,7 +762,7 @@ macro_rules! fragment {
( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
let items = $crate::fragment_internal!( @v $( $inner )* );
items.into_iter().collect::<Result<Vec<_>, _>>()
items.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
});
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
@@ -793,12 +795,13 @@ macro_rules! fragment {
#[cfg(test)]
mod test {
use alloc::string::ToString;
use bitcoin::hashes::hex::ToHex;
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::{Descriptor, Legacy, Segwitv0};
use std::str::FromStr;
use core::str::FromStr;
use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};

View File

@@ -11,6 +11,8 @@
//! Descriptor errors
use core::fmt;
/// Errors related to the parsing and usage of descriptors
#[derive(Debug)]
pub enum Error {
@@ -51,12 +53,32 @@ 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)
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl_error!(bitcoin::util::bip32::Error, Bip32);

View File

@@ -14,14 +14,18 @@
//! This module contains generic utilities to work with descriptors, plus some re-exported types
//! from [`miniscript`].
use std::collections::BTreeMap;
use crate::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::util::{psbt, taproot};
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
use bitcoin::{Network, TxOut};
use miniscript::descriptor::{DefiniteDescriptorKey, 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,
@@ -240,14 +244,34 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
);
}
if !self.2.contains(&network) {
let (desc, keymap, networks) = self;
if !networks.contains(&network) {
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
}
// fixup the network for keys that need it
let translated = self.0.translate_pk(&mut Translator { network })?;
// 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;
}
_ => {}
}
Ok((translated, self.1))
(k, v)
})
.collect();
Ok((translated, fixed_keymap))
}
}
@@ -331,27 +355,27 @@ where
pub(crate) trait DescriptorMeta {
fn is_witness(&self) -> bool;
fn is_taproot(&self) -> bool;
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
fn derive_from_hd_keypaths<'s>(
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>>;
fn derive_from_hd_keypaths(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor>;
fn derive_from_tap_key_origins<'s>(
fn derive_from_tap_key_origins(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_key_origins<'s>(
fn derive_from_psbt_key_origins(
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_input<'s>(
fn derive_from_psbt_input(
&self,
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor>;
}
@@ -372,7 +396,7 @@ impl DescriptorMeta for ExtendedDescriptor {
self.desc_type() == DescriptorType::Tr
}
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>> {
let mut answer = Vec::new();
self.for_each_key(|pk| {
@@ -383,13 +407,13 @@ impl DescriptorMeta for ExtendedDescriptor {
true
});
Ok(answer)
answer
}
fn derive_from_psbt_key_origins<'s>(
fn derive_from_psbt_key_origins(
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor> {
// Ensure that deriving `xpub` with `path` yields `expected`
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
@@ -473,10 +497,10 @@ impl DescriptorMeta for ExtendedDescriptor {
path_found.map(|path| self.at_derivation_index(path))
}
fn derive_from_hd_keypaths<'s>(
fn derive_from_hd_keypaths(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor> {
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
let key_origins = hd_keypaths
@@ -491,10 +515,10 @@ impl DescriptorMeta for ExtendedDescriptor {
self.derive_from_psbt_key_origins(key_origins, secp)
}
fn derive_from_tap_key_origins<'s>(
fn derive_from_tap_key_origins(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor> {
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
let key_origins = tap_key_origins
@@ -504,11 +528,11 @@ impl DescriptorMeta for ExtendedDescriptor {
self.derive_from_psbt_key_origins(key_origins, secp)
}
fn derive_from_psbt_input<'s>(
fn derive_from_psbt_input(
&self,
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
secp: &SecpCtx,
) -> Option<DerivedDescriptor> {
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
return Some(derived);
@@ -557,8 +581,10 @@ impl DescriptorMeta for ExtendedDescriptor {
#[cfg(test)]
mod test {
use std::str::FromStr;
use alloc::string::ToString;
use core::str::FromStr;
use assert_matches::assert_matches;
use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;
use bitcoin::secp256k1::Secp256k1;
@@ -682,23 +708,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
@@ -724,17 +767,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
@@ -768,17 +805,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
@@ -812,11 +843,7 @@ 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/0/*))";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);

View File

@@ -32,13 +32,15 @@
//!
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy)?);
//! println!("policy: {}", serde_json::to_string(&policy).unwrap());
//! # Ok::<(), bdk::Error>(())
//! ```
use std::cmp::max;
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::fmt;
use crate::collections::{BTreeMap, HashSet, VecDeque};
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::max;
use core::fmt;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
@@ -512,10 +514,18 @@ 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"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for PolicyError {}
impl Policy {
@@ -652,11 +662,11 @@ impl Policy {
(0..*threshold).collect()
}
SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(),
_ => vec![],
_ => HashSet::new(),
};
let selected = match path.get(&self.id) {
Some(arr) => arr,
_ => &default,
let selected: HashSet<_> = match path.get(&self.id) {
Some(arr) => arr.iter().copied().collect(),
_ => default,
};
match &self.item {
@@ -664,14 +674,24 @@ impl Policy {
let mapped_req = items
.iter()
.map(|i| i.get_condition(path))
.collect::<Result<Vec<_>, _>>()?;
.collect::<Vec<_>>();
// if all the requirements are null we don't care about `selected` because there
// are no requirements
if mapped_req.iter().all(Condition::is_null) {
if mapped_req
.iter()
.all(|cond| matches!(cond, Ok(c) if c.is_null()))
{
return Ok(Condition::default());
}
// make sure all the indexes in the `selected` list are within range
for index in &selected {
if *index >= items.len() {
return Err(PolicyError::IndexOutOfRange(*index));
}
}
// if we have something, make sure we have enough items. note that the user can set
// an empty value for this step in case of n-of-n, because `selected` is set to all
// the elements above
@@ -680,23 +700,18 @@ impl Policy {
}
// check the selected items, see if there are conflicting requirements
let mut requirements = Condition::default();
for item_index in selected {
requirements = requirements.merge(
mapped_req
.get(*item_index)
.ok_or(PolicyError::IndexOutOfRange(*item_index))?,
)?;
}
Ok(requirements)
mapped_req
.into_iter()
.enumerate()
.filter(|(index, _)| selected.contains(index))
.try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?))
}
SatisfiableItem::Multisig { keys, threshold } => {
if selected.len() < *threshold {
return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
}
if let Some(item) = selected.iter().find(|i| **i >= keys.len()) {
return Err(PolicyError::IndexOutOfRange(*item));
if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) {
return Err(PolicyError::IndexOutOfRange(item));
}
Ok(Condition::default())
@@ -1139,11 +1154,12 @@ mod test {
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use crate::wallet::signer::SignersContainer;
use alloc::{string::ToString, sync::Arc};
use assert_matches::assert_matches;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::bip32;
use bitcoin::Network;
use std::str::FromStr;
use std::sync::Arc;
use core::str::FromStr;
const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
@@ -1182,8 +1198,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
@@ -1195,10 +1211,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
@@ -1217,19 +1231,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()
)
);
}
@@ -1248,18 +1259,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)
)
);
}
@@ -1281,18 +1289,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])
)
);
}
@@ -1313,18 +1318,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])
)
);
}
@@ -1345,8 +1347,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
@@ -1358,10 +1360,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());
}
// single key, 1 prv and 1 pub key descriptor, required 1 prv keys
@@ -1382,18 +1382,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])
)
);
}
@@ -1425,18 +1422,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(sequence))
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
)
);
}
@@ -1459,12 +1452,12 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
let _policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
println!("desc policy = {:?}", policy); // TODO remove
// TODO how should this fail with mixed timelocks?
// println!("desc policy = {:?}", policy); // TODO remove
// TODO how should this fail with mixed timelocks?
}
// - multiple timelocks of the same type should be correctly merged together
@@ -1484,12 +1477,12 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
let _policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
println!("desc policy = {:?}", policy); // TODO remove
// TODO how should this merge timelocks?
// println!("desc policy = {:?}", policy); // TODO remove
// TODO how should this merge timelocks?
let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp);
let locktime_seconds0 = 500000100;
let locktime_seconds1 = 500000200;
@@ -1502,12 +1495,12 @@ mod test {
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = wallet_desc
let _policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap()
.unwrap();
println!("desc policy = {:?}", policy); // TODO remove
// println!("desc policy = {:?}", policy); // TODO remove
// TODO how should this merge timelocks?
}
@@ -1599,11 +1592,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();
@@ -1613,11 +1604,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();
@@ -1625,11 +1614,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]
)
);
}
@@ -1673,11 +1660,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());
@@ -1691,11 +1676,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());
@@ -1711,11 +1694,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());
}
@@ -1790,12 +1771,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));
@@ -1887,19 +1864,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

@@ -17,7 +17,7 @@
use bitcoin::util::bip32;
use bitcoin::Network;
use miniscript::{Legacy, Segwitv0};
use miniscript::{Legacy, Segwitv0, Tap};
use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap};
use crate::descriptor::DescriptorError;
@@ -73,22 +73,16 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::Wallet;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::P2Pkh;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new(
/// P2Pkh(key),
/// None,
/// Network::Testnet,
/// MemoryDatabase::default(),
/// )?;
/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet.get_address(New)?.to_string(),
/// wallet.get_address(New).to_string(),
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -107,22 +101,16 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// # use bdk::Wallet;
/// use bdk::template::P2Wpkh_P2Sh;
/// use bdk::wallet::AddressIndex;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new(
/// P2Wpkh_P2Sh(key),
/// None,
/// Network::Testnet,
/// MemoryDatabase::default(),
/// )?;
/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet.get_address(New)?.to_string(),
/// wallet.get_address(AddressIndex::New).to_string(),
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -143,21 +131,15 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::P2Wpkh;
/// use bdk::wallet::AddressIndex::New;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new(
/// P2Wpkh(key),
/// None,
/// Network::Testnet,
/// MemoryDatabase::default(),
/// )?;
/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet.get_address(New)?.to_string(),
/// wallet.get_address(New).to_string(),
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
@@ -170,6 +152,34 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
}
}
/// P2TR template. Expands to a descriptor `tr(key)`
///
/// ## Example
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::P2TR;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
///
/// assert_eq!(
/// wallet.get_address(New).to_string(),
/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
descriptor!(tr(self.0))
}
}
/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
///
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
@@ -182,20 +192,18 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip44;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip44(key.clone(), KeychainKind::External),
/// Some(Bip44(key, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
@@ -221,28 +229,29 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip44Public;
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// 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)
}
}
@@ -258,20 +267,18 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip49;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip49(key.clone(), KeychainKind::External),
/// Some(Bip49(key, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
@@ -284,7 +291,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.
///
@@ -297,28 +304,29 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip49Public;
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// 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)
}
}
@@ -334,20 +342,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip84;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip84(key.clone(), KeychainKind::External),
/// Some(Bip84(key, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
@@ -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.
///
@@ -373,28 +379,104 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::database::MemoryDatabase;
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip84Public;
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new(
/// let mut wallet = Wallet::new_no_persist(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// MemoryDatabase::default()
/// )?;
///
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// 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)
}
}
/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)`
///
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
///
/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`.
///
/// ## Example
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip86;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip86(key.clone(), KeychainKind::External),
/// Some(Bip86(key, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network)
}
}
/// BIP86 public template. Expands to `tr(key/{0,1}/*)`
///
/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet.
///
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
///
/// See [`Bip86`] for a template that does the full derivation, but requires private data
/// for the key.
///
/// ## Example
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::wallet::AddressIndex::New;
/// use bdk::template::Bip86Public;
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new_no_persist(
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
/// )?;
///
/// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
P2TR(segwit_v1::make_bipxx_public(
86, self.0, self.1, self.2, network,
)?)
.build(network)
}
}
@@ -409,7 +491,7 @@ macro_rules! expand_make_bipxx {
keychain: KeychainKind,
network: Network,
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
let mut derivation_path = Vec::with_capacity(4);
let mut derivation_path = alloc::vec::Vec::with_capacity(4);
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
match network {
@@ -440,6 +522,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 +531,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)?,
]);
@@ -460,17 +546,19 @@ macro_rules! expand_make_bipxx {
expand_make_bipxx!(legacy, Legacy);
expand_make_bipxx!(segwit_v0, Segwitv0);
expand_make_bipxx!(segwit_v1, Tap);
#[cfg(test)]
mod test {
// test existing descriptor templates, make sure they are expanded to the right descriptors
use std::str::FromStr;
use alloc::{string::ToString, vec::Vec};
use core::str::FromStr;
use super::*;
use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::ValidNetworks;
use bitcoin::network::constants::Network::Regtest;
use assert_matches::assert_matches;
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::Descriptor;
@@ -488,9 +576,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();
@@ -502,9 +590,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 });
}
}
@@ -512,11 +600,14 @@ mod test {
fn check(
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
is_witness: bool,
is_taproot: bool,
is_fixed: bool,
network: Network,
expected: &[&str],
) {
let (desc, _key_map, _networks) = desc.unwrap();
assert_eq!(desc.is_witness(), is_witness);
assert_eq!(desc.is_taproot(), is_taproot);
assert_eq!(!desc.has_wildcard(), is_fixed);
for i in 0..expected.len() {
let index = i as u32;
@@ -525,7 +616,7 @@ mod test {
} else {
desc.at_derivation_index(index)
};
let address = child_desc.address(Regtest).unwrap();
let address = child_desc.address(network).unwrap();
assert_eq!(address.to_string(), *expected.get(i).unwrap());
}
}
@@ -539,7 +630,9 @@ mod test {
check(
P2Pkh(prvkey).build(Network::Bitcoin),
false,
false,
true,
Network::Regtest,
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
);
@@ -550,7 +643,9 @@ mod test {
check(
P2Pkh(pubkey).build(Network::Bitcoin),
false,
false,
true,
Network::Regtest,
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
);
}
@@ -564,7 +659,9 @@ mod test {
check(
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
true,
false,
true,
Network::Regtest,
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
);
@@ -575,7 +672,9 @@ mod test {
check(
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
true,
false,
true,
Network::Regtest,
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
);
}
@@ -589,7 +688,9 @@ mod test {
check(
P2Wpkh(prvkey).build(Network::Bitcoin),
true,
false,
true,
Network::Regtest,
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
);
@@ -600,11 +701,42 @@ mod test {
check(
P2Wpkh(pubkey).build(Network::Bitcoin),
true,
false,
true,
Network::Regtest,
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
);
}
// P2TR `tr(key)`
#[test]
fn test_p2tr_template() {
let prvkey =
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
.unwrap();
check(
P2TR(prvkey).build(Network::Bitcoin),
false,
true,
true,
Network::Regtest,
&["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"],
);
let pubkey = bitcoin::PublicKey::from_str(
"03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
)
.unwrap();
check(
P2TR(pubkey).build(Network::Bitcoin),
false,
true,
true,
Network::Regtest,
&["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"],
);
}
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
#[test]
fn test_bip44_template() {
@@ -613,6 +745,8 @@ mod test {
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
false,
false,
false,
Network::Regtest,
&[
"n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5",
"mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP",
@@ -623,6 +757,8 @@ mod test {
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
false,
false,
false,
Network::Regtest,
&[
"muHF98X9KxEzdKrnFAX85KeHv96eXopaip",
"n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR",
@@ -640,6 +776,8 @@ mod test {
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
false,
false,
false,
Network::Regtest,
&[
"miNG7dJTzJqNbFS19svRdTCisC65dsubtR",
"n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg",
@@ -650,6 +788,8 @@ mod test {
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
false,
false,
false,
Network::Regtest,
&[
"moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H",
"ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG",
@@ -666,6 +806,8 @@ mod test {
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV",
"2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS",
@@ -676,6 +818,8 @@ mod test {
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG",
"2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p",
@@ -693,6 +837,8 @@ mod test {
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt",
"2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX",
@@ -703,6 +849,8 @@ mod test {
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ",
"2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH",
@@ -719,6 +867,8 @@ mod test {
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s",
"bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp",
@@ -729,6 +879,8 @@ mod test {
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa",
"bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45",
@@ -746,6 +898,8 @@ mod test {
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h",
"bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana",
@@ -756,6 +910,8 @@ mod test {
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
true,
false,
false,
Network::Regtest,
&[
"bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2",
"bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp",
@@ -763,4 +919,67 @@ mod test {
],
);
}
// BIP86 `tr(key/86'/0'/0'/{0,1}/*)`
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
#[test]
fn test_bip86_template() {
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
check(
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
false,
true,
false,
Network::Bitcoin,
&[
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
"bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
],
);
check(
Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
false,
true,
false,
Network::Bitcoin,
&[
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
"bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
"bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
],
);
}
// BIP86 public `tr(key/{0,1}/*)`
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
#[test]
fn test_bip86_public_template() {
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("73c5da0a").unwrap();
check(
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
false,
true,
false,
Network::Bitcoin,
&[
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
"bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
],
);
check(
Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
false,
true,
false,
Network::Bitcoin,
&[
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
"bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
"bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
],
);
}
}

View File

@@ -9,21 +9,17 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet};
use alloc::{string::String, vec::Vec};
use bitcoin::{OutPoint, Txid};
use core::fmt;
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
#[derive(Debug)]
pub enum Error {
/// Wrong number of bytes found when trying to convert to u32
InvalidU32Bytes(Vec<u8>),
/// Generic error
Generic(String),
/// This error is thrown when trying to convert Bare and Public key script to address
ScriptDoesntHaveAddressForm,
/// Cannot build a tx without recipients
NoRecipients,
/// `manually_selected_only` option is selected but no utxo has been passed
@@ -79,74 +75,18 @@ pub enum Error {
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
/// Signing error
Signer(crate::wallet::signer::SignerError),
/// Invalid network
InvalidNetwork {
/// requested network, for example what is given as bdk-cli option
requested: Network,
/// found network, for example the network of the bitcoin node
found: Network,
},
#[cfg(feature = "verify")]
/// Transaction verification error
Verification(crate::wallet::verify::VerifyError),
/// Progress value must be between `0.0` (included) and `100.0` (included)
InvalidProgressValue(f32),
/// Progress update error (maybe the channel has been closed)
ProgressUpdateError,
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
InvalidOutpoint(OutPoint),
/// Error related to the parsing and usage of descriptors
Descriptor(crate::descriptor::error::Error),
/// 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
Secp256k1(bitcoin::secp256k1::Error),
/// Error serializing or deserializing JSON data
Json(serde_json::Error),
/// Hex decoding error
Hex(bitcoin::hashes::hex::Error),
/// Partially signed bitcoin transaction error
Psbt(bitcoin::util::psbt::Error),
/// Partially signed bitcoin transaction parse error
PsbtParse(bitcoin::util::psbt::PsbtParseError),
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
//MissingInputUTXO(usize),
//InvalidAddressNetwork(Address),
//DifferentTransactions,
//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),
#[cfg(feature = "esplora")]
/// Esplora client error
Esplora(Box<crate::blockchain::esplora::EsploraError>),
#[cfg(feature = "compact_filters")]
/// Compact filters client error)
CompactFilters(crate::blockchain::compact_filters::CompactFiltersError),
#[cfg(feature = "key-value-db")]
/// Sled database error
Sled(sled::Error),
#[cfg(feature = "rpc")]
/// Rpc client error
Rpc(bitcoincore_rpc::Error),
#[cfg(feature = "sqlite")]
/// Rusqlite client error
Rusqlite(rusqlite::Error),
}
/// Errors returned by miniscript when updating inconsistent PSBTs
@@ -157,22 +97,74 @@ pub enum MiniscriptPsbtError {
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
/// 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 {
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
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),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MiniscriptPsbtError {}
#[cfg(feature = "std")]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Generic(err) => write!(f, "Generic error: {}", err),
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::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist in the tx: {}",
outpoint
),
Self::Descriptor(err) => write!(f, "Descriptor 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::Psbt(err) => write!(f, "PSBT error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
macro_rules! impl_error {
@@ -180,7 +172,7 @@ macro_rules! impl_error {
impl_error!($from, $to, Error);
};
( $from:ty, $to:ident, $impl_for:ty ) => {
impl std::convert::From<$from> for $impl_for {
impl core::convert::From<$from> for $impl_for {
fn from(err: $from) -> Self {
<$impl_for>::$to(err)
}
@@ -203,48 +195,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);
impl_error!(bitcoin::hashes::hex::Error, Hex);
impl_error!(bitcoin::util::psbt::Error, Psbt);
impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
#[cfg(feature = "electrum")]
impl_error!(electrum_client::Error, Electrum);
#[cfg(feature = "key-value-db")]
impl_error!(sled::Error, Sled);
#[cfg(feature = "rpc")]
impl_error!(bitcoincore_rpc::Error, Rpc);
#[cfg(feature = "sqlite")]
impl_error!(rusqlite::Error, Rusqlite);
#[cfg(feature = "compact_filters")]
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
fn from(other: crate::blockchain::compact_filters::CompactFiltersError) -> Self {
match other {
crate::blockchain::compact_filters::CompactFiltersError::Global(e) => *e,
err => Error::CompactFilters(err),
}
}
}
#[cfg(feature = "verify")]
impl From<crate::wallet::verify::VerifyError> for Error {
fn from(other: crate::wallet::verify::VerifyError) -> Self {
match other {
crate::wallet::verify::VerifyError::Global(inner) => *inner,
err => Error::Verification(err),
}
}
}
#[cfg(feature = "esplora")]
impl From<crate::blockchain::esplora::EsploraError> for Error {
fn from(other: crate::blockchain::esplora::EsploraError) -> Self {
Error::Esplora(Box::new(other))
}
}

View File

@@ -14,6 +14,7 @@
// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for
// something that should be fairly simple to re-implement.
use alloc::string::String;
use bitcoin::util::bip32;
use bitcoin::Network;
@@ -150,7 +151,8 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
#[cfg(test)]
mod test {
use std::str::FromStr;
use alloc::string::ToString;
use core::str::FromStr;
use bitcoin::util::bip32;

View File

@@ -11,11 +11,14 @@
//! Key formats
use std::any::TypeId;
use std::collections::HashSet;
use std::marker::PhantomData;
use std::ops::Deref;
use std::str::FromStr;
use crate::collections::HashSet;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::any::TypeId;
use core::fmt;
use core::marker::PhantomData;
use core::ops::Deref;
use core::str::FromStr;
use bitcoin::secp256k1::{self, Secp256k1, Signing};
@@ -40,7 +43,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 +98,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(
@@ -277,7 +280,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
///
/// ```compile_fail
/// use bdk::bitcoin::PublicKey;
/// use std::str::FromStr;
/// use core::str::FromStr;
///
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
///
@@ -375,7 +378,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
@@ -460,12 +463,11 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
/// Consume `self` and turn it into an [`ExtendedKey`]
///
/// This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
/// like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
#[cfg_attr(
feature = "keys-bip39",
doc = r##"
This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
```rust
use bdk::bitcoin::Network;
use bdk::keys::{DerivableKey, ExtendedKey};
@@ -619,7 +621,7 @@ pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
/// Extra options required by the `generate_with_entropy`
type Options;
/// Returned error in case of failure
type Error: std::fmt::Debug;
type Error: core::fmt::Debug;
/// Generate a key given the extra options and the entropy
fn generate_with_entropy(
@@ -933,12 +935,20 @@ pub enum KeyError {
impl_error!(miniscript::Error, Miniscript, KeyError);
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)
impl fmt::Display for KeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for KeyError {}
#[cfg(test)]

54
crates/bdk/src/lib.rs Normal file
View File

@@ -0,0 +1,54 @@
#![doc = include_str!("../README.md")]
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
docsrs,
doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
)]
#![no_std]
#![warn(missing_docs)]
#[cfg(feature = "std")]
#[macro_use]
extern crate std;
#[doc(hidden)]
#[macro_use]
pub extern crate alloc;
pub extern crate bitcoin;
#[cfg(feature = "hardware-signer")]
pub extern crate hwi;
extern crate log;
pub extern crate miniscript;
extern crate serde;
extern crate serde_json;
#[cfg(feature = "keys-bip39")]
extern crate bip39;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
pub mod descriptor;
pub mod keys;
pub mod psbt;
pub(crate) mod types;
pub mod wallet;
pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use error::Error;
pub use types::*;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::Wallet;
/// Get the version of BDK at runtime
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown")
}
pub use bdk_chain as chain;
pub(crate) use bdk_chain::collections;

View File

@@ -0,0 +1,79 @@
// 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.
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
use crate::FeeRate;
use alloc::vec::Vec;
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 {
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
let tx = &self.unsigned_tx;
if input_index >= tx.input.len() {
return None;
}
if let Some(input) = self.inputs.get(input_index) {
if let Some(wit_utxo) = &input.witness_utxo {
Some(wit_utxo.clone())
} else if let Some(in_tx) = &input.non_witness_utxo {
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
} else {
None
}
} else {
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)
})
}
}

View File

@@ -9,20 +9,22 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use std::convert::AsRef;
use std::ops::Sub;
use alloc::boxed::Box;
use core::convert::AsRef;
use core::ops::Sub;
use bdk_chain::ConfirmationTime;
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
use bitcoin::{hash_types::Txid, util::psbt};
use serde::{Deserialize, Serialize};
/// Types of keychains
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum KeychainKind {
/// External
/// External keychain, used for deriving recipient addresses.
External = 0,
/// Internal, usually used for change outputs
/// Internal keychain, used for deriving change addresses.
Internal = 1,
}
@@ -112,6 +114,11 @@ impl FeeRate {
self.0
}
/// Return the value as satoshi/kwu
pub fn sat_per_kwu(&self) -> f32 {
self.0 * 250.0_f32
}
/// Calculate absolute fee in Satoshis using size in weight units.
pub fn fee_wu(&self, wu: usize) -> u64 {
self.fee_vb(wu.vbytes())
@@ -123,7 +130,7 @@ impl FeeRate {
}
}
impl std::default::Default for FeeRate {
impl Default for FeeRate {
fn default() -> Self {
FeeRate::default_min_relay_fee()
}
@@ -163,6 +170,10 @@ pub struct LocalUtxo {
pub keychain: KeychainKind,
/// Whether this UTXO is spent or not
pub is_spent: bool,
/// The derivation index for the script pubkey in the wallet
pub derivation_index: u32,
/// The confirmation time for transaction containing this utxo
pub confirmation_time: ConfirmationTime,
}
/// A [`Utxo`] with its `satisfaction_weight`.
@@ -230,104 +241,30 @@ pub struct TransactionDetails {
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 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.
/// Fee value in sats if it was available.
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>,
pub confirmation_time: ConfirmationTime,
}
/// Block height and timestamp of a block
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct BlockTime {
/// confirmation block height
pub height: u32,
/// confirmation block timestamp
pub timestamp: u64,
}
/// **DEPRECATED**: Confirmation time of a transaction
///
/// The structure has been renamed to `BlockTime`
#[deprecated(note = "This structure has been renamed to `BlockTime`")]
pub type ConfirmationTime = BlockTime;
impl BlockTime {
/// Returns `Some` `BlockTime` if both `height` and `timestamp` are `Some`
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
match (height, timestamp) {
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
_ => None,
}
impl PartialOrd for TransactionDetails {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// 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,
)
impl Ord for TransactionDetails {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.confirmation_time
.cmp(&other.confirmation_time)
.then_with(|| self.txid.cmp(&other.txid))
}
}
@@ -397,5 +334,6 @@ mod tests {
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);
assert_eq!(fee.sat_per_kwu(), 250.0);
}
}

View File

@@ -27,17 +27,15 @@
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk::wallet::{self, coin_selection::*};
//! # use bdk::database::Database;
//! # use bdk::*;
//! # use bdk::wallet::coin_selection::decide_change;
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
//! #[derive(Debug)]
//! struct AlwaysSpendEverything;
//!
//! impl<D: Database> CoinSelectionAlgorithm<D> for AlwaysSpendEverything {
//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
//! fn coin_select(
//! &self,
//! database: &D,
//! required_utxos: Vec<WeightedUtxo>,
//! optional_utxos: Vec<WeightedUtxo>,
//! fee_rate: FeeRate,
@@ -79,7 +77,7 @@
//! }
//! }
//!
//! # let wallet = doctest_wallet!();
//! # let mut wallet = doctest_wallet!();
//! // create wallet, sync, ...
//!
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
@@ -96,26 +94,19 @@
use crate::types::FeeRate;
use crate::wallet::utils::IsDust;
use crate::{database::Database, WeightedUtxo};
use crate::WeightedUtxo;
use crate::{error::Error, Utxo};
use alloc::vec::Vec;
use bitcoin::consensus::encode::serialize;
use bitcoin::Script;
use core::convert::TryInto;
use rand::seq::SliceRandom;
#[cfg(not(test))]
use rand::thread_rng;
#[cfg(test)]
use rand::{rngs::StdRng, SeedableRng};
use std::collections::HashMap;
use std::convert::TryInto;
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
/// overridden
#[cfg(not(test))]
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
#[cfg(test)]
pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; // make the tests more predictable
// Base weight of a Txin, not counting the weight needed for satisfying it.
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
@@ -177,7 +168,7 @@ impl CoinSelectionResult {
/// selection algorithm when it creates transactions.
///
/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
pub trait CoinSelectionAlgorithm: core::fmt::Debug {
/// Perform the coin selection
///
/// - `database`: a reference to the wallet's database that can be used to lookup additional
@@ -193,7 +184,6 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
#[allow(clippy::too_many_arguments)]
fn coin_select(
&self,
database: &D,
required_utxos: Vec<WeightedUtxo>,
optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
@@ -209,10 +199,9 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
#[derive(Debug, Default, Clone, Copy)]
pub struct LargestFirstCoinSelection;
impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
fn coin_select(
&self,
_database: &D,
required_utxos: Vec<WeightedUtxo>,
mut optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
@@ -246,46 +235,22 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
#[derive(Debug, Default, Clone, Copy)]
pub struct OldestFirstCoinSelection;
impl<D: Database> CoinSelectionAlgorithm<D> for OldestFirstCoinSelection {
impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
fn coin_select(
&self,
database: &D,
required_utxos: Vec<WeightedUtxo>,
mut optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
target_amount: u64,
drain_script: &Script,
) -> Result<CoinSelectionResult, Error> {
// query db and create a blockheight lookup table
let blockheights = optional_utxos
.iter()
.map(|wu| wu.utxo.outpoint().txid)
// fold is used so we can skip db query for txid that already exist in hashmap acc
.fold(Ok(HashMap::new()), |bh_result_acc, txid| {
bh_result_acc.and_then(|mut bh_acc| {
if bh_acc.contains_key(&txid) {
Ok(bh_acc)
} else {
database.get_tx(&txid, false).map(|details| {
bh_acc.insert(
txid,
details.and_then(|d| d.confirmation_time.map(|ct| ct.height)),
);
bh_acc
})
}
})
})?;
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
// oldest to newest according to blocktime
// For utxo that doesn't exist in DB, they will have lowest priority to be selected
let utxos = {
optional_utxos.sort_unstable_by_key(|wu| {
match blockheights.get(&wu.utxo.outpoint().txid) {
Some(Some(blockheight)) => blockheight,
_ => &u32::MAX,
}
optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
Utxo::Local(local) => Some(local.confirmation_time),
Utxo::Foreign { .. } => None,
});
required_utxos
@@ -399,7 +364,7 @@ impl OutputGroup {
/// Branch and bound coin selection
///
/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: <http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct BranchAndBoundCoinSelection {
size_of_change: u64,
}
@@ -422,10 +387,9 @@ impl BranchAndBoundCoinSelection {
const BNB_TOTAL_TRIES: usize = 100_000;
impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
fn coin_select(
&self,
_database: &D,
required_utxos: Vec<WeightedUtxo>,
optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate,
@@ -667,15 +631,7 @@ impl BranchAndBoundCoinSelection {
drain_script: &Script,
fee_rate: FeeRate,
) -> CoinSelectionResult {
#[cfg(not(test))]
optional_utxos.shuffle(&mut thread_rng());
#[cfg(test)]
{
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
optional_utxos.shuffle(&mut rng);
}
optional_utxos.shuffle(&mut rand::thread_rng());
let selected_utxos = optional_utxos.into_iter().fold(
(curr_value, vec![]),
|(mut amount, mut utxos), utxo| {
@@ -721,18 +677,19 @@ impl BranchAndBoundCoinSelection {
#[cfg(test)]
mod test {
use std::str::FromStr;
use assert_matches::assert_matches;
use core::str::FromStr;
use bdk_chain::ConfirmationTime;
use bitcoin::{OutPoint, Script, TxOut};
use super::*;
use crate::database::{BatchOperations, MemoryDatabase};
use crate::types::*;
use crate::wallet::Vbytes;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rand::{Rng, SeedableRng};
use rand::{Rng, RngCore, SeedableRng};
// n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
// + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
@@ -740,7 +697,7 @@ mod test {
const FEE_AMOUNT: u64 = 50;
fn utxo(value: u64, index: u32) -> WeightedUtxo {
fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo {
assert!(index < 10);
let outpoint = OutPoint::from_str(&format!(
"000000000000000000000000000000000000000000000000000000000000000{}:0",
@@ -757,70 +714,50 @@ mod test {
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
confirmation_time,
}),
}
}
fn get_test_utxos() -> Vec<WeightedUtxo> {
vec![
utxo(100_000, 0),
utxo(FEE_AMOUNT as u64 - 40, 1),
utxo(200_000, 2),
utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }),
utxo(
FEE_AMOUNT - 40,
1,
ConfirmationTime::Unconfirmed { last_seen: 0 },
),
utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }),
]
}
fn setup_database_and_get_oldest_first_test_utxos<D: Database>(
database: &mut D,
) -> Vec<WeightedUtxo> {
fn get_oldest_first_test_utxos() -> Vec<WeightedUtxo> {
// ensure utxos are from different tx
let utxo1 = utxo(120_000, 1);
let utxo2 = utxo(80_000, 2);
let utxo3 = utxo(300_000, 3);
// add tx to DB so utxos are sorted by blocktime asc
// utxos will be selected by the following order
// utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (blockheight 3)
// timestamp are all set as the same to ensure that only block height is used in sorting
let utxo1_tx_details = TransactionDetails {
transaction: None,
txid: utxo1.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
let utxo1 = utxo(
120_000,
1,
ConfirmationTime::Confirmed {
height: 1,
timestamp: 1231006505,
}),
};
let utxo2_tx_details = TransactionDetails {
transaction: None,
txid: utxo2.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
time: 1231006505,
},
);
let utxo2 = utxo(
80_000,
2,
ConfirmationTime::Confirmed {
height: 2,
timestamp: 1231006505,
}),
};
let utxo3_tx_details = TransactionDetails {
transaction: None,
txid: utxo3.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
time: 1231006505,
},
);
let utxo3 = utxo(
300_000,
3,
ConfirmationTime::Confirmed {
height: 3,
timestamp: 1231006505,
}),
};
database.set_tx(&utxo1_tx_details).unwrap();
database.set_tx(&utxo2_tx_details).unwrap();
database.set_tx(&utxo3_tx_details).unwrap();
time: 1231006505,
},
);
vec![utxo1, utxo2, utxo3]
}
@@ -840,6 +777,15 @@ mod test {
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: rng.next_u32(),
confirmation_time: if rng.gen_bool(0.5) {
ConfirmationTime::Confirmed {
height: rng.next_u32(),
time: rng.next_u64(),
}
} else {
ConfirmationTime::Unconfirmed { last_seen: 0 }
},
}),
});
}
@@ -860,6 +806,8 @@ mod test {
},
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
}),
};
vec![utxo; utxos_number]
@@ -877,13 +825,11 @@ mod test {
#[test]
fn test_largest_first_coin_selection_success() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 250_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
@@ -900,13 +846,11 @@ mod test {
#[test]
fn test_largest_first_coin_selection_use_all() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
@@ -923,13 +867,11 @@ mod test {
#[test]
fn test_largest_first_coin_selection_use_only_necessary() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
let result = LargestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -947,13 +889,11 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_largest_first_coin_selection_insufficient_funds() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 500_000 + FEE_AMOUNT;
LargestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -967,13 +907,11 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 250_000 + FEE_AMOUNT;
LargestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1000.0),
@@ -985,14 +923,12 @@ mod test {
#[test]
fn test_oldest_first_coin_selection_success() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let utxos = get_oldest_first_test_utxos();
let drain_script = Script::default();
let target_amount = 180_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1006,75 +942,14 @@ mod test {
assert_eq!(result.fee_amount, 136)
}
#[test]
fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() {
// ensure utxos are from different tx
let utxo1 = utxo(120_000, 1);
let utxo2 = utxo(80_000, 2);
let utxo3 = utxo(300_000, 3);
let drain_script = Script::default();
let mut database = MemoryDatabase::default();
// add tx to DB so utxos are sorted by blocktime asc
// utxos will be selected by the following order
// utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (not exist in DB)
// timestamp are all set as the same to ensure that only block height is used in sorting
let utxo1_tx_details = TransactionDetails {
transaction: None,
txid: utxo1.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 1,
timestamp: 1231006505,
}),
};
let utxo2_tx_details = TransactionDetails {
transaction: None,
txid: utxo2.utxo.outpoint().txid,
received: 1,
sent: 0,
fee: None,
confirmation_time: Some(BlockTime {
height: 2,
timestamp: 1231006505,
}),
};
database.set_tx(&utxo1_tx_details).unwrap();
database.set_tx(&utxo2_tx_details).unwrap();
let target_amount = 180_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
vec![utxo3, utxo1, utxo2],
FeeRate::from_sat_per_vb(1.0),
target_amount,
&drain_script,
)
.unwrap();
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 136)
}
#[test]
fn test_oldest_first_coin_selection_use_all() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let utxos = get_oldest_first_test_utxos();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
@@ -1090,14 +965,12 @@ mod test {
#[test]
fn test_oldest_first_coin_selection_use_only_necessary() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let utxos = get_oldest_first_test_utxos();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
let result = OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1114,14 +987,12 @@ mod test {
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_oldest_first_coin_selection_insufficient_funds() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let utxos = get_oldest_first_test_utxos();
let drain_script = Script::default();
let target_amount = 600_000 + FEE_AMOUNT;
OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1134,15 +1005,13 @@ mod test {
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
let mut database = MemoryDatabase::default();
let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
let utxos = get_oldest_first_test_utxos();
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
let drain_script = Script::default();
OldestFirstCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1000.0),
@@ -1158,14 +1027,12 @@ mod test {
// select three outputs
let utxos = generate_same_value_utxos(100_000, 20);
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 250_000 + FEE_AMOUNT;
let result = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1182,13 +1049,11 @@ mod test {
#[test]
fn test_bnb_coin_selection_required_are_enough() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 20_000 + FEE_AMOUNT;
let result = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
utxos.clone(),
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1205,13 +1070,11 @@ mod test {
#[test]
fn test_bnb_coin_selection_optional_are_enough() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 299756 + FEE_AMOUNT;
let result = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1229,11 +1092,14 @@ mod test {
#[ignore]
fn test_bnb_coin_selection_required_not_enough() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let required = vec![utxos[0].clone()];
let mut optional = utxos[1..].to_vec();
optional.push(utxo(500_000, 3));
optional.push(utxo(
500_000,
3,
ConfirmationTime::Unconfirmed { last_seen: 0 },
));
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
@@ -1246,7 +1112,6 @@ mod test {
let result = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
required,
optional,
FeeRate::from_sat_per_vb(1.0),
@@ -1264,13 +1129,11 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_bnb_coin_selection_insufficient_funds() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 500_000 + FEE_AMOUNT;
BranchAndBoundCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1284,13 +1147,11 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_bnb_coin_selection_insufficient_funds_high_fees() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 250_000 + FEE_AMOUNT;
BranchAndBoundCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1000.0),
@@ -1303,13 +1164,11 @@ mod test {
#[test]
fn test_bnb_coin_selection_check_fee_rate() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let target_amount = 99932; // first utxo's effective value
let result = BranchAndBoundCoinSelection::new(0)
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
@@ -1329,7 +1188,6 @@ mod test {
fn test_bnb_coin_selection_exact_match() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let database = MemoryDatabase::default();
for _i in 0..200 {
let mut optional_utxos = generate_random_utxos(&mut rng, 16);
@@ -1337,7 +1195,6 @@ mod test {
let drain_script = Script::default();
let result = BranchAndBoundCoinSelection::new(0)
.coin_select(
&database,
vec![],
optional_utxos,
FeeRate::from_sat_per_vb(0.0),
@@ -1519,82 +1376,70 @@ mod test {
#[test]
fn test_bnb_exclude_negative_effective_value() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let err = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
)
.unwrap_err();
let selection = BranchAndBoundCoinSelection::default().coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
);
assert!(matches!(
err,
Error::InsufficientFunds {
assert_matches!(
selection,
Err(Error::InsufficientFunds {
available: 300_000,
..
}
));
})
);
}
#[test]
fn test_bnb_include_negative_effective_value_when_required() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let (required, optional) = utxos
.into_iter()
.partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
let err = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
required,
optional,
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
)
.unwrap_err();
let selection = BranchAndBoundCoinSelection::default().coin_select(
required,
optional,
FeeRate::from_sat_per_vb(10.0),
500_000,
&drain_script,
);
assert!(matches!(
err,
Error::InsufficientFunds {
assert_matches!(
selection,
Err(Error::InsufficientFunds {
available: 300_010,
..
}
));
})
);
}
#[test]
fn test_bnb_sum_of_effective_value_negative() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let drain_script = Script::default();
let err = BranchAndBoundCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(10_000.0),
500_000,
&drain_script,
)
.unwrap_err();
let selection = BranchAndBoundCoinSelection::default().coin_select(
utxos,
vec![],
FeeRate::from_sat_per_vb(10_000.0),
500_000,
&drain_script,
);
assert!(matches!(
err,
Error::InsufficientFunds {
assert_matches!(
selection,
Err(Error::InsufficientFunds {
available: 300_010,
..
}
));
})
);
}
}

View File

@@ -20,7 +20,6 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk::database::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let import = r#"{
@@ -30,43 +29,38 @@
//! }"#;
//!
//! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new(
//! let wallet = Wallet::new_no_persist(
//! &import.descriptor(),
//! import.change_descriptor().as_ref(),
//! Network::Testnet,
//! MemoryDatabase::default(),
//! )?;
//! # Ok::<_, bdk::Error>(())
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
//!
//! ### Export a `Wallet`
//! ```
//! # use bitcoin::*;
//! # use bdk::database::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let wallet = Wallet::new(
//! let wallet = Wallet::new_no_persist(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
//! Network::Testnet,
//! MemoryDatabase::default()
//! )?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
//! .map_err(ToString::to_string)
//! .map_err(bdk::Error::Generic)?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
//!
//! println!("Exported: {}", export.to_string());
//! # Ok::<_, bdk::Error>(())
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
use std::str::FromStr;
use core::str::FromStr;
use alloc::string::{String, ToString};
use serde::{Deserialize, Serialize};
use miniscript::descriptor::{ShInner, WshInner};
use miniscript::{Descriptor, ScriptContext, Terminal};
use crate::database::BatchDatabase;
use crate::types::KeychainKind;
use crate::wallet::Wallet;
@@ -116,7 +110,7 @@ impl FullyNodedExport {
///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`.
pub fn export_wallet<D: BatchDatabase>(
pub fn export_wallet<D>(
wallet: &Wallet<D>,
label: &str,
include_blockheight: bool,
@@ -131,18 +125,16 @@ impl FullyNodedExport {
let descriptor = remove_checksum(descriptor);
Self::is_compatible_with_core(&descriptor)?;
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)
}
let blockheight = if include_blockheight {
wallet
.transactions()
.next()
.map_or(0, |canonical_tx| match canonical_tx.observed_as {
bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
})
} else {
0
};
let export = FullyNodedExport {
@@ -151,11 +143,7 @@ impl FullyNodedExport {
blockheight,
};
let change_descriptor = match wallet
.public_descriptor(KeychainKind::Internal)
.map_err(|_| "Invalid change descriptor")?
.is_some()
{
let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
false => None,
true => {
let descriptor = wallet
@@ -225,36 +213,43 @@ impl FullyNodedExport {
#[cfg(test)]
mod test {
use std::str::FromStr;
use core::str::FromStr;
use bitcoin::{Network, Txid};
use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash;
use bitcoin::{BlockHash, Network, Transaction};
use super::*;
use crate::database::{memory::MemoryDatabase, BatchOperations};
use crate::types::TransactionDetails;
use crate::wallet::Wallet;
use crate::BlockTime;
fn get_test_db() -> MemoryDatabase {
let mut db = MemoryDatabase::new();
db.set_tx(&TransactionDetails {
transaction: None,
txid: Txid::from_str(
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
fn get_test_wallet(
descriptor: &str,
change_descriptor: Option<&str>,
network: Network,
) -> Wallet<()> {
let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
let transaction = Transaction {
input: vec![],
output: vec![],
version: 0,
lock_time: bitcoin::PackedLockTime::ZERO,
};
wallet
.insert_checkpoint(BlockId {
height: 5001,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
transaction,
ConfirmationTime::Confirmed {
height: 5000,
time: 0,
},
)
.unwrap(),
received: 100_000,
sent: 0,
fee: Some(500),
confirmation_time: Some(BlockTime {
timestamp: 12345678,
height: 5000,
}),
})
.unwrap();
db
.unwrap();
wallet
}
#[test]
@@ -262,13 +257,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
@@ -286,7 +275,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
@@ -299,13 +288,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
@@ -322,13 +305,7 @@ mod test {
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))";
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
@@ -342,13 +319,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
get_test_db(),
)
.unwrap();
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");

View File

@@ -15,24 +15,25 @@
//! 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 bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
//! # use hwi::{types::HWIChain, HWIClient};
//! # use std::sync::Arc;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let devices = HWIClient::enumerate()?;
//! let first_device = devices.first().expect("No devices found!");
//! let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
//! 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(
//! # let mut wallet = Wallet::new_no_persist(
//! # "",
//! # None,
//! # Network::Testnet,
//! # MemoryDatabase::default(),
//! # )?;
//! #
//! // Adding the hardware signer to the BDK wallet

1905
crates/bdk/src/wallet/mod.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,13 +15,12 @@
//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function.
//!
//! ```
//! # use std::sync::Arc;
//! # use std::str::FromStr;
//! # use alloc::sync::Arc;
//! # use core::str::FromStr;
//! # use bitcoin::secp256k1::{Secp256k1, All};
//! # use bitcoin::*;
//! # use bitcoin::util::psbt;
//! # use bdk::signer::*;
//! # use bdk::database::*;
//! # use bdk::*;
//! # #[derive(Debug)]
//! # struct CustomHSM;
@@ -70,7 +69,7 @@
//! let custom_signer = CustomSigner::connect();
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
@@ -80,11 +79,12 @@
//! # Ok::<_, bdk::Error>(())
//! ```
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
use std::ops::{Bound::Included, Deref};
use std::sync::Arc;
use crate::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cmp::Ordering;
use core::fmt;
use core::ops::{Bound::Included, Deref};
use bitcoin::blockdata::opcodes;
use bitcoin::blockdata::script::Builder as ScriptBuilder;
@@ -180,10 +180,26 @@ 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),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SignerError {}
/// Signing context
@@ -545,7 +561,7 @@ fn sign_psbt_schnorr(
#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
pub struct SignerOrdering(pub usize);
impl std::default::Default for SignerOrdering {
impl Default for SignerOrdering {
fn default() -> Self {
SignerOrdering(100)
}
@@ -998,11 +1014,12 @@ 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;
use core::str::FromStr;
use miniscript::ScriptContext;
use std::str::FromStr;
fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
let secp = Secp256k1::new();
@@ -1067,17 +1084,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)]

View File

@@ -19,7 +19,7 @@
//! # use bdk::*;
//! # use bdk::wallet::tx_builder::CreateTx;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
//! # let wallet = doctest_wallet!();
//! # let mut wallet = doctest_wallet!();
//! // create a TxBuilder from a wallet
//! let mut tx_builder = wallet.build_tx();
//!
@@ -36,22 +36,25 @@
//! # Ok::<(), bdk::Error>(())
//! ```
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::default::Default;
use std::marker::PhantomData;
use crate::collections::BTreeMap;
use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::PersistBackend;
use core::cell::RefCell;
use core::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
use super::ChangeSet;
use crate::{
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
TransactionDetails,
};
use crate::{Error, Utxo, Wallet};
/// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
/// to bumping the fee of an existing one).
@@ -78,7 +81,7 @@ impl TxBuilderContext for BumpFee {}
/// # use bdk::wallet::tx_builder::*;
/// # use bitcoin::*;
/// # use core::str::FromStr;
/// # let wallet = doctest_wallet!();
/// # let mut wallet = doctest_wallet!();
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// # let addr2 = addr1.clone();
/// // chaining
@@ -116,7 +119,7 @@ impl TxBuilderContext for BumpFee {}
/// [`coin_selection`]: Self::coin_selection
#[derive(Debug)]
pub struct TxBuilder<'a, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<D>,
pub(crate) wallet: Rc<RefCell<&'a mut Wallet<D>>>,
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
@@ -161,16 +164,16 @@ pub(crate) enum FeePolicy {
FeeAmount(u64),
}
impl std::default::Default for FeePolicy {
impl Default for FeePolicy {
fn default() -> Self {
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
}
}
impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
fn clone(&self) -> Self {
TxBuilder {
wallet: self.wallet,
wallet: self.wallet.clone(),
params: self.params.clone(),
coin_selection: self.coin_selection.clone(),
phantom: PhantomData,
@@ -179,9 +182,7 @@ impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
}
// methods supported by both contexts, for any CoinSelectionAlgorithm
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
TxBuilder<'a, D, Cs, Ctx>
{
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
/// Set a custom fee rate
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
@@ -189,6 +190,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
}
/// Set an absolute fee
/// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
/// If anyone sets both the fee_absolute method and the fee_rate method,
/// the fee_absolute value will take precedence over the fee_rate.
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
self
@@ -242,7 +246,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// # use bitcoin::*;
/// # use bdk::*;
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// # let wallet = doctest_wallet!();
/// # let mut wallet = doctest_wallet!();
/// let mut path = BTreeMap::new();
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
///
@@ -274,18 +278,21 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
/// the "utxos" and the "unspendable" list, it will be spent.
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> {
let utxos = outpoints
.iter()
.map(|outpoint| self.wallet.get_utxo(*outpoint)?.ok_or(Error::UnknownUtxo))
.collect::<Result<Vec<_>, _>>()?;
{
let wallet = self.wallet.borrow();
let utxos = outpoints
.iter()
.map(|outpoint| wallet.get_utxo(*outpoint).ok_or(Error::UnknownUtxo))
.collect::<Result<Vec<_>, _>>()?;
for utxo in utxos {
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(utxo),
});
for utxo in utxos {
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(utxo),
});
}
}
Ok(self)
@@ -503,7 +510,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
///
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
pub fn coin_selection<P: CoinSelectionAlgorithm>(
self,
coin_selection: P,
) -> TxBuilder<'a, D, P, Ctx> {
@@ -520,8 +527,13 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
self.wallet.create_tx(self.coin_selection, self.params)
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error>
where
D: PersistBackend<ChangeSet>,
{
self.wallet
.borrow_mut()
.create_tx(self.coin_selection, self.params)
}
/// Enable signaling RBF
@@ -569,7 +581,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
}
}
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self {
self.params.recipients = recipients;
@@ -615,7 +627,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
/// # use bdk::*;
/// # use bdk::wallet::tx_builder::CreateTx;
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// # let wallet = doctest_wallet!();
/// # let mut wallet = doctest_wallet!();
/// let mut tx_builder = wallet.build_tx();
///
/// tx_builder
@@ -640,7 +652,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
}
// methods supported only by bump_fee
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
/// will attempt to find a change output to shrink instead.
@@ -695,14 +707,8 @@ impl TxOrdering {
TxOrdering::Untouched => {}
TxOrdering::Shuffle => {
use rand::seq::SliceRandom;
#[cfg(test)]
use rand::SeedableRng;
#[cfg(not(test))]
let mut rng = rand::thread_rng();
#[cfg(test)]
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
tx.input.shuffle(&mut rng);
tx.output.shuffle(&mut rng);
}
TxOrdering::Bip69Lexicographic => {
@@ -788,6 +794,7 @@ mod test {
};
}
use bdk_chain::ConfirmationTime;
use bitcoin::consensus::deserialize;
use bitcoin::hashes::hex::FromHex;
@@ -813,15 +820,25 @@ mod test {
let original_tx = ordering_test_tx!();
let mut tx = original_tx.clone();
TxOrdering::Shuffle.sort_tx(&mut tx);
(0..40)
.find(|_| {
TxOrdering::Shuffle.sort_tx(&mut tx);
original_tx.input != tx.input
})
.expect("it should have moved the inputs at least once");
assert_eq!(original_tx.input, tx.input);
assert_ne!(original_tx.output, tx.output);
let mut tx = original_tx.clone();
(0..40)
.find(|_| {
TxOrdering::Shuffle.sort_tx(&mut tx);
original_tx.output != tx.output
})
.expect("it should have moved the outputs at least once");
}
#[test]
fn test_output_ordering_bip69() {
use std::str::FromStr;
use core::str::FromStr;
let original_tx = ordering_test_tx!();
let mut tx = original_tx;
@@ -867,6 +884,8 @@ mod test {
txout: Default::default(),
keychain: KeychainKind::External,
is_spent: false,
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
derivation_index: 0,
},
LocalUtxo {
outpoint: OutPoint {
@@ -876,6 +895,11 @@ mod test {
txout: Default::default(),
keychain: KeychainKind::Internal,
is_spent: false,
confirmation_time: ConfirmationTime::Confirmed {
height: 32,
time: 42,
},
derivation_index: 1,
},
]
}

View File

@@ -120,7 +120,7 @@ mod test {
use super::{check_nsequence_rbf, IsDust};
use crate::bitcoin::{Address, Sequence};
use std::str::FromStr;
use core::str::FromStr;
#[test]
fn test_is_dust() {

View File

@@ -0,0 +1,93 @@
#![allow(unused)]
use bdk::{wallet::AddressIndex, Wallet};
use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash;
use bitcoin::{BlockHash, Network, Transaction, TxOut};
/// Return a fake wallet that appears to be funded for testing.
pub fn get_funded_wallet_with_change(
descriptor: &str,
change: Option<&str>,
) -> (Wallet, bitcoin::Txid) {
let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
let address = wallet.get_address(AddressIndex::New).address;
let tx = Transaction {
version: 1,
lock_time: bitcoin::PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 50_000,
script_pubkey: address.script_pubkey(),
}],
};
wallet
.insert_checkpoint(BlockId {
height: 1_000,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
tx.clone(),
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
)
.unwrap();
(wallet, tx.txid())
}
pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
get_funded_wallet_with_change(descriptor, None)
}
pub fn get_test_wpkh() -> &'static str {
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
}
pub fn get_test_single_sig_csv() -> &'static str {
// and(pk(Alice),older(6))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
}
pub fn get_test_a_or_b_plus_csv() -> &'static str {
// or(pk(Alice),and(pk(Bob),older(144)))
"wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
}
pub fn get_test_single_sig_cltv() -> &'static str {
// and(pk(Alice),after(100000))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
}
pub fn get_test_tr_single_sig() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
}
pub fn get_test_tr_with_taptree() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
pub fn get_test_tr_with_taptree_both_priv() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
}
pub fn get_test_tr_repeated_key() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
}
pub fn get_test_tr_single_sig_xprv() -> &'static str {
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
}
pub fn get_test_tr_with_taptree_xprv() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
pub fn get_test_tr_dup_keys() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}

158
crates/bdk/tests/psbt.rs Normal file
View File

@@ -0,0 +1,158 @@
use bdk::bitcoin::TxIn;
use bdk::wallet::AddressIndex;
use bdk::wallet::AddressIndex::New;
use bdk::{psbt, FeeRate, SignOptions};
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use core::str::FromStr;
mod common;
use common::*;
// from bip 174
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_legacy() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[0].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_segwit() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[1].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_tx_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
fn test_psbt_sign_with_finalized() {
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
// add a finalized input
psbt.inputs.push(psbt_bip.inputs[0].clone());
psbt.unsigned_tx
.input
.push(psbt_bip.unsigned_tx.input[0].clone());
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 (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New);
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 (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New);
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 (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wpkh_wallet.get_address(New);
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 (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = pkh_wallet.get_address(New);
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());
}

3376
crates/bdk/tests/wallet.rs Normal file

File diff suppressed because it is too large Load Diff

31
crates/chain/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "bdk_chain"
version = "0.5.0"
edition = "2021"
rust-version = "1.57"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_chain"
description = "Collection of core structures for Bitcoin Dev Kit."
license = "MIT OR Apache-2.0"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# For no-std, remember to enable the bitcoin/no-std feature
bitcoin = { version = "0.29", default-features = false }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
# note version 0.13 breaks outs MSRV.
hashbrown = { version = "0.11", optional = true, features = ["serde"] }
miniscript = { version = "9.0.0", optional = true, default-features = false }
[dev-dependencies]
rand = "0.8"
[features]
default = ["std"]
std = ["bitcoin/std", "miniscript/std"]
serde = ["serde_crate", "bitcoin/serde"]

3
crates/chain/README.md Normal file
View File

@@ -0,0 +1,3 @@
# BDK Chain
BDK keychain tracker, tools for storing and indexing chain data.

View File

@@ -0,0 +1,258 @@
use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid};
use crate::{Anchor, COINBASE_MATURITY};
/// Represents the observed position of some chain data.
///
/// The generic `A` should be a [`Anchor`] implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
pub enum ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
/// The chain data is seen in mempool at this given timestamp.
Unconfirmed(u64),
}
impl<A> ChainPosition<A> {
/// Returns whether [`ChainPosition`] is confirmed or not.
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed(_))
}
}
impl<A: Clone> ChainPosition<&A> {
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
pub fn cloned(self) -> ChainPosition<A> {
match self {
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
}
}
}
impl<A: Anchor> ChainPosition<A> {
/// Determines the upper bound of the confirmation height.
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
match self {
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
ChainPosition::Unconfirmed(_) => None,
}
}
}
/// Block height and timestamp at which a transaction is confirmed.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub enum ConfirmationTime {
/// The confirmed variant.
Confirmed {
/// Confirmation height.
height: u32,
/// Confirmation time in unix seconds.
time: u64,
},
/// The unconfirmed variant.
Unconfirmed {
/// The last-seen timestamp in unix seconds.
last_seen: u64,
},
}
impl ConfirmationTime {
/// Construct an unconfirmed variant using the given `last_seen` time in unix seconds.
pub fn unconfirmed(last_seen: u64) -> Self {
Self::Unconfirmed { last_seen }
}
/// Returns whether [`ConfirmationTime`] is the confirmed variant.
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed { .. })
}
}
impl From<ChainPosition<ConfirmationTimeAnchor>> for ConfirmationTime {
fn from(observed_as: ChainPosition<ConfirmationTimeAnchor>) -> Self {
match observed_as {
ChainPosition::Confirmed(a) => Self::Confirmed {
height: a.confirmation_height,
time: a.confirmation_time,
},
ChainPosition::Unconfirmed(_) => Self::Unconfirmed { last_seen: 0 },
}
}
}
/// A reference to a block in the canonical chain.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct BlockId {
/// The height of the block.
pub height: u32,
/// The hash of the block.
pub hash: BlockHash,
}
impl Default for BlockId {
fn default() -> Self {
Self {
height: Default::default(),
hash: BlockHash::from_inner([0u8; 32]),
}
}
}
impl From<(u32, BlockHash)> for BlockId {
fn from((height, hash): (u32, BlockHash)) -> Self {
Self { height, hash }
}
}
impl From<BlockId> for (u32, BlockHash) {
fn from(block_id: BlockId) -> Self {
(block_id.height, block_id.hash)
}
}
impl From<(&u32, &BlockHash)> for BlockId {
fn from((height, hash): (&u32, &BlockHash)) -> Self {
Self {
height: *height,
hash: *hash,
}
}
}
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationHeightAnchor {
/// The anchor block.
pub anchor_block: BlockId,
/// The exact confirmation height of the transaction.
///
/// It is assumed that this value is never larger than the height of the anchor block.
pub confirmation_height: u32,
}
impl Anchor for ConfirmationHeightAnchor {
fn anchor_block(&self) -> BlockId {
self.anchor_block
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
}
}
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
/// transaction.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationTimeAnchor {
/// The anchor block.
pub anchor_block: BlockId,
/// The confirmation height of the chain data being anchored.
pub confirmation_height: u32,
/// The confirmation time of the chain data being anchored.
pub confirmation_time: u64,
}
impl Anchor for ConfirmationTimeAnchor {
fn anchor_block(&self) -> BlockId {
self.anchor_block
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
}
}
/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FullTxOut<A> {
/// The location of the `TxOut`.
pub outpoint: OutPoint,
/// The `TxOut`.
pub txout: TxOut,
/// The position of the transaction in `outpoint` in the overall chain.
pub chain_position: ChainPosition<A>,
/// The txid and chain position of the transaction (if any) that has spent this output.
pub spent_by: Option<(ChainPosition<A>, Txid)>,
/// Whether this output is on a coinbase transaction.
pub is_on_coinbase: bool,
}
impl<A: Anchor> FullTxOut<A> {
/// Whether the `txout` is considered mature.
///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpretted confirmation count may be
/// less than the actual value.
///
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_mature(&self, tip: u32) -> bool {
if self.is_on_coinbase {
let tx_height = match &self.chain_position {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Unconfirmed(_) => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
}
};
let age = tip.saturating_sub(tx_height);
if age + 1 < COINBASE_MATURITY {
return false;
}
}
true
}
/// Whether the utxo is/was/will be spendable with chain `tip`.
///
/// This method does not take into account the locktime.
///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// method may return false-negatives. In other words, interpretted confirmation count may be
/// less than the actual value.
///
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
if !self.is_mature(tip) {
return false;
}
let confirmation_height = match &self.chain_position {
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
ChainPosition::Unconfirmed(_) => return false,
};
if confirmation_height > tip {
return false;
}
// if the spending tx is confirmed within tip height, the txout is no longer spendable
if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
if spending_anchor.anchor_block().height <= tip {
return false;
}
}
true
}
}

View File

@@ -0,0 +1,25 @@
use crate::BlockId;
/// Represents a service that tracks the blockchain.
///
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
/// is an ancestor of another "static block".
///
/// [`is_block_in_chain`]: Self::is_block_in_chain
pub trait ChainOracle {
/// Error type.
type Error: core::fmt::Debug;
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `chain_tip`.
///
/// If `None` is returned, it means the implementation cannot determine whether `block` exists
/// under `chain_tip`.
fn is_block_in_chain(
&self,
block: BlockId,
chain_tip: BlockId,
) -> Result<Option<bool>, Self::Error>;
/// Get the best chain's chain tip.
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>;
}

View File

@@ -0,0 +1,16 @@
use crate::miniscript::{Descriptor, DescriptorPublicKey};
/// A trait to extend the functionality of a miniscript descriptor.
pub trait DescriptorExt {
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
fn dust_value(&self) -> u64;
}
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
fn dust_value(&self) -> u64 {
self.at_derivation_index(0)
.script_pubkey()
.dust_value()
.to_sat()
}
}

View File

@@ -0,0 +1,30 @@
#![allow(unused)]
use alloc::vec::Vec;
use bitcoin::{
consensus,
hashes::{hex::FromHex, Hash},
Transaction,
};
use crate::BlockId;
pub const RAW_TX_1: &str = "0200000000010116d6174da7183d70d0a7d4dc314d517a7d135db79ad63515028b293a76f4f9d10000000000feffffff023a21fc8350060000160014531c405e1881ef192294b8813631e258bf98ea7a1027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c024730440220591b1a172a122da49ba79a3e79f98aaa03fd7a372f9760da18890b6a327e6010022013e82319231da6c99abf8123d7c07e13cf9bd8d76e113e18dc452e5024db156d012102318a2d558b2936c52e320decd6d92a88d7f530be91b6fe0af5caf41661e77da3ef2e0100";
pub const RAW_TX_2: &str = "02000000000101a688607020cfae91a61e7c516b5ef1264d5d77f17200c3866826c6c808ebf1620000000000feffffff021027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c20fd48ff530600001600146886c525e41d4522042bd0b159dfbade2504a6bb024730440220740ff7e665cd20565d4296b549df8d26b941be3f1e3af89a0b60e50c0dbeb69a02206213ab7030cf6edc6c90d4ccf33010644261e029950a688dc0b1a9ebe6ddcc5a012102f2ac6b396a97853cb6cd62242c8ae4842024742074475023532a51e9c53194253e760100";
pub const RAW_TX_3: &str = "0200000000010135d67ee47b557e68b8c6223958f597381965ed719f1207ee2b9e20432a24a5dc0100000000feffffff021027000000000000225120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb62215a5055060000160014070df7671dea67a50c4799a744b5c9be8f4bac690247304402207ebf8d29f71fd03e7e6977b3ea78ca5fcc5c49a42ae822348fc401862fdd766c02201d7e4ff0684ecb008b6142f36ead1b0b4d615524c4f58c261113d361f4427e25012103e6a75e2fab85e5ecad641afc4ffba7222f998649d9f18cac92f0fcc8618883b3ee760100";
pub const RAW_TX_4: &str = "02000000000101d00e8f76ed313e19b339ee293c0f52b0325c95e24c8f3966fa353fb2bedbcf580100000000feffffff021027000000000000225120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc9cda55fe53060000160014852b5864b8edd42fab4060c87f818e50780865ff0247304402201dccbb9bed7fba924b6d249c5837cc9b37470c0e3d8fbea77cb59baba3efe6fa0220700cc170916913b9bfc2bc0fefb6af776e8b542c561702f136cddc1c7aa43141012103acec3fc79dbbca745815c2a807dc4e81010c80e308e84913f59cb42a275dad97f3760100";
pub fn tx_from_hex(s: &str) -> Transaction {
let raw = Vec::from_hex(s).expect("data must be in hex");
consensus::deserialize(raw.as_slice()).expect("must deserialize")
}
pub fn new_hash<H: Hash>(s: &str) -> H {
<H as bitcoin::hashes::Hash>::hash(s.as_bytes())
}
pub fn new_block_id(height: u32, hash: &str) -> BlockId {
BlockId {
height,
hash: new_hash(hash),
}
}

View File

@@ -0,0 +1,245 @@
//! Contains the [`IndexedTxGraph`] structure and associated types.
//!
//! This is essentially a [`TxGraph`] combined with an indexer.
use alloc::vec::Vec;
use bitcoin::{OutPoint, Transaction, TxOut};
use crate::{
keychain::DerivationAdditions,
tx_graph::{Additions, TxGraph},
Anchor, Append,
};
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
///
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
#[derive(Debug)]
pub struct IndexedTxGraph<A, I> {
/// Transaction index.
pub index: I,
graph: TxGraph<A>,
}
impl<A, I: Default> Default for IndexedTxGraph<A, I> {
fn default() -> Self {
Self {
graph: Default::default(),
index: Default::default(),
}
}
}
impl<A, I> IndexedTxGraph<A, I> {
/// Construct a new [`IndexedTxGraph`] with a given `index`.
pub fn new(index: I) -> Self {
Self {
index,
graph: TxGraph::default(),
}
}
/// Get a reference of the internal transaction graph.
pub fn graph(&self) -> &TxGraph<A> {
&self.graph
}
}
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
/// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`].
pub fn apply_additions(&mut self, additions: IndexedAdditions<A, I::Additions>) {
let IndexedAdditions {
graph_additions,
index_additions,
} = additions;
self.index.apply_additions(index_additions);
for tx in &graph_additions.txs {
self.index.index_tx(tx);
}
for (&outpoint, txout) in &graph_additions.txouts {
self.index.index_txout(outpoint, txout);
}
self.graph.apply_additions(graph_additions);
}
}
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
where
I::Additions: Default + Append,
{
/// Apply an `update` directly.
///
/// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`IndexedAdditions`].
pub fn apply_update(&mut self, update: TxGraph<A>) -> IndexedAdditions<A, I::Additions> {
let graph_additions = self.graph.apply_update(update);
let mut index_additions = I::Additions::default();
for added_tx in &graph_additions.txs {
index_additions.append(self.index.index_tx(added_tx));
}
for (&added_outpoint, added_txout) in &graph_additions.txouts {
index_additions.append(self.index.index_txout(added_outpoint, added_txout));
}
IndexedAdditions {
graph_additions,
index_additions,
}
}
/// Insert a floating `txout` of given `outpoint`.
pub fn insert_txout(
&mut self,
outpoint: OutPoint,
txout: &TxOut,
) -> IndexedAdditions<A, I::Additions> {
let mut update = TxGraph::<A>::default();
let _ = update.insert_txout(outpoint, txout.clone());
self.apply_update(update)
}
/// Insert and index a transaction into the graph.
///
/// `anchors` can be provided to anchor the transaction to various blocks. `seen_at` is a
/// unix timestamp of when the transaction is last seen.
pub fn insert_tx(
&mut self,
tx: &Transaction,
anchors: impl IntoIterator<Item = A>,
seen_at: Option<u64>,
) -> IndexedAdditions<A, I::Additions> {
let txid = tx.txid();
let mut update = TxGraph::<A>::default();
if self.graph.get_tx(txid).is_none() {
let _ = update.insert_tx(tx.clone());
}
for anchor in anchors.into_iter() {
let _ = update.insert_anchor(txid, anchor);
}
if let Some(seen_at) = seen_at {
let _ = update.insert_seen_at(txid, seen_at);
}
self.apply_update(update)
}
/// Insert relevant transactions from the given `txs` iterator.
///
/// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant
/// transactions in `txs` will be ignored. `txs` do not need to be in topological order.
///
/// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix
/// timestamp of when the transactions are last seen.
pub fn insert_relevant_txs<'t>(
&mut self,
txs: impl IntoIterator<Item = (&'t Transaction, impl IntoIterator<Item = A>)>,
seen_at: Option<u64>,
) -> IndexedAdditions<A, I::Additions> {
// The algorithm below allows for non-topologically ordered transactions by using two loops.
// This is achieved by:
// 1. insert all txs into the index. If they are irrelevant then that's fine it will just
// not store anything about them.
// 2. decide whether to insert them into the graph depending on whether `is_tx_relevant`
// returns true or not. (in a second loop).
let mut additions = IndexedAdditions::<A, I::Additions>::default();
let mut transactions = Vec::new();
for (tx, anchors) in txs.into_iter() {
additions.index_additions.append(self.index.index_tx(tx));
transactions.push((tx, anchors));
}
additions.append(
transactions
.into_iter()
.filter_map(|(tx, anchors)| match self.index.is_tx_relevant(tx) {
true => Some(self.insert_tx(tx, anchors, seen_at)),
false => None,
})
.fold(Default::default(), |mut acc, other| {
acc.append(other);
acc
}),
);
additions
}
}
/// A structure that represents changes to an [`IndexedTxGraph`].
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(
crate = "serde_crate",
bound(
deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>",
serialize = "A: Ord + serde::Serialize, IA: serde::Serialize"
)
)
)]
#[must_use]
pub struct IndexedAdditions<A, IA> {
/// [`TxGraph`] additions.
pub graph_additions: Additions<A>,
/// [`Indexer`] additions.
pub index_additions: IA,
}
impl<A, IA: Default> Default for IndexedAdditions<A, IA> {
fn default() -> Self {
Self {
graph_additions: Default::default(),
index_additions: Default::default(),
}
}
}
impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> {
fn append(&mut self, other: Self) {
self.graph_additions.append(other.graph_additions);
self.index_additions.append(other.index_additions);
}
fn is_empty(&self) -> bool {
self.graph_additions.is_empty() && self.index_additions.is_empty()
}
}
impl<A, IA: Default> From<Additions<A>> for IndexedAdditions<A, IA> {
fn from(graph_additions: Additions<A>) -> Self {
Self {
graph_additions,
..Default::default()
}
}
}
impl<A, K> From<DerivationAdditions<K>> for IndexedAdditions<A, DerivationAdditions<K>> {
fn from(index_additions: DerivationAdditions<K>) -> Self {
Self {
graph_additions: Default::default(),
index_additions,
}
}
}
/// Represents a structure that can index transaction data.
pub trait Indexer {
/// The resultant "additions" when new transaction data is indexed.
type Additions;
/// Scan and index the given `outpoint` and `txout`.
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions;
/// Scan and index the given transaction.
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions;
/// Apply additions to itself.
fn apply_additions(&mut self, additions: Self::Additions);
/// Determines whether the transaction should be included in the index.
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
}

View File

@@ -0,0 +1,266 @@
//! Module for keychain related structures.
//!
//! A keychain here is a set of application-defined indexes for a miniscript descriptor where we can
//! derive script pubkeys at a particular derivation index. The application's index is simply
//! anything that implements `Ord`.
//!
//! [`KeychainTxOutIndex`] indexes script pubkeys of keychains and scans in relevant outpoints (that
//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but
//! also maintains "revealed" and "lookahead" index counts per keychain.
//!
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
use crate::{
collections::BTreeMap,
indexed_tx_graph::IndexedAdditions,
local_chain::{self, LocalChain},
tx_graph::TxGraph,
Anchor, Append,
};
#[cfg(feature = "miniscript")]
mod txout_index;
#[cfg(feature = "miniscript")]
pub use txout_index::*;
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
///
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_additions`]. [`DerivationAdditions] are
/// monotone in that they will never decrease the revealed derivation index.
///
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
/// [`apply_additions`]: crate::keychain::KeychainTxOutIndex::apply_additions
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(
crate = "serde_crate",
bound(
deserialize = "K: Ord + serde::Deserialize<'de>",
serialize = "K: Ord + serde::Serialize"
)
)
)]
#[must_use]
pub struct DerivationAdditions<K>(pub BTreeMap<K, u32>);
impl<K> DerivationAdditions<K> {
/// Returns whether the additions are empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Get the inner map of the keychain to its new derivation index.
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
&self.0
}
}
impl<K: Ord> Append for DerivationAdditions<K> {
/// Append another [`DerivationAdditions`] into self.
///
/// If the keychain already exists, increase the index when the other's index > self's index.
/// If the keychain did not exist, append the new keychain.
fn append(&mut self, mut other: Self) {
self.0.iter_mut().for_each(|(key, index)| {
if let Some(other_index) = other.0.remove(key) {
*index = other_index.max(*index);
}
});
self.0.append(&mut other.0);
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<K> Default for DerivationAdditions<K> {
fn default() -> Self {
Self(Default::default())
}
}
impl<K> AsRef<BTreeMap<K, u32>> for DerivationAdditions<K> {
fn as_ref(&self) -> &BTreeMap<K, u32> {
&self.0
}
}
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`]
/// atomically.
#[derive(Debug, Clone, PartialEq)]
pub struct LocalUpdate<K, A> {
/// Last active derivation index per keychain (`K`).
pub keychain: BTreeMap<K, u32>,
/// Update for the [`TxGraph`].
pub graph: TxGraph<A>,
/// Update for the [`LocalChain`].
pub chain: LocalChain,
}
impl<K, A> Default for LocalUpdate<K, A> {
fn default() -> Self {
Self {
keychain: Default::default(),
graph: Default::default(),
chain: Default::default(),
}
}
}
/// A structure that records the corresponding changes as result of applying an [`LocalUpdate`].
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(
crate = "serde_crate",
bound(
deserialize = "K: Ord + serde::Deserialize<'de>, A: Ord + serde::Deserialize<'de>",
serialize = "K: Ord + serde::Serialize, A: Ord + serde::Serialize",
)
)
)]
pub struct LocalChangeSet<K, A> {
/// Changes to the [`LocalChain`].
pub chain_changeset: local_chain::ChangeSet,
/// Additions to [`IndexedTxGraph`].
///
/// [`IndexedTxGraph`]: crate::indexed_tx_graph::IndexedTxGraph
pub indexed_additions: IndexedAdditions<A, DerivationAdditions<K>>,
}
impl<K, A> Default for LocalChangeSet<K, A> {
fn default() -> Self {
Self {
chain_changeset: Default::default(),
indexed_additions: Default::default(),
}
}
}
impl<K: Ord, A: Anchor> Append for LocalChangeSet<K, A> {
fn append(&mut self, other: Self) {
Append::append(&mut self.chain_changeset, other.chain_changeset);
Append::append(&mut self.indexed_additions, other.indexed_additions);
}
fn is_empty(&self) -> bool {
self.chain_changeset.is_empty() && self.indexed_additions.is_empty()
}
}
impl<K, A> From<local_chain::ChangeSet> for LocalChangeSet<K, A> {
fn from(chain_changeset: local_chain::ChangeSet) -> Self {
Self {
chain_changeset,
..Default::default()
}
}
}
impl<K, A> From<IndexedAdditions<A, DerivationAdditions<K>>> for LocalChangeSet<K, A> {
fn from(indexed_additions: IndexedAdditions<A, DerivationAdditions<K>>) -> Self {
Self {
indexed_additions,
..Default::default()
}
}
}
/// Balance, differentiated into various categories.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate",)
)]
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.
///
/// This is the balance you can spend right now that shouldn't get cancelled via another party
/// double spending it.
pub fn trusted_spendable(&self) -> u64 {
self.confirmed + self.trusted_pending
}
/// Get the whole balance visible to the wallet.
pub fn total(&self) -> u64 {
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
}
}
impl core::fmt::Display for Balance {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
)
}
}
impl core::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,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn append_keychain_derivation_indices() {
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
enum Keychain {
One,
Two,
Three,
Four,
}
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
lhs_di.insert(Keychain::One, 7);
lhs_di.insert(Keychain::Two, 0);
rhs_di.insert(Keychain::One, 3);
rhs_di.insert(Keychain::Two, 5);
lhs_di.insert(Keychain::Three, 3);
rhs_di.insert(Keychain::Four, 4);
let mut lhs = DerivationAdditions(lhs_di);
let rhs = DerivationAdditions(rhs_di);
lhs.append(rhs);
// Exiting index doesn't update if the new index in `other` is lower than `self`.
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
// Existing index updates if the new index in `other` is higher than `self`.
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
// Existing index is unchanged if keychain doesn't exist in `other`.
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
// New keychain gets added if the keychain is in `other` but not in `self`.
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
}
}

View File

@@ -0,0 +1,588 @@
use crate::{
collections::*,
indexed_tx_graph::Indexer,
miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::BIP32_MAX_INDEX,
ForEachTxOut, SpkIterator, SpkTxOutIndex,
};
use alloc::vec::Vec;
use bitcoin::{OutPoint, Script, TxOut};
use core::{fmt::Debug, ops::Deref};
use crate::Append;
use super::DerivationAdditions;
/// A convenient wrapper around [`SpkTxOutIndex`] that relates script pubkeys to miniscript public
/// [`Descriptor`]s.
///
/// Descriptors are referenced by the provided keychain generic (`K`).
///
/// Script pubkeys for a descriptor are revealed chronologically from index 0. I.e., If the last
/// revealed index of a descriptor is 5; scripts of indices 0 to 4 are guaranteed to be already
/// revealed. In addition to revealed scripts, we have a `lookahead` parameter for each keychain,
/// which defines the number of script pubkeys to store ahead of the last revealed index.
///
/// Methods that could update the last revealed index will return [`DerivationAdditions`] to report
/// these changes. This can be persisted for future recovery.
///
/// ## Synopsis
///
/// ```
/// use bdk_chain::keychain::KeychainTxOutIndex;
/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} };
/// # use core::str::FromStr;
///
/// // imagine our service has internal and external addresses but also addresses for users
/// #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
/// enum MyKeychain {
/// External,
/// Internal,
/// MyAppUser {
/// user_id: u32
/// }
/// }
///
/// let mut txout_index = KeychainTxOutIndex::<MyKeychain>::default();
///
/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
/// # let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
/// # let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
/// # let descriptor_for_user_42 = external_descriptor.clone();
/// txout_index.add_keychain(MyKeychain::External, external_descriptor);
/// txout_index.add_keychain(MyKeychain::Internal, internal_descriptor);
/// txout_index.add_keychain(MyKeychain::MyAppUser { user_id: 42 }, descriptor_for_user_42);
///
/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 });
/// ```
///
/// [`Ord`]: core::cmp::Ord
/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex
/// [`Descriptor`]: crate::miniscript::Descriptor
#[derive(Clone, Debug)]
pub struct KeychainTxOutIndex<K> {
inner: SpkTxOutIndex<(K, u32)>,
// descriptors of each keychain
keychains: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
// last revealed indexes
last_revealed: BTreeMap<K, u32>,
// lookahead settings for each keychain
lookahead: BTreeMap<K, u32>,
}
impl<K> Default for KeychainTxOutIndex<K> {
fn default() -> Self {
Self {
inner: SpkTxOutIndex::default(),
keychains: BTreeMap::default(),
last_revealed: BTreeMap::default(),
lookahead: BTreeMap::default(),
}
}
}
impl<K> Deref for KeychainTxOutIndex<K> {
type Target = SpkTxOutIndex<(K, u32)>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
type Additions = DerivationAdditions<K>;
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
self.scan_txout(outpoint, txout)
}
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions {
self.scan(tx)
}
fn apply_additions(&mut self, additions: Self::Additions) {
self.apply_additions(additions)
}
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
self.is_relevant(tx)
}
}
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Scans an object for relevant outpoints, which are stored and indexed internally.
///
/// If the matched script pubkey is part of the lookahead, the last stored index is updated for
/// the script pubkey's keychain and the [`DerivationAdditions`] returned will reflect the
/// change.
///
/// Typically, this method is used in two situations:
///
/// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
/// your txouts.
/// 2. When getting new data from the chain, you usually scan it before incorporating it into
/// your chain state (i.e., `SparseChain`, `ChainGraph`).
///
/// See [`ForEachTxout`] for the types that support this.
///
/// [`ForEachTxout`]: crate::ForEachTxOut
pub fn scan(&mut self, txouts: &impl ForEachTxOut) -> DerivationAdditions<K> {
let mut additions = DerivationAdditions::<K>::default();
txouts.for_each_txout(|(op, txout)| additions.append(self.scan_txout(op, txout)));
additions
}
/// Scan a single outpoint for a matching script pubkey.
///
/// If it matches, this will store and index it.
pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> DerivationAdditions<K> {
match self.inner.scan_txout(op, txout).cloned() {
Some((keychain, index)) => self.reveal_to_target(&keychain, index).1,
None => DerivationAdditions::default(),
}
}
/// Return a reference to the internal [`SpkTxOutIndex`].
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
&self.inner
}
/// Get a reference to the set of indexed outpoints.
pub fn outpoints(&self) -> &BTreeSet<((K, u32), OutPoint)> {
self.inner.outpoints()
}
/// Return a reference to the internal map of the keychain to descriptors.
pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
&self.keychains
}
/// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses.
///
/// Adding a keychain means you will be able to derive new script pubkeys under that keychain
/// and the txout index will discover transaction outputs with those script pubkeys.
///
/// # Panics
///
/// This will panic if a different `descriptor` is introduced to the same `keychain`.
pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
let old_descriptor = &*self
.keychains
.entry(keychain)
.or_insert_with(|| descriptor.clone());
assert_eq!(
&descriptor, old_descriptor,
"keychain already contains a different descriptor"
);
}
/// Return the lookahead setting for each keychain.
///
/// Refer to [`set_lookahead`] for a deeper explanation of the `lookahead`.
///
/// [`set_lookahead`]: Self::set_lookahead
pub fn lookaheads(&self) -> &BTreeMap<K, u32> {
&self.lookahead
}
/// Convenience method to call [`set_lookahead`] for all keychains.
///
/// [`set_lookahead`]: Self::set_lookahead
pub fn set_lookahead_for_all(&mut self, lookahead: u32) {
for keychain in &self.keychains.keys().cloned().collect::<Vec<_>>() {
self.lookahead.insert(keychain.clone(), lookahead);
self.replenish_lookahead(keychain);
}
}
/// Set the lookahead count for `keychain`.
///
/// The lookahead is the number of scripts to cache ahead of the last stored script index. This
/// is useful during a scan via [`scan`] or [`scan_txout`].
///
/// # Panics
///
/// This will panic if the `keychain` does not exist.
///
/// [`scan`]: Self::scan
/// [`scan_txout`]: Self::scan_txout
pub fn set_lookahead(&mut self, keychain: &K, lookahead: u32) {
self.lookahead.insert(keychain.clone(), lookahead);
self.replenish_lookahead(keychain);
}
/// Convenience method to call [`lookahead_to_target`] for multiple keychains.
///
/// [`lookahead_to_target`]: Self::lookahead_to_target
pub fn lookahead_to_target_multi(&mut self, target_indexes: BTreeMap<K, u32>) {
for (keychain, target_index) in target_indexes {
self.lookahead_to_target(&keychain, target_index)
}
}
/// Store lookahead scripts until `target_index`.
///
/// This does not change the `lookahead` setting.
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
let next_index = self.next_store_index(keychain);
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
let old_lookahead = self.lookahead.insert(keychain.clone(), temp_lookahead);
self.replenish_lookahead(keychain);
// revert
match old_lookahead {
Some(lookahead) => self.lookahead.insert(keychain.clone(), lookahead),
None => self.lookahead.remove(keychain),
};
}
}
fn replenish_lookahead(&mut self, keychain: &K) {
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
let next_store_index = self.next_store_index(keychain);
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v);
for (new_index, new_spk) in
SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead)
{
let _inserted = self
.inner
.insert_spk((keychain.clone(), new_index), new_spk);
debug_assert!(_inserted, "replenish lookahead: must not have existing spk: keychain={:?}, lookahead={}, next_store_index={}, next_reveal_index={}", keychain, lookahead, next_store_index, next_reveal_index);
}
}
fn next_store_index(&self, keychain: &K) -> u32 {
self.inner()
.all_spks()
.range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
.last()
.map_or(0, |((_, v), _)| *v + 1)
}
/// Generates script pubkey iterators for every `keychain`. The iterators iterate over all
/// derivable script pubkeys.
pub fn spks_of_all_keychains(
&self,
) -> BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> {
self.keychains
.iter()
.map(|(keychain, descriptor)| {
(
keychain.clone(),
SpkIterator::new_with_range(descriptor.clone(), 0..),
)
})
.collect()
}
/// Generates a script pubkey iterator for the given `keychain`'s descriptor (if it exists). The
/// iterator iterates over all derivable scripts of the keychain's descriptor.
///
/// # Panics
///
/// This will panic if the `keychain` does not exist.
pub fn spks_of_keychain(&self, keychain: &K) -> SpkIterator<Descriptor<DescriptorPublicKey>> {
let descriptor = self
.keychains
.get(keychain)
.expect("keychain must exist")
.clone();
SpkIterator::new_with_range(descriptor, 0..)
}
/// Convenience method to get [`revealed_spks_of_keychain`] of all keychains.
///
/// [`revealed_spks_of_keychain`]: Self::revealed_spks_of_keychain
pub fn revealed_spks_of_all_keychains(
&self,
) -> BTreeMap<K, impl Iterator<Item = (u32, &Script)> + Clone> {
self.keychains
.keys()
.map(|keychain| (keychain.clone(), self.revealed_spks_of_keychain(keychain)))
.collect()
}
/// Iterates over the script pubkeys revealed by this index under `keychain`.
pub fn revealed_spks_of_keychain(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
let next_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
self.inner
.all_spks()
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_index))
.map(|((_, derivation_index), spk)| (*derivation_index, spk))
}
/// Get the next derivation index for `keychain`. The next index is the index after the last revealed
/// derivation index.
///
/// The second field in the returned tuple represents whether the next derivation index is new.
/// There are two scenarios where the next derivation index is reused (not new):
///
/// 1. The keychain's descriptor has no wildcard, and a script has already been revealed.
/// 2. The number of revealed scripts has already reached 2^31 (refer to BIP-32).
///
/// Not checking the second field of the tuple may result in address reuse.
///
/// # Panics
///
/// Panics if the `keychain` does not exist.
pub fn next_index(&self, keychain: &K) -> (u32, bool) {
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
let last_index = self.last_revealed.get(keychain).cloned();
// we can only get the next index if the wildcard exists.
let has_wildcard = descriptor.has_wildcard();
match last_index {
// if there is no index, next_index is always 0.
None => (0, true),
// descriptors without wildcards can only have one index.
Some(_) if !has_wildcard => (0, false),
// derivation index must be < 2^31 (BIP-32).
Some(index) if index > BIP32_MAX_INDEX => {
unreachable!("index is out of bounds")
}
Some(index) if index == BIP32_MAX_INDEX => (index, false),
// get the next derivation index.
Some(index) => (index + 1, true),
}
}
/// Get the last derivation index that is revealed for each keychain.
///
/// Keychains with no revealed indices will not be included in the returned [`BTreeMap`].
pub fn last_revealed_indices(&self) -> &BTreeMap<K, u32> {
&self.last_revealed
}
/// Get the last derivation index revealed for `keychain`.
pub fn last_revealed_index(&self, keychain: &K) -> Option<u32> {
self.last_revealed.get(keychain).cloned()
}
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
pub fn reveal_to_target_multi(
&mut self,
keychains: &BTreeMap<K, u32>,
) -> (
BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>>,
DerivationAdditions<K>,
) {
let mut additions = DerivationAdditions::default();
let mut spks = BTreeMap::new();
for (keychain, &index) in keychains {
let (new_spks, new_additions) = self.reveal_to_target(keychain, index);
if !new_additions.is_empty() {
spks.insert(keychain.clone(), new_spks);
additions.append(new_additions.clone());
}
}
(spks, additions)
}
/// Reveals script pubkeys of the `keychain`'s descriptor **up to and including** the
/// `target_index`.
///
/// If the `target_index` cannot be reached (due to the descriptor having no wildcard and/or
/// the `target_index` is in the hardened index range), this method will make a best-effort and
/// reveal up to the last possible index.
///
/// This returns an iterator of newly revealed indices (alongside their scripts) and a
/// [`DerivationAdditions`], which reports updates to the latest revealed index. If no new script
/// pubkeys are revealed, then both of these will be empty.
///
/// # Panics
///
/// Panics if `keychain` does not exist.
pub fn reveal_to_target(
&mut self,
keychain: &K,
target_index: u32,
) -> (
SpkIterator<Descriptor<DescriptorPublicKey>>,
DerivationAdditions<K>,
) {
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
let has_wildcard = descriptor.has_wildcard();
let target_index = if has_wildcard { target_index } else { 0 };
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
let lookahead = self.lookahead.get(keychain).map_or(0, |v| *v);
debug_assert_eq!(
next_reveal_index + lookahead,
self.next_store_index(keychain)
);
// if we need to reveal new indices, the latest revealed index goes here
let mut reveal_to_index = None;
// if the target is not yet revealed, but is already stored (due to lookahead), we need to
// set the `reveal_to_index` as target here (as the `for` loop below only updates
// `reveal_to_index` for indexes that are NOT stored)
if next_reveal_index <= target_index && target_index < next_reveal_index + lookahead {
reveal_to_index = Some(target_index);
}
// we range over indexes that are not stored
let range = next_reveal_index + lookahead..=target_index + lookahead;
for (new_index, new_spk) in SpkIterator::new_with_range(descriptor, range) {
let _inserted = self
.inner
.insert_spk((keychain.clone(), new_index), new_spk);
debug_assert!(_inserted, "must not have existing spk",);
// everything after `target_index` is stored for lookahead only
if new_index <= target_index {
reveal_to_index = Some(new_index);
}
}
match reveal_to_index {
Some(index) => {
let _old_index = self.last_revealed.insert(keychain.clone(), index);
debug_assert!(_old_index < Some(index));
(
SpkIterator::new_with_range(descriptor.clone(), next_reveal_index..index + 1),
DerivationAdditions(core::iter::once((keychain.clone(), index)).collect()),
)
}
None => (
SpkIterator::new_with_range(
descriptor.clone(),
next_reveal_index..next_reveal_index,
),
DerivationAdditions::default(),
),
}
}
/// Attempts to reveal the next script pubkey for `keychain`.
///
/// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a
/// [`DerivationAdditions`] which represents changes in the last revealed index (if any).
///
/// When a new script cannot be revealed, we return the last revealed script and an empty
/// [`DerivationAdditions`]. There are two scenarios when a new script pubkey cannot be derived:
///
/// 1. The descriptor has no wildcard and already has one script revealed.
/// 2. The descriptor has already revealed scripts up to the numeric bound.
///
/// # Panics
///
/// Panics if the `keychain` does not exist.
pub fn reveal_next_spk(&mut self, keychain: &K) -> ((u32, &Script), DerivationAdditions<K>) {
let (next_index, _) = self.next_index(keychain);
let additions = self.reveal_to_target(keychain, next_index).1;
let script = self
.inner
.spk_at_index(&(keychain.clone(), next_index))
.expect("script must already be stored");
((next_index, script), additions)
}
/// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest
/// index that has not been used yet.
///
/// This will derive and reveal a new script pubkey if no more unused script pubkeys exist.
///
/// If the descriptor has no wildcard and already has a used script pubkey or if a descriptor
/// has used all scripts up to the derivation bounds, then the last derived script pubkey will be
/// returned.
///
/// # Panics
///
/// Panics if `keychain` has never been added to the index
pub fn next_unused_spk(&mut self, keychain: &K) -> ((u32, &Script), DerivationAdditions<K>) {
let need_new = self.unused_spks_of_keychain(keychain).next().is_none();
// this rather strange branch is needed because of some lifetime issues
if need_new {
self.reveal_next_spk(keychain)
} else {
(
self.unused_spks_of_keychain(keychain)
.next()
.expect("we already know next exists"),
DerivationAdditions::default(),
)
}
}
/// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output with it.
/// This only has an effect when the `index` had been added to `self` already and was unused.
///
/// Returns whether the `index` was initially present as `unused`.
///
/// This is useful when you want to reserve a script pubkey for something but don't want to add
/// the transaction output using it to the index yet. Other callers will consider `index` on
/// `keychain` used until you call [`unmark_used`].
///
/// [`unmark_used`]: Self::unmark_used
pub fn mark_used(&mut self, keychain: &K, index: u32) -> bool {
self.inner.mark_used(&(keychain.clone(), index))
}
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
/// `unused`.
///
/// Note that if `self` has scanned an output with this script pubkey, then this will have no
/// effect.
///
/// [`mark_used`]: Self::mark_used
pub fn unmark_used(&mut self, keychain: &K, index: u32) -> bool {
self.inner.unmark_used(&(keychain.clone(), index))
}
/// Iterates over all unused script pubkeys for a `keychain` stored in the index.
pub fn unused_spks_of_keychain(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, &Script)> {
let next_index = self.last_revealed.get(keychain).map_or(0, |&v| v + 1);
let range = (keychain.clone(), u32::MIN)..(keychain.clone(), next_index);
self.inner
.unused_spks(range)
.map(|((_, i), script)| (*i, script))
}
/// Iterates over all the [`OutPoint`] that have a `TxOut` with a script pubkey derived from
/// `keychain`.
pub fn txouts_of_keychain(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
self.inner
.outputs_in_range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
.map(|((_, i), op)| (*i, op))
}
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
/// found a [`TxOut`] with it's script pubkey.
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
self.txouts_of_keychain(keychain).last().map(|(i, _)| i)
}
/// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found
/// a [`TxOut`] with it's script pubkey.
pub fn last_used_indices(&self) -> BTreeMap<K, u32> {
self.keychains
.iter()
.filter_map(|(keychain, _)| {
self.last_used_index(keychain)
.map(|index| (keychain.clone(), index))
})
.collect()
}
/// Applies the derivation additions to the [`KeychainTxOutIndex`], extending the number of
/// derived scripts per keychain, as specified in the `additions`.
pub fn apply_additions(&mut self, additions: DerivationAdditions<K>) {
let _ = self.reveal_to_target_multi(&additions.0);
}
}

102
crates/chain/src/lib.rs Normal file
View File

@@ -0,0 +1,102 @@
//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release).
//!
//! The goal of this crate is to give wallets the mechanisms needed to:
//!
//! 1. Figure out what data they need to fetch.
//! 2. Process the data in a way that never leads to inconsistent states.
//! 3. Fully index that data and expose it to be consumed without friction.
//!
//! Our design goals for these mechanisms are:
//!
//! 1. Data source agnostic -- nothing in `bdk_chain` cares about where you get data from or whether
//! you do it synchronously or asynchronously. If you know a fact about the blockchain, you can just
//! tell `bdk_chain`'s APIs about it, and that information will be integrated, if it can be done
//! consistently.
//! 2. Error-free APIs.
//! 3. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
//! cache or how you fetch it.
//!
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
#![no_std]
#![warn(missing_docs)]
pub use bitcoin;
mod spk_txout_index;
pub use spk_txout_index::*;
mod chain_data;
pub use chain_data::*;
pub mod indexed_tx_graph;
pub use indexed_tx_graph::IndexedTxGraph;
pub mod keychain;
pub mod local_chain;
mod tx_data_traits;
pub mod tx_graph;
pub use tx_data_traits::*;
pub use tx_graph::TxGraph;
mod chain_oracle;
pub use chain_oracle::*;
mod persist;
pub use persist::*;
#[doc(hidden)]
pub mod example_utils;
#[cfg(feature = "miniscript")]
pub use miniscript;
#[cfg(feature = "miniscript")]
mod descriptor_ext;
#[cfg(feature = "miniscript")]
pub use descriptor_ext::DescriptorExt;
#[cfg(feature = "miniscript")]
mod spk_iter;
#[cfg(feature = "miniscript")]
pub use spk_iter::*;
#[allow(unused_imports)]
#[macro_use]
extern crate alloc;
#[cfg(feature = "serde")]
pub extern crate serde_crate as serde;
#[cfg(feature = "bincode")]
extern crate bincode;
#[cfg(feature = "std")]
#[macro_use]
extern crate std;
#[cfg(all(not(feature = "std"), feature = "hashbrown"))]
extern crate hashbrown;
// When no-std use `alloc`'s Hash collections. This is activated by default
#[cfg(all(not(feature = "std"), not(feature = "hashbrown")))]
#[doc(hidden)]
pub mod collections {
#![allow(dead_code)]
pub type HashSet<K> = alloc::collections::BTreeSet<K>;
pub type HashMap<K, V> = alloc::collections::BTreeMap<K, V>;
pub use alloc::collections::{btree_map as hash_map, *};
}
// When we have std, use `std`'s all collections
#[cfg(all(feature = "std", not(feature = "hashbrown")))]
#[doc(hidden)]
pub mod collections {
pub use std::collections::{hash_map, *};
}
// With this special feature `hashbrown`, use `hashbrown`'s hash collections, and else from `alloc`.
#[cfg(feature = "hashbrown")]
#[doc(hidden)]
pub mod collections {
#![allow(dead_code)]
pub type HashSet<K> = hashbrown::HashSet<K>;
pub type HashMap<K, V> = hashbrown::HashMap<K, V>;
pub use alloc::collections::*;
pub use hashbrown::hash_map;
}
/// How many confirmations are needed f or a coinbase output to be spent.
pub const COINBASE_MATURITY: u32 = 100;

View File

@@ -0,0 +1,250 @@
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
use core::convert::Infallible;
use alloc::collections::BTreeMap;
use bitcoin::BlockHash;
use crate::{BlockId, ChainOracle};
/// This is a local implementation of [`ChainOracle`].
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LocalChain {
blocks: BTreeMap<u32, BlockHash>,
}
impl ChainOracle for LocalChain {
type Error = Infallible;
fn is_block_in_chain(
&self,
block: BlockId,
static_block: BlockId,
) -> Result<Option<bool>, Self::Error> {
if block.height > static_block.height {
return Ok(None);
}
Ok(
match (
self.blocks.get(&block.height),
self.blocks.get(&static_block.height),
) {
(Some(&hash), Some(&static_hash)) => {
Some(hash == block.hash && static_hash == static_block.hash)
}
_ => None,
},
)
}
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
Ok(self.tip())
}
}
impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
fn as_ref(&self) -> &BTreeMap<u32, BlockHash> {
&self.blocks
}
}
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
fn from(value: LocalChain) -> Self {
value.blocks
}
}
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
Self { blocks: value }
}
}
impl LocalChain {
/// Contruct a [`LocalChain`] from a list of [`BlockId`]s.
pub fn from_blocks<B>(blocks: B) -> Self
where
B: IntoIterator<Item = BlockId>,
{
Self {
blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
}
}
/// Get a reference to a map of block height to hash.
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
&self.blocks
}
/// Get the chain tip.
pub fn tip(&self) -> Option<BlockId> {
self.blocks
.iter()
.last()
.map(|(&height, &hash)| BlockId { height, hash })
}
/// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
/// are to be re-filled.
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> {
let update = update.as_ref();
let update_tip = match update.keys().last().cloned() {
Some(tip) => tip,
None => return Ok(ChangeSet::default()),
};
// this is the latest height where both the update and local chain has the same block hash
let agreement_height = update
.iter()
.rev()
.find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash))
.map(|(&height, _)| height);
// the lower bound of the range to invalidate
let invalidate_lb = match agreement_height {
Some(height) if height == update_tip => u32::MAX,
Some(height) => height + 1,
None => 0,
};
// the first block's height to invalidate in the local chain
let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h);
// the first block of height to invalidate (if any) should be represented in the update
if let Some(first_invalid_height) = invalidate_from_height {
if !update.contains_key(&first_invalid_height) {
return Err(UpdateNotConnectedError(first_invalid_height));
}
}
let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
Some(first_invalid_height) => {
// the first block of height to invalidate should be represented in the update
if !update.contains_key(&first_invalid_height) {
return Err(UpdateNotConnectedError(first_invalid_height));
}
self.blocks
.range(first_invalid_height..)
.map(|(height, _)| (*height, None))
.collect()
}
None => BTreeMap::new(),
};
for (height, update_hash) in update {
let original_hash = self.blocks.get(height);
if Some(update_hash) != original_hash {
changeset.insert(*height, Some(*update_hash));
}
}
Ok(changeset)
}
/// Applies the given `changeset`.
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
for (height, blockhash) in changeset {
match blockhash {
Some(blockhash) => self.blocks.insert(height, blockhash),
None => self.blocks.remove(&height),
};
}
}
/// Updates [`LocalChain`] with an update [`LocalChain`].
///
/// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
///
/// [`determine_changeset`]: Self::determine_changeset
/// [`apply_changeset`]: Self::apply_changeset
pub fn apply_update(&mut self, update: Self) -> Result<ChangeSet, UpdateNotConnectedError> {
let changeset = self.determine_changeset(&update)?;
self.apply_changeset(changeset.clone());
Ok(changeset)
}
/// Derives a [`ChangeSet`] that assumes that there are no preceding changesets.
///
/// The changeset returned will record additions of all blocks included in [`Self`].
pub fn initial_changeset(&self) -> ChangeSet {
self.blocks
.iter()
.map(|(&height, &hash)| (height, Some(hash)))
.collect()
}
/// Insert a block of [`BlockId`] into the [`LocalChain`].
///
/// # Error
///
/// If the insertion height already contains a block, and the block has a different blockhash,
/// this will result in an [`InsertBlockNotMatchingError`].
pub fn insert_block(
&mut self,
block_id: BlockId,
) -> Result<ChangeSet, InsertBlockNotMatchingError> {
let mut update = Self::from_blocks(self.tip());
if let Some(original_hash) = update.blocks.insert(block_id.height, block_id.hash) {
if original_hash != block_id.hash {
return Err(InsertBlockNotMatchingError {
height: block_id.height,
original_hash,
update_hash: block_id.hash,
});
}
}
Ok(self.apply_update(update).expect("should always connect"))
}
}
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
///
/// [`determine_changeset`]: LocalChain::determine_changeset
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original
/// chain.
///
/// The update cannot be applied to the chain because the chain suffix it represents did not
/// connect to the existing chain. This error case contains the checkpoint height to include so
/// that the chains can connect.
#[derive(Clone, Debug, PartialEq)]
pub struct UpdateNotConnectedError(pub u32);
impl core::fmt::Display for UpdateNotConnectedError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"the update cannot connect with the chain, try include block at height {}",
self.0
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UpdateNotConnectedError {}
/// Represents a failure when trying to insert a checkpoint into [`LocalChain`].
#[derive(Clone, Debug, PartialEq)]
pub struct InsertBlockNotMatchingError {
/// The checkpoints' height.
pub height: u32,
/// Original checkpoint's block hash.
pub original_hash: BlockHash,
/// Update checkpoint's block hash.
pub update_hash: BlockHash,
}
impl core::fmt::Display for InsertBlockNotMatchingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"failed to insert block at height {} as blockhashes conflict: original={}, update={}",
self.height, self.original_hash, self.update_hash
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for InsertBlockNotMatchingError {}

View File

@@ -0,0 +1,97 @@
use core::convert::Infallible;
use crate::Append;
/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
/// before they are persisted.
///
/// Not all changes to the in-memory representation needs to be written to disk right away, so
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
/// to write changes to disk.
#[derive(Debug)]
pub struct Persist<B, C> {
backend: B,
stage: C,
}
impl<B, C> Persist<B, C>
where
B: PersistBackend<C>,
C: Default + Append,
{
/// Create a new [`Persist`] from [`PersistBackend`].
pub fn new(backend: B) -> Self {
Self {
backend,
stage: Default::default(),
}
}
/// Stage a `changeset` to be commited later with [`commit`].
///
/// [`commit`]: Self::commit
pub fn stage(&mut self, changeset: C) {
self.stage.append(changeset)
}
/// Get the changes that have not been commited yet.
pub fn staged(&self) -> &C {
&self.stage
}
/// Commit the staged changes to the underlying persistance backend.
///
/// Changes that are committed (if any) are returned.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
pub fn commit(&mut self) -> Result<Option<C>, B::WriteError> {
if self.stage.is_empty() {
return Ok(None);
}
self.backend
.write_changes(&self.stage)
// if written successfully, take and return `self.stage`
.map(|_| Some(core::mem::take(&mut self.stage)))
}
}
/// A persistence backend for [`Persist`].
///
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
/// that are to be persisted, or retrieved from persistence.
pub trait PersistBackend<C> {
/// The error the backend returns when it fails to write.
type WriteError: core::fmt::Debug;
/// The error the backend returns when it fails to load changesets `C`.
type LoadError: core::fmt::Debug;
/// Writes a changeset to the persistence backend.
///
/// It is up to the backend what it does with this. It could store every changeset in a list or
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
/// changesets had been applied sequentially.
///
/// [`load_from_persistence`]: Self::load_from_persistence
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
/// Return the aggregate changeset `C` from persistence.
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>;
}
impl<C: Default> PersistBackend<C> for () {
type WriteError = Infallible;
type LoadError = Infallible;
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
Ok(())
}
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
Ok(C::default())
}
}

View File

@@ -0,0 +1,215 @@
use crate::{
bitcoin::{secp256k1::Secp256k1, Script},
miniscript::{Descriptor, DescriptorPublicKey},
};
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
/// Maximum [BIP32](https://bips.xyz/32) derivation index.
pub const BIP32_MAX_INDEX: u32 = (1 << 31) - 1;
/// An iterator for derived script pubkeys.
///
/// [`SpkIterator`] is an implementation of the [`Iterator`] trait which possesses its own `next()`
/// and `nth()` functions, both of which circumvent the unnecessary intermediate derivations required
/// when using their default implementations.
///
/// ## Examples
///
/// ```
/// use bdk_chain::SpkIterator;
/// # use miniscript::{Descriptor, DescriptorPublicKey};
/// # use bitcoin::{secp256k1::Secp256k1};
/// # use std::str::FromStr;
/// # let secp = bitcoin::secp256k1::Secp256k1::signing_only();
/// # let (descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
/// # let external_spk_0 = descriptor.at_derivation_index(0).script_pubkey();
/// # let external_spk_3 = descriptor.at_derivation_index(3).script_pubkey();
/// # let external_spk_4 = descriptor.at_derivation_index(4).script_pubkey();
///
/// // Creates a new script pubkey iterator starting at 0 from a descriptor.
/// let mut spk_iter = SpkIterator::new(&descriptor);
/// assert_eq!(spk_iter.next(), Some((0, external_spk_0)));
/// assert_eq!(spk_iter.next(), None);
/// ```
#[derive(Clone)]
pub struct SpkIterator<D> {
next_index: u32,
end: u32,
descriptor: D,
secp: Secp256k1<bitcoin::secp256k1::VerifyOnly>,
}
impl<D> SpkIterator<D>
where
D: Borrow<Descriptor<DescriptorPublicKey>>,
{
/// Creates a new script pubkey iterator starting at 0 from a descriptor.
pub fn new(descriptor: D) -> Self {
let end = if descriptor.borrow().has_wildcard() {
BIP32_MAX_INDEX
} else {
0
};
SpkIterator::new_with_range(descriptor, 0..=end)
}
// Creates a new script pubkey iterator from a descriptor with a given range.
pub(crate) fn new_with_range<R>(descriptor: D, range: R) -> Self
where
R: RangeBounds<u32>,
{
let mut end = match range.end_bound() {
Bound::Included(end) => *end + 1,
Bound::Excluded(end) => *end,
Bound::Unbounded => u32::MAX,
};
// Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
end = end.min(BIP32_MAX_INDEX + 1);
Self {
next_index: match range.start_bound() {
Bound::Included(start) => *start,
Bound::Excluded(start) => *start + 1,
Bound::Unbounded => u32::MIN,
},
end,
descriptor,
secp: Secp256k1::verification_only(),
}
}
}
impl<D> Iterator for SpkIterator<D>
where
D: Borrow<Descriptor<DescriptorPublicKey>>,
{
type Item = (u32, Script);
fn next(&mut self) -> Option<Self::Item> {
// For non-wildcard descriptors, we expect the first element to be Some((0, spk)), then None after.
// For wildcard descriptors, we expect it to keep iterating until exhausted.
if self.next_index >= self.end {
return None;
}
let script = self
.descriptor
.borrow()
.at_derivation_index(self.next_index)
.derived_descriptor(&self.secp)
.expect("the descriptor cannot need hardened derivation")
.script_pubkey();
let output = (self.next_index, script);
self.next_index += 1;
Some(output)
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.next_index = self
.next_index
.saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
self.next()
}
}
#[cfg(test)]
mod test {
use crate::{
bitcoin::secp256k1::Secp256k1,
keychain::KeychainTxOutIndex,
miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::{SpkIterator, BIP32_MAX_INDEX},
};
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
enum TestKeychain {
External,
Internal,
}
fn init_txout_index() -> (
KeychainTxOutIndex<TestKeychain>,
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
let secp = Secp256k1::signing_only();
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
}
#[test]
#[allow(clippy::iter_nth_zero)]
fn test_spkiterator_wildcard() {
let (_, external_desc, _) = init_txout_index();
let external_spk_0 = external_desc.at_derivation_index(0).script_pubkey();
let external_spk_16 = external_desc.at_derivation_index(16).script_pubkey();
let external_spk_20 = external_desc.at_derivation_index(20).script_pubkey();
let external_spk_21 = external_desc.at_derivation_index(21).script_pubkey();
let external_spk_max = external_desc
.at_derivation_index(BIP32_MAX_INDEX)
.script_pubkey();
let mut external_spk = SpkIterator::new(&external_desc);
let max_index = BIP32_MAX_INDEX - 22;
assert_eq!(external_spk.next().unwrap(), (0, external_spk_0));
assert_eq!(external_spk.nth(15).unwrap(), (16, external_spk_16));
assert_eq!(external_spk.nth(3).unwrap(), (20, external_spk_20.clone()));
assert_eq!(external_spk.next().unwrap(), (21, external_spk_21));
assert_eq!(
external_spk.nth(max_index as usize).unwrap(),
(BIP32_MAX_INDEX, external_spk_max)
);
assert_eq!(external_spk.nth(0), None);
let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
assert_eq!(external_spk.nth(20).unwrap(), (20, external_spk_20));
assert_eq!(external_spk.next(), None);
let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
assert_eq!(external_spk.nth(21), None);
}
#[test]
#[allow(clippy::iter_nth_zero)]
fn test_spkiterator_non_wildcard() {
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
let external_spk_0 = no_wildcard_descriptor
.at_derivation_index(0)
.script_pubkey();
let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
assert_eq!(external_spk.next().unwrap(), (0, external_spk_0.clone()));
assert_eq!(external_spk.next(), None);
let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
assert_eq!(external_spk.nth(0).unwrap(), (0, external_spk_0));
assert_eq!(external_spk.nth(0), None);
}
// The following dummy traits were created to test if SpkIterator is working properly.
trait TestSendStatic: Send + 'static {
fn test(&self) -> u32 {
20
}
}
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
fn test(&self) -> u32 {
20
}
}
}

View File

@@ -0,0 +1,337 @@
use core::ops::RangeBounds;
use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
indexed_tx_graph::Indexer,
ForEachTxOut,
};
use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
///
/// The basic idea is that you insert script pubkeys you care about into the index with
/// [`insert_spk`] and then when you call [`scan`], the index will look at any txouts you pass in and
/// store and index any txouts matching one of its script pubkeys.
///
/// Each script pubkey is associated with an application-defined index script index `I`, which must be
/// [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even a
/// combination of `(keychain, derivation_index)`.
///
/// Note there is no harm in scanning transactions that disappear from the blockchain or were never
/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or
/// modify txouts that have been indexed. To find out which txouts from the index are actually in the
/// chain or unspent, you must use other sources of information like a [`TxGraph`].
///
/// [`TxOut`]: bitcoin::TxOut
/// [`insert_spk`]: Self::insert_spk
/// [`Ord`]: core::cmp::Ord
/// [`scan`]: Self::scan
/// [`TxGraph`]: crate::tx_graph::TxGraph
#[derive(Clone, Debug)]
pub struct SpkTxOutIndex<I> {
/// script pubkeys ordered by index
spks: BTreeMap<I, Script>,
/// A reverse lookup from spk to spk index
spk_indices: HashMap<Script, I>,
/// The set of unused indexes.
unused: BTreeSet<I>,
/// Lookup index and txout by outpoint.
txouts: BTreeMap<OutPoint, (I, TxOut)>,
/// Lookup from spk index to outpoints that had that spk
spk_txouts: BTreeSet<(I, OutPoint)>,
}
impl<I> Default for SpkTxOutIndex<I> {
fn default() -> Self {
Self {
txouts: Default::default(),
spks: Default::default(),
spk_indices: Default::default(),
spk_txouts: Default::default(),
unused: Default::default(),
}
}
}
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
type Additions = ();
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
self.scan_txout(outpoint, txout);
Default::default()
}
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
self.scan(tx);
Default::default()
}
fn apply_additions(&mut self, _additions: Self::Additions) {
// This applies nothing.
}
fn is_tx_relevant(&self, tx: &Transaction) -> bool {
self.is_relevant(tx)
}
}
/// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a
/// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a
/// reference out of the `ForEachTxOut` closure during scanning.
macro_rules! scan_txout {
($self:ident, $op:expr, $txout:expr) => {{
let spk_i = $self.spk_indices.get(&$txout.script_pubkey);
if let Some(spk_i) = spk_i {
$self.txouts.insert($op, (spk_i.clone(), $txout.clone()));
$self.spk_txouts.insert((spk_i.clone(), $op));
$self.unused.remove(&spk_i);
}
spk_i
}};
}
impl<I: Clone + Ord> SpkTxOutIndex<I> {
/// Scans an object containing many txouts.
///
/// Typically, this is used in two situations:
///
/// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
/// your txouts.
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
///
/// See [`ForEachTxout`] for the types that support this.
///
/// [`ForEachTxout`]: crate::ForEachTxOut
pub fn scan(&mut self, txouts: &impl ForEachTxOut) -> BTreeSet<I> {
let mut scanned_indices = BTreeSet::new();
txouts.for_each_txout(|(op, txout)| {
if let Some(spk_i) = scan_txout!(self, op, txout) {
scanned_indices.insert(spk_i.clone());
}
});
scanned_indices
}
/// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the
/// script pubkey (if any).
pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> {
scan_txout!(self, op, txout)
}
/// Get a reference to the set of indexed outpoints.
pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
&self.spk_txouts
}
/// Iterate over all known txouts that spend to tracked script pubkeys.
pub fn txouts(
&self,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
self.txouts
.iter()
.map(|(op, (index, txout))| (index, *op, txout))
}
/// Finds all txouts on a transaction that has previously been scanned and indexed.
pub fn txouts_in_tx(
&self,
txid: Txid,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
self.txouts
.range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
.map(|(op, (index, txout))| (index, *op, txout))
}
/// Iterates over all the outputs with script pubkeys in an index range.
pub fn outputs_in_range(
&self,
range: impl RangeBounds<I>,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
use bitcoin::hashes::Hash;
use core::ops::Bound::*;
let min_op = OutPoint {
txid: Txid::from_inner([0x00; 32]),
vout: u32::MIN,
};
let max_op = OutPoint {
txid: Txid::from_inner([0xff; 32]),
vout: u32::MAX,
};
let start = match range.start_bound() {
Included(index) => Included((index.clone(), min_op)),
Excluded(index) => Excluded((index.clone(), max_op)),
Unbounded => Unbounded,
};
let end = match range.end_bound() {
Included(index) => Included((index.clone(), max_op)),
Excluded(index) => Excluded((index.clone(), min_op)),
Unbounded => Unbounded,
};
self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
}
/// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`.
///
/// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
self.txouts
.get(&outpoint)
.map(|(spk_i, txout)| (spk_i, txout))
}
/// Returns the script that has been inserted at the `index`.
///
/// If that index hasn't been inserted yet, it will return `None`.
pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
self.spks.get(index)
}
/// The script pubkeys that are being tracked by the index.
pub fn all_spks(&self) -> &BTreeMap<I, Script> {
&self.spks
}
/// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map
///
/// the index will look for outputs spending to this spk whenever it scans new data.
pub fn insert_spk(&mut self, index: I, spk: Script) -> bool {
match self.spk_indices.entry(spk.clone()) {
Entry::Vacant(value) => {
value.insert(index.clone());
self.spks.insert(index.clone(), spk);
self.unused.insert(index);
true
}
Entry::Occupied(_) => false,
}
}
/// Iterates over all unused script pubkeys in an index range.
///
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
/// never scanned a transaction output with it.
///
/// # Example
///
/// ```rust
/// # use bdk_chain::SpkTxOutIndex;
///
/// // imagine our spks are indexed like (keychain, derivation_index).
/// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
/// let all_unused_spks = txout_index.unused_spks(..);
/// let change_index = 1;
/// let unused_change_spks =
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
/// ```
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)>
where
R: RangeBounds<I>,
{
self.unused
.range(range)
.map(move |index| (index, self.spk_at_index(index).expect("must exist")))
}
/// Returns whether the script pubkey at `index` has been used or not.
///
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
/// never scanned a transaction output with it.
pub fn is_used(&self, index: &I) -> bool {
self.unused.get(index).is_none()
}
/// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
/// This only affects when the `index` had already been added to `self` and was unused.
///
/// Returns whether the `index` was initially present as `unused`.
///
/// This is useful when you want to reserve a script pubkey for something but don't want to add
/// the transaction output using it to the index yet. Other callers will consider the `index` used
/// until you call [`unmark_used`].
///
/// [`unmark_used`]: Self::unmark_used
pub fn mark_used(&mut self, index: &I) -> bool {
self.unused.remove(index)
}
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
/// `unused`.
///
/// Note that if `self` has scanned an output with this script pubkey then this will have no
/// effect.
///
/// [`mark_used`]: Self::mark_used
pub fn unmark_used(&mut self, index: &I) -> bool {
// we cannot set the index as unused when it does not exist
if !self.spks.contains_key(index) {
return false;
}
// we cannot set the index as unused when txouts are indexed under it
if self.outputs_in_range(index..=index).next().is_some() {
return false;
}
self.unused.insert(index.clone())
}
/// Returns the index associated with the script pubkey.
pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
self.spk_indices.get(script)
}
/// Computes total input value going from script pubkeys in the index (sent) and the total output
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
/// correctly, the output being spent must have already been scanned by the index. Calculating
/// received just uses the transaction outputs directly, so it will be correct even if it has not
/// been scanned.
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
let mut sent = 0;
let mut received = 0;
for txin in &tx.input {
if let Some((_, txout)) = self.txout(txin.previous_output) {
sent += txout.value;
}
}
for txout in &tx.output {
if self.index_of_spk(&txout.script_pubkey).is_some() {
received += txout.value;
}
}
(sent, received)
}
/// Computes the net value that this transaction gives to the script pubkeys in the index and
/// *takes* from the transaction outputs in the index. Shorthand for calling
/// [`sent_and_received`] and subtracting sent from received.
///
/// [`sent_and_received`]: Self::sent_and_received
pub fn net_value(&self, tx: &Transaction) -> i64 {
let (sent, received) = self.sent_and_received(tx);
received as i64 - sent as i64
}
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output
/// matches one of our script pubkeys.
///
/// It is easily possible to misuse this method and get false negatives by calling it before you
/// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out
/// all the transactions in a block that are irrelevant, you **must first scan all the
/// transactions in the block** and only then use this method.
pub fn is_relevant(&self, tx: &Transaction) -> bool {
let input_matches = tx
.input
.iter()
.any(|input| self.txouts.contains_key(&input.previous_output));
let output_matches = tx
.output
.iter()
.any(|output| self.spk_indices.contains_key(&output.script_pubkey));
input_matches || output_matches
}
}

View File

@@ -0,0 +1,120 @@
use crate::collections::BTreeMap;
use crate::collections::BTreeSet;
use crate::BlockId;
use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut};
/// Trait to do something with every txout contained in a structure.
///
/// We would prefer to just work with things that can give us an `Iterator<Item=(OutPoint, &TxOut)>`
/// here, but rust's type system makes it extremely hard to do this (without trait objects).
pub trait ForEachTxOut {
/// The provided closure `f` will be called with each `outpoint/txout` pair.
fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut)));
}
impl ForEachTxOut for Block {
fn for_each_txout(&self, mut f: impl FnMut((OutPoint, &TxOut))) {
for tx in self.txdata.iter() {
tx.for_each_txout(&mut f)
}
}
}
impl ForEachTxOut for Transaction {
fn for_each_txout(&self, mut f: impl FnMut((OutPoint, &TxOut))) {
let txid = self.txid();
for (i, txout) in self.output.iter().enumerate() {
f((
OutPoint {
txid,
vout: i as u32,
},
txout,
))
}
}
}
/// Trait that "anchors" blockchain data to a specific block of height and hash.
///
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
/// parent block of B.
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
fn anchor_block(&self) -> BlockId;
/// Get the upper bound of the chain data's confirmation height.
///
/// The default definition gives a pessimistic answer. This can be overridden by the `Anchor`
/// implementation for a more accurate value.
fn confirmation_height_upper_bound(&self) -> u32 {
self.anchor_block().height
}
}
impl<A: Anchor> Anchor for &'static A {
fn anchor_block(&self) -> BlockId {
<A as Anchor>::anchor_block(self)
}
}
/// Trait that makes an object appendable.
pub trait Append {
/// Append another object of the same type onto `self`.
fn append(&mut self, other: Self);
/// Returns whether the structure is considered empty.
fn is_empty(&self) -> bool;
}
impl Append for () {
fn append(&mut self, _other: Self) {}
fn is_empty(&self) -> bool {
true
}
}
impl<K: Ord, V> Append for BTreeMap<K, V> {
fn append(&mut self, mut other: Self) {
BTreeMap::append(self, &mut other)
}
fn is_empty(&self) -> bool {
BTreeMap::is_empty(self)
}
}
impl<T: Ord> Append for BTreeSet<T> {
fn append(&mut self, mut other: Self) {
BTreeSet::append(self, &mut other)
}
fn is_empty(&self) -> bool {
BTreeSet::is_empty(self)
}
}
impl<T> Append for Vec<T> {
fn append(&mut self, mut other: Self) {
Vec::append(self, &mut other)
}
fn is_empty(&self) -> bool {
Vec::is_empty(self)
}
}
impl<A: Append, B: Append> Append for (A, B) {
fn append(&mut self, other: Self) {
Append::append(&mut self.0, other.0);
Append::append(&mut self.1, other.1);
}
fn is_empty(&self) -> bool {
Append::is_empty(&self.0) && Append::is_empty(&self.1)
}
}

1185
crates/chain/src/tx_graph.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
#[allow(unused_macros)]
macro_rules! h {
($index:literal) => {{
bitcoin::hashes::Hash::hash($index.as_bytes())
}};
}
#[allow(unused_macros)]
macro_rules! local_chain {
[ $(($height:expr, $block_hash:expr)), * ] => {{
#[allow(unused_mut)]
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*])
}};
}
#[allow(unused_macros)]
macro_rules! chain {
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
(checkpoints: $($tail:tt)*) => { chain!( index: TxHeight, checkpoints: $($tail)*) };
(index: $ind:ty, checkpoints: [ $([$height:expr, $block_hash:expr]),* ] $(,txids: [$(($txid:expr, $tx_height:expr)),*])?) => {{
#[allow(unused_mut)]
let mut chain = bdk_chain::sparse_chain::SparseChain::<$ind>::from_checkpoints([$(($height, $block_hash).into()),*]);
$(
$(
let _ = chain.insert_tx($txid, $tx_height).expect("should succeed");
)*
)?
chain
}};
}
#[allow(unused_macros)]
macro_rules! changeset {
(checkpoints: $($tail:tt)*) => { changeset!(index: TxHeight, checkpoints: $($tail)*) };
(
index: $ind:ty,
checkpoints: [ $(( $height:expr, $cp_to:expr )),* ]
$(,txids: [ $(( $txid:expr, $tx_to:expr )),* ])?
) => {{
use bdk_chain::collections::BTreeMap;
#[allow(unused_mut)]
bdk_chain::sparse_chain::ChangeSet::<$ind> {
checkpoints: {
let mut changes = BTreeMap::default();
$(changes.insert($height, $cp_to);)*
changes
},
txids: {
let mut changes = BTreeMap::default();
$($(changes.insert($txid, $tx_to.map(|h: TxHeight| h.into()));)*)?
changes
}
}
}};
}
#[allow(unused)]
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
bitcoin::Transaction {
version: 0x00,
lock_time: bitcoin::PackedLockTime(lt),
input: vec![],
output: vec![],
}
}

View File

@@ -0,0 +1,467 @@
#[macro_use]
mod common;
use std::collections::{BTreeMap, BTreeSet};
use bdk_chain::{
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
local_chain::LocalChain,
tx_graph::Additions,
BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut};
use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
/// in topological order.
///
/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A.
/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints
/// are associated with owned spks in the index). Ensure insertion and indexing is topological-
/// agnostic.
#[test]
fn insert_relevant_txs() {
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
.expect("must be valid");
let spk_0 = descriptor.at_derivation_index(0).script_pubkey();
let spk_1 = descriptor.at_derivation_index(9).script_pubkey();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
graph.index.add_keychain((), descriptor);
graph.index.set_lookahead(&(), 10);
let tx_a = Transaction {
output: vec![
TxOut {
value: 10_000,
script_pubkey: spk_0,
},
TxOut {
value: 20_000,
script_pubkey: spk_1,
},
],
..common::new_tx(0)
};
let tx_b = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
..Default::default()
}],
..common::new_tx(1)
};
let tx_c = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 1),
..Default::default()
}],
..common::new_tx(2)
};
let txs = [tx_c, tx_b, tx_a];
assert_eq!(
graph.insert_relevant_txs(txs.iter().map(|tx| (tx, None)), None),
IndexedAdditions {
graph_additions: Additions {
txs: txs.into(),
..Default::default()
},
index_additions: DerivationAdditions([((), 9_u32)].into()),
}
)
}
#[test]
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
///
/// Test Setup:
///
/// Local Chain => <0> ----- <1> ----- <2> ----- <3> ---- ... ---- <150>
///
/// Keychains:
///
/// keychain_1: Trusted
/// keychain_2: Untrusted
///
/// Transactions:
///
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
/// tx6: Complete unrelated tx. [Block 3]
///
/// Different transactions are added via `insert_relevant_txs`.
/// `list_owned_txout`, `list_owned_utxos` and `balance` method is asserted
/// with expected values at Block height 0, 1, and 2.
///
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
fn test_list_owned_txouts() {
// Create Local chains
let local_chain = (0..150)
.map(|i| (i as u32, h!("random")))
.collect::<BTreeMap<u32, BlockHash>>();
let local_chain = LocalChain::from(local_chain);
// Initiate IndexedTxGraph
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
let mut graph =
IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::default();
graph.index.add_keychain("keychain_1".into(), desc_1);
graph.index.add_keychain("keychain_2".into(), desc_2);
graph.index.set_lookahead_for_all(10);
// Get trusted and untrusted addresses
let mut trusted_spks = Vec::new();
let mut untrusted_spks = Vec::new();
{
// we need to scope here to take immutanble reference of the graph
for _ in 0..10 {
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
// TODO Assert indexes
trusted_spks.push(script.clone());
}
}
{
for _ in 0..10 {
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
untrusted_spks.push(script.clone());
}
}
// Create test transactions
// tx1 is the genesis coinbase
let tx1 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut {
value: 70000,
script_pubkey: trusted_spks[0].clone(),
}],
..common::new_tx(0)
};
// tx2 is an incoming transaction received at untrusted keychain at block 1.
let tx2 = Transaction {
output: vec![TxOut {
value: 30000,
script_pubkey: untrusted_spks[0].clone(),
}],
..common::new_tx(0)
};
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
let tx3 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx2.txid(), 0),
..Default::default()
}],
output: vec![TxOut {
value: 10000,
script_pubkey: trusted_spks[1].clone(),
}],
..common::new_tx(0)
};
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
let tx4 = Transaction {
output: vec![TxOut {
value: 20000,
script_pubkey: untrusted_spks[1].clone(),
}],
..common::new_tx(0)
};
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
let tx5 = Transaction {
output: vec![TxOut {
value: 15000,
script_pubkey: trusted_spks[2].clone(),
}],
..common::new_tx(0)
};
// tx6 is an unrelated transaction confirmed at 3.
let tx6 = common::new_tx(0);
// Insert transactions into graph with respective anchors
// For unconfirmed txs we pass in `None`.
let _ = graph.insert_relevant_txs(
[&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
let height = i as u32;
(
*tx,
local_chain
.blocks()
.get(&height)
.map(|&hash| BlockId { height, hash })
.map(|anchor_block| ConfirmationHeightAnchor {
anchor_block,
confirmation_height: anchor_block.height,
}),
)
}),
None,
);
let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100));
// A helper lambda to extract and filter data from the graph.
let fetch =
|height: u32,
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
let chain_tip = local_chain
.blocks()
.get(&height)
.map(|&hash| BlockId { height, hash })
.expect("block must exist");
let txouts = graph
.graph()
.filter_chain_txouts(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
)
.collect::<Vec<_>>();
let utxos = graph
.graph()
.filter_chain_unspents(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
)
.collect::<Vec<_>>();
let balance = graph.graph().balance(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
|_, spk: &Script| trusted_spks.contains(spk),
);
assert_eq!(txouts.len(), 5);
assert_eq!(utxos.len(), 4);
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
}
})
.collect::<BTreeSet<_>>();
let unconfirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
}
})
.collect::<BTreeSet<_>>();
let confirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
}
})
.collect::<BTreeSet<_>>();
let unconfirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
Some(full_txout.outpoint.txid)
} else {
None
}
})
.collect::<BTreeSet<_>>();
(
confirmed_txouts_txid,
unconfirmed_txouts_txid,
confirmed_utxos_txid,
unconfirmed_utxos_txid,
balance,
)
};
// ----- TEST BLOCK -----
// AT Block 0
{
let (
confirmed_txouts_txid,
unconfirmed_txouts_txid,
confirmed_utxos_txid,
unconfirmed_utxos_txid,
balance,
) = fetch(0, &graph);
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
}
);
}
// AT Block 1
{
let (
confirmed_txouts_txid,
unconfirmed_txouts_txid,
confirmed_utxos_txid,
unconfirmed_utxos_txid,
balance,
) = fetch(1, &graph);
// tx2 gets into confirmed txout set
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
// tx2 doesn't get into confirmed utxos set
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
}
);
}
// AT Block 2
{
let (
confirmed_txouts_txid,
unconfirmed_txouts_txid,
confirmed_utxos_txid,
unconfirmed_utxos_txid,
balance,
) = fetch(2, &graph);
// tx3 now gets into the confirmed txout set
assert_eq!(
confirmed_txouts_txid,
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
// tx3 also gets into confirmed utxo set
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx3 got confirmed
}
);
}
// AT Block 98
{
let (
confirmed_txouts_txid,
unconfirmed_txouts_txid,
confirmed_utxos_txid,
unconfirmed_utxos_txid,
balance,
) = fetch(98, &graph);
assert_eq!(
confirmed_txouts_txid,
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
// Coinbase is still immature
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx1 got matured
}
);
}
// AT Block 99
{
let (_, _, _, _, balance) = fetch(100, &graph);
// Coinbase maturity hits
assert_eq!(
balance,
Balance {
immature: 0, // coinbase matured
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 80000 // tx1 + tx3
}
);
}
}

View File

@@ -0,0 +1,368 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use bdk_chain::{
collections::BTreeMap,
keychain::{DerivationAdditions, KeychainTxOutIndex},
};
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, Transaction, TxOut};
use miniscript::{Descriptor, DescriptorPublicKey};
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
enum TestKeychain {
External,
Internal,
}
fn init_txout_index() -> (
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::default();
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
}
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Script {
descriptor
.derived_descriptor(&Secp256k1::verification_only(), index)
.expect("must derive")
.script_pubkey()
}
#[test]
fn test_set_all_derivation_indices() {
let (mut txout_index, _, _) = init_txout_index();
let derive_to: BTreeMap<_, _> =
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
&derive_to
);
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1,
DerivationAdditions::default(),
"no changes if we set to the same thing"
);
}
#[test]
fn test_lookahead() {
let (mut txout_index, external_desc, internal_desc) = init_txout_index();
// ensure it does not break anything if lookahead is set multiple times
(0..=10).for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::External, lookahead));
(0..=20)
.filter(|v| v % 2 == 0)
.for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::Internal, lookahead));
assert_eq!(txout_index.inner().all_spks().len(), 30);
// given:
// - external lookahead set to 10
// - internal lookahead set to 20
// when:
// - set external derivation index to value higher than last, but within the lookahead value
// expect:
// - scripts cached in spk_txout_index should increase correctly
// - stored scripts of external keychain should be of expected counts
for index in (0..20).skip_while(|i| i % 2 == 1) {
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::External, index);
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
vec![(index, spk_at_index(&external_desc, index))],
);
assert_eq!(
revealed_additions.as_inner(),
&[(TestKeychain::External, index)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
20 /* internal lookahead */ +
index as usize + 1 /* `derived` count */
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
assert_eq!(
txout_index
.unused_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.unused_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
}
// given:
// - internal lookahead is 20
// - internal derivation index is `None`
// when:
// - derivation index is set ahead of current derivation index + lookahead
// expect:
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
(0..=24)
.map(|index| (index, spk_at_index(&internal_desc, index)))
.collect::<Vec<_>>(),
);
assert_eq!(
revealed_additions.as_inner(),
&[(TestKeychain::Internal, 24)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
20 /* internal lookahead */ +
20 /* external stored index count */ +
25 /* internal stored index count */
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
25,
);
// ensure derivation indices are expected for each keychain
let last_external_index = txout_index
.last_revealed_index(&TestKeychain::External)
.expect("already derived");
let last_internal_index = txout_index
.last_revealed_index(&TestKeychain::Internal)
.expect("already derived");
assert_eq!(last_external_index, 19);
assert_eq!(last_internal_index, 24);
// when:
// - scanning txouts with spks within stored indexes
// expect:
// - no changes to stored index counts
let external_iter = 0..=last_external_index;
let internal_iter = last_internal_index - last_external_index..=last_internal_index;
for (external_index, internal_index) in external_iter.zip(internal_iter) {
let tx = Transaction {
output: vec![
TxOut {
script_pubkey: external_desc
.at_derivation_index(external_index)
.script_pubkey(),
value: 10_000,
},
TxOut {
script_pubkey: internal_desc
.at_derivation_index(internal_index)
.script_pubkey(),
value: 10_000,
},
],
..common::new_tx(external_index)
};
assert_eq!(txout_index.scan(&tx), DerivationAdditions::default());
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
Some(last_external_index)
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::Internal),
Some(last_internal_index)
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
last_external_index as usize + 1,
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
last_internal_index as usize + 1,
);
}
}
// when:
// - scanning txouts with spks above last stored index
// expect:
// - last revealed index should increase as expected
// - last used index should change as expected
#[test]
fn test_scan_with_lookahead() {
let (mut txout_index, external_desc, _) = init_txout_index();
txout_index.set_lookahead_for_all(10);
let spks: BTreeMap<u32, Script> = [0, 10, 20, 30]
.into_iter()
.map(|i| (i, external_desc.at_derivation_index(i).script_pubkey()))
.collect();
for (&spk_i, spk) in &spks {
let op = OutPoint::new(h!("fake tx"), spk_i);
let txout = TxOut {
script_pubkey: spk.clone(),
value: 0,
};
let additions = txout_index.scan_txout(op, &txout);
assert_eq!(
additions.as_inner(),
&[(TestKeychain::External, spk_i)].into()
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
Some(spk_i)
);
assert_eq!(
txout_index.last_used_index(&TestKeychain::External),
Some(spk_i)
);
}
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
let spk_41 = external_desc.at_derivation_index(41).script_pubkey();
let op = OutPoint::new(h!("fake tx"), 41);
let txout = TxOut {
script_pubkey: spk_41,
value: 0,
};
let additions = txout_index.scan_txout(op, &txout);
assert!(additions.is_empty());
}
#[test]
fn test_wildcard_derivations() {
let (mut txout_index, external_desc, _) = init_txout_index();
let external_spk_0 = external_desc.at_derivation_index(0).script_pubkey();
let external_spk_16 = external_desc.at_derivation_index(16).script_pubkey();
let external_spk_26 = external_desc.at_derivation_index(26).script_pubkey();
let external_spk_27 = external_desc.at_derivation_index(27).script_pubkey();
// - nothing is derived
// - unused list is also empty
//
// - next_derivation_index() == (0, true)
// - derive_new() == ((0, <spk>), DerivationAdditions)
// - next_unused() == ((0, <spk>), DerivationAdditions:is_empty())
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, &external_spk_0));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, &external_spk_0));
assert_eq!(changeset.as_inner(), &[].into());
// - derived till 25
// - used all spks till 15.
// - used list : [0..=15, 17, 20, 23]
// - unused list: [16, 18, 19, 21, 22, 24, 25]
// - next_derivation_index() = (26, true)
// - derive_new() = ((26, <spk>), DerivationAdditions)
// - next_unused() == ((16, <spk>), DerivationAdditions::is_empty())
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
(0..=15)
.chain(vec![17, 20, 23].into_iter())
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (26, &external_spk_26));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (16, &external_spk_16));
assert_eq!(changeset.as_inner(), &[].into());
// - Use all the derived till 26.
// - next_unused() = ((27, <spk>), DerivationAdditions)
(0..=26).for_each(|index| {
txout_index.mark_used(&TestKeychain::External, index);
});
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (27, &external_spk_27));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
}
#[test]
fn test_non_wildcard_derivations() {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
let external_spk = no_wildcard_descriptor
.at_derivation_index(0)
.script_pubkey();
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
// given:
// - `txout_index` with no stored scripts
// expect:
// - next derivation index should be new
// - when we derive a new script, script @ index 0
// - when we get the next unused script, script @ index 0
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
// given:
// - the non-wildcard descriptor already has a stored and used script
// expect:
// - next derivation index should not be new
// - derive new and next unused should return the old script
// - store_up_to should not panic and return empty additions
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
txout_index.mark_used(&TestKeychain::External, 0);
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::External, 200);
assert_eq!(revealed_spks.count(), 0);
assert!(revealed_additions.is_empty());
}

View File

@@ -0,0 +1,228 @@
use bdk_chain::local_chain::{
ChangeSet, InsertBlockNotMatchingError, LocalChain, UpdateNotConnectedError,
};
use bitcoin::BlockHash;
#[macro_use]
mod common;
#[test]
fn add_first_tip() {
let chain = LocalChain::default();
assert_eq!(
chain.determine_changeset(&local_chain![(0, h!("A"))]),
Ok([(0, Some(h!("A")))].into()),
"add first tip"
);
}
#[test]
fn add_second_tip() {
let chain = local_chain![(0, h!("A"))];
assert_eq!(
chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]),
Ok([(1, Some(h!("B")))].into())
);
}
#[test]
fn two_disjoint_chains_cannot_merge() {
let chain1 = local_chain![(0, h!("A"))];
let chain2 = local_chain![(1, h!("B"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateNotConnectedError(0))
);
}
#[test]
fn duplicate_chains_should_merge() {
let chain1 = local_chain![(0, h!("A"))];
let chain2 = local_chain![(0, h!("A"))];
assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default()));
}
#[test]
fn can_introduce_older_checkpoints() {
let chain1 = local_chain![(2, h!("C")), (3, h!("D"))];
let chain2 = local_chain![(1, h!("B")), (2, h!("C"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Ok([(1, Some(h!("B")))].into())
);
}
#[test]
fn fix_blockhash_before_agreement_point() {
let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))];
let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Ok([(0, Some(h!("fix")))].into())
)
}
/// B and C are in both chain and update
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C
/// update | A B C D
/// ```
/// This should succeed with the point of agreement being C and A should be added in addition.
#[test]
fn two_points_of_agreement() {
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()),
);
}
/// Update and chain does not connect:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C
/// update | A B D
/// ```
/// This should fail as we cannot figure out whether C & D are on the same chain
#[test]
fn update_and_chain_does_not_connect() {
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateNotConnectedError(2)),
);
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4 | 5
/// chain | A B C E
/// update | A B' C' D
/// ```
/// This should succeed and invalidate B,C and E with point of agreement being A.
#[test]
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))];
let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Ok([
(2, Some(h!("B'"))),
(3, Some(h!("C'"))),
(4, Some(h!("D"))),
(5, None),
]
.into())
);
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C E
/// update | B' C' D
/// ```
///
/// This should succeed and invalidate B, C and E with no point of agreement
#[test]
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
let chain1 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))];
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Ok([
(1, Some(h!("B'"))),
(2, Some(h!("C'"))),
(3, Some(h!("D"))),
(4, None)
]
.into())
)
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | A B C E
/// update | B' C' D
/// ```
///
/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
/// A was invalid.
#[test]
fn invalidation_but_no_connection() {
let chain1 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))];
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateNotConnectedError(0))
)
}
#[test]
fn insert_block() {
struct TestCase {
original: LocalChain,
insert: (u32, BlockHash),
expected_result: Result<ChangeSet, InsertBlockNotMatchingError>,
expected_final: LocalChain,
}
let test_cases = [
TestCase {
original: local_chain![],
insert: (5, h!("block5")),
expected_result: Ok([(5, Some(h!("block5")))].into()),
expected_final: local_chain![(5, h!("block5"))],
},
TestCase {
original: local_chain![(3, h!("A"))],
insert: (4, h!("B")),
expected_result: Ok([(4, Some(h!("B")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(4, h!("B"))],
insert: (3, h!("A")),
expected_result: Ok([(3, Some(h!("A")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
insert: (2, h!("K")),
expected_result: Ok([].into()),
expected_final: local_chain![(2, h!("K"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
insert: (2, h!("J")),
expected_result: Err(InsertBlockNotMatchingError {
height: 2,
original_hash: h!("K"),
update_hash: h!("J"),
}),
expected_final: local_chain![(2, h!("K"))],
},
];
for (i, t) in test_cases.into_iter().enumerate() {
let mut chain = t.original;
assert_eq!(
chain.insert_block(t.insert.into()),
t.expected_result,
"[{}] unexpected result when inserting block",
i,
);
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
}
}

View File

@@ -0,0 +1,100 @@
use bdk_chain::SpkTxOutIndex;
use bitcoin::{hashes::hex::FromHex, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut};
#[test]
fn spk_txout_sent_and_received() {
let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
let mut index = SpkTxOutIndex::default();
index.insert_spk(0, spk1.clone());
index.insert_spk(1, spk2.clone());
let tx1 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: spk1.clone(),
}],
};
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
assert_eq!(index.net_value(&tx1), 42_000);
index.scan(&tx1);
assert_eq!(
index.sent_and_received(&tx1),
(0, 42_000),
"shouldn't change after scanning"
);
let tx2 = Transaction {
version: 0x1,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint {
txid: tx1.txid(),
vout: 0,
},
..Default::default()
}],
output: vec![
TxOut {
value: 20_000,
script_pubkey: spk2,
},
TxOut {
script_pubkey: spk1,
value: 30_000,
},
],
};
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
assert_eq!(index.net_value(&tx2), 8_000);
}
#[test]
fn mark_used() {
let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
let mut spk_index = SpkTxOutIndex::default();
spk_index.insert_spk(1, spk1.clone());
spk_index.insert_spk(2, spk2);
assert!(!spk_index.is_used(&1));
spk_index.mark_used(&1);
assert!(spk_index.is_used(&1));
spk_index.unmark_used(&1);
assert!(!spk_index.is_used(&1));
spk_index.mark_used(&1);
assert!(spk_index.is_used(&1));
let tx1 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: spk1,
}],
};
spk_index.scan(&tx1);
spk_index.unmark_used(&1);
assert!(
spk_index.is_used(&1),
"even though we unmark_used it doesn't matter because there was a tx scanned that used it"
);
}
#[test]
fn unmark_used_does_not_result_in_invalid_representation() {
let mut spk_index = SpkTxOutIndex::default();
assert!(!spk_index.unmark_used(&0));
assert!(!spk_index.unmark_used(&1));
assert!(!spk_index.unmark_used(&2));
assert!(spk_index.unused_spks(..).collect::<Vec<_>>().is_empty());
}

View File

@@ -0,0 +1,824 @@
#[macro_use]
mod common;
use bdk_chain::{
collections::*,
local_chain::LocalChain,
tx_graph::{Additions, TxGraph},
Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
};
use core::iter;
use std::vec;
#[test]
fn insert_txouts() {
// 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions.
let original_ops = [
(
OutPoint::new(h!("tx1"), 1),
TxOut {
value: 10_000,
script_pubkey: Script::new(),
},
),
(
OutPoint::new(h!("tx1"), 2),
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
),
];
// Another (OutPoint, TxOut) tupple to be used as update as partial transaction.
let update_ops = [(
OutPoint::new(h!("tx2"), 0),
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
)];
// One full transaction to be included in the update
let update_txs = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut {
value: 30_000,
script_pubkey: Script::new(),
}],
};
// Conf anchor used to mark the full transaction as confirmed.
let conf_anchor = ChainPosition::Confirmed(BlockId {
height: 100,
hash: h!("random blockhash"),
});
// Unconfirmed anchor to mark the partial transactions as unconfirmed
let unconf_anchor = ChainPosition::<BlockId>::Unconfirmed(1000000);
// Make the original graph
let mut graph = {
let mut graph = TxGraph::<ChainPosition<BlockId>>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Additions {
txouts: [(*outpoint, txout.clone())].into(),
..Default::default()
}
);
}
graph
};
// Make the update graph
let update = {
let mut graph = TxGraph::default();
for (outpoint, txout) in &update_ops {
// Insert partials transactions
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Additions {
txouts: [(*outpoint, txout.clone())].into(),
..Default::default()
}
);
// Mark them unconfirmed.
assert_eq!(
graph.insert_anchor(outpoint.txid, unconf_anchor),
Additions {
txs: [].into(),
txouts: [].into(),
anchors: [(unconf_anchor, outpoint.txid)].into(),
last_seen: [].into()
}
);
// Mark them last seen at.
assert_eq!(
graph.insert_seen_at(outpoint.txid, 1000000),
Additions {
txs: [].into(),
txouts: [].into(),
anchors: [].into(),
last_seen: [(outpoint.txid, 1000000)].into()
}
);
}
// Insert the full transaction
assert_eq!(
graph.insert_tx(update_txs.clone()),
Additions {
txs: [update_txs.clone()].into(),
..Default::default()
}
);
// Mark it as confirmed.
assert_eq!(
graph.insert_anchor(update_txs.txid(), conf_anchor),
Additions {
txs: [].into(),
txouts: [].into(),
anchors: [(conf_anchor, update_txs.txid())].into(),
last_seen: [].into()
}
);
graph
};
// Check the resulting addition.
let additions = graph.determine_additions(&update);
assert_eq!(
additions,
Additions {
txs: [update_txs.clone()].into(),
txouts: update_ops.into(),
anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
last_seen: [(h!("tx2"), 1000000)].into()
}
);
// Apply addition and check the new graph counts.
graph.apply_additions(additions);
assert_eq!(graph.all_txouts().count(), 4);
assert_eq!(graph.full_txs().count(), 1);
assert_eq!(graph.floating_txouts().count(), 3);
// Check TxOuts are fetched correctly from the graph.
assert_eq!(
graph.tx_outputs(h!("tx1")).expect("should exists"),
[
(
1u32,
&TxOut {
value: 10_000,
script_pubkey: Script::new(),
}
),
(
2u32,
&TxOut {
value: 20_000,
script_pubkey: Script::new(),
}
)
]
.into()
);
assert_eq!(
graph.tx_outputs(update_txs.txid()).expect("should exists"),
[(
0u32,
&TxOut {
value: 30_000,
script_pubkey: Script::new()
}
)]
.into()
);
}
#[test]
fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![],
};
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx);
assert!(graph.outspends(OutPoint::null()).is_empty());
assert!(graph.tx_spends(Txid::all_zeros()).next().is_none());
}
#[test]
fn insert_tx_graph_keeps_track_of_spend() {
let tx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let op = OutPoint {
txid: tx1.txid(),
vout: 0,
};
let tx2 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: op,
..Default::default()
}],
output: vec![],
};
let mut graph1 = TxGraph::<()>::default();
let mut graph2 = TxGraph::<()>::default();
// insert in different order
let _ = graph1.insert_tx(tx1.clone());
let _ = graph1.insert_tx(tx2.clone());
let _ = graph2.insert_tx(tx2.clone());
let _ = graph2.insert_tx(tx1);
assert_eq!(
graph1.outspends(op),
&iter::once(tx2.txid()).collect::<HashSet<_>>()
);
assert_eq!(graph2.outspends(op), graph1.outspends(op));
}
#[test]
fn insert_tx_can_retrieve_full_tx_from_graph() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut::default()],
};
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx.clone());
assert_eq!(graph.get_tx(tx.txid()), Some(&tx));
}
#[test]
fn insert_tx_displaces_txouts() {
let mut tx_graph = TxGraph::<()>::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: Script::default(),
}],
};
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_337_000,
script_pubkey: Script::default(),
},
);
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_000_000_000,
script_pubkey: Script::default(),
},
);
let _additions = tx_graph.insert_tx(tx.clone());
assert_eq!(
tx_graph
.get_txout(OutPoint {
txid: tx.txid(),
vout: 0
})
.unwrap()
.value,
42_000
);
assert_eq!(
tx_graph.get_txout(OutPoint {
txid: tx.txid(),
vout: 1
}),
None
);
}
#[test]
fn insert_txout_does_not_displace_tx() {
let mut tx_graph = TxGraph::<()>::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: Script::default(),
}],
};
let _additions = tx_graph.insert_tx(tx.clone());
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_337_000,
script_pubkey: Script::default(),
},
);
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_000_000_000,
script_pubkey: Script::default(),
},
);
assert_eq!(
tx_graph
.get_txout(OutPoint {
txid: tx.txid(),
vout: 0
})
.unwrap()
.value,
42_000
);
assert_eq!(
tx_graph.get_txout(OutPoint {
txid: tx.txid(),
vout: 1
}),
None
);
}
#[test]
fn test_calculate_fee() {
let mut graph = TxGraph::<()>::default();
let intx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 100,
..Default::default()
}],
};
let intx2 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 200,
..Default::default()
}],
};
let intxout1 = (
OutPoint {
txid: h!("dangling output"),
vout: 0,
},
TxOut {
value: 300,
..Default::default()
},
);
let _ = graph.insert_tx(intx1.clone());
let _ = graph.insert_tx(intx2.clone());
let _ = graph.insert_txout(intxout1.0, intxout1.1);
let mut tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![
TxIn {
previous_output: OutPoint {
txid: intx1.txid(),
vout: 0,
},
..Default::default()
},
TxIn {
previous_output: OutPoint {
txid: intx2.txid(),
vout: 0,
},
..Default::default()
},
TxIn {
previous_output: intxout1.0,
..Default::default()
},
],
output: vec![TxOut {
value: 500,
..Default::default()
}],
};
assert_eq!(graph.calculate_fee(&tx), Some(100));
tx.input.remove(2);
// fee would be negative
assert_eq!(graph.calculate_fee(&tx), Some(-200));
// If we have an unknown outpoint, fee should return None.
tx.input.push(TxIn {
previous_output: OutPoint {
txid: h!("unknown_txid"),
vout: 0,
},
..Default::default()
});
assert_eq!(graph.calculate_fee(&tx), None);
}
#[test]
fn test_calculate_fee_on_coinbase() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut::default()],
};
let graph = TxGraph::<()>::default();
assert_eq!(graph.calculate_fee(&tx), Some(0));
}
#[test]
fn test_conflicting_descendants() {
let previous_output = OutPoint::new(h!("op"), 2);
// tx_a spends previous_output
let tx_a = Transaction {
input: vec![TxIn {
previous_output,
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(0)
};
// tx_a2 spends previous_output and conflicts with tx_a
let tx_a2 = Transaction {
input: vec![TxIn {
previous_output,
..TxIn::default()
}],
output: vec![TxOut::default(), TxOut::default()],
..common::new_tx(1)
};
// tx_b spends tx_a
let tx_b = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(2)
};
let txid_a = tx_a.txid();
let txid_b = tx_b.txid();
let mut graph = TxGraph::<()>::default();
let _ = graph.insert_tx(tx_a);
let _ = graph.insert_tx(tx_b);
assert_eq!(
graph
.walk_conflicts(&tx_a2, |depth, txid| Some((depth, txid)))
.collect::<Vec<_>>(),
vec![(0_usize, txid_a), (1_usize, txid_b),],
);
}
#[test]
fn test_descendants_no_repeat() {
let tx_a = Transaction {
output: vec![TxOut::default(), TxOut::default(), TxOut::default()],
..common::new_tx(0)
};
let txs_b = (0..3)
.map(|vout| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), vout),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(1)
})
.collect::<Vec<_>>();
let txs_c = (0..2)
.map(|vout| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(txs_b[vout as usize].txid(), vout),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(2)
})
.collect::<Vec<_>>();
let tx_d = Transaction {
input: vec![
TxIn {
previous_output: OutPoint::new(txs_c[0].txid(), 0),
..TxIn::default()
},
TxIn {
previous_output: OutPoint::new(txs_c[1].txid(), 0),
..TxIn::default()
},
],
output: vec![TxOut::default()],
..common::new_tx(3)
};
let tx_e = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_d.txid(), 0),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(4)
};
let txs_not_connected = (10..20)
.map(|v| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(h!("tx_does_not_exist"), v),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(v)
})
.collect::<Vec<_>>();
let mut graph = TxGraph::<()>::default();
let mut expected_txids = BTreeSet::new();
// these are NOT descendants of `tx_a`
for tx in txs_not_connected {
let _ = graph.insert_tx(tx.clone());
}
// these are the expected descendants of `tx_a`
for tx in txs_b
.iter()
.chain(&txs_c)
.chain(core::iter::once(&tx_d))
.chain(core::iter::once(&tx_e))
{
let _ = graph.insert_tx(tx.clone());
assert!(expected_txids.insert(tx.txid()));
}
let descendants = graph
.walk_descendants(tx_a.txid(), |_, txid| Some(txid))
.collect::<Vec<_>>();
assert_eq!(descendants.len(), expected_txids.len());
for txid in descendants {
assert!(expected_txids.remove(&txid));
}
assert!(expected_txids.is_empty());
}
#[test]
fn test_chain_spends() {
let local_chain: LocalChain = (0..=100)
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.collect::<BTreeMap<u32, BlockHash>>()
.into();
let tip = local_chain.tip().expect("must have tip");
// The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
// The parent tx is confirmed at block 95.
let tx_0 = Transaction {
input: vec![],
output: vec![
TxOut {
value: 10_000,
script_pubkey: Script::new(),
},
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
],
..common::new_tx(0)
};
// The first confirmed transaction spends vout: 0. And is confirmed at block 98.
let tx_1 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_0.txid(), 0),
..TxIn::default()
}],
output: vec![
TxOut {
value: 5_000,
script_pubkey: Script::new(),
},
TxOut {
value: 5_000,
script_pubkey: Script::new(),
},
],
..common::new_tx(0)
};
// The second transactions spends vout:1, and is unconfirmed.
let tx_2 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_0.txid(), 1),
..TxIn::default()
}],
output: vec![
TxOut {
value: 10_000,
script_pubkey: Script::new(),
},
TxOut {
value: 10_000,
script_pubkey: Script::new(),
},
],
..common::new_tx(0)
};
let mut graph = TxGraph::<ConfirmationHeightAnchor>::default();
let _ = graph.insert_tx(tx_0.clone());
let _ = graph.insert_tx(tx_1.clone());
let _ = graph.insert_tx(tx_2.clone());
[95, 98]
.iter()
.zip([&tx_0, &tx_1].into_iter())
.for_each(|(ht, tx)| {
let _ = graph.insert_anchor(
tx.txid(),
ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: *ht,
},
);
});
// Assert that confirmed spends are returned correctly.
assert_eq!(
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)),
Some((
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: 98
}),
tx_1.txid(),
)),
);
// Check if chain position is returned correctly.
assert_eq!(
graph.get_chain_position(&local_chain, tip, tx_0.txid()),
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: 95
}))
);
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
assert_eq!(
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)),
Some((ChainPosition::Unconfirmed(0), tx_2.txid())),
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
let _ = graph.insert_seen_at(tx_2.txid(), 1234567);
// Check chain spend returned correctly.
assert_eq!(
graph
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
.unwrap(),
(ChainPosition::Unconfirmed(1234567), tx_2.txid())
);
// A conflicting transaction that conflicts with tx_1.
let tx_1_conflict = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_0.txid(), 0),
..Default::default()
}],
..common::new_tx(0)
};
let _ = graph.insert_tx(tx_1_conflict.clone());
// Because this tx conflicts with an already confirmed transaction, chain position should return none.
assert!(graph
.get_chain_position(&local_chain, tip, tx_1_conflict.txid())
.is_none());
// Another conflicting tx that conflicts with tx_2.
let tx_2_conflict = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_0.txid(), 1),
..Default::default()
}],
..common::new_tx(0)
};
// Insert in graph and mark it as seen.
let _ = graph.insert_tx(tx_2_conflict.clone());
let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568);
// This should return a valid observation with correct last seen.
assert_eq!(
graph
.get_chain_position(&local_chain, tip, tx_2_conflict.txid())
.expect("position expected"),
ChainPosition::Unconfirmed(1234568)
);
// Chain_spend now catches the new transaction as the spend.
assert_eq!(
graph
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
.expect("expect observation"),
(ChainPosition::Unconfirmed(1234568), tx_2_conflict.txid())
);
// Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
assert!(graph
.get_chain_position(&local_chain, tip, tx_2.txid())
.is_none());
}
/// Ensure that `last_seen` values only increase during [`Append::append`].
#[test]
fn test_additions_last_seen_append() {
let txid: Txid = h!("test txid");
let test_cases: &[(Option<u64>, Option<u64>)] = &[
(Some(5), Some(6)),
(Some(5), Some(5)),
(Some(6), Some(5)),
(None, Some(5)),
(Some(5), None),
];
for (original_ls, update_ls) in test_cases {
let mut original = Additions::<()> {
last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(),
..Default::default()
};
let update = Additions::<()> {
last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(),
..Default::default()
};
original.append(update);
assert_eq!(
&original.last_seen.get(&txid).cloned(),
Ord::max(original_ls, update_ls),
);
}
}

View File

@@ -0,0 +1,16 @@
[package]
name = "bdk_electrum"
version = "0.3.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_electrum"
description = "Fetch data from electrum in the form BDK accepts"
license = "MIT OR Apache-2.0"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../chain", version = "0.5.0", features = ["serde", "miniscript"] }
electrum-client = { version = "0.12" }

View File

@@ -0,0 +1,3 @@
# BDK Electrum
BDK Electrum client library for updating the keychain tracker.

View File

@@ -0,0 +1,486 @@
use bdk_chain::{
bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
keychain::LocalUpdate,
local_chain::LocalChain,
tx_graph::{self, TxGraph},
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeAnchor,
};
use electrum_client::{Client, ElectrumApi, Error};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
};
#[derive(Debug, Clone)]
pub struct ElectrumUpdate<K, A> {
pub graph_update: HashMap<Txid, BTreeSet<A>>,
pub chain_update: LocalChain,
pub keychain_update: BTreeMap<K, u32>,
}
impl<K, A> Default for ElectrumUpdate<K, A> {
fn default() -> Self {
Self {
graph_update: Default::default(),
chain_update: Default::default(),
keychain_update: Default::default(),
}
}
}
impl<K, A: Anchor> ElectrumUpdate<K, A> {
pub fn missing_full_txs<A2>(&self, graph: &TxGraph<A2>) -> Vec<Txid> {
self.graph_update
.keys()
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
.cloned()
.collect()
}
pub fn finalize(
self,
client: &Client,
seen_at: Option<u64>,
missing: Vec<Txid>,
) -> Result<LocalUpdate<K, A>, Error> {
let new_txs = client.batch_transaction_get(&missing)?;
let mut graph_update = TxGraph::<A>::new(new_txs);
for (txid, anchors) in self.graph_update {
if let Some(seen_at) = seen_at {
let _ = graph_update.insert_seen_at(txid, seen_at);
}
for anchor in anchors {
let _ = graph_update.insert_anchor(txid, anchor);
}
}
Ok(LocalUpdate {
keychain: self.keychain_update,
graph: graph_update,
chain: self.chain_update,
})
}
}
impl<K> ElectrumUpdate<K, ConfirmationHeightAnchor> {
/// Finalizes the [`ElectrumUpdate`] with `new_txs` and anchors of type
/// [`ConfirmationTimeAnchor`].
///
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
/// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
/// use it.
pub fn finalize_as_confirmation_time(
self,
client: &Client,
seen_at: Option<u64>,
missing: Vec<Txid>,
) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
let update = self.finalize(client, seen_at, missing)?;
let relevant_heights = {
let mut visited_heights = HashSet::new();
update
.graph
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height_upper_bound())
.filter(move |&h| visited_heights.insert(h))
.collect::<Vec<_>>()
};
let height_to_time = relevant_heights
.clone()
.into_iter()
.zip(
client
.batch_block_header(relevant_heights)?
.into_iter()
.map(|bh| bh.time as u64),
)
.collect::<HashMap<u32, u64>>();
let graph_additions = {
let old_additions = TxGraph::default().determine_additions(&update.graph);
tx_graph::Additions {
txs: old_additions.txs,
txouts: old_additions.txouts,
last_seen: old_additions.last_seen,
anchors: old_additions
.anchors
.into_iter()
.map(|(height_anchor, txid)| {
let confirmation_height = height_anchor.confirmation_height;
let confirmation_time = height_to_time[&confirmation_height];
let time_anchor = ConfirmationTimeAnchor {
anchor_block: height_anchor.anchor_block,
confirmation_height,
confirmation_time,
};
(time_anchor, txid)
})
.collect(),
}
};
Ok(LocalUpdate {
keychain: update.keychain,
graph: {
let mut graph = TxGraph::default();
graph.apply_additions(graph_additions);
graph
},
chain: update.chain,
})
}
}
pub trait ElectrumExt<A> {
fn get_tip(&self) -> Result<(u32, BlockHash), Error>;
fn scan<K: Ord + Clone>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
batch_size: usize,
) -> Result<ElectrumUpdate<K, A>, Error>;
fn scan_without_keychain(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
misc_spks: impl IntoIterator<Item = Script>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
batch_size: usize,
) -> Result<ElectrumUpdate<(), A>, Error> {
let spk_iter = misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk));
self.scan(
local_chain,
[((), spk_iter)].into(),
txids,
outpoints,
usize::MAX,
batch_size,
)
}
}
impl ElectrumExt<ConfirmationHeightAnchor> for Client {
fn get_tip(&self) -> Result<(u32, BlockHash), Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
self.block_headers_subscribe()
.map(|data| (data.height as u32, data.header.block_hash()))
}
fn scan<K: Ord + Clone>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
batch_size: usize,
) -> Result<ElectrumUpdate<K, ConfirmationHeightAnchor>, Error> {
let mut request_spks = keychain_spks
.into_iter()
.map(|(k, s)| (k, s.into_iter()))
.collect::<BTreeMap<K, _>>();
let mut scanned_spks = BTreeMap::<(K, u32), (Script, bool)>::new();
let txids = txids.into_iter().collect::<Vec<_>>();
let outpoints = outpoints.into_iter().collect::<Vec<_>>();
let update = loop {
let mut update = ElectrumUpdate::<K, ConfirmationHeightAnchor> {
chain_update: prepare_chain_update(self, local_chain)?,
..Default::default()
};
let anchor_block = update
.chain_update
.tip()
.expect("must have atleast one block");
if !request_spks.is_empty() {
if !scanned_spks.is_empty() {
scanned_spks.append(&mut populate_with_spks(
self,
anchor_block,
&mut update,
&mut scanned_spks
.iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
stop_gap,
batch_size,
)?);
}
for (keychain, keychain_spks) in &mut request_spks {
scanned_spks.extend(
populate_with_spks(
self,
anchor_block,
&mut update,
keychain_spks,
stop_gap,
batch_size,
)?
.into_iter()
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
);
}
}
populate_with_txids(self, anchor_block, &mut update, &mut txids.iter().cloned())?;
let _txs = populate_with_outpoints(
self,
anchor_block,
&mut update,
&mut outpoints.iter().cloned(),
)?;
// check for reorgs during scan process
let server_blockhash = self
.block_header(anchor_block.height as usize)?
.block_hash();
if anchor_block.hash != server_blockhash {
continue; // reorg
}
update.keychain_update = request_spks
.into_keys()
.filter_map(|k| {
scanned_spks
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
.rev()
.find(|(_, (_, active))| *active)
.map(|((_, i), _)| (k, *i))
})
.collect::<BTreeMap<_, _>>();
break update;
};
Ok(update)
}
}
/// Prepare an update "template" based on the checkpoints of the `local_chain`.
fn prepare_chain_update(
client: &Client,
local_chain: &BTreeMap<u32, BlockHash>,
) -> Result<LocalChain, Error> {
let mut update = LocalChain::default();
// Find the local chain block that is still there so our update can connect to the local chain.
for (&existing_height, &existing_hash) in local_chain.iter().rev() {
// TODO: a batch request may be safer, as a reorg that happens when we are obtaining
// `block_header`s will result in inconsistencies
let current_hash = client.block_header(existing_height as usize)?.block_hash();
let _ = update
.insert_block(BlockId {
height: existing_height,
hash: current_hash,
})
.expect("This never errors because we are working with a fresh chain");
if current_hash == existing_hash {
break;
}
}
// Insert the new tip so new transactions will be accepted into the sparsechain.
let tip = {
let (height, hash) = crate::get_tip(client)?;
BlockId { height, hash }
};
if update.insert_block(tip).is_err() {
// There has been a re-org before we even begin scanning addresses.
// Just recursively call (this should never happen).
return prepare_chain_update(client, local_chain);
}
Ok(update)
}
fn determine_tx_anchor(
anchor_block: BlockId,
raw_height: i32,
txid: Txid,
) -> Option<ConfirmationHeightAnchor> {
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
// height of 0. To avoid invalid representation in our data structures, we manually set
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
// unconfirmed for all other transactions.
if txid
== Txid::from_hex("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
.expect("must deserialize genesis coinbase txid")
{
return Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: 0,
});
}
match raw_height {
h if h <= 0 => {
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
None
}
h => {
let h = h as u32;
if h > anchor_block.height {
None
} else {
Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: h,
})
}
}
}
}
fn populate_with_outpoints<K>(
client: &Client,
anchor_block: BlockId,
update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
outpoints: &mut impl Iterator<Item = OutPoint>,
) -> Result<HashMap<Txid, Transaction>, Error> {
let mut full_txs = HashMap::new();
for outpoint in outpoints {
let txid = outpoint.txid;
let tx = client.transaction_get(&txid)?;
debug_assert_eq!(tx.txid(), txid);
let txout = match tx.output.get(outpoint.vout as usize) {
Some(txout) => txout,
None => continue,
};
// attempt to find the following transactions (alongside their chain positions), and
// add to our sparsechain `update`:
let mut has_residing = false; // tx in which the outpoint resides
let mut has_spending = false; // tx that spends the outpoint
for res in client.script_get_history(&txout.script_pubkey)? {
if has_residing && has_spending {
break;
}
if res.tx_hash == txid {
if has_residing {
continue;
}
has_residing = true;
full_txs.insert(res.tx_hash, tx.clone());
} else {
if has_spending {
continue;
}
let res_tx = match full_txs.get(&res.tx_hash) {
Some(tx) => tx,
None => {
let res_tx = client.transaction_get(&res.tx_hash)?;
full_txs.insert(res.tx_hash, res_tx);
full_txs.get(&res.tx_hash).expect("just inserted")
}
};
has_spending = res_tx
.input
.iter()
.any(|txin| txin.previous_output == outpoint);
if !has_spending {
continue;
}
};
let anchor = determine_tx_anchor(anchor_block, res.height, res.tx_hash);
let tx_entry = update.graph_update.entry(res.tx_hash).or_default();
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
}
}
}
Ok(full_txs)
}
fn populate_with_txids<K>(
client: &Client,
anchor_block: BlockId,
update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
txids: &mut impl Iterator<Item = Txid>,
) -> Result<(), Error> {
for txid in txids {
let tx = match client.transaction_get(&txid) {
Ok(tx) => tx,
Err(electrum_client::Error::Protocol(_)) => continue,
Err(other_err) => return Err(other_err),
};
let spk = tx
.output
.get(0)
.map(|txo| &txo.script_pubkey)
.expect("tx must have an output");
let anchor = match client
.script_get_history(spk)?
.into_iter()
.find(|r| r.tx_hash == txid)
{
Some(r) => determine_tx_anchor(anchor_block, r.height, txid),
None => continue,
};
let tx_entry = update.graph_update.entry(txid).or_default();
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
}
}
Ok(())
}
fn populate_with_spks<K, I: Ord + Clone>(
client: &Client,
anchor_block: BlockId,
update: &mut ElectrumUpdate<K, ConfirmationHeightAnchor>,
spks: &mut impl Iterator<Item = (I, Script)>,
stop_gap: usize,
batch_size: usize,
) -> Result<BTreeMap<I, (Script, bool)>, Error> {
let mut unused_spk_count = 0_usize;
let mut scanned_spks = BTreeMap::new();
loop {
let spks = (0..batch_size)
.map_while(|_| spks.next())
.collect::<Vec<_>>();
if spks.is_empty() {
return Ok(scanned_spks);
}
let spk_histories = client.batch_script_get_history(spks.iter().map(|(_, s)| s))?;
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
if spk_history.is_empty() {
scanned_spks.insert(spk_index, (spk, false));
unused_spk_count += 1;
if unused_spk_count > stop_gap {
return Ok(scanned_spks);
}
continue;
} else {
scanned_spks.insert(spk_index, (spk, true));
unused_spk_count = 0;
}
for tx in spk_history {
let tx_entry = update.graph_update.entry(tx.tx_hash).or_default();
if let Some(anchor) = determine_tx_anchor(anchor_block, tx.height, tx.tx_hash) {
tx_entry.insert(anchor);
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
//! This crate is used for updating structures of the [`bdk_chain`] crate with data from electrum.
//!
//! The star of the show is the [`ElectrumExt::scan`] method, which scans for relevant blockchain
//! data (via electrum) and outputs an [`ElectrumUpdate`].
//!
//! An [`ElectrumUpdate`] only includes `txid`s and no full transactions. The caller is responsible
//! for obtaining full transactions before applying. This can be done with
//! these steps:
//!
//! 1. Determine which full transactions are missing. The method [`missing_full_txs`] of
//! [`ElectrumUpdate`] can be used.
//!
//! 2. Obtaining the full transactions. To do this via electrum, the method
//! [`batch_transaction_get`] can be used.
//!
//! Refer to [`bdk_electrum_example`] for a complete example.
//!
//! [`ElectrumClient::scan`]: ElectrumClient::scan
//! [`missing_full_txs`]: ElectrumUpdate::missing_full_txs
//! [`batch_transaction_get`]: ElectrumApi::batch_transaction_get
//! [`bdk_electrum_example`]: https://github.com/LLFourn/bdk_core_staging/tree/master/bdk_electrum_example
use bdk_chain::bitcoin::BlockHash;
use electrum_client::{Client, ElectrumApi, Error};
mod electrum_ext;
pub use bdk_chain;
pub use electrum_client;
pub use electrum_ext::*;
fn get_tip(client: &Client) -> Result<(u32, BlockHash), Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
client
.block_headers_subscribe()
.map(|data| (data.height as u32, data.header.block_hash()))
}

29
crates/esplora/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "bdk_esplora"
version = "0.3.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_esplora"
description = "Fetch data from esplora in the form that accepts"
license = "MIT OR Apache-2.0"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../chain", version = "0.5.0", default-features = false, features = ["serde", "miniscript"] }
esplora-client = { version = "0.5", default-features = false }
async-trait = { version = "0.1.66", optional = true }
futures = { version = "0.3.26", optional = true }
# use these dependencies if you need to enable their /no-std features
bitcoin = { version = "0.29", optional = true, default-features = false }
miniscript = { version = "9.0.0", optional = true, default-features = false }
[features]
default = ["std", "async-https", "blocking"]
std = ["bdk_chain/std"]
async = ["async-trait", "futures", "esplora-client/async"]
async-https = ["async", "esplora-client/async-https"]
blocking = ["esplora-client/blocking"]

33
crates/esplora/README.md Normal file
View File

@@ -0,0 +1,33 @@
# BDK Esplora
BDK Esplora extends [`esplora_client`](crate::esplora_client) to update [`bdk_chain`] structures
from an Esplora server.
## Usage
There are two versions of the extension trait (blocking and async).
For blocking-only:
```toml
bdk_esplora = { version = "0.3", features = ["blocking"] }
```
For async-only:
```toml
bdk_esplora = { version = "0.3", features = ["async"] }
```
For async-only (with https):
```toml
bdk_esplora = { version = "0.3", features = ["async-https"] }
```
To use the extension traits:
```rust
// for blocking
use bdk_esplora::EsploraExt;
// for async
// use bdk_esplora::EsploraAsyncExt;
```
For full examples, refer to [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora) (blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).

View File

@@ -0,0 +1,269 @@
use async_trait::async_trait;
use bdk_chain::{
bitcoin::{BlockHash, OutPoint, Script, Txid},
collections::BTreeMap,
keychain::LocalUpdate,
BlockId, ConfirmationTimeAnchor,
};
use esplora_client::{Error, OutputStatus, TxStatus};
use futures::{stream::FuturesOrdered, TryStreamExt};
use crate::map_confirmation_time_anchor;
/// Trait to extend [`esplora_client::AsyncClient`] functionality.
///
/// This is the async version of [`EsploraExt`]. Refer to
/// [crate-level documentation] for more.
///
/// [`EsploraExt`]: crate::EsploraExt
/// [crate-level documentation]: crate
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EsploraAsyncExt {
/// Scan the blockchain (via esplora) for the data specified and returns a
/// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
///
/// - `local_chain`: the most recent block hashes present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
/// - `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to included in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
#[allow(clippy::result_large_err)] // FIXME
async fn scan<K: Ord + Clone + Send>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, Script)> + Send> + Send,
>,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
/// Convenience method to call [`scan`] without requiring a keychain.
///
/// [`scan`]: EsploraAsyncExt::scan
#[allow(clippy::result_large_err)] // FIXME
async fn scan_without_keychain(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = Script> + Send> + Send,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
self.scan(
local_chain,
[(
(),
misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk)),
)]
.into(),
txids,
outpoints,
usize::MAX,
parallel_requests,
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EsploraAsyncExt for esplora_client::AsyncClient {
#[allow(clippy::result_large_err)] // FIXME
async fn scan<K: Ord + Clone + Send>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, Script)> + Send> + Send,
>,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
let parallel_requests = Ord::max(parallel_requests, 1);
let (mut update, tip_at_start) = loop {
let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
for (&height, &original_hash) in local_chain.iter().rev() {
let update_block_id = BlockId {
height,
hash: self.get_block_hash(height).await?,
};
let _ = update
.chain
.insert_block(update_block_id)
.expect("cannot repeat height here");
if update_block_id.hash == original_hash {
break;
}
}
let tip_at_start = BlockId {
height: self.get_height().await?,
hash: self.get_tip_hash().await?,
};
if update.chain.insert_block(tip_at_start).is_ok() {
break (update, tip_at_start);
}
};
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
let mut last_active_index = None;
let mut empty_scripts = 0;
type IndexWithTxs = (u32, Vec<esplora_client::Tx>);
loop {
let futures = (0..parallel_requests)
.filter_map(|_| {
let (index, script) = spks.next()?;
let client = self.clone();
Some(async move {
let mut related_txs = client.scripthash_txs(&script, None).await?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
// esplora pages on 25 confirmed transactions. If there are 25 or more we
// keep requesting to see if there's more.
if n_confirmed >= 25 {
loop {
let new_related_txs = client
.scripthash_txs(
&script,
Some(related_txs.last().unwrap().txid),
)
.await?;
let n = new_related_txs.len();
related_txs.extend(new_related_txs);
// we've reached the end
if n < 25 {
break;
}
}
}
Result::<_, esplora_client::Error>::Ok((index, related_txs))
})
})
.collect::<FuturesOrdered<_>>();
let n_futures = futures.len();
for (index, related_txs) in futures.try_collect::<Vec<IndexWithTxs>>().await? {
if related_txs.is_empty() {
empty_scripts += 1;
} else {
last_active_index = Some(index);
empty_scripts = 0;
}
for tx in related_txs {
let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
let _ = update.graph.insert_tx(tx.to_tx());
if let Some(anchor) = anchor {
let _ = update.graph.insert_anchor(tx.txid, anchor);
}
}
}
if n_futures == 0 || empty_scripts >= stop_gap {
break;
}
}
if let Some(last_active_index) = last_active_index {
update.keychain.insert(keychain, last_active_index);
}
}
for txid in txids.into_iter() {
if update.graph.get_tx(txid).is_none() {
match self.get_tx(&txid).await? {
Some(tx) => {
let _ = update.graph.insert_tx(tx);
}
None => continue,
}
}
match self.get_tx_status(&txid).await? {
tx_status if tx_status.confirmed => {
if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
let _ = update.graph.insert_anchor(txid, anchor);
}
}
_ => continue,
}
}
for op in outpoints.into_iter() {
let mut op_txs = Vec::with_capacity(2);
if let (
Some(tx),
tx_status @ TxStatus {
confirmed: true, ..
},
) = (
self.get_tx(&op.txid).await?,
self.get_tx_status(&op.txid).await?,
) {
op_txs.push((tx, tx_status));
if let Some(OutputStatus {
txid: Some(txid),
status: Some(spend_status),
..
}) = self.get_output_status(&op.txid, op.vout as _).await?
{
if let Some(spend_tx) = self.get_tx(&txid).await? {
op_txs.push((spend_tx, spend_status));
}
}
}
for (tx, status) in op_txs {
let txid = tx.txid();
let anchor = map_confirmation_time_anchor(&status, tip_at_start);
let _ = update.graph.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = update.graph.insert_anchor(txid, anchor);
}
}
}
if tip_at_start.hash != self.get_block_hash(tip_at_start.height).await? {
// A reorg occurred, so let's find out where all the txids we found are now in the chain
let txids_found = update
.graph
.full_txs()
.map(|tx_node| tx_node.txid)
.collect::<Vec<_>>();
update.chain = EsploraAsyncExt::scan_without_keychain(
self,
local_chain,
[],
txids_found,
[],
parallel_requests,
)
.await?
.chain;
}
Ok(update)
}
}

View File

@@ -0,0 +1,251 @@
use bdk_chain::bitcoin::{BlockHash, OutPoint, Script, Txid};
use bdk_chain::collections::BTreeMap;
use bdk_chain::BlockId;
use bdk_chain::{keychain::LocalUpdate, ConfirmationTimeAnchor};
use esplora_client::{Error, OutputStatus, TxStatus};
use crate::map_confirmation_time_anchor;
/// Trait to extend [`esplora_client::BlockingClient`] functionality.
///
/// Refer to [crate-level documentation] for more.
///
/// [crate-level documentation]: crate
pub trait EsploraExt {
/// Scan the blockchain (via esplora) for the data specified and returns a
/// [`LocalUpdate<K, ConfirmationTimeAnchor>`].
///
/// - `local_chain`: the most recent block hashes present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
/// - `txids`: transactions for which we want updated [`ConfirmationTimeAnchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to included in the update
///
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
#[allow(clippy::result_large_err)] // FIXME
fn scan<K: Ord + Clone>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error>;
/// Convenience method to call [`scan`] without requiring a keychain.
///
/// [`scan`]: EsploraExt::scan
#[allow(clippy::result_large_err)] // FIXME
fn scan_without_keychain(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
misc_spks: impl IntoIterator<Item = Script>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
) -> Result<LocalUpdate<(), ConfirmationTimeAnchor>, Error> {
self.scan(
local_chain,
[(
(),
misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk)),
)]
.into(),
txids,
outpoints,
usize::MAX,
parallel_requests,
)
}
}
impl EsploraExt for esplora_client::BlockingClient {
fn scan<K: Ord + Clone>(
&self,
local_chain: &BTreeMap<u32, BlockHash>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<LocalUpdate<K, ConfirmationTimeAnchor>, Error> {
let parallel_requests = Ord::max(parallel_requests, 1);
let (mut update, tip_at_start) = loop {
let mut update = LocalUpdate::<K, ConfirmationTimeAnchor>::default();
for (&height, &original_hash) in local_chain.iter().rev() {
let update_block_id = BlockId {
height,
hash: self.get_block_hash(height)?,
};
let _ = update
.chain
.insert_block(update_block_id)
.expect("cannot repeat height here");
if update_block_id.hash == original_hash {
break;
}
}
let tip_at_start = BlockId {
height: self.get_height()?,
hash: self.get_tip_hash()?,
};
if update.chain.insert_block(tip_at_start).is_ok() {
break (update, tip_at_start);
}
};
for (keychain, spks) in keychain_spks {
let mut spks = spks.into_iter();
let mut last_active_index = None;
let mut empty_scripts = 0;
type IndexWithTxs = (u32, Vec<esplora_client::Tx>);
loop {
let handles = (0..parallel_requests)
.filter_map(
|_| -> Option<std::thread::JoinHandle<Result<IndexWithTxs, _>>> {
let (index, script) = spks.next()?;
let client = self.clone();
Some(std::thread::spawn(move || {
let mut related_txs = client.scripthash_txs(&script, None)?;
let n_confirmed =
related_txs.iter().filter(|tx| tx.status.confirmed).count();
// esplora pages on 25 confirmed transactions. If there are 25 or more we
// keep requesting to see if there's more.
if n_confirmed >= 25 {
loop {
let new_related_txs = client.scripthash_txs(
&script,
Some(related_txs.last().unwrap().txid),
)?;
let n = new_related_txs.len();
related_txs.extend(new_related_txs);
// we've reached the end
if n < 25 {
break;
}
}
}
Result::<_, esplora_client::Error>::Ok((index, related_txs))
}))
},
)
.collect::<Vec<_>>();
let n_handles = handles.len();
for handle in handles {
let (index, related_txs) = handle.join().unwrap()?; // TODO: don't unwrap
if related_txs.is_empty() {
empty_scripts += 1;
} else {
last_active_index = Some(index);
empty_scripts = 0;
}
for tx in related_txs {
let anchor = map_confirmation_time_anchor(&tx.status, tip_at_start);
let _ = update.graph.insert_tx(tx.to_tx());
if let Some(anchor) = anchor {
let _ = update.graph.insert_anchor(tx.txid, anchor);
}
}
}
if n_handles == 0 || empty_scripts >= stop_gap {
break;
}
}
if let Some(last_active_index) = last_active_index {
update.keychain.insert(keychain, last_active_index);
}
}
for txid in txids.into_iter() {
if update.graph.get_tx(txid).is_none() {
match self.get_tx(&txid)? {
Some(tx) => {
let _ = update.graph.insert_tx(tx);
}
None => continue,
}
}
match self.get_tx_status(&txid)? {
tx_status @ TxStatus {
confirmed: true, ..
} => {
if let Some(anchor) = map_confirmation_time_anchor(&tx_status, tip_at_start) {
let _ = update.graph.insert_anchor(txid, anchor);
}
}
_ => continue,
}
}
for op in outpoints.into_iter() {
let mut op_txs = Vec::with_capacity(2);
if let (
Some(tx),
tx_status @ TxStatus {
confirmed: true, ..
},
) = (self.get_tx(&op.txid)?, self.get_tx_status(&op.txid)?)
{
op_txs.push((tx, tx_status));
if let Some(OutputStatus {
txid: Some(txid),
status: Some(spend_status),
..
}) = self.get_output_status(&op.txid, op.vout as _)?
{
if let Some(spend_tx) = self.get_tx(&txid)? {
op_txs.push((spend_tx, spend_status));
}
}
}
for (tx, status) in op_txs {
let txid = tx.txid();
let anchor = map_confirmation_time_anchor(&status, tip_at_start);
let _ = update.graph.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = update.graph.insert_anchor(txid, anchor);
}
}
}
if tip_at_start.hash != self.get_block_hash(tip_at_start.height)? {
// A reorg occurred, so let's find out where all the txids we found are now in the chain
let txids_found = update
.graph
.full_txs()
.map(|tx_node| tx_node.txid)
.collect::<Vec<_>>();
update.chain = EsploraExt::scan_without_keychain(
self,
local_chain,
[],
txids_found,
[],
parallel_requests,
)?
.chain;
}
Ok(update)
}
}

29
crates/esplora/src/lib.rs Normal file
View File

@@ -0,0 +1,29 @@
#![doc = include_str!("../README.md")]
use bdk_chain::{BlockId, ConfirmationTimeAnchor};
use esplora_client::TxStatus;
pub use esplora_client;
#[cfg(feature = "blocking")]
mod blocking_ext;
#[cfg(feature = "blocking")]
pub use blocking_ext::*;
#[cfg(feature = "async")]
mod async_ext;
#[cfg(feature = "async")]
pub use async_ext::*;
pub(crate) fn map_confirmation_time_anchor(
tx_status: &TxStatus,
tip_at_start: BlockId,
) -> Option<ConfirmationTimeAnchor> {
match (tx_status.block_time, tx_status.block_height) {
(Some(confirmation_time), Some(confirmation_height)) => Some(ConfirmationTimeAnchor {
anchor_block: tip_at_start,
confirmation_height,
confirmation_time,
}),
_ => None,
}
}

View File

@@ -0,0 +1,19 @@
[package]
name = "bdk_file_store"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_file_store"
description = "A simple append-only flat file implementation of Persist for Bitcoin Dev Kit."
keywords = ["bitcoin", "persist", "persistence", "bdk", "file"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.5.0", features = [ "serde", "miniscript" ] }
bincode = { version = "1" }
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
tempfile = "3"

View File

@@ -0,0 +1,10 @@
# BDK File Store
This is a simple append-only flat file implementation of
[`Persist`](`bdk_chain::Persist`).
The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
`Wallet` to persist wallet data into a flat file.
[`bdk`]: https://docs.rs/bdk/latest
[`bdk_chain`]: https://docs.rs/bdk_chain/latest

View File

@@ -0,0 +1,100 @@
use bincode::Options;
use std::{
fs::File,
io::{self, Seek},
marker::PhantomData,
};
use crate::bincode_options;
/// Iterator over entries in a file store.
///
/// Reads and returns an entry each time [`next`] is called. If an error occurs while reading the
/// iterator will yield a `Result::Err(_)` instead and then `None` for the next call to `next`.
///
/// [`next`]: Self::next
pub struct EntryIter<'t, T> {
db_file: Option<&'t mut File>,
/// The file position for the first read of `db_file`.
start_pos: Option<u64>,
types: PhantomData<T>,
}
impl<'t, T> EntryIter<'t, T> {
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
Self {
db_file: Some(db_file),
start_pos: Some(start_pos),
types: PhantomData,
}
}
}
impl<'t, T> Iterator for EntryIter<'t, T>
where
T: serde::de::DeserializeOwned,
{
type Item = Result<T, IterError>;
fn next(&mut self) -> Option<Self::Item> {
// closure which reads a single entry starting from `self.pos`
let read_one = |f: &mut File, start_pos: Option<u64>| -> Result<Option<T>, IterError> {
let pos = match start_pos {
Some(pos) => f.seek(io::SeekFrom::Start(pos))?,
None => f.stream_position()?,
};
match bincode_options().deserialize_from(&*f) {
Ok(changeset) => {
f.stream_position()?;
Ok(Some(changeset))
}
Err(e) => {
if let bincode::ErrorKind::Io(inner) = &*e {
if inner.kind() == io::ErrorKind::UnexpectedEof {
let eof = f.seek(io::SeekFrom::End(0))?;
if pos == eof {
return Ok(None);
}
}
}
f.seek(io::SeekFrom::Start(pos))?;
Err(IterError::Bincode(*e))
}
}
};
let result = read_one(self.db_file.as_mut()?, self.start_pos.take());
if result.is_err() {
self.db_file = None;
}
result.transpose()
}
}
impl From<io::Error> for IterError {
fn from(value: io::Error) -> Self {
IterError::Io(value)
}
}
/// Error type for [`EntryIter`].
#[derive(Debug)]
pub enum IterError {
/// Failure to read from the file.
Io(io::Error),
/// Failure to decode data from the file.
Bincode(bincode::ErrorKind),
}
impl core::fmt::Display for IterError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IterError::Io(e) => write!(f, "io error trying to read entry {}", e),
IterError::Bincode(e) => write!(f, "bincode error while reading entry {}", e),
}
}
}
impl std::error::Error for IterError {}

View File

@@ -0,0 +1,42 @@
#![doc = include_str!("../README.md")]
mod entry_iter;
mod store;
use std::io;
use bincode::{DefaultOptions, Options};
pub use entry_iter::*;
pub use store::*;
pub(crate) fn bincode_options() -> impl bincode::Options {
DefaultOptions::new().with_varint_encoding()
}
/// Error that occurs due to problems encountered with the file.
#[derive(Debug)]
pub enum FileError<'a> {
/// IO error, this may mean that the file is too short.
Io(io::Error),
/// Magic bytes do not match what is expected.
InvalidMagicBytes { got: Vec<u8>, expected: &'a [u8] },
}
impl<'a> core::fmt::Display for FileError<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io(e) => write!(f, "io error trying to read file: {}", e),
Self::InvalidMagicBytes { got, expected } => write!(
f,
"file has invalid magic bytes: expected={:?} got={:?}",
expected, got,
),
}
}
}
impl<'a> From<io::Error> for FileError<'a> {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl<'a> std::error::Error for FileError<'a> {}

View File

@@ -0,0 +1,255 @@
use std::{
fmt::Debug,
fs::{File, OpenOptions},
io::{self, Read, Seek, Write},
marker::PhantomData,
path::Path,
};
use bdk_chain::{Append, PersistBackend};
use bincode::Options;
use crate::{bincode_options, EntryIter, FileError, IterError};
/// Persists an append-only list of changesets (`C`) to a single file.
///
/// The changesets are the results of altering a tracker implementation (`T`).
#[derive(Debug)]
pub struct Store<'a, C> {
magic: &'a [u8],
db_file: File,
marker: PhantomData<C>,
}
impl<'a, C> PersistBackend<C> for Store<'a, C>
where
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
{
type WriteError = std::io::Error;
type LoadError = IterError;
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
self.append_changeset(changeset)
}
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
let (changeset, result) = self.aggregate_changesets();
result.map(|_| changeset)
}
}
impl<'a, C> Store<'a, C>
where
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
{
/// Creates a new store from a [`File`].
///
/// The file must have been opened with read and write permissions.
///
/// `magic` is the expected prefixed bytes of the file. If this does not match, an error will be
/// returned.
///
/// [`File`]: std::fs::File
pub fn new(magic: &'a [u8], mut db_file: File) -> Result<Self, FileError> {
db_file.rewind()?;
let mut magic_buf = vec![0_u8; magic.len()];
db_file.read_exact(magic_buf.as_mut())?;
if magic_buf != magic {
return Err(FileError::InvalidMagicBytes {
got: magic_buf,
expected: magic,
});
}
Ok(Self {
magic,
db_file,
marker: Default::default(),
})
}
/// Creates or loads a store from `db_path`.
///
/// If no file exists there, it will be created.
///
/// Refer to [`new`] for documentation on the `magic` input.
///
/// [`new`]: Self::new
pub fn new_from_path<P>(magic: &'a [u8], db_path: P) -> Result<Self, FileError>
where
P: AsRef<Path>,
{
let already_exists = db_path.as_ref().exists();
let mut db_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(db_path)?;
if !already_exists {
db_file.write_all(magic)?;
}
Self::new(magic, db_file)
}
/// Iterates over the stored changeset from first to last, changing the seek position at each
/// iteration.
///
/// The iterator may fail to read an entry and therefore return an error. However, the first time
/// it returns an error will be the last. After doing so, the iterator will always yield `None`.
///
/// **WARNING**: This method changes the write position in the underlying file. You should
/// always iterate over all entries until `None` is returned if you want your next write to go
/// at the end; otherwise, you will write over existing entries.
pub fn iter_changesets(&mut self) -> EntryIter<C> {
EntryIter::new(self.magic.len() as u64, &mut self.db_file)
}
/// Loads all the changesets that have been stored as one giant changeset.
///
/// This function returns a tuple of the aggregate changeset and a result that indicates
/// whether an error occurred while reading or deserializing one of the entries. If so the
/// changeset will consist of all of those it was able to read.
///
/// You should usually check the error. In many applications, it may make sense to do a full
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
/// changesets it was unable to read changed the derivation indices of the tracker.
///
/// **WARNING**: This method changes the write position of the underlying file. The next
/// changeset will be written over the erroring entry (or the end of the file if none existed).
pub fn aggregate_changesets(&mut self) -> (C, Result<(), IterError>) {
let mut changeset = C::default();
let result = (|| {
for next_changeset in self.iter_changesets() {
changeset.append(next_changeset?);
}
Ok(())
})();
(changeset, result)
}
/// Append a new changeset to the file and truncate the file to the end of the appended
/// changeset.
///
/// The truncation is to avoid the possibility of having a valid but inconsistent changeset
/// directly after the appended changeset.
pub fn append_changeset(&mut self, changeset: &C) -> Result<(), io::Error> {
// no need to write anything if changeset is empty
if changeset.is_empty() {
return Ok(());
}
bincode_options()
.serialize_into(&mut self.db_file, changeset)
.map_err(|e| match *e {
bincode::ErrorKind::Io(inner) => inner,
unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
})?;
// truncate file after this changeset addition
// if this is not done, data after this changeset may represent valid changesets, however
// applying those changesets on top of this one may result in an inconsistent state
let pos = self.db_file.stream_position()?;
self.db_file.set_len(pos)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use bincode::DefaultOptions;
use std::{
io::{Read, Write},
vec::Vec,
};
use tempfile::NamedTempFile;
const TEST_MAGIC_BYTES_LEN: usize = 12;
const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] =
[98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49];
type TestChangeSet = Vec<String>;
#[derive(Debug)]
struct TestTracker;
#[test]
fn new_fails_if_file_is_too_short() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(&TEST_MAGIC_BYTES[..TEST_MAGIC_BYTES_LEN - 1])
.expect("should write");
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) {
Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
unexpected => panic!("unexpected result: {:?}", unexpected),
};
}
#[test]
fn new_fails_if_magic_bytes_are_invalid() {
let invalid_magic_bytes = "ldkfs0000000";
let mut file = NamedTempFile::new().unwrap();
file.write_all(invalid_magic_bytes.as_bytes())
.expect("should write");
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) {
Err(FileError::InvalidMagicBytes { got, .. }) => {
assert_eq!(got, invalid_magic_bytes.as_bytes())
}
unexpected => panic!("unexpected result: {:?}", unexpected),
};
}
#[test]
fn append_changeset_truncates_invalid_bytes() {
// initial data to write to file (magic bytes + invalid data)
let mut data = [255_u8; 2000];
data[..TEST_MAGIC_BYTES_LEN].copy_from_slice(&TEST_MAGIC_BYTES);
let changeset = vec!["one".into(), "two".into(), "three!".into()];
let mut file = NamedTempFile::new().unwrap();
file.write_all(&data).expect("should write");
let mut store = Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap())
.expect("should open");
match store.iter_changesets().next() {
Some(Err(IterError::Bincode(_))) => {}
unexpected_res => panic!("unexpected result: {:?}", unexpected_res),
}
store.append_changeset(&changeset).expect("should append");
drop(store);
let got_bytes = {
let mut buf = Vec::new();
file.reopen()
.unwrap()
.read_to_end(&mut buf)
.expect("should read");
buf
};
let expected_bytes = {
let mut buf = TEST_MAGIC_BYTES.to_vec();
DefaultOptions::new()
.with_varint_encoding()
.serialize_into(&mut buf, &changeset)
.expect("should encode");
buf
};
assert_eq!(got_bytes, expected_bytes);
}
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,17 @@
[package]
name = "example_cli"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
bdk_file_store = { path = "../../crates/file_store" }
bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
bdk_coin_select = { path = "../../nursery/coin_select" }
clap = { version = "3.2.23", features = ["derive", "env"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "^1.0" }

View File

@@ -0,0 +1,736 @@
pub use anyhow;
use anyhow::Context;
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
use bdk_file_store::Store;
use serde::{de::DeserializeOwned, Serialize};
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
use bdk_chain::{
bitcoin::{
psbt::Prevouts, secp256k1::Secp256k1, util::sighash::SighashCache, Address, LockTime,
Network, Sequence, Transaction, TxIn, TxOut,
},
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::{DerivationAdditions, KeychainTxOutIndex},
miniscript::{
descriptor::{DescriptorSecretKey, KeyMap},
Descriptor, DescriptorPublicKey,
},
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
};
pub use bdk_file_store;
pub use clap;
use clap::{Parser, Subcommand};
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
pub type KeychainAdditions<A> = IndexedAdditions<A, DerivationAdditions<Keychain>>;
pub type Database<'m, C> = Persist<Store<'m, C>, C>;
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
pub struct Args<S: clap::Subcommand> {
#[clap(env = "DESCRIPTOR")]
pub descriptor: String,
#[clap(env = "CHANGE_DESCRIPTOR")]
pub change_descriptor: Option<String>,
#[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
pub network: Network,
#[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
pub db_path: PathBuf,
#[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
pub cp_limit: usize,
#[clap(subcommand)]
pub command: Commands<S>,
}
#[allow(clippy::almost_swapped)]
#[derive(Subcommand, Debug, Clone)]
pub enum Commands<S: clap::Subcommand> {
#[clap(flatten)]
ChainSpecific(S),
/// Address generation and inspection.
Address {
#[clap(subcommand)]
addr_cmd: AddressCmd,
},
/// Get the wallet balance.
Balance,
/// TxOut related commands.
#[clap(name = "txout")]
TxOut {
#[clap(subcommand)]
txout_cmd: TxOutCmd,
},
/// Send coins to an address.
Send {
value: u64,
address: Address,
#[clap(short, default_value = "bnb")]
coin_select: CoinSelectionAlgo,
},
}
#[derive(Clone, Debug)]
pub enum CoinSelectionAlgo {
LargestFirst,
SmallestFirst,
OldestFirst,
NewestFirst,
BranchAndBound,
}
impl Default for CoinSelectionAlgo {
fn default() -> Self {
Self::LargestFirst
}
}
impl core::str::FromStr for CoinSelectionAlgo {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use CoinSelectionAlgo::*;
Ok(match s {
"largest-first" => LargestFirst,
"smallest-first" => SmallestFirst,
"oldest-first" => OldestFirst,
"newest-first" => NewestFirst,
"bnb" => BranchAndBound,
unknown => {
return Err(anyhow::anyhow!(
"unknown coin selection algorithm '{}'",
unknown
))
}
})
}
}
impl core::fmt::Display for CoinSelectionAlgo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use CoinSelectionAlgo::*;
write!(
f,
"{}",
match self {
LargestFirst => "largest-first",
SmallestFirst => "smallest-first",
OldestFirst => "oldest-first",
NewestFirst => "newest-first",
BranchAndBound => "bnb",
}
)
}
}
#[allow(clippy::almost_swapped)]
#[derive(Subcommand, Debug, Clone)]
pub enum AddressCmd {
/// Get the next unused address.
Next,
/// Get a new address regardless of the existing unused addresses.
New,
/// List all addresses
List {
#[clap(long)]
change: bool,
},
Index,
}
#[derive(Subcommand, Debug, Clone)]
pub enum TxOutCmd {
List {
/// Return only spent outputs.
#[clap(short, long)]
spent: bool,
/// Return only unspent outputs.
#[clap(short, long)]
unspent: bool,
/// Return only confirmed outputs.
#[clap(long)]
confirmed: bool,
/// Return only unconfirmed outputs.
#[clap(long)]
unconfirmed: bool,
},
}
#[derive(
Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
)]
pub enum Keychain {
External,
Internal,
}
impl core::fmt::Display for Keychain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Keychain::External => write!(f, "external"),
Keychain::Internal => write!(f, "internal"),
}
}
}
pub fn run_address_cmd<A, C>(
graph: &mut KeychainTxGraph<A>,
db: &Mutex<Database<C>>,
network: Network,
cmd: AddressCmd,
) -> anyhow::Result<()>
where
C: Default + Append + DeserializeOwned + Serialize + From<KeychainAdditions<A>>,
{
let index = &mut graph.index;
match cmd {
AddressCmd::Next | AddressCmd::New => {
let spk_chooser = match cmd {
AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
_ => unreachable!("only these two variants exist in match arm"),
};
let ((spk_i, spk), index_additions) = spk_chooser(index, &Keychain::External);
let db = &mut *db.lock().unwrap();
db.stage(C::from(KeychainAdditions::from(index_additions)));
db.commit()?;
let addr = Address::from_script(spk, network).context("failed to derive address")?;
println!("[address @ {}] {}", spk_i, addr);
Ok(())
}
AddressCmd::Index => {
for (keychain, derivation_index) in index.last_revealed_indices() {
println!("{:?}: {}", keychain, derivation_index);
}
Ok(())
}
AddressCmd::List { change } => {
let target_keychain = match change {
true => Keychain::Internal,
false => Keychain::External,
};
for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
let address = Address::from_script(spk, network)
.expect("should always be able to derive address");
println!(
"{:?} {} used:{}",
spk_i,
address,
index.is_used(&(target_keychain, spk_i))
);
}
Ok(())
}
}
}
pub fn run_balance_cmd<A: Anchor, O: ChainOracle>(
graph: &KeychainTxGraph<A>,
chain: &O,
) -> Result<(), O::Error> {
fn print_balances<'a>(title_str: &'a str, items: impl IntoIterator<Item = (&'a str, u64)>) {
println!("{}:", title_str);
for (name, amount) in items.into_iter() {
println!(" {:<10} {:>12} sats", name, amount)
}
}
let balance = graph.graph().try_balance(
chain,
chain.get_chain_tip()?.unwrap_or_default(),
graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal,
)?;
let confirmed_total = balance.confirmed + balance.immature;
let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
print_balances(
"confirmed",
[
("total", confirmed_total),
("spendable", balance.confirmed),
("immature", balance.immature),
],
);
print_balances(
"unconfirmed",
[
("total", unconfirmed_total),
("trusted", balance.trusted_pending),
("untrusted", balance.untrusted_pending),
],
);
Ok(())
}
pub fn run_txo_cmd<A: Anchor, O: ChainOracle>(
graph: &KeychainTxGraph<A>,
chain: &O,
network: Network,
cmd: TxOutCmd,
) -> anyhow::Result<()>
where
O::Error: std::error::Error + Send + Sync + 'static,
{
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
let outpoints = graph.index.outpoints().iter().cloned();
match cmd {
TxOutCmd::List {
spent,
unspent,
confirmed,
unconfirmed,
} => {
let txouts = graph
.graph()
.try_filter_chain_txouts(chain, chain_tip, outpoints)
.filter(|r| match r {
Ok((_, full_txo)) => match (spent, unspent) {
(true, false) => full_txo.spent_by.is_some(),
(false, true) => full_txo.spent_by.is_none(),
_ => true,
},
// always keep errored items
Err(_) => true,
})
.filter(|r| match r {
Ok((_, full_txo)) => match (confirmed, unconfirmed) {
(true, false) => full_txo.chain_position.is_confirmed(),
(false, true) => !full_txo.chain_position.is_confirmed(),
_ => true,
},
// always keep errored items
Err(_) => true,
})
.collect::<Result<Vec<_>, _>>()?;
for (spk_i, full_txo) in txouts {
let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
println!(
"{:?} {} {} {} spent:{:?}",
spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
)
}
Ok(())
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_send_cmd<A: Anchor, O: ChainOracle, C>(
graph: &Mutex<KeychainTxGraph<A>>,
db: &Mutex<Database<'_, C>>,
chain: &O,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
cs_algorithm: CoinSelectionAlgo,
address: Address,
value: u64,
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
) -> anyhow::Result<()>
where
O::Error: std::error::Error + Send + Sync + 'static,
C: Default + Append + DeserializeOwned + Serialize + From<KeychainAdditions<A>>,
{
let (transaction, change_index) = {
let graph = &mut *graph.lock().unwrap();
// take mutable ref to construct tx -- it is only open for a short time while building it.
let (tx, change_info) = create_tx(graph, chain, keymap, cs_algorithm, address, value)?;
if let Some((index_additions, (change_keychain, index))) = change_info {
// We must first persist to disk the fact that we've got a new address from the
// change keychain so future scans will find the tx we're about to broadcast.
// If we're unable to persist this, then we don't want to broadcast.
{
let db = &mut *db.lock().unwrap();
db.stage(C::from(KeychainAdditions::from(index_additions)));
db.commit()?;
}
// We don't want other callers/threads to use this address while we're using it
// but we also don't want to scan the tx we just created because it's not
// technically in the blockchain yet.
graph.index.mark_used(&change_keychain, index);
(tx, Some((change_keychain, index)))
} else {
(tx, None)
}
};
match (broadcast)(&transaction) {
Ok(_) => {
println!("Broadcasted Tx : {}", transaction.txid());
let keychain_additions = graph.lock().unwrap().insert_tx(&transaction, None, None);
// We know the tx is at least unconfirmed now. Note if persisting here fails,
// it's not a big deal since we can always find it again form
// blockchain.
db.lock().unwrap().stage(C::from(keychain_additions));
Ok(())
}
Err(e) => {
if let Some((keychain, index)) = change_index {
// We failed to broadcast, so allow our change address to be used in the future
graph.lock().unwrap().index.unmark_used(&keychain, index);
}
Err(e)
}
}
}
#[allow(clippy::type_complexity)]
pub fn create_tx<A: Anchor, O: ChainOracle>(
graph: &mut KeychainTxGraph<A>,
chain: &O,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
cs_algorithm: CoinSelectionAlgo,
address: Address,
value: u64,
) -> anyhow::Result<(
Transaction,
Option<(DerivationAdditions<Keychain>, (Keychain, u32))>,
)>
where
O::Error: std::error::Error + Send + Sync + 'static,
{
let mut additions = DerivationAdditions::default();
let assets = bdk_tmp_plan::Assets {
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
..Default::default()
};
// TODO use planning module
let mut candidates = planned_utxos(graph, chain, &assets)?;
// apply coin selection algorithm
match cs_algorithm {
CoinSelectionAlgo::LargestFirst => {
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
}
CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
CoinSelectionAlgo::OldestFirst => {
candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
}
CoinSelectionAlgo::NewestFirst => {
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
}
CoinSelectionAlgo::BranchAndBound => {}
}
// turn the txos we chose into weight and value
let wv_candidates = candidates
.iter()
.map(|(plan, utxo)| {
WeightedValue::new(
utxo.txout.value,
plan.expected_weight() as _,
plan.witness_version().is_some(),
)
})
.collect();
let mut outputs = vec![TxOut {
value,
script_pubkey: address.script_pubkey(),
}];
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
Keychain::Internal
} else {
Keychain::External
};
let ((change_index, change_script), change_additions) =
graph.index.next_unused_spk(&internal_keychain);
additions.append(change_additions);
// Clone to drop the immutable reference.
let change_script = change_script.clone();
let change_plan = bdk_tmp_plan::plan_satisfaction(
&graph
.index
.keychains()
.get(&internal_keychain)
.expect("must exist")
.at_derivation_index(change_index),
&assets,
)
.expect("failed to obtain change plan");
let mut change_output = TxOut {
value: 0,
script_pubkey: change_script,
};
let cs_opts = CoinSelectorOpt {
target_feerate: 0.5,
min_drain_value: graph
.index
.keychains()
.get(&internal_keychain)
.expect("must exist")
.dust_value(),
..CoinSelectorOpt::fund_outputs(
&outputs,
&change_output,
change_plan.expected_weight() as u32,
)
};
// TODO: How can we make it easy to shuffle in order of inputs and outputs here?
// apply coin selection by saying we need to fund these outputs
let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
// just select coins in the order provided until we have enough
// only use the first result (least waste)
let selection = match cs_algorithm {
CoinSelectionAlgo::BranchAndBound => {
coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
.map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
}
_ => coin_selector.select_until_finished()?,
};
let (_, selection_meta) = selection.best_strategy();
// get the selected utxos
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
if let Some(drain_value) = selection_meta.drain_value {
change_output.value = drain_value;
// if the selection tells us to use change and the change value is sufficient, we add it as an output
outputs.push(change_output)
}
let mut transaction = Transaction {
version: 0x02,
// because the temporary planning module does not support timelocks, we can use the chain
// tip as the `lock_time` for anti-fee-sniping purposes
lock_time: chain
.get_chain_tip()?
.and_then(|block_id| LockTime::from_height(block_id.height).ok())
.unwrap_or(LockTime::ZERO)
.into(),
input: selected_txos
.iter()
.map(|(_, utxo)| TxIn {
previous_output: utxo.outpoint,
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
..Default::default()
})
.collect(),
output: outputs,
};
let prevouts = selected_txos
.iter()
.map(|(_, utxo)| utxo.txout.clone())
.collect::<Vec<_>>();
let sighash_prevouts = Prevouts::All(&prevouts);
// first, set tx values for the plan so that we don't change them while signing
for (i, (plan, _)) in selected_txos.iter().enumerate() {
if let Some(sequence) = plan.required_sequence() {
transaction.input[i].sequence = sequence
}
}
// create a short lived transaction
let _sighash_tx = transaction.clone();
let mut sighash_cache = SighashCache::new(&_sighash_tx);
for (i, (plan, _)) in selected_txos.iter().enumerate() {
let requirements = plan.requirements();
let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
assert!(
!requirements.requires_hash_preimages(),
"can't have hash pre-images since we didn't provide any."
);
assert!(
requirements.signatures.sign_with_keymap(
i,
keymap,
&sighash_prevouts,
None,
None,
&mut sighash_cache,
&mut auth_data,
&Secp256k1::default(),
)?,
"we should have signed with this input."
);
match plan.try_complete(&auth_data) {
bdk_tmp_plan::PlanState::Complete {
final_script_sig,
final_script_witness,
} => {
if let Some(witness) = final_script_witness {
transaction.input[i].witness = witness;
}
if let Some(script_sig) = final_script_sig {
transaction.input[i].script_sig = script_sig;
}
}
bdk_tmp_plan::PlanState::Incomplete(_) => {
return Err(anyhow::anyhow!(
"we weren't able to complete the plan with our keys."
));
}
}
}
let change_info = if selection_meta.drain_value.is_some() {
Some((additions, (internal_keychain, change_index)))
} else {
None
};
Ok((transaction, change_info))
}
#[allow(clippy::type_complexity)]
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
graph: &KeychainTxGraph<A>,
chain: &O,
assets: &bdk_tmp_plan::Assets<K>,
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
let outpoints = graph.index.outpoints().iter().cloned();
graph
.graph()
.try_filter_chain_unspents(chain, chain_tip, outpoints)
.filter_map(
#[allow(clippy::type_complexity)]
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
let (k, i, full_txo) = match r {
Err(err) => return Some(Err(err)),
Ok(((k, i), full_txo)) => (k, i, full_txo),
};
let desc = graph
.index
.keychains()
.get(&k)
.expect("keychain must exist")
.at_derivation_index(i);
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
Some(Ok((plan, full_txo)))
},
)
.collect()
}
pub fn handle_commands<S: clap::Subcommand, A: Anchor, O: ChainOracle, C>(
graph: &Mutex<KeychainTxGraph<A>>,
db: &Mutex<Database<C>>,
chain: &Mutex<O>,
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
network: Network,
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
cmd: Commands<S>,
) -> anyhow::Result<()>
where
O::Error: std::error::Error + Send + Sync + 'static,
C: Default + Append + DeserializeOwned + Serialize + From<KeychainAdditions<A>>,
{
match cmd {
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
Commands::Address { addr_cmd } => {
let graph = &mut *graph.lock().unwrap();
run_address_cmd(graph, db, network, addr_cmd)
}
Commands::Balance => {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
}
Commands::TxOut { txout_cmd } => {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
run_txo_cmd(graph, chain, network, txout_cmd)
}
Commands::Send {
value,
address,
coin_select,
} => {
let chain = &*chain.lock().unwrap();
run_send_cmd(
graph,
db,
chain,
keymap,
coin_select,
address,
value,
broadcast,
)
}
}
}
#[allow(clippy::type_complexity)]
pub fn init<'m, S: clap::Subcommand, C>(
db_magic: &'m [u8],
db_default_path: &str,
) -> anyhow::Result<(
Args<S>,
KeyMap,
KeychainTxOutIndex<Keychain>,
Mutex<Database<'m, C>>,
C,
)>
where
C: Default + Append + Serialize + DeserializeOwned,
{
if std::env::var("BDK_DB_PATH").is_err() {
std::env::set_var("BDK_DB_PATH", db_default_path);
}
let args = Args::<S>::parse();
let secp = Secp256k1::default();
let mut index = KeychainTxOutIndex::<Keychain>::default();
let (descriptor, mut keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
index.add_keychain(Keychain::External, descriptor);
if let Some((internal_descriptor, internal_keymap)) = args
.change_descriptor
.as_ref()
.map(|desc_str| Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, desc_str))
.transpose()?
{
keymap.extend(internal_keymap);
index.add_keychain(Keychain::Internal, internal_descriptor);
}
let mut db_backend = match Store::<'m, C>::new_from_path(db_magic, &args.db_path) {
Ok(db_backend) => db_backend,
// we cannot return `err` directly as it has lifetime `'m`
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
};
let init_changeset = db_backend.load_from_persistence()?;
Ok((
args,
keymap,
index,
Mutex::new(Database::new(db_backend)),
init_changeset,
))
}

View File

@@ -0,0 +1,11 @@
[package]
name = "example_electrum"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
bdk_electrum = { path = "../../crates/electrum" }
example_cli = { path = "../example_cli" }

View File

@@ -0,0 +1,318 @@
use std::{
collections::BTreeMap,
io::{self, Write},
sync::Mutex,
};
use bdk_chain::{
bitcoin::{Address, BlockHash, Network, OutPoint, Txid},
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::LocalChangeSet,
local_chain::LocalChain,
Append, ConfirmationHeightAnchor,
};
use bdk_electrum::{
electrum_client::{self, ElectrumApi},
ElectrumExt, ElectrumUpdate,
};
use example_cli::{
anyhow::{self, Context},
clap::{self, Parser, Subcommand},
Keychain,
};
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
const DB_PATH: &str = ".bdk_electrum_example.db";
const ASSUME_FINAL_DEPTH: usize = 10;
#[derive(Subcommand, Debug, Clone)]
enum ElectrumCommands {
/// Scans the addresses in the wallet using the electrum API.
Scan {
/// When a gap this large has been found for a keychain, it will stop.
#[clap(long, default_value = "5")]
stop_gap: usize,
#[clap(flatten)]
scan_options: ScanOptions,
},
/// Scans particular addresses using the electrum API.
Sync {
/// Scan all the unused addresses.
#[clap(long)]
unused_spks: bool,
/// Scan every address that you have derived.
#[clap(long)]
all_spks: bool,
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
#[clap(long)]
utxos: bool,
/// Scan unconfirmed transactions for updates.
#[clap(long)]
unconfirmed: bool,
#[clap(flatten)]
scan_options: ScanOptions,
},
}
#[derive(Parser, Debug, Clone, PartialEq)]
pub struct ScanOptions {
/// Set batch size for each script_history call to electrum client.
#[clap(long, default_value = "25")]
pub batch_size: usize,
}
type ChangeSet = LocalChangeSet<Keychain, ConfirmationHeightAnchor>;
fn main() -> anyhow::Result<()> {
let (args, keymap, index, db, init_changeset) =
example_cli::init::<ElectrumCommands, ChangeSet>(DB_MAGIC, DB_PATH)?;
let graph = Mutex::new({
let mut graph = IndexedTxGraph::new(index);
graph.apply_additions(init_changeset.indexed_additions);
graph
});
let chain = Mutex::new({
let mut chain = LocalChain::default();
chain.apply_changeset(init_changeset.chain_changeset);
chain
});
let electrum_url = match args.network {
Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
Network::Testnet => "ssl://electrum.blockstream.info:60002",
Network::Regtest => "tcp://localhost:60401",
Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
};
let config = electrum_client::Config::builder()
.validate_domain(matches!(args.network, Network::Bitcoin))
.build();
let client = electrum_client::Client::from_config(electrum_url, config)?;
let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
general_cmd => {
let res = example_cli::handle_commands(
&graph,
&db,
&chain,
&keymap,
args.network,
|tx| {
client
.transaction_broadcast(tx)
.map(|_| ())
.map_err(anyhow::Error::from)
},
general_cmd.clone(),
);
db.lock().unwrap().commit()?;
return res;
}
};
let response = match electrum_cmd.clone() {
ElectrumCommands::Scan {
stop_gap,
scan_options,
} => {
let (keychain_spks, local_chain) = {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
let keychain_spks = graph
.index
.spks_of_all_keychains()
.into_iter()
.map(|(keychain, iter)| {
let mut first = true;
let spk_iter = iter.inspect(move |(i, _)| {
if first {
eprint!("\nscanning {}: ", keychain);
first = false;
}
eprint!("{} ", i);
let _ = io::stdout().flush();
});
(keychain, spk_iter)
})
.collect::<BTreeMap<_, _>>();
let c = chain
.blocks()
.iter()
.rev()
.take(ASSUME_FINAL_DEPTH)
.map(|(k, v)| (*k, *v))
.collect::<BTreeMap<u32, BlockHash>>();
(keychain_spks, c)
};
client
.scan(
&local_chain,
keychain_spks,
core::iter::empty(),
core::iter::empty(),
stop_gap,
scan_options.batch_size,
)
.context("scanning the blockchain")?
}
ElectrumCommands::Sync {
mut unused_spks,
all_spks,
mut utxos,
mut unconfirmed,
scan_options,
} => {
// Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap();
let chain_tip = chain.tip().unwrap_or_default();
if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true;
unconfirmed = true;
utxos = true;
} else if all_spks {
unused_spks = false;
}
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
Box::new(core::iter::empty());
if all_spks {
let all_spks = graph
.index
.all_spks()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
eprintln!("scanning {:?}", index);
script
})));
}
if unused_spks {
let unused_spks = graph
.index
.unused_spks(..)
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
eprintln!(
"Checking if address {} {:?} has been used",
Address::from_script(&script, args.network).unwrap(),
index
);
script
})));
}
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
if utxos {
let init_outpoints = graph.index.outpoints().iter().cloned();
let utxos = graph
.graph()
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
.map(|(_, utxo)| utxo)
.collect::<Vec<_>>();
outpoints = Box::new(
utxos
.into_iter()
.inspect(|utxo| {
eprintln!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
})
.map(|utxo| utxo.outpoint),
);
};
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
if unconfirmed {
let unconfirmed_txids = graph
.graph()
.list_chain_txs(&*chain, chain_tip)
.filter(|canonical_tx| !canonical_tx.observed_as.is_confirmed())
.map(|canonical_tx| canonical_tx.node.txid)
.collect::<Vec<Txid>>();
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
eprintln!("Checking if {} is confirmed yet", txid);
}));
}
let c = chain
.blocks()
.iter()
.rev()
.take(ASSUME_FINAL_DEPTH)
.map(|(k, v)| (*k, *v))
.collect::<BTreeMap<u32, BlockHash>>();
// drop lock on graph and chain
drop((graph, chain));
let update = client
.scan_without_keychain(&c, spks, txids, outpoints, scan_options.batch_size)
.context("scanning the blockchain")?;
ElectrumUpdate {
graph_update: update.graph_update,
chain_update: update.chain_update,
keychain_update: BTreeMap::new(),
}
}
};
let missing_txids = {
let graph = &*graph.lock().unwrap();
response.missing_full_txs(graph.graph())
};
let now = std::time::UNIX_EPOCH
.elapsed()
.expect("must get time")
.as_secs();
let final_update = response.finalize(&client, Some(now), missing_txids)?;
let db_changeset = {
let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap();
let chain_changeset = chain.apply_update(final_update.chain)?;
let indexed_additions = {
let mut additions = IndexedAdditions::<ConfirmationHeightAnchor, _>::default();
let (_, index_additions) = graph.index.reveal_to_target_multi(&final_update.keychain);
additions.append(IndexedAdditions {
index_additions,
..Default::default()
});
additions.append(graph.apply_update(final_update.graph));
additions
};
ChangeSet {
indexed_additions,
chain_changeset,
}
};
let mut db = db.lock().unwrap();
db.stage(db_changeset);
db.commit()?;
Ok(())
}

View File

@@ -0,0 +1,9 @@
[package]
name = "wallet_electrum_example"
version = "0.2.0"
edition = "2021"
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" }

View File

@@ -0,0 +1,93 @@
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5;
use std::io::Write;
use std::str::FromStr;
use bdk::bitcoin::Address;
use bdk::SignOptions;
use bdk::{bitcoin::Network, Wallet};
use bdk_electrum::electrum_client::{self, ElectrumApi};
use bdk_electrum::ElectrumExt;
use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-electrum-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new(
external_descriptor,
Some(internal_descriptor),
db,
Network::Testnet,
)?;
let address = wallet.get_address(bdk::wallet::AddressIndex::New);
println!("Generated Address: {}", address);
let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?;
let local_chain = wallet.checkpoints();
let keychain_spks = wallet
.spks_of_all_keychains()
.into_iter()
.map(|(k, k_spks)| {
let mut once = Some(());
let mut stdout = std::io::stdout();
let k_spks = k_spks
.inspect(move |(spk_i, _)| match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", k),
None => print!(" {:<3}", spk_i),
})
.inspect(move |_| stdout.flush().expect("must flush"));
(k, k_spks)
})
.collect();
let electrum_update =
client.scan(local_chain, keychain_spks, None, None, STOP_GAP, BATCH_SIZE)?;
println!();
let missing = electrum_update.missing_full_txs(wallet.as_ref());
let update = electrum_update.finalize_as_confirmation_time(&client, None, missing)?;
wallet.apply_update(update)?;
wallet.commit()?;
let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT {
println!(
"Please send at least {} sats to the receiving address",
SEND_AMOUNT
);
std::process::exit(0);
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();
let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);
let tx = psbt.extract_tx();
client.transaction_broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(())
}

View File

@@ -0,0 +1,12 @@
[package]
name = "wallet_esplora"
version = "0.2.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
bdk_file_store = { path = "../../crates/file_store" }

View File

@@ -0,0 +1,94 @@
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
use std::{io::Write, str::FromStr};
use bdk::{
bitcoin::{Address, Network},
wallet::AddressIndex,
SignOptions, Wallet,
};
use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new(
external_descriptor,
Some(internal_descriptor),
db,
Network::Testnet,
)?;
let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address);
let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client =
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking()?;
let local_chain = wallet.checkpoints();
let keychain_spks = wallet
.spks_of_all_keychains()
.into_iter()
.map(|(k, k_spks)| {
let mut once = Some(());
let mut stdout = std::io::stdout();
let k_spks = k_spks
.inspect(move |(spk_i, _)| match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", k),
None => print!(" {:<3}", spk_i),
})
.inspect(move |_| stdout.flush().expect("must flush"));
(k, k_spks)
})
.collect();
let update = client.scan(
local_chain,
keychain_spks,
None,
None,
STOP_GAP,
PARALLEL_REQUESTS,
)?;
println!();
wallet.apply_update(update)?;
wallet.commit()?;
let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT {
println!(
"Please send at least {} sats to the receiving address",
SEND_AMOUNT
);
std::process::exit(0);
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();
let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);
let tx = psbt.extract_tx();
client.broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(())
}

View File

@@ -0,0 +1,12 @@
[package]
name = "wallet_esplora_async"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
bdk_file_store = { path = "../../crates/file_store" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }

View File

@@ -0,0 +1,97 @@
use std::{io::Write, str::FromStr};
use bdk::{
bitcoin::{Address, Network},
wallet::AddressIndex,
SignOptions, Wallet,
};
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_file_store::Store;
const DB_MAGIC: &str = "bdk_wallet_esplora_async_example";
const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new(
external_descriptor,
Some(internal_descriptor),
db,
Network::Testnet,
)?;
let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address);
let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client =
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_async()?;
let local_chain = wallet.checkpoints();
let keychain_spks = wallet
.spks_of_all_keychains()
.into_iter()
.map(|(k, k_spks)| {
let mut once = Some(());
let mut stdout = std::io::stdout();
let k_spks = k_spks
.inspect(move |(spk_i, _)| match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", k),
None => print!(" {:<3}", spk_i),
})
.inspect(move |_| stdout.flush().expect("must flush"));
(k, k_spks)
})
.collect();
let update = client
.scan(
local_chain,
keychain_spks,
[],
[],
STOP_GAP,
PARALLEL_REQUESTS,
)
.await?;
println!();
wallet.apply_update(update)?;
wallet.commit()?;
let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT {
println!(
"Please send at least {} sats to the receiving address",
SEND_AMOUNT
);
std::process::exit(0);
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();
let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);
let tx = psbt.extract_tx();
client.broadcast(&tx).await?;
println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(())
}

View File

@@ -1,41 +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 bdk::blockchain::compact_filters::*;
use bdk::database::MemoryDatabase;
use bdk::*;
use bitcoin::*;
use blockchain::compact_filters::CompactFiltersBlockchain;
use blockchain::compact_filters::CompactFiltersError;
use log::info;
use std::sync::Arc;
/// This will return wallet balance using compact filters
/// Requires a synced local bitcoin node 0.21 running on testnet with blockfilterindex=1 and peerblockfilters=1
fn main() -> Result<(), CompactFiltersError> {
env_logger::init();
info!("start");
let num_threads = 4;
let mempool = Arc::new(Mempool::default());
let peers = (0..num_threads)
.map(|_| Peer::connect("localhost:18333", Arc::clone(&mempool), Network::Testnet))
.collect::<Result<_, _>>()?;
let blockchain = CompactFiltersBlockchain::new(peers, "./wallet-filters", Some(500_000))?;
info!("done {:?}", blockchain);
let descriptor = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
let database = MemoryDatabase::default();
let wallet = Arc::new(Wallet::new(descriptor, None, Network::Testnet, database).unwrap());
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
info!("balance: {}", wallet.get_balance()?);
Ok(())
}

View File

@@ -1,103 +0,0 @@
use bdk::bitcoin::{Address, Network};
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::hwi::{types::HWIChain, HWIClient};
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 devices = HWIClient::enumerate()?;
let first_device = devices
.first()
.expect("No devices found. Either plug in a hardware wallet, or start a simulator.");
// ...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(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],
Some(&descriptors.internal[0]),
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

@@ -1,120 +0,0 @@
// 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

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

View File

@@ -1,24 +0,0 @@
[package]
name = "bdk-macros"
version = "0.6.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-macros"
description = "Supporting macros for `bdk`"
keywords = ["bdk"]
license = "MIT OR Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
syn = { version = "1.0", features = ["parsing", "full"] }
proc-macro2 = "1.0"
quote = "1.0"
[features]
debug = ["syn/extra-traits"]
[lib]
proc-macro = true

View File

@@ -1,146 +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.
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
use syn::spanned::Spanned;
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")))]
#parsed
};
for mut item in &mut parsed.items {
if let syn::TraitItem::Method(m) = &mut item {
m.sig.asyncness = Some(Token![async](m.span()));
}
}
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[async_trait(?Send)]
#parsed
};
output.into()
}
fn add_async_method(mut parsed: ImplItemMethod) -> TokenStream {
let output = quote! {
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#parsed
};
parsed.sig.asyncness = Some(Token![async](parsed.span()));
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#parsed
};
output.into()
}
fn add_async_impl_trait(mut parsed: ItemImpl) -> TokenStream {
let output = quote! {
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
#parsed
};
for mut item in &mut parsed.items {
if let syn::ImplItem::Method(m) = &mut item {
m.sig.asyncness = Some(Token![async](m.span()));
}
}
let output = quote! {
#output
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
#[async_trait(?Send)]
#parsed
};
output.into()
}
/// Makes a method or every method of a trait "async" only if the target_arch is "wasm32"
///
/// Requires the `async-trait` crate as a dependency whenever this attribute is used on a trait
/// definition or trait implementation.
#[proc_macro_attribute]
pub fn maybe_async(_attr: TokenStream, item: TokenStream) -> TokenStream {
if let Ok(parsed) = parse(item.clone()) {
add_async_trait(parsed)
} else if let Ok(parsed) = parse(item.clone()) {
add_async_method(parsed)
} else if let Ok(parsed) = parse(item) {
add_async_impl_trait(parsed)
} else {
(quote! {
compile_error!("#[maybe_async] can only be used on methods, trait or trait impl blocks")
})
.into()
}
}
/// Awaits if target_arch is "wasm32", does nothing otherwise
#[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")))]
{
#expr
}
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
{
#expr.await
}
}
};
quoted.into()
}
/// Awaits if target_arch is "wasm32", uses `tokio::Runtime::block_on()` otherwise
///
/// Requires the `tokio` crate as a dependecy with `rt-core` or `rt-threaded` to build on non-wasm32 platforms.
#[proc_macro]
pub fn await_or_block(expr: TokenStream) -> TokenStream {
let expr: proc_macro2::TokenStream = expr.into();
let quoted = quote! {
{
#[cfg(all(not(target_arch = "wasm32"), not(feature = "async-interface")))]
{
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(#expr)
}
#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
{
#expr.await
}
}
};
quoted.into()
}

5
nursery/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Bitcoin Dev Kit Nursery
This is a directory for crates that are experimental and have not been released yet.
Keep in mind that they may never be released.
Things in `/example-crates` may use them to demonsrate how things might look in the future.

View File

@@ -0,0 +1,11 @@
[package]
name = "bdk_coin_select"
version = "0.0.1"
authors = [ "LLFourn <lloyd.fourn@gmail.com>" ]
[dependencies]
bdk_chain = { path = "../../crates/chain" }
[features]
default = ["std"]
std = []

View File

@@ -0,0 +1,645 @@
use super::*;
/// Strategy in which we should branch.
pub enum BranchStrategy {
/// We continue exploring subtrees of this node, starting with the inclusion branch.
Continue,
/// We continue exploring ONLY the omission branch of this node, skipping the inclusion branch.
SkipInclusion,
/// We skip both the inclusion and omission branches of this node.
SkipBoth,
}
impl BranchStrategy {
pub fn will_continue(&self) -> bool {
matches!(self, Self::Continue | Self::SkipInclusion)
}
}
/// Closure to decide the branching strategy, alongside a score (if the current selection is a
/// candidate solution).
pub type DecideStrategy<'c, S> = dyn Fn(&Bnb<'c, S>) -> (BranchStrategy, Option<S>);
/// [`Bnb`] represents the current state of the BnB algorithm.
pub struct Bnb<'c, S> {
pub pool: Vec<(usize, &'c WeightedValue)>,
pub pool_pos: usize,
pub best_score: S,
pub selection: CoinSelector<'c>,
pub rem_abs: u64,
pub rem_eff: i64,
}
impl<'c, S: Ord> Bnb<'c, S> {
/// Creates a new [`Bnb`].
pub fn new(selector: CoinSelector<'c>, pool: Vec<(usize, &'c WeightedValue)>, max: S) -> Self {
let (rem_abs, rem_eff) = pool.iter().fold((0, 0), |(abs, eff), (_, c)| {
(
abs + c.value,
eff + c.effective_value(selector.opts.target_feerate),
)
});
Self {
pool,
pool_pos: 0,
best_score: max,
selection: selector,
rem_abs,
rem_eff,
}
}
/// Turns our [`Bnb`] state into an iterator.
///
/// `strategy` should assess our current selection/node and determine the branching strategy and
/// whether this selection is a candidate solution (if so, return the selection score).
pub fn into_iter<'f>(self, strategy: &'f DecideStrategy<'c, S>) -> BnbIter<'c, 'f, S> {
BnbIter {
state: self,
done: false,
strategy,
}
}
/// Attempt to backtrack to the previously selected node's omission branch, return false
/// otherwise (no more solutions).
pub fn backtrack(&mut self) -> bool {
(0..self.pool_pos).rev().any(|pos| {
let (index, candidate) = self.pool[pos];
if self.selection.is_selected(index) {
// deselect the last `pos`, so the next round will check the omission branch
self.pool_pos = pos;
self.selection.deselect(index);
true
} else {
self.rem_abs += candidate.value;
self.rem_eff += candidate.effective_value(self.selection.opts.target_feerate);
false
}
})
}
/// Continue down this branch and skip the inclusion branch if specified.
pub fn forward(&mut self, skip: bool) {
let (index, candidate) = self.pool[self.pool_pos];
self.rem_abs -= candidate.value;
self.rem_eff -= candidate.effective_value(self.selection.opts.target_feerate);
if !skip {
self.selection.select(index);
}
}
/// Compare the advertised score with the current best. The new best will be the smaller value. Return true
/// if best is replaced.
pub fn advertise_new_score(&mut self, score: S) -> bool {
if score <= self.best_score {
self.best_score = score;
return true;
}
false
}
}
pub struct BnbIter<'c, 'f, S> {
state: Bnb<'c, S>,
done: bool,
/// Check our current selection (node) and returns the branching strategy alongside a score
/// (if the current selection is a candidate solution).
strategy: &'f DecideStrategy<'c, S>,
}
impl<'c, 'f, S: Ord + Copy + Display> Iterator for BnbIter<'c, 'f, S> {
type Item = Option<CoinSelector<'c>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let (strategy, score) = (self.strategy)(&self.state);
let mut found_best = Option::<CoinSelector>::None;
if let Some(score) = score {
if self.state.advertise_new_score(score) {
found_best = Some(self.state.selection.clone());
}
}
debug_assert!(
!strategy.will_continue() || self.state.pool_pos < self.state.pool.len(),
"Faulty strategy implementation! Strategy suggested that we continue traversing, however, we have already reached the end of the candidates pool! pool_len={}, pool_pos={}",
self.state.pool.len(), self.state.pool_pos,
);
match strategy {
BranchStrategy::Continue => {
self.state.forward(false);
}
BranchStrategy::SkipInclusion => {
self.state.forward(true);
}
BranchStrategy::SkipBoth => {
if !self.state.backtrack() {
self.done = true;
}
}
};
// increment selection pool position for next round
self.state.pool_pos += 1;
if found_best.is_some() || !self.done {
Some(found_best)
} else {
// we have traversed all branches
None
}
}
}
/// Determines how we should limit rounds of branch and bound.
pub enum BnbLimit {
Rounds(usize),
#[cfg(feature = "std")]
Duration(core::time::Duration),
}
impl From<usize> for BnbLimit {
fn from(v: usize) -> Self {
Self::Rounds(v)
}
}
#[cfg(feature = "std")]
impl From<core::time::Duration> for BnbLimit {
fn from(v: core::time::Duration) -> Self {
Self::Duration(v)
}
}
/// This is a variation of the Branch and Bound Coin Selection algorithm designed by Murch (as seen
/// in Bitcoin Core).
///
/// The differences are as follows:
/// * In addition to working with effective values, we also work with absolute values.
/// This way, we can use bounds of the absolute values to enforce `min_absolute_fee` (which is used by
/// RBF), and `max_extra_target` (which can be used to increase the possible solution set, given
/// that the sender is okay with sending extra to the receiver).
///
/// Murch's Master Thesis: <https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
/// Bitcoin Core Implementation: <https://github.com/bitcoin/bitcoin/blob/23.x/src/wallet/coinselection.cpp#L65>
///
/// TODO: Another optimization we could do is figure out candidates with the smallest waste, and
/// if we find a result with waste equal to this, we can just break.
pub fn coin_select_bnb<L>(limit: L, selector: CoinSelector) -> Option<CoinSelector>
where
L: Into<BnbLimit>,
{
let opts = selector.opts;
// prepare the pool of candidates to select from:
// * filter out candidates with negative/zero effective values
// * sort candidates by descending effective value
let pool = {
let mut pool = selector
.unselected()
.filter(|(_, c)| c.effective_value(opts.target_feerate) > 0)
.collect::<Vec<_>>();
pool.sort_unstable_by(|(_, a), (_, b)| {
let a = a.effective_value(opts.target_feerate);
let b = b.effective_value(opts.target_feerate);
b.cmp(&a)
});
pool
};
let feerate_decreases = opts.target_feerate > opts.long_term_feerate();
let target_abs = opts.target_value.unwrap_or(0) + opts.min_absolute_fee;
let target_eff = selector.effective_target();
let upper_bound_abs = target_abs + (opts.drain_weight as f32 * opts.target_feerate) as u64;
let upper_bound_eff = target_eff + opts.drain_waste();
let strategy = move |bnb: &Bnb<i64>| -> (BranchStrategy, Option<i64>) {
let selected_abs = bnb.selection.selected_absolute_value();
let selected_eff = bnb.selection.selected_effective_value();
// backtrack if the remaining value is not enough to reach the target
if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
return (BranchStrategy::SkipBoth, None);
}
// backtrack if the selected value has already surpassed upper bounds
if selected_abs > upper_bound_abs && selected_eff > upper_bound_eff {
return (BranchStrategy::SkipBoth, None);
}
let selected_waste = bnb.selection.selected_waste();
// when feerate decreases, waste without excess is guaranteed to increase with each
// selection. So if we have already surpassed the best score, we can backtrack.
if feerate_decreases && selected_waste > bnb.best_score {
return (BranchStrategy::SkipBoth, None);
}
// solution?
if selected_abs >= target_abs && selected_eff >= target_eff {
let waste = selected_waste + bnb.selection.current_excess();
return (BranchStrategy::SkipBoth, Some(waste));
}
// early bailout optimization:
// If the candidate at the previous position is NOT selected and has the same weight and
// value as the current candidate, we can skip selecting the current candidate.
if bnb.pool_pos > 0 && !bnb.selection.is_empty() {
let (_, candidate) = bnb.pool[bnb.pool_pos];
let (prev_index, prev_candidate) = bnb.pool[bnb.pool_pos - 1];
if !bnb.selection.is_selected(prev_index)
&& candidate.value == prev_candidate.value
&& candidate.weight == prev_candidate.weight
{
return (BranchStrategy::SkipInclusion, None);
}
}
// check out the inclusion branch first
(BranchStrategy::Continue, None)
};
// determine the sum of absolute and effective values for the current selection
let (selected_abs, selected_eff) = selector.selected().fold((0, 0), |(abs, eff), (_, c)| {
(
abs + c.value,
eff + c.effective_value(selector.opts.target_feerate),
)
});
let bnb = Bnb::new(selector, pool, i64::MAX);
// not enough to select anyway
if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
return None;
}
match limit.into() {
BnbLimit::Rounds(rounds) => {
bnb.into_iter(&strategy)
.take(rounds)
.reduce(|b, c| if c.is_some() { c } else { b })
}
#[cfg(feature = "std")]
BnbLimit::Duration(duration) => {
let start = std::time::SystemTime::now();
bnb.into_iter(&strategy)
.take_while(|_| start.elapsed().expect("failed to get system time") <= duration)
.reduce(|b, c| if c.is_some() { c } else { b })
}
}?
}
#[cfg(all(test, feature = "miniscript"))]
mod test {
use bitcoin::secp256k1::Secp256k1;
use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind};
use super::{
coin_select_bnb,
evaluate_cs::{Evaluation, EvaluationError},
tester::Tester,
CoinSelector, CoinSelectorOpt, Vec, WeightedValue,
};
fn tester() -> Tester {
const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
Tester::new(&Secp256k1::default(), DESC_STR)
}
fn evaluate_bnb(
initial_selector: CoinSelector,
max_tries: usize,
) -> Result<Evaluation, EvaluationError> {
evaluate(initial_selector, |cs| {
coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| {
*cs = new_cs;
true
})
})
}
#[test]
fn not_enough_coins() {
let t = tester();
let candidates: Vec<WeightedValue> = vec![
t.gen_candidate(0, 100_000).into(),
t.gen_candidate(1, 100_000).into(),
];
let opts = t.gen_opts(200_000);
let selector = CoinSelector::new(&candidates, &opts);
assert!(!coin_select_bnb(10_000, selector).is_some());
}
#[test]
fn exactly_enough_coins_preselected() {
let t = tester();
let candidates: Vec<WeightedValue> = vec![
t.gen_candidate(0, 100_000).into(), // to preselect
t.gen_candidate(1, 100_000).into(), // to preselect
t.gen_candidate(2, 100_000).into(),
];
let opts = CoinSelectorOpt {
target_feerate: 0.0,
..t.gen_opts(200_000)
};
let selector = {
let mut selector = CoinSelector::new(&candidates, &opts);
selector.select(0); // preselect
selector.select(1); // preselect
selector
};
let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed");
println!("{}", evaluation);
assert_eq!(evaluation.solution.selected, (0..=1).collect());
assert_eq!(evaluation.solution.excess_strategies.len(), 1);
assert_eq!(
evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(),
0.0
);
}
/// `cost_of_change` acts as the upper-bound in Bnb; we check whether these boundaries are
/// enforced in code
#[test]
fn cost_of_change() {
let t = tester();
let candidates: Vec<WeightedValue> = vec![
t.gen_candidate(0, 200_000).into(),
t.gen_candidate(1, 200_000).into(),
t.gen_candidate(2, 200_000).into(),
];
// lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming
// that we want 2 candidates selected
let (lowest_opts, highest_opts) = {
let opts = t.gen_opts(0);
let fee_from_inputs =
(candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2;
let fee_from_template =
((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64;
let lowest_opts = CoinSelectorOpt {
target_value: Some(
400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64,
),
..opts
};
let highest_opts = CoinSelectorOpt {
target_value: Some(400_000 - fee_from_inputs - fee_from_template),
..opts
};
(lowest_opts, highest_opts)
};
// test lowest possible target we can select
let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000);
assert!(lowest_eval.is_ok());
let lowest_eval = lowest_eval.unwrap();
println!("LB {}", lowest_eval);
assert_eq!(lowest_eval.solution.selected.len(), 2);
assert_eq!(lowest_eval.solution.excess_strategies.len(), 1);
assert_eq!(
lowest_eval
.feerate_offset(ExcessStrategyKind::ToFee)
.floor(),
0.0
);
// test the highest possible target we can select
let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000);
assert!(highest_eval.is_ok());
let highest_eval = highest_eval.unwrap();
println!("UB {}", highest_eval);
assert_eq!(highest_eval.solution.selected.len(), 2);
assert_eq!(highest_eval.solution.excess_strategies.len(), 1);
assert_eq!(
highest_eval
.feerate_offset(ExcessStrategyKind::ToFee)
.floor(),
0.0
);
// test lower out of bounds
let loob_opts = CoinSelectorOpt {
target_value: lowest_opts.target_value.map(|v| v - 1),
..lowest_opts
};
let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000);
assert!(loob_eval.is_err());
println!("Lower OOB: {}", loob_eval.unwrap_err());
// test upper out of bounds
let uoob_opts = CoinSelectorOpt {
target_value: highest_opts.target_value.map(|v| v + 1),
..highest_opts
};
let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000);
assert!(uoob_eval.is_err());
println!("Upper OOB: {}", uoob_eval.unwrap_err());
}
#[test]
fn try_select() {
let t = tester();
let candidates: Vec<WeightedValue> = vec![
t.gen_candidate(0, 300_000).into(),
t.gen_candidate(1, 300_000).into(),
t.gen_candidate(2, 300_000).into(),
t.gen_candidate(3, 200_000).into(),
t.gen_candidate(4, 200_000).into(),
];
let make_opts = |v: u64| -> CoinSelectorOpt {
CoinSelectorOpt {
target_feerate: 0.0,
..t.gen_opts(v)
}
};
let test_cases = vec![
(make_opts(100_000), false, 0),
(make_opts(200_000), true, 1),
(make_opts(300_000), true, 1),
(make_opts(500_000), true, 2),
(make_opts(1_000_000), true, 4),
(make_opts(1_200_000), false, 0),
(make_opts(1_300_000), true, 5),
(make_opts(1_400_000), false, 0),
];
for (opts, expect_solution, expect_selected) in test_cases {
let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000);
assert_eq!(res.is_ok(), expect_solution);
match res {
Ok(eval) => {
println!("{}", eval);
assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0);
assert_eq!(eval.solution.selected.len(), expect_selected as _);
}
Err(err) => println!("expected failure: {}", err),
}
}
}
#[test]
fn early_bailout_optimization() {
let t = tester();
// target: 300_000
// candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000
// expected solution: 2x 125_000, 1x 50_000
// set bnb max tries: 1100, should succeed
let candidates = {
let mut candidates: Vec<WeightedValue> = vec![
t.gen_candidate(0, 125_000).into(),
t.gen_candidate(1, 125_000).into(),
t.gen_candidate(2, 50_000).into(),
];
(3..3 + 1000_u32)
.for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into()));
candidates
};
let opts = CoinSelectorOpt {
target_feerate: 0.0,
..t.gen_opts(300_000)
};
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100);
assert!(result.is_ok());
let eval = result.unwrap();
println!("{}", eval);
assert_eq!(eval.solution.selected, (0..=2).collect());
}
#[test]
fn should_exhaust_iteration() {
static MAX_TRIES: usize = 1000;
let t = tester();
let candidates = (0..MAX_TRIES + 1)
.map(|index| t.gen_candidate(index as _, 10_000).into())
.collect::<Vec<WeightedValue>>();
let opts = t.gen_opts(10_001 * MAX_TRIES as u64);
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES);
assert!(result.is_err());
println!("error as expected: {}", result.unwrap_err());
}
/// Solution should have fee >= min_absolute_fee (or no solution at all)
#[test]
fn min_absolute_fee() {
let t = tester();
let candidates = {
let mut candidates = Vec::new();
t.gen_weighted_values(&mut candidates, 5, 10_000);
t.gen_weighted_values(&mut candidates, 5, 20_000);
t.gen_weighted_values(&mut candidates, 5, 30_000);
t.gen_weighted_values(&mut candidates, 10, 10_300);
t.gen_weighted_values(&mut candidates, 10, 10_500);
t.gen_weighted_values(&mut candidates, 10, 10_700);
t.gen_weighted_values(&mut candidates, 10, 10_900);
t.gen_weighted_values(&mut candidates, 10, 11_000);
t.gen_weighted_values(&mut candidates, 10, 12_000);
t.gen_weighted_values(&mut candidates, 10, 13_000);
candidates
};
let mut opts = CoinSelectorOpt {
min_absolute_fee: 1,
..t.gen_opts(100_000)
};
(1..=120_u64).for_each(|fee_factor| {
opts.min_absolute_fee = fee_factor * 31;
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000);
match result {
Ok(result) => {
println!("Solution {}", result);
let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee;
assert!(fee >= opts.min_absolute_fee);
assert_eq!(result.solution.excess_strategies.len(), 1);
}
Err(err) => {
println!("No Solution: {}", err);
}
}
});
}
/// For a decreasing feerate (long-term feerate is lower than effective feerate), we should
/// select less. For increasing feerate (long-term feerate is higher than effective feerate), we
/// should select more.
#[test]
fn feerate_difference() {
let t = tester();
let candidates = {
let mut candidates = Vec::new();
t.gen_weighted_values(&mut candidates, 10, 2_000);
t.gen_weighted_values(&mut candidates, 10, 5_000);
t.gen_weighted_values(&mut candidates, 10, 20_000);
candidates
};
let decreasing_feerate_opts = CoinSelectorOpt {
target_feerate: 1.25,
long_term_feerate: Some(0.25),
..t.gen_opts(100_000)
};
let increasing_feerate_opts = CoinSelectorOpt {
target_feerate: 0.25,
long_term_feerate: Some(1.25),
..t.gen_opts(100_000)
};
let decreasing_res = evaluate_bnb(
CoinSelector::new(&candidates, &decreasing_feerate_opts),
21_000,
)
.expect("no result");
let decreasing_len = decreasing_res.solution.selected.len();
let increasing_res = evaluate_bnb(
CoinSelector::new(&candidates, &increasing_feerate_opts),
21_000,
)
.expect("no result");
let increasing_len = increasing_res.solution.selected.len();
println!("decreasing_len: {}", decreasing_len);
println!("increasing_len: {}", increasing_len);
assert!(decreasing_len < increasing_len);
}
/// TODO: UNIMPLEMENTED TESTS:
/// * Excess strategies:
/// * We should always have `ExcessStrategy::ToFee`.
/// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`.
/// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`.
/// * Fuzz
/// * Solution feerate should never be lower than target feerate
/// * Solution fee should never be lower than `min_absolute_fee`.
/// * Preselected should always remain selected
fn _todo() {}
}

View File

@@ -0,0 +1,615 @@
use super::*;
/// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a
/// single UTXO, or a group of UTXOs that should be spent together.
#[derive(Debug, Clone, Copy)]
pub struct WeightedValue {
/// Total value of the UTXO(s) that this [`WeightedValue`] represents.
pub value: u64,
/// Total weight of including this/these UTXO(s).
/// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`,
/// `scriptWitness` should all be included.
pub weight: u32,
/// The total number of inputs; so we can calculate extra `varint` weight due to `vin` length changes.
pub input_count: usize,
/// Whether this [`WeightedValue`] contains at least one segwit spend.
pub is_segwit: bool,
}
impl WeightedValue {
/// Create a new [`WeightedValue`] that represents a single input.
///
/// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen +
/// scriptWitness`.
pub fn new(value: u64, satisfaction_weight: u32, is_segwit: bool) -> WeightedValue {
let weight = TXIN_BASE_WEIGHT + satisfaction_weight;
WeightedValue {
value,
weight,
input_count: 1,
is_segwit,
}
}
/// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`.
pub fn effective_value(&self, effective_feerate: f32) -> i64 {
// We prefer undershooting the candidate's effective value (so we over-estimate the fee of a
// candidate). If we overshoot the candidate's effective value, it may be possible to find a
// solution which does not meet the target feerate.
self.value as i64 - (self.weight as f32 * effective_feerate).ceil() as i64
}
}
#[derive(Debug, Clone, Copy)]
pub struct CoinSelectorOpt {
/// The value we need to select.
/// If the value is `None`, then the selection will be complete if it can pay for the drain
/// output and satisfy the other constraints (e.g., minimum fees).
pub target_value: Option<u64>,
/// Additional leeway for the target value.
pub max_extra_target: u64, // TODO: Maybe out of scope here?
/// The feerate we should try and achieve in sats per weight unit.
pub target_feerate: f32,
/// The feerate
pub long_term_feerate: Option<f32>, // TODO: Maybe out of scope? (waste)
/// The minimum absolute fee. I.e., needed for RBF.
pub min_absolute_fee: u64,
/// The weight of the template transaction, including fixed fields and outputs.
pub base_weight: u32,
/// Additional weight if we include the drain (change) output.
pub drain_weight: u32,
/// Weight of spending the drain (change) output in the future.
pub spend_drain_weight: u32, // TODO: Maybe out of scope? (waste)
/// Minimum value allowed for a drain (change) output.
pub min_drain_value: u64,
}
impl CoinSelectorOpt {
fn from_weights(base_weight: u32, drain_weight: u32, spend_drain_weight: u32) -> Self {
// 0.25 sats/wu == 1 sat/vb
let target_feerate = 0.25_f32;
// set `min_drain_value` to dust limit
let min_drain_value =
3 * ((drain_weight + spend_drain_weight) as f32 * target_feerate) as u64;
Self {
target_value: None,
max_extra_target: 0,
target_feerate,
long_term_feerate: None,
min_absolute_fee: 0,
base_weight,
drain_weight,
spend_drain_weight,
min_drain_value,
}
}
pub fn fund_outputs(
txouts: &[TxOut],
drain_output: &TxOut,
drain_satisfaction_weight: u32,
) -> Self {
let mut tx = Transaction {
input: vec![],
version: 1,
lock_time: LockTime::ZERO.into(),
output: txouts.to_vec(),
};
let base_weight = tx.weight();
// this awkward calculation is necessary since TxOut doesn't have \.weight()
let drain_weight = {
tx.output.push(drain_output.clone());
tx.weight() - base_weight
};
Self {
target_value: if txouts.is_empty() {
None
} else {
Some(txouts.iter().map(|txout| txout.value).sum())
},
..Self::from_weights(
base_weight as u32,
drain_weight as u32,
TXIN_BASE_WEIGHT + drain_satisfaction_weight,
)
}
}
pub fn long_term_feerate(&self) -> f32 {
self.long_term_feerate.unwrap_or(self.target_feerate)
}
pub fn drain_waste(&self) -> i64 {
(self.drain_weight as f32 * self.target_feerate
+ self.spend_drain_weight as f32 * self.long_term_feerate()) as i64
}
}
/// [`CoinSelector`] selects and deselects from a set of candidates.
#[derive(Debug, Clone)]
pub struct CoinSelector<'a> {
pub opts: &'a CoinSelectorOpt,
pub candidates: &'a Vec<WeightedValue>,
selected: BTreeSet<usize>,
}
impl<'a> CoinSelector<'a> {
pub fn candidate(&self, index: usize) -> &WeightedValue {
&self.candidates[index]
}
pub fn new(candidates: &'a Vec<WeightedValue>, opts: &'a CoinSelectorOpt) -> Self {
Self {
candidates,
selected: Default::default(),
opts,
}
}
pub fn select(&mut self, index: usize) -> bool {
assert!(index < self.candidates.len());
self.selected.insert(index)
}
pub fn deselect(&mut self, index: usize) -> bool {
self.selected.remove(&index)
}
pub fn is_selected(&self, index: usize) -> bool {
self.selected.contains(&index)
}
pub fn is_empty(&self) -> bool {
self.selected.is_empty()
}
/// Weight sum of all selected inputs.
pub fn selected_weight(&self) -> u32 {
self.selected
.iter()
.map(|&index| self.candidates[index].weight)
.sum()
}
/// Effective value sum of all selected inputs.
pub fn selected_effective_value(&self) -> i64 {
self.selected
.iter()
.map(|&index| self.candidates[index].effective_value(self.opts.target_feerate))
.sum()
}
/// Absolute value sum of all selected inputs.
pub fn selected_absolute_value(&self) -> u64 {
self.selected
.iter()
.map(|&index| self.candidates[index].value)
.sum()
}
/// Waste sum of all selected inputs.
pub fn selected_waste(&self) -> i64 {
(self.selected_weight() as f32 * (self.opts.target_feerate - self.opts.long_term_feerate()))
as i64
}
/// Current weight of template tx + selected inputs.
pub fn current_weight(&self) -> u32 {
let witness_header_extra_weight = self
.selected()
.find(|(_, wv)| wv.is_segwit)
.map(|_| 2)
.unwrap_or(0);
let vin_count_varint_extra_weight = {
let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::<usize>();
(varint_size(input_count) - 1) * 4
};
self.opts.base_weight
+ self.selected_weight()
+ witness_header_extra_weight
+ vin_count_varint_extra_weight
}
/// Current excess.
pub fn current_excess(&self) -> i64 {
self.selected_effective_value() - self.effective_target()
}
/// This is the effective target value.
pub fn effective_target(&self) -> i64 {
let (has_segwit, max_input_count) = self
.candidates
.iter()
.fold((false, 0_usize), |(is_segwit, input_count), c| {
(is_segwit || c.is_segwit, input_count + c.input_count)
});
let effective_base_weight = self.opts.base_weight
+ if has_segwit { 2_u32 } else { 0_u32 }
+ (varint_size(max_input_count) - 1) * 4;
self.opts.target_value.unwrap_or(0) as i64
+ (effective_base_weight as f32 * self.opts.target_feerate).ceil() as i64
}
pub fn selected_count(&self) -> usize {
self.selected.len()
}
pub fn selected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
self.selected
.iter()
.map(move |&index| (index, &self.candidates[index]))
}
pub fn unselected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
self.candidates
.iter()
.enumerate()
.filter(move |(index, _)| !self.selected.contains(index))
}
pub fn selected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
self.selected.iter().cloned()
}
pub fn unselected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
(0..self.candidates.len()).filter(move |index| !self.selected.contains(index))
}
pub fn all_selected(&self) -> bool {
self.selected.len() == self.candidates.len()
}
pub fn select_all(&mut self) {
self.selected = (0..self.candidates.len()).collect();
}
pub fn select_until_finished(&mut self) -> Result<Selection, SelectionError> {
let mut selection = self.finish();
if selection.is_ok() {
return selection;
}
let unselected = self.unselected_indexes().collect::<Vec<_>>();
for index in unselected {
self.select(index);
selection = self.finish();
if selection.is_ok() {
break;
}
}
selection
}
pub fn finish(&self) -> Result<Selection, SelectionError> {
let weight_without_drain = self.current_weight();
let weight_with_drain = weight_without_drain + self.opts.drain_weight;
let fee_without_drain =
(weight_without_drain as f32 * self.opts.target_feerate).ceil() as u64;
let fee_with_drain = (weight_with_drain as f32 * self.opts.target_feerate).ceil() as u64;
let inputs_minus_outputs = {
let target_value = self.opts.target_value.unwrap_or(0);
let selected = self.selected_absolute_value();
// find the largest unsatisfied constraint (if any), and return the error of that constraint
// "selected" should always be greater than or equal to these selected values
[
(
SelectionConstraint::TargetValue,
target_value.saturating_sub(selected),
),
(
SelectionConstraint::TargetFee,
(target_value + fee_without_drain).saturating_sub(selected),
),
(
SelectionConstraint::MinAbsoluteFee,
(target_value + self.opts.min_absolute_fee).saturating_sub(selected),
),
(
SelectionConstraint::MinDrainValue,
// when we have no target value (hence no recipient txouts), we need to ensure
// the selected amount can satisfy requirements for a drain output (so we at least have one txout)
if self.opts.target_value.is_none() {
(fee_with_drain + self.opts.min_drain_value).saturating_sub(selected)
} else {
0
},
),
]
.iter()
.filter(|&(_, v)| v > &0)
.max_by_key(|&(_, v)| v)
.map_or(Ok(()), |(constraint, missing)| {
Err(SelectionError {
selected,
missing: *missing,
constraint: *constraint,
})
})?;
selected - target_value
};
let fee_without_drain = fee_without_drain.max(self.opts.min_absolute_fee);
let fee_with_drain = fee_with_drain.max(self.opts.min_absolute_fee);
let excess_without_drain = inputs_minus_outputs - fee_without_drain;
let input_waste = self.selected_waste();
// begin preparing excess strategies for final selection
let mut excess_strategies = HashMap::new();
// only allow `ToFee` and `ToRecipient` excess strategies when we have a `target_value`,
// otherwise, we will result in a result with no txouts, or attempt to add value to an output
// that does not exist.
if self.opts.target_value.is_some() {
// no drain, excess to fee
excess_strategies.insert(
ExcessStrategyKind::ToFee,
ExcessStrategy {
recipient_value: self.opts.target_value,
drain_value: None,
fee: fee_without_drain + excess_without_drain,
weight: weight_without_drain,
waste: input_waste + excess_without_drain as i64,
},
);
// no drain, send the excess to the recipient
// if `excess == 0`, this result will be the same as the previous, so don't consider it
// if `max_extra_target == 0`, there is no leeway for this strategy
if excess_without_drain > 0 && self.opts.max_extra_target > 0 {
let extra_recipient_value =
core::cmp::min(self.opts.max_extra_target, excess_without_drain);
let extra_fee = excess_without_drain - extra_recipient_value;
excess_strategies.insert(
ExcessStrategyKind::ToRecipient,
ExcessStrategy {
recipient_value: self.opts.target_value.map(|v| v + extra_recipient_value),
drain_value: None,
fee: fee_without_drain + extra_fee,
weight: weight_without_drain,
waste: input_waste + extra_fee as i64,
},
);
}
}
// with drain
if fee_with_drain >= self.opts.min_absolute_fee
&& inputs_minus_outputs >= fee_with_drain + self.opts.min_drain_value
{
excess_strategies.insert(
ExcessStrategyKind::ToDrain,
ExcessStrategy {
recipient_value: self.opts.target_value,
drain_value: Some(inputs_minus_outputs.saturating_sub(fee_with_drain)),
fee: fee_with_drain,
weight: weight_with_drain,
waste: input_waste + self.opts.drain_waste(),
},
);
}
debug_assert!(
!excess_strategies.is_empty(),
"should have at least one excess strategy."
);
Ok(Selection {
selected: self.selected.clone(),
excess: excess_without_drain,
excess_strategies,
})
}
}
#[derive(Clone, Debug)]
pub struct SelectionError {
selected: u64,
missing: u64,
constraint: SelectionConstraint,
}
impl core::fmt::Display for SelectionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let SelectionError {
selected,
missing,
constraint,
} = self;
write!(
f,
"insufficient coins selected; selected={}, missing={}, unsatisfied_constraint={:?}",
selected, missing, constraint
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for SelectionError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelectionConstraint {
/// The target is not met
TargetValue,
/// The target fee (given the feerate) is not met
TargetFee,
/// Min absolute fee is not met
MinAbsoluteFee,
/// Min drain value is not met
MinDrainValue,
}
impl core::fmt::Display for SelectionConstraint {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SelectionConstraint::TargetValue => core::write!(f, "target_value"),
SelectionConstraint::TargetFee => core::write!(f, "target_fee"),
SelectionConstraint::MinAbsoluteFee => core::write!(f, "min_absolute_fee"),
SelectionConstraint::MinDrainValue => core::write!(f, "min_drain_value"),
}
}
}
#[derive(Clone, Debug)]
pub struct Selection {
pub selected: BTreeSet<usize>,
pub excess: u64,
pub excess_strategies: HashMap<ExcessStrategyKind, ExcessStrategy>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
pub enum ExcessStrategyKind {
ToFee,
ToRecipient,
ToDrain,
}
#[derive(Clone, Copy, Debug)]
pub struct ExcessStrategy {
pub recipient_value: Option<u64>,
pub drain_value: Option<u64>,
pub fee: u64,
pub weight: u32,
pub waste: i64,
}
impl Selection {
pub fn apply_selection<'a, T>(
&'a self,
candidates: &'a [T],
) -> impl Iterator<Item = &'a T> + 'a {
self.selected.iter().map(move |i| &candidates[*i])
}
/// Returns the [`ExcessStrategy`] that results in the least waste.
pub fn best_strategy(&self) -> (&ExcessStrategyKind, &ExcessStrategy) {
self.excess_strategies
.iter()
.min_by_key(|&(_, a)| a.waste)
.expect("selection has no excess strategy")
}
}
impl core::fmt::Display for ExcessStrategyKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ExcessStrategyKind::ToFee => core::write!(f, "to_fee"),
ExcessStrategyKind::ToRecipient => core::write!(f, "to_recipient"),
ExcessStrategyKind::ToDrain => core::write!(f, "to_drain"),
}
}
}
impl ExcessStrategy {
/// Returns feerate in sats/wu.
pub fn feerate(&self) -> f32 {
self.fee as f32 / self.weight as f32
}
}
#[cfg(test)]
mod test {
use crate::{ExcessStrategyKind, SelectionConstraint};
use super::{CoinSelector, CoinSelectorOpt, WeightedValue};
/// Ensure `target_value` is respected. Can't have any disrespect.
#[test]
fn target_value_respected() {
let target_value = 1000_u64;
let candidates = (500..1500_u64)
.map(|value| WeightedValue {
value,
weight: 100,
input_count: 1,
is_segwit: false,
})
.collect::<super::Vec<_>>();
let opts = CoinSelectorOpt {
target_value: Some(target_value),
max_extra_target: 0,
target_feerate: 0.00,
long_term_feerate: None,
min_absolute_fee: 0,
base_weight: 10,
drain_weight: 10,
spend_drain_weight: 10,
min_drain_value: 10,
};
for (index, v) in candidates.iter().enumerate() {
let mut selector = CoinSelector::new(&candidates, &opts);
assert!(selector.select(index));
let res = selector.finish();
if v.value < opts.target_value.unwrap_or(0) {
let err = res.expect_err("should have failed");
assert_eq!(err.selected, v.value);
assert_eq!(err.missing, target_value - v.value);
assert_eq!(err.constraint, SelectionConstraint::MinAbsoluteFee);
} else {
let sel = res.expect("should have succeeded");
assert_eq!(sel.excess, v.value - opts.target_value.unwrap_or(0));
}
}
}
#[test]
fn drain_all() {
let candidates = (0..100)
.map(|_| WeightedValue {
value: 666,
weight: 166,
input_count: 1,
is_segwit: false,
})
.collect::<super::Vec<_>>();
let opts = CoinSelectorOpt {
target_value: None,
max_extra_target: 0,
target_feerate: 0.25,
long_term_feerate: None,
min_absolute_fee: 0,
base_weight: 10,
drain_weight: 100,
spend_drain_weight: 66,
min_drain_value: 1000,
};
let selection = CoinSelector::new(&candidates, &opts)
.select_until_finished()
.expect("should succeed");
assert!(selection.selected.len() > 1);
assert_eq!(selection.excess_strategies.len(), 1);
let (kind, strategy) = selection.best_strategy();
assert_eq!(*kind, ExcessStrategyKind::ToDrain);
assert!(strategy.recipient_value.is_none());
assert!(strategy.drain_value.is_some());
}
/// TODO: Tests to add:
/// * `finish` should ensure at least `target_value` is selected.
/// * actual feerate should be equal or higher than `target_feerate`.
/// * actual drain value should be equal to or higher than `min_drain_value` (or else no drain).
fn _todo() {}
}

View File

@@ -0,0 +1,33 @@
#![no_std]
#[cfg(feature = "std")]
extern crate std;
#[macro_use]
extern crate alloc;
extern crate bdk_chain;
use alloc::vec::Vec;
use bdk_chain::{
bitcoin,
collections::{BTreeSet, HashMap},
};
use bitcoin::{LockTime, Transaction, TxOut};
use core::fmt::{Debug, Display};
mod coin_selector;
pub use coin_selector::*;
mod bnb;
pub use bnb::*;
/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include
/// `scriptSigLen` or `scriptSig`.
pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4;
/// Helper to calculate varint size. `v` is the value the varint represents.
// Shamelessly copied from
// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8
pub(crate) fn varint_size(v: usize) -> u32 {
bitcoin::VarInt(v as u64).len() as u32
}

View File

@@ -0,0 +1,13 @@
[package]
name = "bdk_tmp_plan"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk_chain = { path = "../../crates/chain", features = ["miniscript"] }
[features]
default = ["std"]
std = []

Some files were not shown because too many files have changed in this diff Show More