Compare commits

...

61 Commits

Author SHA1 Message Date
Steve Myers
4a8452f9b8 Merge bitcoindevkit/bdk#1450: Bump bdk version to 1.0.0-alpha.12
108061dddb Bump bdk version to 1.0.0-alpha.12 (Steve Myers)

Pull request description:

  ### Description

  fixes #1449

  bdk_chain to 0.15.0
  bdk_bitcoind_rpc to 0.11.0
  bdk_electrum to 0.14.0
  bdk_esplora to 0.14.0
  bdk_persist to 0.3.0
  bdk_file_store to 0.12.0
  bdk_sqlite keep at 0.1.0
  bdk_testenv to 0.5.0
  bdk_hwi to 0.1.0
  bdk_wallet to 1.0.0-alpha.12

  ### Notes to the reviewers

  I also (hopefully) fixed the `bdk_hwi` crate so it can be published.

ACKs for top commit:
  ValuedMammal:
    ACK 108061dddb
  storopoli:
    ACK 108061dddb

Tree-SHA512: 2a80c51e254ca011b8e9bb72ad3d720dd8d09a3142e85cf230ffffad747257d91c45950cce64b049bab91f21c70c622f14d46def24ea78459497b42472a1fe48
2024-05-23 17:38:49 -05:00
Steve Myers
108061dddb Bump bdk version to 1.0.0-alpha.12
bdk_chain to 0.15.0
bdk_bitcoind_rpc to 0.11.0
bdk_electrum to 0.14.0
bdk_esplora to 0.14.0
bdk_persist to 0.3.0
bdk_file_store to 0.12.0
bdk_sqlite keep at 0.1.0
bdk_testenv to 0.5.0
bdk_hwi to 0.1.0
bdk_wallet to 1.0.0-alpha.12
2024-05-23 11:15:30 -05:00
Steve Myers
a2d940132d Merge bitcoindevkit/bdk#1393: fix(export): add tr descriptor
1b7c6df569 fix(export): add tr descriptor (rustaceanrob)

Pull request description:

  ### Description

  Resolves #860 by adding export of taproot descriptors

  ### Notes to the reviewers

  Allows export as Core accepts taproot.

  ### Changelog notice

  - Export taproot descriptors

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

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

Tree-SHA512: 80bee93a1ec531717e79a5e7a91e840532ae4d3daf677a207bc53162c69410e47db4893024b3eccfd848f1628062e7c49f0e92ebeb7230e2ebb9b47eb84b9e56
2024-05-23 10:19:01 -05:00
Steve Myers
2a055de555 Merge bitcoindevkit/bdk#1386: Remove TxBuilder allow_shrinking() and unneeded context param
096b8ef781 fix(wallet): remove TxBuilder::allow_shrinking function and TxBuilderContext (Steve Myers)

Pull request description:

  ### Description

  Remove wallet::TxBuilder::allow_shrinking() and unneeded TxBuilder context param.

  Fixes #1374

  ### Notes to the reviewers

  The allow_shrinking function was the only one using the TxBuilder FeeBump context and it's useful to have CreateTx context  functions available for building FeeBump transactions, see updated tests.

  ### Changelog notice

  Changed

  - Removed TxBuilder::allow_shrinking() function.

  ### 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'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: f4f05ce4cfaced9e61eebc0ebfe77a299fae13df4459fc26c90657297a1347a5d16f22b4e1c8b9b10458cfec80ca3b5d698eee11c7a560fd83f94a1ac7101a86
2024-05-23 10:08:45 -05:00
Steve Myers
096b8ef781 fix(wallet): remove TxBuilder::allow_shrinking function and TxBuilderContext 2024-05-23 09:45:49 -05:00
志宇
2eea0f4e90 Merge bitcoindevkit/bdk#1128: feat: add bdk_sqlite crate implementing PersistBackend
475c5024ec feat(sqlite): add bdk_sqlite crate implementing PersistBackend backed by a SQLite database (Steve Myers)
b8aa76cd05 feat(wallet): use the new `CombinedChangeSet` of `bdk_persist` (志宇)
0958ff56b2 feat(persist): introduce `CombinedChangeSet` (志宇)
54942a902d ci: bump build_docs rust version to nightly-2024-05-12 (Steve Myers)
d975a48e7c docs: update README MSRV pinning to match CI (Steve Myers)

Pull request description:

  ### Description

  Add "bdk_sqlite_store" crate implementing `PersistBackend` backed by a SQLite database.

  ### Notes to the reviewers

  In addition to adding a SQLite based `PersistenceBackend` this PR also:

  * add  `CombinedChangeSet` in `bdk_persist` and update `wallet` crate to use it.
  * updates the `wallet/tests` to also use this new sqlite store crate.
  * updates `example-crates/wallet_esplora_async` to use this new sqlite store crate.
  * fixes out of date README instructions for MSRV.
  * bumps the ci `build_docs` job to rust `nightly-2024-05-12`.

  ### Changelog notice

  Changed

  - Update Wallet to use CombinedChangeSet for persistence.

  Added

  - Add  CombinedChangeSet in bdk_persist crate.
  - Add bdk_sqlite crate implementing SQLite based PersistenceBackend storage for CombinedChangeSet.

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

Tree-SHA512: 72565127994fbfff34d7359e1d36d6f13e552995bb7ea61dd38ba5329b0d15e500fa106214a83fd870f1017dc659ff1bb8fc71bc6aad0a793730ec7f23179d8c
2024-05-23 22:34:24 +08:00
Steve Myers
475c5024ec feat(sqlite): add bdk_sqlite crate implementing PersistBackend backed by a SQLite database 2024-05-23 08:59:45 -05:00
志宇
b8aa76cd05 feat(wallet): use the new CombinedChangeSet of bdk_persist 2024-05-22 23:02:55 -05:00
志宇
0958ff56b2 feat(persist): introduce CombinedChangeSet
It is a good idea to have common changeset types stored in
`bdk_persist`. This will make it convenient for persistence crates and
`bdk_wallet` interaction.
2024-05-22 23:02:54 -05:00
Steve Myers
54942a902d ci: bump build_docs rust version to nightly-2024-05-12 2024-05-22 23:02:52 -05:00
Steve Myers
d975a48e7c docs: update README MSRV pinning to match CI 2024-05-22 23:02:51 -05:00
志宇
2f059a1588 Merge bitcoindevkit/bdk#1443: fix(electrum): Fix fetch_prev_txout
af15ebba94 fix(electrum): Fix `fetch_prev_txout` (valued mammal)

Pull request description:

  Previously we inserted every `TxOut` of a previous tx at the same outpoint, which is incorrect because an outpoint only points to a single `TxOut`. Now just get the `TxOut` corresponding to the txin prevout and insert it with its outpoint.

  ### Notes to the reviewers

  The bug in question was demonstrated in a discord comment https://discord.com/channels/753336465005608961/1239693193159639073/1239704153400414298 but I don't think we've opened an issue yet. Essentially, because of a mismatch between the outpoint and txout stored in TxGraph, we weren't summing the inputs correctly which caused `calculate_fee` to fail with `NegativeFee` error.

  fixes #1419

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

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

ACKs for top commit:
  LagginTimes:
    ACK af15ebba94
  evanlinjin:
    ACK af15ebba94

Tree-SHA512: c3a2c374069b0863076784856d90c7760b8f411e4881c3e42367015542b02ea6010c37745fb6dde95422af7222b7939ec51bc0fd1f63f816813c2159a17e08e5
2024-05-15 20:35:02 +08:00
valued mammal
af15ebba94 fix(electrum): Fix fetch_prev_txout
Previously we inserted every TxOut of a previous tx
at the same outpoint, which is incorrect because an outpoint
only points to a single TxOut. Now just get the TxOut
corresponding to the txin prevout and insert it with
its outpoint.
2024-05-14 10:50:02 -04:00
rustaceanrob
1b7c6df569 fix(export): add tr descriptor 2024-05-13 09:41:27 -10:00
Steve Myers
7607b49283 Merge bitcoindevkit/bdk#1326: chore: rename bdk crate to bdk_wallet
f6781652b7 chore: rename bdk crate to bdk_wallet (Steve Myers)

Pull request description:

  ### Description

  Fixes #1305 .

  ### Notes to the reviewers

  Once this properly builds, even before all reviews is done, I will publish it to crates.io to reserve the name.

  ### Changelog notice

  Changed

  - Renamed `bdk` crate to `bdk_wallet`.

  ### 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: 437c79d9d41721bc9cffd3be81e898068378803dcde6b4106d33bf34ffa756d090dce36d15d4ecd232e5b55ce09c393e6956fc0af4b8477886dabb506b679256
2024-05-13 12:25:08 -05:00
Steve Myers
f6781652b7 chore: rename bdk crate to bdk_wallet 2024-05-13 12:10:58 -05:00
Steve Myers
7876c8fd06 Merge bitcoindevkit/bdk#1437: Bump bdk version to 1.0.0-alpha.11
db9fdccc18 Bump bdk version to 1.0.0-alpha.11 (Steve Myers)

Pull request description:

  ### Description

  fixes #1435

  bdk_chain to 0.14.0
  bdk_bitcoind_rpc to 0.10.0
  bdk_electrum to 0.13.0
  bdk_esplora to 0.13.0
  bdk_file_store to 0.11.0
  bdk_testenv to 0.4.0
  bdk_persist to 0.2.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:
  ValuedMammal:
    ACK db9fdccc18
  storopoli:
    ACK db9fdccc18

Tree-SHA512: 804475927461a4bcf37786313123a0a0cc68390af6c556111551dc126a06614296eb657a8a5662a36cf4569ab332f5f9285c99c5f1992d93f43568e881961895
2024-05-10 17:23:21 -05:00
Steve Myers
db9fdccc18 Bump bdk version to 1.0.0-alpha.11
bdk_chain to 0.14.0
bdk_bitcoind_rpc to 0.10.0
bdk_electrum to 0.13.0
bdk_esplora to 0.13.0
bdk_file_store to 0.11.0
bdk_testenv to 0.4.0
bdk_persist to 0.2.0
2024-05-10 14:05:40 -05:00
Steve Myers
63e3bbe820 Merge bitcoindevkit/bdk#1403: Update bdk_electrum crate to use sync/full-scan structs
b45897e6fe feat(electrum): update docs and simplify logic of `ElectrumExt` (志宇)
92fb6cb373 chore(electrum): do not use `anyhow::Result` directly (志宇)
b2f3cacce6 feat(electrum): include option for previous `TxOut`s for fee calculation (Wei Chen)
c0d7d60a58 feat(chain)!: use custom return types for `ElectrumExt` methods (志宇)
2945c6be88 fix(electrum): fixed `sync` functionality (Wei Chen)
9ed33c25ea docs(electrum): fixed `full_scan`, `sync`, and crate documentation (Wei Chen)
b1f861b932 feat: update logging of electrum examples (志宇)
a6fdfb2ae4 feat(electrum)!: use new sync/full-scan structs for `ElectrumExt` (志宇)
653e4fed6d feat(wallet): cache txs when constructing full-scan/sync requests (志宇)
58f27b38eb feat(chain): introduce `TxCache` to `SyncRequest` and `FullScanRequest` (志宇)
721bb7f519 fix(chain): Make `Anchor` type in `FullScanResult` generic (志宇)
e3cfb84898 feat(chain): `TxGraph::insert_tx` reuses `Arc` (志宇)
2ffb65618a refactor(electrum): remove `RelevantTxids` and track txs in `TxGraph` (Wei Chen)

Pull request description:

  Fixes #1265
  Possibly fixes #1419

  ### Context

  Previous changes such as

  * Universal structures for full-scan/sync (PR #1413)
  * Making `CheckPoint` linked list query-able (PR #1369)
  * Making `Transaction`s cheaply-clonable (PR #1373)

  has allowed us to simplify the interaction between chain-source and receiving-structures (`bdk_chain`).

  The motivation is to accomplish something like this ([as mentioned here](https://github.com/bitcoindevkit/bdk/issues/1153#issuecomment-1752263555)):
  ```rust
  let things_I_am_interested_in = wallet.lock().unwrap().start_sync();
  let update = electrum_or_esplora.sync(things_i_am_interested_in)?;
  wallet.lock().unwrap().apply_update(update)?:
  ```

  ### Description

  This PR greatly simplifies the API of our Electrum chain-source (`bdk_electrum`) by making use of the aforementioned changes. Instead of referring back to the receiving `TxGraph` mid-sync/scan to determine which full transaction to fetch, we provide the Electrum chain-source already-fetched full transactions to start sync/scan (this is cheap, as transactions are wrapped in `Arc`s since #1373).

  In addition, an option has been added to include the previous `TxOut` for transactions received from an external wallet for fee calculation.

  ### Changelog notice

  * Change `TxGraph::insert_tx` to take in anything that satisfies `Into<Arc<Transaction>>`. This allows us to reuse the `Arc` pointer of what is being inserted.
  * Add `tx_cache` field to `SyncRequest` and `FullScanRequest`.
  * Make `Anchor` type in `FullScanResult` generic for more flexibility.
  * Change `ElectrumExt` methods to take in `SyncRequest`/`FullScanRequest` and return `SyncResult`/`FullScanResult`. Also update electrum examples accordingly.
  * Add `ElectrumResultExt` trait which allows us to convert the update `TxGraph` of `SyncResult`/`FullScanResult` for `bdk_electrum`.
  * Added an option for `full_scan` and `sync` to also fetch previous `TxOut`s to allow for fee 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:

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

ACKs for top commit:
  ValuedMammal:
    ACK b45897e6fe
  notmandatory:
    ACK b45897e6fe

Tree-SHA512: 1e274546015e7c7257965b36079ffe0cb3c2c0b7c2e0c322bcf32a06925a0c3e1119da1c8fd5318f1dbd82c2e952f6a07f227a9b023c48f506a62c93045d96d3
2024-05-10 12:33:09 -05:00
志宇
b45897e6fe feat(electrum): update docs and simplify logic of ElectrumExt
Helper method docs are updated to explain what they are updating. Logic
is simplified as we do not need to check whether a tx exists already in
`update_graph` before inserting it.
2024-05-10 16:40:55 +08:00
志宇
92fb6cb373 chore(electrum): do not use anyhow::Result directly 2024-05-10 14:54:29 +08:00
Wei Chen
b2f3cacce6 feat(electrum): include option for previous TxOuts for fee calculation
The previous `TxOut` for transactions received from an external
wallet may be optionally added as floating `TxOut`s to `TxGraph`
to allow for fee calculation.
2024-05-10 14:54:29 +08:00
志宇
c0d7d60a58 feat(chain)!: use custom return types for ElectrumExt methods
This is more code, but a much more elegant solution than having
`ElectrumExt` methods return `SyncResult`/`FullScanResult` and having an
`ElectrumResultExt` extention trait.
2024-05-10 14:54:29 +08:00
Wei Chen
2945c6be88 fix(electrum): fixed sync functionality 2024-05-10 14:54:28 +08:00
Wei Chen
9ed33c25ea docs(electrum): fixed full_scan, sync, and crate documentation 2024-05-10 14:54:28 +08:00
志宇
b1f861b932 feat: update logging of electrum examples
* Syncing with `example_electrum` now shows progress as a percentage.
* Flush stdout more aggressively.
2024-05-10 14:54:28 +08:00
志宇
a6fdfb2ae4 feat(electrum)!: use new sync/full-scan structs for ElectrumExt
`ElectrumResultExt` trait is also introduced that adds methods which can
convert the `Anchor` type for the update `TxGraph`.

We also make use of the new `TxCache` fields in
`SyncRequest`/`FullScanRequest`. This way, we can avoid re-fetching full
transactions from Electrum if not needed.

Examples and tests are updated to use the new `ElectrumExt` API.
2024-05-10 14:54:28 +08:00
志宇
653e4fed6d feat(wallet): cache txs when constructing full-scan/sync requests 2024-05-10 14:11:20 +08:00
志宇
58f27b38eb feat(chain): introduce TxCache to SyncRequest and FullScanRequest
This transaction cache can be provided so the chain-source can avoid
re-fetching transactions.
2024-05-10 14:11:19 +08:00
志宇
721bb7f519 fix(chain): Make Anchor type in FullScanResult generic 2024-05-10 14:11:19 +08:00
志宇
e3cfb84898 feat(chain): TxGraph::insert_tx reuses Arc
When we insert a transaction that is already wrapped in `Arc`, we should
reuse the `Arc`.
2024-05-10 14:11:19 +08:00
Wei Chen
2ffb65618a refactor(electrum): remove RelevantTxids and track txs in TxGraph
This PR removes `RelevantTxids` from the electrum crate and tracks
transactions in a `TxGraph`. This removes the need to separately
construct a `TxGraph` after a `full_scan` or `sync`.
2024-05-10 14:11:18 +08:00
Steve Myers
fb7ff298a4 Merge bitcoindevkit/bdk#1203: Include the descriptor in keychain::Changeset
86711d4f46 doc(chain): add section for non-recommended K to descriptor assignments (Daniela Brozzoni)
de53d72191 test: Only the highest ord keychain is returned (Daniela Brozzoni)
9d8023bf56 fix(chain): introduce keychain-variant-ranking to `KeychainTxOutIndex` (志宇)
6c8748124f chore(chain): move `use` in `indexed_tx_graph.rs` so clippy is happy (志宇)
537aa03ae0 chore(chain): update test so clippy does not complain (志宇)
ed117de7a5 test(chain): applying changesets one-by-one vs aggregate should be same (志宇)
6a3fb849e8 fix(chain): simplify `Append::append` impl for `keychain::ChangeSet` (志宇)
1d294b734d fix: Run tests only if the miniscript feature is.. ..enabled, enable it by default (Daniela Brozzoni)
0e3e136f6f doc(bdk): Add instructions for manually inserting... ...secret keys in the wallet in Wallet::load (Daniela Brozzoni)
76afccc555 fix(wallet): add expected descriptors as signers after creating from wallet::ChangeSet (Steve Myers)
4f05441a00 keychain::ChangeSet includes the descriptor (Daniela Brozzoni)
8ff99f27df ref(chain): Define test descriptors, use them... ...everywhere (Daniela Brozzoni)
b9902936a0 ref(chain): move `keychain::ChangeSet` into `txout_index.rs` (志宇)

Pull request description:

  Fixes #1101

  - Moves keychain::ChangeSet inside `keychain/txout_index.rs` as now the `ChangeSet` depends on miniscript
  - Slightly cleans up tests by introducing some constant descriptors
  - The KeychainTxOutIndex's internal SpkIterator now uses DescriptorId
  instead of K. The DescriptorId -> K translation is made at the
  KeychainTxOutIndex level.
  - The keychain::Changeset is now a struct, which includes a map for last
  revealed indexes, and one for newly added keychains and their
  descriptor.

  ### Changelog notice

  API changes in bdk:
  - Wallet::keychains returns a `impl Iterator` instead of `BTreeMap`
  - Wallet::load doesn't take descriptors anymore, since they're stored in the db
  - Wallet::new_or_load checks if the loaded descriptor from db is the same as the provided one

  API changes in bdk_chain:
  - `ChangeSet` is now a struct, which includes a map for last revealed
        indexes, and one for keychains and descriptors.
  - `KeychainTxOutIndex::inner` returns a `SpkIterator<(DescriptorId, u32)>`
  - `KeychainTxOutIndex::outpoints` returns a `BTreeSet` instead of `&BTreeSet`
  - `KeychainTxOutIndex::keychains` returns a `impl Iterator` instead of
        `&BTreeMap`
  - `KeychainTxOutIndex::txouts` doesn't return a ExactSizeIterator anymore
  - `KeychainTxOutIndex::last_revealed_indices` returns a `BTreeMap`
        instead of `&BTreeMap`
  - `KeychainTxOutIndex::add_keychain` has been renamed to `KeychainTxOutIndex::insert_descriptor`, and now it returns a ChangeSet
  - `KeychainTxOutIndex::reveal_next_spk` returns Option
  - `KeychainTxOutIndex::next_unused_spk` returns Option
  - `KeychainTxOutIndex::unbounded_spk_iter` returns Option
  - `KeychainTxOutIndex::next_index` returns Option
  - `KeychainTxOutIndex::reveal_to_target` returns Option
  - `KeychainTxOutIndex::revealed_keychain_spks` returns Option
  - `KeychainTxOutIndex::unused_keychain_spks` returns Option
  - `KeychainTxOutIndex::last_revealed_index` returns Option
  - `KeychainTxOutIndex::keychain_outpoints` returns Option
  - `KeychainTxOutIndex::keychain_outpoints_in_range` returns Option
  - `KeychainTxOutIndex::last_used_index` returns None if the keychain has never been used, or if it doesn't exist

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

Tree-SHA512: 4b1c9a31951f67b18037b7dd9837acbc35823f21de644ab833754b74d20f5373549f81e66965ecd3953ebf4f99644c9fd834812acfa65f9188950f1bda17ab60
2024-05-09 13:18:57 -05:00
Daniela Brozzoni
86711d4f46 doc(chain): add section for non-recommended K to descriptor assignments 2024-05-09 14:40:19 +08:00
Steve Myers
86408b90a5 Merge bitcoindevkit/bdk#1430: ci: Pin clippy to rust 1.78.0
de2763a4b8 ci: Pin clippy to rust 1.78.0 (valued mammal)

Pull request description:

  This PR pins clippy check in CI to the rust 1.78 toolchain, which prevents new lints in stable releases from interrupting the usual workflow. Because rust versions are released on a predictable schedule, we can revisit this setting in the future as needed (say 3-6 months).

  ### 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)
  * [ ] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  danielabrozzoni:
    ACK de2763a4b8
  storopoli:
    ACK de2763a4b8
  notmandatory:
    ACK de2763a4b8
  oleonardolima:
    ACK de2763a4b8

Tree-SHA512: 73cad29a5ff437290aca8f85a011c4f5fc4d9ff5755f3d3ef9fa1820f5631eda857b1a67955adfc6ef98145958c674cc09f7613b96f38cc30c75a656d872edbc
2024-05-08 19:37:06 -05:00
Daniela Brozzoni
de53d72191 test: Only the highest ord keychain is returned 2024-05-08 15:49:51 +02:00
志宇
9d8023bf56 fix(chain): introduce keychain-variant-ranking to KeychainTxOutIndex
This fixes the bug with changesets not being monotone. Previously, the
result of applying changesets individually v.s. applying the aggregate
of changesets may result in different `KeychainTxOutIndex` states.

The nature of the changeset allows different keychain types to share the
same descriptor. However, the previous design did not take this into
account. To do this properly, we should keep track of all keychains
currently associated with a given descriptor. However, the API only
allows returning one keychain per spk/txout/outpoint (which is a good
API).

Therefore, we rank keychain variants by `Ord`. Earlier keychain variants
have a higher rank, and the first keychain will be returned.
2024-05-08 15:49:50 +02:00
志宇
6c8748124f chore(chain): move use in indexed_tx_graph.rs so clippy is happy 2024-05-08 15:49:48 +02:00
志宇
537aa03ae0 chore(chain): update test so clippy does not complain 2024-05-08 15:49:47 +02:00
志宇
ed117de7a5 test(chain): applying changesets one-by-one vs aggregate should be same 2024-05-08 15:49:46 +02:00
志宇
6a3fb849e8 fix(chain): simplify Append::append impl for keychain::ChangeSet
We only need to loop though entries of `other`. The logic before was
wasteful because we were also looping though all entries of `self` even
if we do not need to modify the `self` entry.
2024-05-08 15:49:45 +02:00
Daniela Brozzoni
1d294b734d fix: Run tests only if the miniscript feature is..
..enabled, enable it by default
2024-05-08 15:49:44 +02:00
Daniela Brozzoni
0e3e136f6f doc(bdk): Add instructions for manually inserting...
...secret keys in the wallet in Wallet::load
2024-05-08 15:49:43 +02:00
Steve Myers
76afccc555 fix(wallet): add expected descriptors as signers after creating from wallet::ChangeSet 2024-05-08 15:49:42 +02:00
Daniela Brozzoni
4f05441a00 keychain::ChangeSet includes the descriptor
- The KeychainTxOutIndex's internal SpkIterator now uses DescriptorId
  instead of K. The DescriptorId -> K translation is made at the
  KeychainTxOutIndex level.
- The keychain::Changeset is now a struct, which includes a map for last
  revealed indexes, and one for newly added keychains and their
  descriptor.

API changes in bdk:
- Wallet::keychains returns a `impl Iterator` instead of `BTreeMap`
- Wallet::load doesn't take descriptors anymore, since they're stored in
  the db
- Wallet::new_or_load checks if the loaded descriptor from db is the
  same as the provided one

API changes in bdk_chain:
- `ChangeSet` is now a struct, which includes a map for last revealed
  indexes, and one for keychains and descriptors.
- `KeychainTxOutIndex::inner` returns a `SpkIterator<(DescriptorId, u32)>`
- `KeychainTxOutIndex::outpoints` returns a `impl Iterator` instead of `&BTreeSet`
- `KeychainTxOutIndex::keychains` returns a `impl Iterator` instead of
  `&BTreeMap`
- `KeychainTxOutIndex::txouts` doesn't return a ExactSizeIterator
  anymore
- `KeychainTxOutIndex::unbounded_spk_iter` returns an `Option`
- `KeychainTxOutIndex::next_index` returns an `Option`
- `KeychainTxOutIndex::last_revealed_indices` returns a `BTreeMap`
  instead of `&BTreeMap`
- `KeychainTxOutIndex::reveal_to_target` returns an `Option`
- `KeychainTxOutIndex::reveal_next_spk` returns an `Option`
- `KeychainTxOutIndex::next_unused_spk` returns an `Option`
- `KeychainTxOutIndex::add_keychain` has been renamed to
  `KeychainTxOutIndex::insert_descriptor`, and now it returns a
  ChangeSet
2024-05-08 15:49:41 +02:00
Daniela Brozzoni
8ff99f27df ref(chain): Define test descriptors, use them...
...everywhere
2024-05-08 13:23:28 +02:00
志宇
b9902936a0 ref(chain): move keychain::ChangeSet into txout_index.rs
We plan to record `Descriptor` additions into persistence. Hence, we
need to add `Descriptor`s to the changeset. This depends on
`miniscript`. Moving this into `txout_index.rs` makes sense as this is
consistent with all the other files. The only reason why this wasn't
this way before, is because the changeset didn't need miniscript.

Co-Authored-By: Daniela Brozzoni <danielabrozzoni@protonmail.com>
2024-05-08 13:23:27 +02:00
Steve Myers
66abc73c3d Merge bitcoindevkit/bdk#1423: fix(persist): add default feature to enable bdk_chain/std
a577c22b12 fix(persist): add default feature to enable bdk_chain/std (Steve Myers)

Pull request description:

  ### Description

  This PR adds a `default` feature to `bdk_persist` so it can be build on its own.  Once #1422 is done we can remove the `default`again.

  ### Notes to the reviewers

  I need to be able to build `bdk_persist` on its own so I can publish it to crates.io.

  ### 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:
  ValuedMammal:
    ACK a577c22b12
  oleonardolima:
    ACK a577c22b12
  storopoli:
    ACK a577c22b12

Tree-SHA512: 8b07a9e4974dec8812ca19ce7226dcaece064270a0be8b83d3c326fdf1e89b051eb0bd8aa0eda9362b2c8233ecd6003b70c92ee046603973d8d40611418c3841
2024-05-07 19:32:09 -05:00
valued mammal
de2763a4b8 ci: Pin clippy to rust 1.78.0 2024-05-07 09:55:14 -04:00
志宇
dcd2d4741d Merge bitcoindevkit/bdk#1411: feat: update keychain::Balance to use bitcoin::Amount
22aa534d76 feat: use `Amount` on `TxBuilder::add_recipient` (Leonardo Lima)
d5c0e7200c feat: use `Amount` on `spk_txout_index` and related (Leonardo Lima)
8a33d98db9 feat: update `wallet::Balance` to use `bitcoin::Amount` (Leonardo Lima)

Pull request description:

  fixes #823

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

  ### Description

  It's being used on `Balance`, and throughout the code, an `u64` represents the amount, which relies on the user to infer its sats, not millisats, or any other representation.

  It updates the usage of `u64` on `Balance`, and other APIs:
  - `TxParams::add_recipient`
  - `KeyChainTxOutIndex::sent_and_received`, `KeyChainTxOutIndex::net_value`
  -  `SpkTxOutIndex::sent_and_received`, `SpkTxOutIndex::net_value`

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

  It updates some of the APIs to expect the `bitcoin::Amount`, but it does not update internal usage of u64, such as `TxParams` still expects and uses `u64`, please see the PR comments for related discussion.

  ### Changelog notice

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->

  - Changed the `keychain::Balance` struct fields to use `Amount` instead of `u64`.
  - Changed the `add_recipient` method on `TxBuilder` implementation to expect `bitcoin::Amount`.
  - Changed the `sent_and_received`, and `net_value` methods on `KeyChainTxOutIndex` to expect `bitcoin::Amount`.
  - Changed the `sent_and_received`, and `net_value` methods on `SpkTxOutIndex` to expect `bitcoin::Amount`.

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

  * [x] 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:
  evanlinjin:
    ACK 22aa534d76

Tree-SHA512: c4e8198d96c0d66cc3d2e4149e8a56bb7565b9cd49ff42113eaebd24b1d7bfeecd7124db0b06524b78b8891ee1bde1546705b80afad408f48495cf3c02446d02
2024-05-06 20:23:48 +08:00
志宇
23538c4039 Merge bitcoindevkit/bdk#1414: chore: clean up electrsd and anyhow dev dependencies
f6218e4741 chore: reexport crates in `TestEnv` (Wei Chen)
125959976f chore: remove `anyhow` dev dependency from `electrum`, `esplora`, and `bitcoind_rpc` (Wei Chen)

Pull request description:

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

  ### Description

  Reexports `electrsd` in `TestEnv` to remove the `electrsd` dev depedency out of `bdk_electrum` and `bdk_esplora`.
  Credit to @ValuedMammal for the idea.

  Since `bitcoind` reexports `anyhow`, this dev dependency was also removed from `bdk_electrum`, `bdk_esplora`, and `bdk_bitcoind_rpc`. `bitcoind`, `bitcoincore_rpc` and `electrum_client` were also reexported for convenience.

  ### Changelog notice

  * Change `bdk_testenv` to re-export internally used crates.

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

Tree-SHA512: c7645eb91d08d4ccb80982a992f691b5a8c0df39df506f6b361bc6f2bb076d62cbe5bb5d88b4c684c36e22464c0674f21f6ef4e23733f89b03aa12ec43a67cba
2024-05-06 20:12:23 +08:00
志宇
a9f7377934 Merge bitcoindevkit/bdk#1427: docs(esplora): fixed full_scan and sync documentation
f6dc6890c3 docs(esplora): fixed `full_scan` and `sync` documentation (Wei Chen)

Pull request description:

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

  ### Description

  Fixed documentation for `full_scan` and `sync` in `bdk_esplora`.

  ### Changelog notice

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
  * Updated documentation for `full_scan` and `sync` in `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:
  evanlinjin:
    ACK f6dc6890c3
  storopoli:
    ACK f6dc6890c3

Tree-SHA512: 900fb1a2839379af867a6effad32ec4bdfb897330a72ee1e1ec203299e7f3d5fae576550aeed8fd93c5c70a13ad2b0e898033d8b45b490319b5d74216b93f332
2024-05-06 20:09:31 +08:00
Wei Chen
f6dc6890c3 docs(esplora): fixed full_scan and sync documentation 2024-05-06 16:51:19 +08:00
Leonardo Lima
22aa534d76 feat: use Amount on TxBuilder::add_recipient 2024-05-05 12:07:07 -03:00
Leonardo Lima
d5c0e7200c feat: use Amount on spk_txout_index and related
- update `wallet.rs` fns: `sent_and_received` fn
- update `keychain` `txout_index` fn: `sent_and_received and `net_value`
2024-05-05 12:07:01 -03:00
Wei Chen
f6218e4741 chore: reexport crates in TestEnv 2024-05-05 19:28:18 +08:00
Wei Chen
125959976f chore: remove anyhow dev dependency from electrum, esplora, and bitcoind_rpc 2024-05-05 19:28:18 +08:00
Leonardo Lima
8a33d98db9 feat: update wallet::Balance to use bitcoin::Amount
- update all fields `immature`, ` trusted_pending`, `unstrusted_pending`
  and `confirmed` to use the `bitcoin::Amount` instead of `u64`
- update all `impl Balance` methods to use `bitcoin::Amount`
- update all tests that relies on `keychain::Balance`
2024-05-04 21:59:07 -03:00
志宇
2703cc6e78 Merge bitcoindevkit/bdk#1417: test(wallet): add thread safety test
db47347472 test(wallet): add thread safety test (Rob N)

Pull request description:

  ### Description

  `Wallet` auto-implements `Send` and `Sync` after removing the generic. This test is a compile time error if there are changes to `Wallet` in the future that make it unsafe to send between threads. See #1387 for discussion.

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

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

Tree-SHA512: 490e666bc503f15286268db7e5e2f75ee44ad2f80251d6f7a01af2a435023b87607eee33623712433ea8d27511be63c6c1e9cad4159b3fe66a4644cfa9e344fb
2024-05-04 20:24:30 +08:00
Rob N
db47347472 test(wallet): add thread safety test 2024-05-02 08:43:02 -10:00
Steve Myers
a577c22b12 fix(persist): add default feature to enable bdk_chain/std 2024-05-02 13:30:13 -05:00
93 changed files with 4099 additions and 2197 deletions

View File

@@ -58,8 +58,8 @@ jobs:
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
- name: Check bdk wallet
working-directory: ./crates/wallet
# 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
@@ -89,8 +89,8 @@ jobs:
target: "wasm32-unknown-unknown"
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk
working-directory: ./crates/bdk
- name: Check bdk wallet
working-directory: ./crates/wallet
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
@@ -118,7 +118,7 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.78.0
components: clippy
override: true
- name: Rust Cache

View File

@@ -10,15 +10,13 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v2
- name: Set default toolchain
run: rustup default nightly-2022-12-14
run: rustup default nightly-2024-05-12
- 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: Pin dependencies for MSRV
run: cargo update -p home --precise "0.5.5"
- name: Build docs
run: cargo doc --no-deps
env:

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ Cargo.lock
# Example persisted files.
*.db
*.sqlite*

View File

@@ -1,9 +1,10 @@
[workspace]
resolver = "2"
members = [
"crates/bdk",
"crates/wallet",
"crates/chain",
"crates/file_store",
"crates/sqlite",
"crates/electrum",
"crates/esplora",
"crates/bitcoind_rpc",

View File

@@ -10,11 +10,11 @@
</p>
<p>
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.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://docs.rs/bdk_wallet"><img alt="Wallet API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.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>
@@ -22,7 +22,7 @@
<h4>
<a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span>
<a href="https://docs.rs/bdk">Documentation</a>
<a href="https://docs.rs/bdk_wallet">Documentation</a>
</h4>
</div>
@@ -39,7 +39,7 @@ It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
The project is split up into several crates in the `/crates` directory:
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
- [`wallet`](./crates/wallet): 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
- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
@@ -47,10 +47,10 @@ The project is split up into several crates in the `/crates` directory:
- [`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.
Fully working examples of how to use these components are in `/example-crates`:
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk` library.
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk` library.
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
@@ -68,15 +68,9 @@ This library should compile with any combination of features with Rust 1.63.0.
To build with the MSRV you will need to pin dependencies as follows:
```shell
# zip 0.6.3 has MSRV 1.64.0
cargo update -p zip --precise "0.6.2"
# time 0.3.21 has MSRV 1.65.0
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
cargo update -p time --precise "0.3.20"
# jobserver 0.1.27 has MSRV 1.66.0
cargo update -p jobserver --precise "0.1.26"
# home 0.5.9 has MSRV 1.70.0
cargo update -p home --precise "0.5.5"
# proptest 1.4.0 has MSRV 1.65.0
cargo update -p proptest --precise "1.2.0"
```

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_bitcoind_rpc"
version = "0.9.0"
version = "0.11.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -16,11 +16,10 @@ readme = "README.md"
# For no-std, remember to enable the bitcoin/no-std feature
bitcoin = { version = "0.31", default-features = false }
bitcoincore-rpc = { version = "0.18" }
bdk_chain = { path = "../chain", version = "0.13", default-features = false }
bdk_chain = { path = "../chain", version = "0.15", default-features = false }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default_features = false }
anyhow = { version = "1" }
bdk_testenv = { path = "../testenv", default-features = false }
[features]
default = ["std"]

View File

@@ -7,7 +7,7 @@ use bdk_chain::{
local_chain::{CheckPoint, LocalChain},
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
};
use bdk_testenv::TestEnv;
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
use bitcoincore_rpc::RpcApi;
@@ -377,7 +377,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT.to_sat() * ADDITIONAL_COUNT as u64,
confirmed: SEND_AMOUNT * ADDITIONAL_COUNT as u64,
..Balance::default()
},
"initial balance must be correct",
@@ -391,8 +391,8 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT.to_sat() * (ADDITIONAL_COUNT - reorg_count) as u64,
trusted_pending: SEND_AMOUNT.to_sat() * reorg_count as u64,
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
trusted_pending: SEND_AMOUNT * reorg_count as u64,
..Balance::default()
},
"reorg_count: {}",

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_chain"
version = "0.13.0"
version = "0.15.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -26,6 +26,6 @@ rand = "0.8"
proptest = "1.2.0"
[features]
default = ["std"]
std = ["bitcoin/std", "miniscript/std"]
serde = ["serde_crate", "bitcoin/serde"]
default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]

View File

@@ -1,10 +1,29 @@
use crate::miniscript::{Descriptor, DescriptorPublicKey};
use crate::{
alloc::{string::ToString, vec::Vec},
miniscript::{Descriptor, DescriptorPublicKey},
};
use bitcoin::hashes::{hash_newtype, sha256, Hash};
hash_newtype! {
/// Represents the ID of a descriptor, defined as the sha256 hash of
/// the descriptor string, checksum excluded.
///
/// This is useful for having a fixed-length unique representation of a descriptor,
/// in particular, we use it to persist application state changes related to the
/// descriptor without having to re-write the whole descriptor each time.
///
pub struct DescriptorId(pub sha256::Hash);
}
/// 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.
/// Panics if the descriptor wildcard is hardened.
fn dust_value(&self) -> u64;
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
/// included.
fn descriptor_id(&self) -> DescriptorId;
}
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
@@ -15,4 +34,11 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
.dust_value()
.to_sat()
}
fn descriptor_id(&self) -> DescriptorId {
let desc = self.to_string();
let desc_without_checksum = desc.split('#').next().expect("Must be here");
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
}
}

View File

@@ -4,7 +4,6 @@ use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
use crate::{
keychain,
tx_graph::{self, TxGraph},
Anchor, AnchorFromBlockPosition, Append, BlockId,
};
@@ -320,8 +319,9 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
}
}
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
fn from(indexer: keychain::ChangeSet<K>) -> Self {
#[cfg(feature = "miniscript")]
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
Self {
graph: Default::default(),
indexer,

View File

@@ -10,77 +10,12 @@
//!
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
use crate::{collections::BTreeMap, Append};
#[cfg(feature = "miniscript")]
mod txout_index;
use bitcoin::Amount;
#[cfg(feature = "miniscript")]
pub use txout_index::*;
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
/// It maps each keychain `K` to its last revealed index.
///
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet`]s are
/// monotone in that they will never decrease the revealed derivation index.
///
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
#[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 ChangeSet<K>(pub BTreeMap<K, u32>);
impl<K> ChangeSet<K> {
/// 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 ChangeSet<K> {
/// Append another [`ChangeSet`] 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);
}
});
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
self.0.extend(other.0);
}
/// Returns whether the changeset are empty.
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<K> Default for ChangeSet<K> {
fn default() -> Self {
Self(Default::default())
}
}
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
fn as_ref(&self) -> &BTreeMap<K, u32> {
&self.0
}
}
/// Balance, differentiated into various categories.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(
@@ -90,13 +25,13 @@ impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
)]
pub struct Balance {
/// All coinbase outputs not yet matured
pub immature: u64,
pub immature: Amount,
/// Unconfirmed UTXOs generated by a wallet tx
pub trusted_pending: u64,
pub trusted_pending: Amount,
/// Unconfirmed UTXOs received from an external wallet
pub untrusted_pending: u64,
pub untrusted_pending: Amount,
/// Confirmed and immediately spendable balance
pub confirmed: u64,
pub confirmed: Amount,
}
impl Balance {
@@ -104,12 +39,12 @@ impl Balance {
///
/// 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 {
pub fn trusted_spendable(&self) -> Amount {
self.confirmed + self.trusted_pending
}
/// Get the whole balance visible to the wallet.
pub fn total(&self) -> u64 {
pub fn total(&self) -> Amount {
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
}
}
@@ -136,40 +71,3 @@ impl core::ops::Add for Balance {
}
}
}
#[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 = ChangeSet(lhs_di);
let rhs = ChangeSet(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

@@ -3,9 +3,9 @@ use crate::{
indexed_tx_graph::Indexer,
miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::BIP32_MAX_INDEX,
SpkIterator, SpkTxOutIndex,
DescriptorExt, DescriptorId, SpkIterator, SpkTxOutIndex,
};
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
use bitcoin::{hashes::Hash, Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
use core::{
fmt::Debug,
ops::{Bound, RangeBounds},
@@ -13,6 +13,79 @@ use core::{
use crate::Append;
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
/// It maps each keychain `K` to a descriptor and its last revealed index.
///
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet] are
/// monotone in that they will never decrease the revealed derivation index.
///
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
#[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 ChangeSet<K> {
/// Contains the keychains that have been added and their respective descriptor
pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
/// Contains for each descriptor_id the last revealed index of derivation
pub last_revealed: BTreeMap<DescriptorId, u32>,
}
impl<K: Ord> Append for ChangeSet<K> {
/// Append another [`ChangeSet`] into self.
///
/// For each keychain in `keychains_added` in the given [`ChangeSet`]:
/// If the keychain already exist with a different descriptor, we overwrite the old descriptor.
///
/// For each `last_revealed` in the given [`ChangeSet`]:
/// If the keychain already exists, increase the index when the other's index > self's index.
fn append(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
self.keychains_added.extend(other.keychains_added);
// for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than
// what was originally in `self`.
for (desc_id, index) in other.last_revealed {
use crate::collections::btree_map::Entry;
match self.last_revealed.entry(desc_id) {
Entry::Vacant(entry) => {
entry.insert(index);
}
Entry::Occupied(mut entry) => {
if *entry.get() < index {
entry.insert(index);
}
}
}
}
}
/// Returns whether the changeset are empty.
fn is_empty(&self) -> bool {
self.last_revealed.is_empty() && self.keychains_added.is_empty()
}
}
impl<K> Default for ChangeSet<K> {
fn default() -> Self {
Self {
last_revealed: BTreeMap::default(),
keychains_added: BTreeMap::default(),
}
}
}
const DEFAULT_LOOKAHEAD: u32 = 25;
/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and
@@ -54,7 +127,7 @@ const DEFAULT_LOOKAHEAD: u32 = 25;
///
/// # Change sets
///
/// Methods that can update the last revealed index will return [`super::ChangeSet`] to report
/// Methods that can update the last revealed index or add keychains will return [`super::ChangeSet`] to report
/// these changes. This can be persisted for future recovery.
///
/// ## Synopsis
@@ -79,14 +152,43 @@ const DEFAULT_LOOKAHEAD: u32 = 25;
/// # 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, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap();
/// 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 (descriptor_42, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap();
/// let _ = txout_index.insert_descriptor(MyKeychain::External, external_descriptor);
/// let _ = txout_index.insert_descriptor(MyKeychain::Internal, internal_descriptor);
/// let _ = txout_index.insert_descriptor(MyKeychain::MyAppUser { user_id: 42 }, descriptor_42);
///
/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 });
/// ```
///
/// # Non-recommend keychain to descriptor assignments
///
/// A keychain (`K`) is used to identify a descriptor. However, the following keychain to descriptor
/// arrangements result in behavior that is harder to reason about and is not recommended.
///
/// ## Multiple keychains identifying the same descriptor
///
/// Although a single keychain variant can only identify a single descriptor, multiple keychain
/// variants can identify the same descriptor.
///
/// If multiple keychains identify the same descriptor:
/// 1. Methods that take in a keychain (such as [`reveal_next_spk`]) will work normally when any
/// keychain (that identifies that descriptor) is passed in.
/// 2. Methods that return data which associates with a descriptor (such as [`outpoints`],
/// [`txouts`], [`unused_spks`], etc.) the method will return the highest-ranked keychain variant
/// that identifies the descriptor. Rank is determined by the [`Ord`] implementation of the keychain
/// type.
///
/// This arrangement is not recommended since some methods will return a single keychain variant
/// even though multiple keychain variants identify the same descriptor.
///
/// ## Reassigning the descriptor of a single keychain
///
/// Descriptors added to [`KeychainTxOutIndex`] are never removed. However, a keychain that
/// identifies a descriptor can be reassigned to identify a different descriptor. This may result in
/// a situation where a descriptor has no associated keychain(s), and relevant [`TxOut`]s,
/// [`OutPoint`]s and [`Script`]s (of that descriptor) will not be return by [`KeychainTxOutIndex`].
/// Therefore, reassigning the descriptor of a single keychain is not recommended.
///
/// [`Ord`]: core::cmp::Ord
/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex
/// [`Descriptor`]: crate::miniscript::Descriptor
@@ -99,13 +201,27 @@ const DEFAULT_LOOKAHEAD: u32 = 25;
/// [`new`]: KeychainTxOutIndex::new
/// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter
/// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters
/// [`outpoints`]: KeychainTxOutIndex::outpoints
/// [`txouts`]: KeychainTxOutIndex::txouts
/// [`unused_spks`]: KeychainTxOutIndex::unused_spks
#[derive(Clone, Debug)]
pub struct KeychainTxOutIndex<K> {
inner: SpkTxOutIndex<(K, u32)>,
// descriptors of each keychain
keychains: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
inner: SpkTxOutIndex<(DescriptorId, u32)>,
// keychain -> (descriptor, descriptor id) map
keychains_to_descriptors: BTreeMap<K, (DescriptorId, Descriptor<DescriptorPublicKey>)>,
// descriptor id -> keychain set
// Because different keychains can have the same descriptor, we rank keychains by `Ord` so that
// that the first keychain variant (according to `Ord`) has the highest rank. When associated
// data (such as spks, outpoints) are returned with a keychain, we return the highest-ranked
// keychain with it.
descriptor_ids_to_keychain_set: HashMap<DescriptorId, BTreeSet<K>>,
// descriptor_id -> descriptor map
// This is a "monotone" map, meaning that its size keeps growing, i.e., we never delete
// descriptors from it. This is useful for revealing spks for descriptors that don't have
// keychains associated.
descriptor_ids_to_descriptors: BTreeMap<DescriptorId, Descriptor<DescriptorPublicKey>>,
// last revealed indexes
last_revealed: BTreeMap<K, u32>,
last_revealed: BTreeMap<DescriptorId, u32>,
// lookahead settings for each keychain
lookahead: u32,
}
@@ -121,7 +237,13 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
match self.inner.scan_txout(outpoint, txout).cloned() {
Some((keychain, index)) => self.reveal_to_target(&keychain, index).1,
Some((descriptor_id, index)) => {
// We want to reveal spks for descriptors that aren't tracked by any keychain, and
// so we call reveal with descriptor_id
let (_, changeset) = self.reveal_to_target_with_id(descriptor_id, index)
.expect("descriptors are added in a monotone manner, there cannot be a descriptor id with no corresponding descriptor");
changeset
}
None => super::ChangeSet::default(),
}
}
@@ -135,7 +257,13 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
}
fn initial_changeset(&self) -> Self::ChangeSet {
super::ChangeSet(self.last_revealed.clone())
super::ChangeSet {
keychains_added: self
.keychains()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
last_revealed: self.last_revealed.clone(),
}
}
fn apply_changeset(&mut self, changeset: Self::ChangeSet) {
@@ -161,7 +289,9 @@ impl<K> KeychainTxOutIndex<K> {
pub fn new(lookahead: u32) -> Self {
Self {
inner: SpkTxOutIndex::default(),
keychains: BTreeMap::new(),
keychains_to_descriptors: BTreeMap::new(),
descriptor_ids_to_keychain_set: HashMap::new(),
descriptor_ids_to_descriptors: BTreeMap::new(),
last_revealed: BTreeMap::new(),
lookahead,
}
@@ -170,26 +300,37 @@ impl<K> KeychainTxOutIndex<K> {
/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Get the highest-ranked keychain that is currently associated with the given `desc_id`.
fn keychain_of_desc_id(&self, desc_id: &DescriptorId) -> Option<&K> {
let keychains = self.descriptor_ids_to_keychain_set.get(desc_id)?;
keychains.iter().next()
}
/// Return a reference to the internal [`SpkTxOutIndex`].
///
/// **WARNING:** The internal index will contain lookahead spks. Refer to
/// [struct-level docs](KeychainTxOutIndex) for more about `lookahead`.
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
pub fn inner(&self) -> &SpkTxOutIndex<(DescriptorId, u32)> {
&self.inner
}
/// Get a reference to the set of indexed outpoints.
pub fn outpoints(&self) -> &BTreeSet<((K, u32), OutPoint)> {
self.inner.outpoints()
/// Get the set of indexed outpoints, corresponding to tracked keychains.
pub fn outpoints(&self) -> impl DoubleEndedIterator<Item = ((K, u32), OutPoint)> + '_ {
self.inner
.outpoints()
.iter()
.filter_map(|((desc_id, index), op)| {
let keychain = self.keychain_of_desc_id(desc_id)?;
Some(((keychain.clone(), *index), *op))
})
}
/// Iterate over known txouts that spend to tracked script pubkeys.
pub fn txouts(
&self,
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> + ExactSizeIterator {
self.inner
.txouts()
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
pub fn txouts(&self) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> + '_ {
self.inner.txouts().filter_map(|((desc_id, i), op, txo)| {
let keychain = self.keychain_of_desc_id(desc_id)?;
Some((keychain.clone(), *i, op, txo))
})
}
/// Finds all txouts on a transaction that has previously been scanned and indexed.
@@ -199,32 +340,39 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> {
self.inner
.txouts_in_tx(txid)
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
.filter_map(|((desc_id, i), op, txo)| {
let keychain = self.keychain_of_desc_id(desc_id)?;
Some((keychain.clone(), *i, op, txo))
})
}
/// Return the [`TxOut`] of `outpoint` if it has been indexed.
/// Return the [`TxOut`] of `outpoint` if it has been indexed, and if it corresponds to a
/// tracked keychain.
///
/// The associated keychain and keychain index of the txout's spk is also returned.
///
/// This calls [`SpkTxOutIndex::txout`] internally.
pub fn txout(&self, outpoint: OutPoint) -> Option<(K, u32, &TxOut)> {
self.inner
.txout(outpoint)
.map(|((k, i), txo)| (k.clone(), *i, txo))
let ((descriptor_id, index), txo) = self.inner.txout(outpoint)?;
let keychain = self.keychain_of_desc_id(descriptor_id)?;
Some((keychain.clone(), *index, txo))
}
/// Return the script that exists under the given `keychain`'s `index`.
///
/// This calls [`SpkTxOutIndex::spk_at_index`] internally.
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> {
self.inner.spk_at_index(&(keychain, index))
let descriptor_id = self.keychains_to_descriptors.get(&keychain)?.0;
self.inner.spk_at_index(&(descriptor_id, index))
}
/// Returns the keychain and keychain index associated with the spk.
///
/// This calls [`SpkTxOutIndex::index_of_spk`] internally.
pub fn index_of_spk(&self, script: &Script) -> Option<(K, u32)> {
self.inner.index_of_spk(script).cloned()
let (desc_id, last_index) = self.inner.index_of_spk(script)?;
let keychain = self.keychain_of_desc_id(desc_id)?;
Some((keychain.clone(), *last_index))
}
/// Returns whether the spk under the `keychain`'s `index` has been used.
@@ -234,7 +382,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// This calls [`SpkTxOutIndex::is_used`] internally.
pub fn is_used(&self, keychain: K, index: u32) -> bool {
self.inner.is_used(&(keychain, index))
let descriptor_id = self.keychains_to_descriptors.get(&keychain).map(|k| k.0);
match descriptor_id {
Some(descriptor_id) => self.inner.is_used(&(descriptor_id, index)),
None => false,
}
}
/// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output
@@ -242,7 +394,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// 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`.
/// Returns whether the spk under the given `keychain` and `index` is successfully
/// marked as used. Returns false either when there is no descriptor under the given
/// keychain, or when the spk is already marked as used.
///
/// 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
@@ -252,7 +406,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// [`unmark_used`]: Self::unmark_used
pub fn mark_used(&mut self, keychain: K, index: u32) -> bool {
self.inner.mark_used(&(keychain, index))
let descriptor_id = self.keychains_to_descriptors.get(&keychain).map(|k| k.0);
match descriptor_id {
Some(descriptor_id) => self.inner.mark_used(&(descriptor_id, index)),
None => false,
}
}
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
@@ -265,7 +423,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// [`mark_used`]: Self::mark_used
pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool {
self.inner.unmark_used(&(keychain, index))
let descriptor_id = self.keychains_to_descriptors.get(&keychain).map(|k| k.0);
match descriptor_id {
Some(descriptor_id) => self.inner.unmark_used(&(descriptor_id, index)),
None => false,
}
}
/// Computes the total value transfer effect `tx` has on the script pubkeys belonging to the
@@ -273,9 +435,13 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// *received* when it is on an output. For `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, range: impl RangeBounds<K>) -> (u64, u64) {
pub fn sent_and_received(
&self,
tx: &Transaction,
range: impl RangeBounds<K>,
) -> (Amount, Amount) {
self.inner
.sent_and_received(tx, Self::map_to_inner_bounds(range))
.sent_and_received(tx, self.map_to_inner_bounds(range))
}
/// Computes the net value that this transaction gives to the script pubkeys in the index and
@@ -285,35 +451,77 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// This calls [`SpkTxOutIndex::net_value`] internally.
///
/// [`sent_and_received`]: Self::sent_and_received
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<K>) -> i64 {
self.inner.net_value(tx, Self::map_to_inner_bounds(range))
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<K>) -> SignedAmount {
self.inner.net_value(tx, self.map_to_inner_bounds(range))
}
}
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Return a reference to the internal map of keychain to descriptors.
pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
&self.keychains
/// Return the map of the keychain to descriptors.
pub fn keychains(
&self,
) -> impl DoubleEndedIterator<Item = (&K, &Descriptor<DescriptorPublicKey>)> + ExactSizeIterator + '_
{
self.keychains_to_descriptors
.iter()
.map(|(k, (_, d))| (k, d))
}
/// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses.
/// Insert a descriptor with a keychain associated to it.
///
/// Adding a keychain means you will be able to derive new script pubkeys under that keychain
/// Adding a descriptor means you will be able to derive new script pubkeys under it
/// 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.clone())
.or_insert_with(|| descriptor.clone());
assert_eq!(
&descriptor, old_descriptor,
"keychain already contains a different descriptor"
);
/// When trying to add a keychain that already existed under a different descriptor, or a descriptor
/// that already existed with a different keychain, the old keychain (or descriptor) will be
/// overwritten.
pub fn insert_descriptor(
&mut self,
keychain: K,
descriptor: Descriptor<DescriptorPublicKey>,
) -> super::ChangeSet<K> {
let mut changeset = super::ChangeSet::<K>::default();
let desc_id = descriptor.descriptor_id();
let old_desc = self
.keychains_to_descriptors
.insert(keychain.clone(), (desc_id, descriptor.clone()));
if let Some((old_desc_id, _)) = old_desc {
// nothing needs to be done if caller reinsterted the same descriptor under the same
// keychain
if old_desc_id == desc_id {
return changeset;
}
// we should remove old descriptor that is associated with this keychain as the index
// is designed to track one descriptor per keychain (however different keychains can
// share the same descriptor)
let _is_keychain_removed = self
.descriptor_ids_to_keychain_set
.get_mut(&old_desc_id)
.expect("we must have already inserted this descriptor")
.remove(&keychain);
debug_assert!(_is_keychain_removed);
}
self.descriptor_ids_to_keychain_set
.entry(desc_id)
.or_default()
.insert(keychain.clone());
self.descriptor_ids_to_descriptors
.insert(desc_id, descriptor.clone());
self.replenish_lookahead(&keychain, self.lookahead);
changeset
.keychains_added
.insert(keychain.clone(), descriptor);
changeset
}
/// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't
/// have a descriptor associated with it.
pub fn get_descriptor(&self, keychain: &K) -> Option<&Descriptor<DescriptorPublicKey>> {
self.keychains_to_descriptors.get(keychain).map(|(_, d)| d)
}
/// Get the lookahead setting.
@@ -329,63 +537,60 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// This does not change the global `lookahead` setting.
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
let (next_index, _) = self.next_index(keychain);
if let Some((next_index, _)) = self.next_index(keychain) {
let temp_lookahead = (target_index + 1)
.checked_sub(next_index)
.filter(|&index| index > 0);
let temp_lookahead = (target_index + 1)
.checked_sub(next_index)
.filter(|&index| index > 0);
if let Some(temp_lookahead) = temp_lookahead {
self.replenish_lookahead(keychain, temp_lookahead);
if let Some(temp_lookahead) = temp_lookahead {
self.replenish_lookahead(keychain, temp_lookahead);
}
}
}
fn replenish_lookahead(&mut self, keychain: &K, lookahead: u32) {
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 descriptor_opt = self.keychains_to_descriptors.get(keychain).cloned();
if let Some((descriptor_id, descriptor)) = descriptor_opt {
let next_store_index = self.next_store_index(descriptor_id);
let next_reveal_index = self.last_revealed.get(&descriptor_id).map_or(0, |v| *v + 1);
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);
for (new_index, new_spk) in SpkIterator::new_with_range(
descriptor,
next_store_index..next_reveal_index + lookahead,
) {
let _inserted = self.inner.insert_spk((descriptor_id, 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 {
fn next_store_index(&self, descriptor_id: DescriptorId) -> u32 {
self.inner()
.all_spks()
// This range is filtering out the spks with a keychain different than
// `keychain`. We don't use filter here as range is more optimized.
.range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
// This range is keeping only the spks with descriptor_id equal to
// `descriptor_id`. We don't use filter here as range is more optimized.
.range((descriptor_id, u32::MIN)..(descriptor_id, u32::MAX))
.last()
.map_or(0, |((_, index), _)| *index + 1)
}
/// Get an unbounded spk iterator over a given `keychain`.
///
/// # Panics
///
/// This will panic if the given `keychain`'s descriptor does not exist.
pub fn unbounded_spk_iter(&self, keychain: &K) -> SpkIterator<Descriptor<DescriptorPublicKey>> {
SpkIterator::new(
self.keychains
.get(keychain)
.expect("keychain does not exist")
.clone(),
)
/// Get an unbounded spk iterator over a given `keychain`. Returns `None` if the provided
/// keychain doesn't exist
pub fn unbounded_spk_iter(
&self,
keychain: &K,
) -> Option<SpkIterator<Descriptor<DescriptorPublicKey>>> {
let descriptor = self.keychains_to_descriptors.get(keychain)?.1.clone();
Some(SpkIterator::new(descriptor))
}
/// Get unbounded spk iterators for all keychains.
pub fn all_unbounded_spk_iters(
&self,
) -> BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> {
self.keychains
self.keychains_to_descriptors
.iter()
.map(|(k, descriptor)| (k.clone(), SpkIterator::new(descriptor.clone())))
.map(|(k, (_, descriptor))| (k.clone(), SpkIterator::new(descriptor.clone())))
.collect()
}
@@ -394,18 +599,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
&self,
range: impl RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = (&K, u32, &Script)> + Clone {
self.keychains.range(range).flat_map(|(keychain, _)| {
let start = Bound::Included((keychain.clone(), u32::MIN));
let end = match self.last_revealed.get(keychain) {
Some(last_revealed) => Bound::Included((keychain.clone(), *last_revealed)),
None => Bound::Excluded((keychain.clone(), u32::MIN)),
};
self.keychains_to_descriptors
.range(range)
.flat_map(|(_, (descriptor_id, _))| {
let start = Bound::Included((*descriptor_id, u32::MIN));
let end = match self.last_revealed.get(descriptor_id) {
Some(last_revealed) => Bound::Included((*descriptor_id, *last_revealed)),
None => Bound::Excluded((*descriptor_id, u32::MIN)),
};
self.inner
.all_spks()
.range((start, end))
.map(|((keychain, i), spk)| (keychain, *i, spk.as_script()))
})
self.inner
.all_spks()
.range((start, end))
.map(|((descriptor_id, i), spk)| {
(
self.keychain_of_desc_id(descriptor_id)
.expect("must have keychain"),
*i,
spk.as_script(),
)
})
})
}
/// Iterate over revealed spks of the given `keychain`.
@@ -419,20 +633,29 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Iterate over revealed, but unused, spks of all keychains.
pub fn unused_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
self.keychains.keys().flat_map(|keychain| {
self.keychains_to_descriptors.keys().flat_map(|keychain| {
self.unused_keychain_spks(keychain)
.map(|(i, spk)| (keychain.clone(), i, spk))
})
}
/// Iterate over revealed, but unused, spks of the given `keychain`.
/// Returns an empty iterator if the provided keychain doesn't exist.
pub fn unused_keychain_spks(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
let desc_id = self
.keychains_to_descriptors
.get(keychain)
.map(|(desc_id, _)| *desc_id)
// We use a dummy desc id if we can't find the real one in our map. In this way,
// if this method was to be called with a non-existent keychain, we would return an
// empty iterator
.unwrap_or_else(|| DescriptorId::from_byte_array([0; 32]));
let next_i = self.last_revealed.get(&desc_id).map_or(0, |&i| i + 1);
self.inner
.unused_spks((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
.unused_spks((desc_id, u32::MIN)..(desc_id, next_i))
.map(|((_, i), spk)| (*i, spk))
}
@@ -447,17 +670,15 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// 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();
/// Returns None if the provided `keychain` doesn't exist.
pub fn next_index(&self, keychain: &K) -> Option<(u32, bool)> {
let (descriptor_id, descriptor) = self.keychains_to_descriptors.get(keychain)?;
let last_index = self.last_revealed.get(descriptor_id).cloned();
// we can only get the next index if the wildcard exists.
let has_wildcard = descriptor.has_wildcard();
match last_index {
Some(match last_index {
// if there is no index, next_index is always 0.
None => (0, true),
// descriptors without wildcards can only have one index.
@@ -469,19 +690,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
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
pub fn last_revealed_indices(&self) -> BTreeMap<K, u32> {
self.last_revealed
.iter()
.filter_map(|(desc_id, index)| {
let keychain = self.keychain_of_desc_id(desc_id)?;
Some((keychain.clone(), *index))
})
.collect()
}
/// Get the last derivation index revealed for `keychain`.
/// Get the last derivation index revealed for `keychain`. Returns None if the keychain doesn't
/// exist, or if the keychain doesn't have any revealed scripts.
pub fn last_revealed_index(&self, keychain: &K) -> Option<u32> {
self.last_revealed.get(keychain).cloned()
let descriptor_id = self.keychains_to_descriptors.get(keychain)?.0;
self.last_revealed.get(&descriptor_id).cloned()
}
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
@@ -496,16 +725,77 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
let mut spks = BTreeMap::new();
for (keychain, &index) in keychains {
let (new_spks, new_changeset) = self.reveal_to_target(keychain, index);
if !new_changeset.is_empty() {
spks.insert(keychain.clone(), new_spks);
changeset.append(new_changeset.clone());
if let Some((new_spks, new_changeset)) = self.reveal_to_target(keychain, index) {
if !new_changeset.is_empty() {
spks.insert(keychain.clone(), new_spks);
changeset.append(new_changeset.clone());
}
}
}
(spks, changeset)
}
/// Convenience method to call `reveal_to_target` with a descriptor_id instead of a keychain.
/// This is useful for revealing spks of descriptors for which we don't have a keychain
/// tracked.
/// Refer to the `reveal_to_target` documentation for more.
///
/// Returns None if the provided `descriptor_id` doesn't correspond to a tracked descriptor.
fn reveal_to_target_with_id(
&mut self,
descriptor_id: DescriptorId,
target_index: u32,
) -> Option<(
SpkIterator<Descriptor<DescriptorPublicKey>>,
super::ChangeSet<K>,
)> {
let descriptor = self
.descriptor_ids_to_descriptors
.get(&descriptor_id)?
.clone();
let has_wildcard = descriptor.has_wildcard();
let target_index = if has_wildcard { target_index } else { 0 };
let next_reveal_index = self
.last_revealed
.get(&descriptor_id)
.map_or(0, |index| *index + 1);
debug_assert!(next_reveal_index + self.lookahead >= self.next_store_index(descriptor_id));
// If the target_index is already revealed, we are done
if next_reveal_index > target_index {
return Some((
SpkIterator::new_with_range(descriptor, next_reveal_index..next_reveal_index),
super::ChangeSet::default(),
));
}
// We range over the indexes that are not stored and insert their spks in the index.
// Indexes from next_reveal_index to next_reveal_index + lookahead are already stored (due
// to lookahead), so we only range from next_reveal_index + lookahead to target + lookahead
let range = next_reveal_index + self.lookahead..=target_index + self.lookahead;
for (new_index, new_spk) in SpkIterator::new_with_range(descriptor.clone(), range) {
let _inserted = self.inner.insert_spk((descriptor_id, new_index), new_spk);
debug_assert!(_inserted, "must not have existing spk");
debug_assert!(
has_wildcard || new_index == 0,
"non-wildcard descriptors must not iterate past index 0"
);
}
let _old_index = self.last_revealed.insert(descriptor_id, target_index);
debug_assert!(_old_index < Some(target_index));
Some((
SpkIterator::new_with_range(descriptor, next_reveal_index..target_index + 1),
super::ChangeSet {
keychains_added: BTreeMap::new(),
last_revealed: core::iter::once((descriptor_id, target_index)).collect(),
},
))
}
/// Reveals script pubkeys of the `keychain`'s descriptor **up to and including** the
/// `target_index`.
///
@@ -517,84 +807,46 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// [`super::ChangeSet`], 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.
/// Returns None if the provided `keychain` doesn't exist.
pub fn reveal_to_target(
&mut self,
keychain: &K,
target_index: u32,
) -> (
) -> Option<(
SpkIterator<Descriptor<DescriptorPublicKey>>,
super::ChangeSet<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, |index| *index + 1);
debug_assert!(next_reveal_index + self.lookahead >= self.next_store_index(keychain));
// If the target_index is already revealed, we are done
if next_reveal_index > target_index {
return (
SpkIterator::new_with_range(
descriptor.clone(),
next_reveal_index..next_reveal_index,
),
super::ChangeSet::default(),
);
}
// We range over the indexes that are not stored and insert their spks in the index.
// Indexes from next_reveal_index to next_reveal_index + lookahead are already stored (due
// to lookahead), so we only range from next_reveal_index + lookahead to target + lookahead
let range = next_reveal_index + self.lookahead..=target_index + self.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");
debug_assert!(
has_wildcard || new_index == 0,
"non-wildcard descriptors must not iterate past index 0"
);
}
let _old_index = self.last_revealed.insert(keychain.clone(), target_index);
debug_assert!(_old_index < Some(target_index));
(
SpkIterator::new_with_range(descriptor.clone(), next_reveal_index..target_index + 1),
super::ChangeSet(core::iter::once((keychain.clone(), target_index)).collect()),
)
)> {
let descriptor_id = self.keychains_to_descriptors.get(keychain)?.0;
self.reveal_to_target_with_id(descriptor_id, target_index)
}
/// Attempts to reveal the next script pubkey for `keychain`.
///
/// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a
/// [`super::ChangeSet`] which represents changes in the last revealed index (if any).
/// Returns None if the provided keychain doesn't exist.
///
/// When a new script cannot be revealed, we return the last revealed script and an empty
/// [`super::ChangeSet`]. 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), super::ChangeSet<K>) {
let (next_index, _) = self.next_index(keychain);
let changeset = self.reveal_to_target(keychain, next_index).1;
/// 3. There is no descriptor associated with the given keychain.
pub fn reveal_next_spk(
&mut self,
keychain: &K,
) -> Option<((u32, &Script), super::ChangeSet<K>)> {
let descriptor_id = self.keychains_to_descriptors.get(keychain)?.0;
let (next_index, _) = self.next_index(keychain).expect("We know keychain exists");
let changeset = self
.reveal_to_target(keychain, next_index)
.expect("We know keychain exists")
.1;
let script = self
.inner
.spk_at_index(&(keychain.clone(), next_index))
.spk_at_index(&(descriptor_id, next_index))
.expect("script must already be stored");
((next_index, script), changeset)
Some(((next_index, script), changeset))
}
/// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest
@@ -606,21 +858,22 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 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), super::ChangeSet<K>) {
/// Returns None if the provided keychain doesn't exist.
pub fn next_unused_spk(
&mut self,
keychain: &K,
) -> Option<((u32, &Script), super::ChangeSet<K>)> {
let need_new = self.unused_keychain_spks(keychain).next().is_none();
// this rather strange branch is needed because of some lifetime issues
if need_new {
self.reveal_next_spk(keychain)
} else {
(
Some((
self.unused_keychain_spks(keychain)
.next()
.expect("we already know next exists"),
super::ChangeSet::default(),
)
))
}
}
@@ -639,21 +892,35 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
&'a self,
range: impl RangeBounds<K> + 'a,
) -> impl DoubleEndedIterator<Item = (&'a K, u32, OutPoint)> + 'a {
let bounds = Self::map_to_inner_bounds(range);
let bounds = self.map_to_inner_bounds(range);
self.inner
.outputs_in_range(bounds)
.map(move |((keychain, i), op)| (keychain, *i, op))
.map(move |((desc_id, i), op)| {
let keychain = self
.keychain_of_desc_id(desc_id)
.expect("keychain must exist");
(keychain, *i, op)
})
}
fn map_to_inner_bounds(bound: impl RangeBounds<K>) -> impl RangeBounds<(K, u32)> {
fn map_to_inner_bounds(
&self,
bound: impl RangeBounds<K>,
) -> impl RangeBounds<(DescriptorId, u32)> {
let get_desc_id = |keychain| {
self.keychains_to_descriptors
.get(keychain)
.map(|(desc_id, _)| *desc_id)
.unwrap_or_else(|| DescriptorId::from_byte_array([0; 32]))
};
let start = match bound.start_bound() {
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MIN)),
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MAX)),
Bound::Included(keychain) => Bound::Included((get_desc_id(keychain), u32::MIN)),
Bound::Excluded(keychain) => Bound::Excluded((get_desc_id(keychain), u32::MAX)),
Bound::Unbounded => Bound::Unbounded,
};
let end = match bound.end_bound() {
Bound::Included(keychain) => Bound::Included((keychain.clone(), u32::MAX)),
Bound::Excluded(keychain) => Bound::Excluded((keychain.clone(), u32::MIN)),
Bound::Included(keychain) => Bound::Included((get_desc_id(keychain), u32::MAX)),
Bound::Excluded(keychain) => Bound::Excluded((get_desc_id(keychain), u32::MIN)),
Bound::Unbounded => Bound::Unbounded,
};
@@ -669,7 +936,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 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
self.keychains_to_descriptors
.iter()
.filter_map(|(keychain, _)| {
self.last_used_index(keychain)
@@ -678,9 +945,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
.collect()
}
/// Applies the derivation changeset to the [`KeychainTxOutIndex`], extending the number of
/// derived scripts per keychain, as specified in the `changeset`.
/// Applies the derivation changeset to the [`KeychainTxOutIndex`], as specified in the
/// [`ChangeSet::append`] documentation:
/// - Extends the number of derived scripts per keychain
/// - Adds new descriptors introduced
/// - If a descriptor is introduced for a keychain that already had a descriptor, overwrites
/// the old descriptor
pub fn apply_changeset(&mut self, changeset: super::ChangeSet<K>) {
let _ = self.reveal_to_target_multi(&changeset.0);
let ChangeSet {
keychains_added,
last_revealed,
} = changeset;
for (keychain, descriptor) in keychains_added {
let _ = self.insert_descriptor(keychain, descriptor);
}
let last_revealed = last_revealed
.into_iter()
.filter_map(|(desc_id, index)| {
let keychain = self.keychain_of_desc_id(&desc_id)?;
Some((keychain.clone(), index))
})
.collect();
let _ = self.reveal_to_target_multi(&last_revealed);
}
}

View File

@@ -44,7 +44,7 @@ pub use miniscript;
#[cfg(feature = "miniscript")]
mod descriptor_ext;
#[cfg(feature = "miniscript")]
pub use descriptor_ext::DescriptorExt;
pub use descriptor_ext::{DescriptorExt, DescriptorId};
#[cfg(feature = "miniscript")]
mod spk_iter;
#[cfg(feature = "miniscript")]
@@ -58,9 +58,6 @@ 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;

View File

@@ -1,11 +1,18 @@
//! Helper types for spk-based blockchain clients.
use crate::{
collections::{BTreeMap, HashMap},
local_chain::CheckPoint,
ConfirmationTimeHeightAnchor, TxGraph,
};
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, Txid};
use core::{fmt::Debug, marker::PhantomData, ops::RangeBounds};
use alloc::{boxed::Box, collections::BTreeMap, vec::Vec};
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
use crate::{local_chain::CheckPoint, ConfirmationTimeHeightAnchor, TxGraph};
/// A cache of [`Arc`]-wrapped full transactions, identified by their [`Txid`]s.
///
/// This is used by the chain-source to avoid re-fetching full transactions.
pub type TxCache = HashMap<Txid, Arc<Transaction>>;
/// Data required to perform a spk-based blockchain client sync.
///
@@ -17,6 +24,8 @@ pub struct SyncRequest {
///
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
pub chain_tip: CheckPoint,
/// Cache of full transactions, so the chain-source can avoid re-fetching.
pub tx_cache: TxCache,
/// Transactions that spend from or to these indexed script pubkeys.
pub spks: Box<dyn ExactSizeIterator<Item = ScriptBuf> + Send>,
/// Transactions with these txids.
@@ -30,12 +39,36 @@ impl SyncRequest {
pub fn from_chain_tip(cp: CheckPoint) -> Self {
Self {
chain_tip: cp,
tx_cache: TxCache::new(),
spks: Box::new(core::iter::empty()),
txids: Box::new(core::iter::empty()),
outpoints: Box::new(core::iter::empty()),
}
}
/// Add to the [`TxCache`] held by the request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn cache_txs<T>(mut self, full_txs: impl IntoIterator<Item = (Txid, T)>) -> Self
where
T: Into<Arc<Transaction>>,
{
self.tx_cache = full_txs
.into_iter()
.map(|(txid, tx)| (txid, tx.into()))
.collect();
self
}
/// Add all transactions from [`TxGraph`] into the [`TxCache`].
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn cache_graph_txs<A>(self, graph: &TxGraph<A>) -> Self {
self.cache_txs(graph.full_txs().map(|tx_node| (tx_node.txid, tx_node.tx)))
}
/// Set the [`Script`]s that will be synced against.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
@@ -194,6 +227,8 @@ pub struct FullScanRequest<K> {
///
/// [`LocalChain::tip`]: crate::local_chain::LocalChain::tip
pub chain_tip: CheckPoint,
/// Cache of full transactions, so the chain-source can avoid re-fetching.
pub tx_cache: TxCache,
/// Iterators of script pubkeys indexed by the keychain index.
pub spks_by_keychain: BTreeMap<K, Box<dyn Iterator<Item = (u32, ScriptBuf)> + Send>>,
}
@@ -204,10 +239,34 @@ impl<K: Ord + Clone> FullScanRequest<K> {
pub fn from_chain_tip(chain_tip: CheckPoint) -> Self {
Self {
chain_tip,
tx_cache: TxCache::new(),
spks_by_keychain: BTreeMap::new(),
}
}
/// Add to the [`TxCache`] held by the request.
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn cache_txs<T>(mut self, full_txs: impl IntoIterator<Item = (Txid, T)>) -> Self
where
T: Into<Arc<Transaction>>,
{
self.tx_cache = full_txs
.into_iter()
.map(|(txid, tx)| (txid, tx.into()))
.collect();
self
}
/// Add all transactions from [`TxGraph`] into the [`TxCache`].
///
/// This consumes the [`SyncRequest`] and returns the updated one.
#[must_use]
pub fn cache_graph_txs<A>(self, graph: &TxGraph<A>) -> Self {
self.cache_txs(graph.full_txs().map(|tx_node| (tx_node.txid, tx_node.tx)))
}
/// Construct a new [`FullScanRequest`] from a given `chain_tip` and `index`.
///
/// Unbounded script pubkey iterators for each keychain (`K`) are extracted using
@@ -316,9 +375,9 @@ impl<K: Ord + Clone> FullScanRequest<K> {
/// Data returned from a spk-based blockchain client full scan.
///
/// See also [`FullScanRequest`].
pub struct FullScanResult<K> {
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`TxGraph`].
pub chain_update: CheckPoint,
/// Last active indices for the corresponding keychains (`K`).

View File

@@ -158,8 +158,8 @@ mod test {
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());
let _ = txout_index.insert_descriptor(TestKeychain::External, external_descriptor.clone());
let _ = txout_index.insert_descriptor(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
}
@@ -258,18 +258,10 @@ mod test {
None
);
}
// The following dummy traits were created to test if SpkIterator is working properly.
#[allow(unused)]
trait TestSendStatic: Send + 'static {
fn test(&self) -> u32 {
20
}
}
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
fn test(&self) -> u32 {
20
}
}
}
#[test]
fn spk_iterator_is_send_and_static() {
fn is_send_and_static<A: Send + 'static>() {}
is_send_and_static::<SpkIterator<Descriptor<DescriptorPublicKey>>>()
}

View File

@@ -4,7 +4,7 @@ use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
indexed_tx_graph::Indexer,
};
use bitcoin::{OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
///
@@ -275,21 +275,25 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
/// output. For `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, range: impl RangeBounds<I>) -> (u64, u64) {
let mut sent = 0;
let mut received = 0;
pub fn sent_and_received(
&self,
tx: &Transaction,
range: impl RangeBounds<I>,
) -> (Amount, Amount) {
let mut sent = Amount::ZERO;
let mut received = Amount::ZERO;
for txin in &tx.input {
if let Some((index, txout)) = self.txout(txin.previous_output) {
if range.contains(index) {
sent += txout.value.to_sat();
sent += txout.value;
}
}
}
for txout in &tx.output {
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
if range.contains(index) {
received += txout.value.to_sat();
received += txout.value;
}
}
}
@@ -301,9 +305,10 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
/// for calling [`sent_and_received`] and subtracting sent from received.
///
/// [`sent_and_received`]: Self::sent_and_received
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> i64 {
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
let (sent, received) = self.sent_and_received(tx, range);
received as i64 - sent as i64
received.to_signed().expect("valid `SignedAmount`")
- sent.to_signed().expect("valid `SignedAmount`")
}
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output

View File

@@ -95,7 +95,7 @@ use crate::{
use alloc::collections::vec_deque::VecDeque;
use alloc::sync::Arc;
use alloc::vec::Vec;
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, Script, Transaction, TxOut, Txid};
use core::fmt::{self, Formatter};
use core::{
convert::Infallible,
@@ -516,12 +516,12 @@ impl<A: Clone + Ord> TxGraph<A> {
/// Inserts the given transaction into [`TxGraph`].
///
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A> {
pub fn insert_tx<T: Into<Arc<Transaction>>>(&mut self, tx: T) -> ChangeSet<A> {
let tx = tx.into();
let mut update = Self::default();
update.txs.insert(
tx.txid(),
(TxNodeInternal::Whole(tx.into()), BTreeSet::new(), 0),
);
update
.txs
.insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0));
self.apply_update(update)
}
@@ -1155,10 +1155,10 @@ impl<A: Anchor> TxGraph<A> {
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
) -> Result<Balance, C::Error> {
let mut immature = 0;
let mut trusted_pending = 0;
let mut untrusted_pending = 0;
let mut confirmed = 0;
let mut immature = Amount::ZERO;
let mut trusted_pending = Amount::ZERO;
let mut untrusted_pending = Amount::ZERO;
let mut confirmed = Amount::ZERO;
for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) {
let (spk_i, txout) = res?;
@@ -1166,16 +1166,16 @@ impl<A: Anchor> TxGraph<A> {
match &txout.chain_position {
ChainPosition::Confirmed(_) => {
if txout.is_confirmed_and_spendable(chain_tip.height) {
confirmed += txout.txout.value.to_sat();
confirmed += txout.txout.value;
} else if !txout.is_mature(chain_tip.height) {
immature += txout.txout.value.to_sat();
immature += txout.txout.value;
}
}
ChainPosition::Unconfirmed(_) => {
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
trusted_pending += txout.txout.value.to_sat();
trusted_pending += txout.txout.value;
} else {
untrusted_pending += txout.txout.value.to_sat();
untrusted_pending += txout.txout.value;
}
}
}

View File

@@ -1,3 +1,5 @@
#![cfg(feature = "miniscript")]
mod tx_template;
#[allow(unused_imports)]
pub use tx_template::*;
@@ -73,3 +75,15 @@ pub fn new_tx(lt: u32) -> bitcoin::Transaction {
output: vec![],
}
}
#[allow(unused)]
pub const DESCRIPTORS: [&str; 7] = [
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)",
"tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)",
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0/*)",
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)",
"tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)",
"wpkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX/1/0/*)",
// non-wildcard
"wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)",
];

View File

@@ -1,3 +1,5 @@
#![cfg(feature = "miniscript")]
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
@@ -52,7 +54,8 @@ impl TxOutTemplate {
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
let (descriptor, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), super::DESCRIPTORS[2]).unwrap();
let mut graph = TxGraph::<A>::default();
let mut spk_index = SpkTxOutIndex::default();
(0..10).for_each(|index| {

View File

@@ -1,13 +1,16 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use std::{collections::BTreeSet, sync::Arc};
use crate::common::DESCRIPTORS;
use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph},
keychain::{self, Balance, KeychainTxOutIndex},
local_chain::LocalChain,
tx_graph, ChainPosition, ConfirmationHeightAnchor,
tx_graph, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
};
use bitcoin::{
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
@@ -23,8 +26,7 @@ use miniscript::Descriptor;
/// 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)
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0])
.expect("must be valid");
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
@@ -32,7 +34,7 @@ fn insert_relevant_txs() {
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
KeychainTxOutIndex::new(10),
);
graph.index.add_keychain((), descriptor);
let _ = graph.index.insert_descriptor((), descriptor.clone());
let tx_a = Transaction {
output: vec![
@@ -71,7 +73,10 @@ fn insert_relevant_txs() {
txs: txs.iter().cloned().map(Arc::new).collect(),
..Default::default()
},
indexer: keychain::ChangeSet([((), 9_u32)].into()),
indexer: keychain::ChangeSet {
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
keychains_added: [].into(),
},
};
assert_eq!(
@@ -79,7 +84,16 @@ fn insert_relevant_txs() {
changeset,
);
assert_eq!(graph.initial_changeset(), changeset,);
// The initial changeset will also contain info about the keychain we added
let initial_changeset = indexed_tx_graph::ChangeSet {
graph: changeset.graph,
indexer: keychain::ChangeSet {
last_revealed: changeset.indexer.last_revealed,
keychains_added: [((), descriptor)].into(),
},
};
assert_eq!(graph.initial_changeset(), initial_changeset);
}
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
@@ -117,15 +131,17 @@ fn test_list_owned_txouts() {
// 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 (desc_1, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[2]).unwrap();
let (desc_2, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
KeychainTxOutIndex::new(10),
);
graph.index.add_keychain("keychain_1".into(), desc_1);
graph.index.add_keychain("keychain_2".into(), desc_2);
let _ = graph.index.insert_descriptor("keychain_1".into(), desc_1);
let _ = graph.index.insert_descriptor("keychain_2".into(), desc_2);
// Get trusted and untrusted addresses
@@ -135,14 +151,20 @@ fn test_list_owned_txouts() {
{
// 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());
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_1".to_string())
.unwrap();
// TODO Assert indexes
trusted_spks.push(script.to_owned());
}
}
{
for _ in 0..10 {
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_2".to_string())
.unwrap();
untrusted_spks.push(script.to_owned());
}
}
@@ -235,26 +257,18 @@ fn test_list_owned_txouts() {
.unwrap_or_else(|| panic!("block must exist at {}", height));
let txouts = graph
.graph()
.filter_chain_txouts(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
)
.filter_chain_txouts(&local_chain, chain_tip, graph.index.outpoints())
.collect::<Vec<_>>();
let utxos = graph
.graph()
.filter_chain_unspents(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
)
.filter_chain_unspents(&local_chain, chain_tip, graph.index.outpoints())
.collect::<Vec<_>>();
let balance = graph.graph().balance(
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
graph.index.outpoints(),
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
);
@@ -341,10 +355,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
}
);
}
@@ -376,10 +390,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 25000, // tx3 + tx5
untrusted_pending: 20000, // tx4
confirmed: 0 // Nothing is confirmed yet
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
}
);
}
@@ -408,10 +422,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx3 got confirmed
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(10000) // tx3 got confirmed
}
);
}
@@ -439,10 +453,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: 70000, // immature coinbase
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 10000 // tx1 got matured
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(10000) // tx1 got matured
}
);
}
@@ -455,10 +469,10 @@ fn test_list_owned_txouts() {
assert_eq!(
balance,
Balance {
immature: 0, // coinbase matured
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
confirmed: 80000 // tx1 + tx3
immature: Amount::ZERO, // coinbase matured
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(80000) // tx1 + tx3
}
);
}

View File

@@ -5,36 +5,39 @@ mod common;
use bdk_chain::{
collections::BTreeMap,
indexed_tx_graph::Indexer,
keychain::{self, KeychainTxOutIndex},
Append,
keychain::{self, ChangeSet, KeychainTxOutIndex},
Append, DescriptorExt, DescriptorId,
};
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
use miniscript::{Descriptor, DescriptorPublicKey};
use crate::common::DESCRIPTORS;
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
enum TestKeychain {
External,
Internal,
}
fn parse_descriptor(descriptor: &str) -> Descriptor<DescriptorPublicKey> {
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, descriptor)
.unwrap()
.0
}
fn init_txout_index(
external_descriptor: Descriptor<DescriptorPublicKey>,
internal_descriptor: Descriptor<DescriptorPublicKey>,
lookahead: u32,
) -> (
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
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 _ = txout_index.insert_descriptor(TestKeychain::External, external_descriptor);
let _ = txout_index.insert_descriptor(TestKeychain::Internal, internal_descriptor);
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
txout_index
}
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
@@ -44,29 +47,136 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
.script_pubkey()
}
// We create two empty changesets lhs and rhs, we then insert various descriptors with various
// last_revealed, append rhs to lhs, and check that the result is consistent with these rules:
// - Existing index doesn't update if the new index in `other` is lower than `self`.
// - Existing index updates if the new index in `other` is higher than `self`.
// - Existing index is unchanged if keychain doesn't exist in `other`.
// - New keychain gets added if the keychain is in `other` but not in `self`.
#[test]
fn append_changesets_check_last_revealed() {
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let descriptor_ids: Vec<_> = DESCRIPTORS
.iter()
.take(4)
.map(|d| {
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, d)
.unwrap()
.0
.descriptor_id()
})
.collect();
let mut lhs_di = BTreeMap::<DescriptorId, u32>::default();
let mut rhs_di = BTreeMap::<DescriptorId, u32>::default();
lhs_di.insert(descriptor_ids[0], 7);
lhs_di.insert(descriptor_ids[1], 0);
lhs_di.insert(descriptor_ids[2], 3);
rhs_di.insert(descriptor_ids[0], 3); // value less than lhs desc 0
rhs_di.insert(descriptor_ids[1], 5); // value more than lhs desc 1
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
let mut lhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: lhs_di,
};
let rhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: rhs_di,
};
lhs.append(rhs);
// Existing index doesn't update if the new index in `other` is lower than `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[0]), Some(&7));
// Existing index updates if the new index in `other` is higher than `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[1]), Some(&5));
// Existing index is unchanged if keychain doesn't exist in `other`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[2]), Some(&3));
// New keychain gets added if the keychain is in `other` but not in `self`.
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
}
#[test]
fn test_apply_changeset_with_different_descriptors_to_same_keychain() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &external_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
let changeset = ChangeSet {
keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
last_revealed: [].into(),
};
txout_index.apply_changeset(changeset);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &internal_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
let changeset = ChangeSet {
keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
last_revealed: [].into(),
};
txout_index.apply_changeset(changeset);
assert_eq!(
txout_index.keychains().collect::<Vec<_>>(),
vec![
(&TestKeychain::External, &internal_descriptor),
(&TestKeychain::Internal, &external_descriptor)
]
);
}
#[test]
fn test_set_all_derivation_indices() {
use bdk_chain::indexed_tx_graph::Indexer;
let (mut txout_index, _, _) = init_txout_index(0);
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
let derive_to: BTreeMap<_, _> =
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
let last_revealed: BTreeMap<_, _> = [
(external_descriptor.descriptor_id(), 12),
(internal_descriptor.descriptor_id(), 24),
]
.into();
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
&derive_to
txout_index.reveal_to_target_multi(&derive_to).1,
ChangeSet {
keychains_added: BTreeMap::new(),
last_revealed: last_revealed.clone()
}
);
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
assert_eq!(txout_index.last_revealed_indices(), derive_to);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1,
keychain::ChangeSet::default(),
"no changes if we set to the same thing"
);
assert_eq!(txout_index.initial_changeset().as_inner(), &derive_to);
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
}
#[test]
fn test_lookahead() {
let (mut txout_index, external_desc, internal_desc) = init_txout_index(10);
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
// given:
// - external lookahead set to 10
@@ -76,15 +186,16 @@ fn test_lookahead() {
// - 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_changeset) =
txout_index.reveal_to_target(&TestKeychain::External, index);
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::External, index)
.unwrap();
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
vec![(index, spk_at_index(&external_desc, index))],
vec![(index, spk_at_index(&external_descriptor, index))],
);
assert_eq!(
revealed_changeset.as_inner(),
&[(TestKeychain::External, index)].into()
&revealed_changeset.last_revealed,
&[(external_descriptor.descriptor_id(), index)].into()
);
assert_eq!(
@@ -126,17 +237,18 @@ fn test_lookahead() {
// - 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_changeset) =
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::Internal, 24)
.unwrap();
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
(0..=24)
.map(|index| (index, spk_at_index(&internal_desc, index)))
.map(|index| (index, spk_at_index(&internal_descriptor, index)))
.collect::<Vec<_>>(),
);
assert_eq!(
revealed_changeset.as_inner(),
&[(TestKeychain::Internal, 24)].into()
&revealed_changeset.last_revealed,
&[(internal_descriptor.descriptor_id(), 24)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
@@ -172,14 +284,14 @@ fn test_lookahead() {
let tx = Transaction {
output: vec![
TxOut {
script_pubkey: external_desc
script_pubkey: external_descriptor
.at_derivation_index(external_index)
.unwrap()
.script_pubkey(),
value: Amount::from_sat(10_000),
},
TxOut {
script_pubkey: internal_desc
script_pubkey: internal_descriptor
.at_derivation_index(internal_index)
.unwrap()
.script_pubkey(),
@@ -219,14 +331,17 @@ fn test_lookahead() {
// - last used index should change as expected
#[test]
fn test_scan_with_lookahead() {
let (mut txout_index, external_desc, _) = init_txout_index(10);
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 10);
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
.into_iter()
.map(|i| {
(
i,
external_desc
external_descriptor
.at_derivation_index(i)
.unwrap()
.script_pubkey(),
@@ -243,8 +358,8 @@ fn test_scan_with_lookahead() {
let changeset = txout_index.index_txout(op, &txout);
assert_eq!(
changeset.as_inner(),
&[(TestKeychain::External, spk_i)].into()
&changeset.last_revealed,
&[(external_descriptor.descriptor_id(), spk_i)].into()
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
@@ -257,7 +372,7 @@ fn test_scan_with_lookahead() {
}
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
let spk_41 = external_desc
let spk_41 = external_descriptor
.at_derivation_index(41)
.unwrap()
.script_pubkey();
@@ -273,11 +388,13 @@ fn test_scan_with_lookahead() {
#[test]
#[rustfmt::skip]
fn test_wildcard_derivations() {
let (mut txout_index, external_desc, _) = init_txout_index(0);
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
let external_spk_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
let external_spk_27 = external_desc.at_derivation_index(27).unwrap().script_pubkey();
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index = init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
let external_spk_0 = external_descriptor.at_derivation_index(0).unwrap().script_pubkey();
let external_spk_16 = external_descriptor.at_derivation_index(16).unwrap().script_pubkey();
let external_spk_26 = external_descriptor.at_derivation_index(26).unwrap().script_pubkey();
let external_spk_27 = external_descriptor.at_derivation_index(27).unwrap().script_pubkey();
// - nothing is derived
// - unused list is also empty
@@ -285,13 +402,13 @@ fn test_wildcard_derivations() {
// - next_derivation_index() == (0, true)
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
assert_eq!(&changeset.last_revealed, &[].into());
// - derived till 25
// - used all spks till 15.
@@ -307,16 +424,16 @@ fn test_wildcard_derivations() {
.chain([17, 20, 23])
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (26, external_spk_26.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (16, external_spk_16.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
assert_eq!(&changeset.last_revealed, &[].into());
// - Use all the derived till 26.
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
@@ -324,9 +441,9 @@ fn test_wildcard_derivations() {
txout_index.mark_used(TestKeychain::External, index);
});
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
assert_eq!(spk, (27, external_spk_27.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
}
#[test]
@@ -334,13 +451,14 @@ fn test_non_wildcard_derivations() {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
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 (no_wildcard_descriptor, _) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[6]).unwrap();
let external_spk = no_wildcard_descriptor
.at_derivation_index(0)
.unwrap()
.script_pubkey();
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
let _ = txout_index.insert_descriptor(TestKeychain::External, no_wildcard_descriptor.clone());
// given:
// - `txout_index` with no stored scripts
@@ -348,14 +466,24 @@ fn test_non_wildcard_derivations() {
// - 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!(
txout_index.next_index(&TestKeychain::External).unwrap(),
(0, true)
);
let (spk, changeset) = txout_index
.reveal_next_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
assert_eq!(
&changeset.last_revealed,
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
);
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
assert_eq!(&changeset.last_revealed, &[].into());
// given:
// - the non-wildcard descriptor already has a stored and used script
@@ -363,18 +491,26 @@ fn test_non_wildcard_derivations() {
// - 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 changeset
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
assert_eq!(
txout_index.next_index(&TestKeychain::External).unwrap(),
(0, false)
);
txout_index.mark_used(TestKeychain::External, 0);
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
let (spk, changeset) = txout_index
.reveal_next_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
assert_eq!(&changeset.last_revealed, &[].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
assert_eq!(spk, (0, external_spk.as_script()));
assert_eq!(changeset.as_inner(), &[].into());
let (revealed_spks, revealed_changeset) =
txout_index.reveal_to_target(&TestKeychain::External, 200);
assert_eq!(&changeset.last_revealed, &[].into());
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::External, 200)
.unwrap();
assert_eq!(revealed_spks.count(), 0);
assert!(revealed_changeset.is_empty());
@@ -438,7 +574,13 @@ fn lookahead_to_target() {
];
for t in test_cases {
let (mut index, _, _) = init_txout_index(t.lookahead);
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut index = init_txout_index(
external_descriptor.clone(),
internal_descriptor.clone(),
t.lookahead,
);
if let Some(last_revealed) = t.external_last_revealed {
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
@@ -449,17 +591,19 @@ fn lookahead_to_target() {
let keychain_test_cases = [
(
external_descriptor.descriptor_id(),
TestKeychain::External,
t.external_last_revealed,
t.external_target,
),
(
internal_descriptor.descriptor_id(),
TestKeychain::Internal,
t.internal_last_revealed,
t.internal_target,
),
];
for (keychain, last_revealed, target) in keychain_test_cases {
for (descriptor_id, keychain, last_revealed, target) in keychain_test_cases {
if let Some(target) = target {
let original_last_stored_index = match last_revealed {
Some(last_revealed) => Some(last_revealed + t.lookahead),
@@ -475,10 +619,10 @@ fn lookahead_to_target() {
let keys = index
.inner()
.all_spks()
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
.map(|(k, _)| k.clone())
.range((descriptor_id, 0)..=(descriptor_id, u32::MAX))
.map(|(k, _)| *k)
.collect::<Vec<_>>();
let exp_keys = core::iter::repeat(keychain)
let exp_keys = core::iter::repeat(descriptor_id)
.zip(0_u32..=exp_last_stored_index)
.collect::<Vec<_>>();
assert_eq!(keys, exp_keys);
@@ -486,3 +630,150 @@ fn lookahead_to_target() {
}
}
}
/// `::index_txout` should still index txouts with spks derived from descriptors without keychains.
/// This includes properly refilling the lookahead for said descriptors.
#[test]
fn index_txout_after_changing_descriptor_under_keychain() {
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
let (desc_a, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0])
.expect("descriptor 0 must be valid");
let (desc_b, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[1])
.expect("descriptor 1 must be valid");
let desc_id_a = desc_a.descriptor_id();
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<()>::new(10);
// Introduce `desc_a` under keychain `()` and replace the descriptor.
let _ = txout_index.insert_descriptor((), desc_a.clone());
let _ = txout_index.insert_descriptor((), desc_b.clone());
// Loop through spks in intervals of `lookahead` to create outputs with. We should always be
// able to index these outputs if `lookahead` is respected.
let spk_indices = [9, 19, 29, 39];
for i in spk_indices {
let spk_at_index = desc_a
.at_derivation_index(i)
.expect("must derive")
.script_pubkey();
let index_changeset = txout_index.index_txout(
// Use spk derivation index as vout as we just want an unique outpoint.
OutPoint::new(h!("mock_tx"), i as _),
&TxOut {
value: Amount::from_sat(10_000),
script_pubkey: spk_at_index,
},
);
assert_eq!(
index_changeset,
bdk_chain::keychain::ChangeSet {
keychains_added: BTreeMap::default(),
last_revealed: [(desc_id_a, i)].into(),
},
"must always increase last active if impl respects lookahead"
);
}
}
#[test]
fn insert_descriptor_no_change() {
let secp = Secp256k1::signing_only();
let (desc, _) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
let mut txout_index = KeychainTxOutIndex::<()>::default();
assert_eq!(
txout_index.insert_descriptor((), desc.clone()),
keychain::ChangeSet {
keychains_added: [((), desc.clone())].into(),
last_revealed: Default::default()
},
);
assert_eq!(
txout_index.insert_descriptor((), desc.clone()),
keychain::ChangeSet::default(),
"inserting the same descriptor for keychain should return an empty changeset",
);
}
#[test]
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
let desc = parse_descriptor(DESCRIPTORS[0]);
let changesets: &[ChangeSet<TestKeychain>] = &[
ChangeSet {
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
last_revealed: [].into(),
},
ChangeSet {
keychains_added: [(TestKeychain::External, desc.clone())].into(),
last_revealed: [(desc.descriptor_id(), 12)].into(),
},
];
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
for changeset in changesets {
indexer_a.apply_changeset(changeset.clone());
}
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
let aggregate_changesets = changesets
.iter()
.cloned()
.reduce(|mut agg, cs| {
agg.append(cs);
agg
})
.expect("must aggregate changesets");
indexer_b.apply_changeset(aggregate_changesets);
assert_eq!(
indexer_a.keychains().collect::<Vec<_>>(),
indexer_b.keychains().collect::<Vec<_>>()
);
assert_eq!(
indexer_a.spk_at_index(TestKeychain::External, 0),
indexer_b.spk_at_index(TestKeychain::External, 0)
);
assert_eq!(
indexer_a.spk_at_index(TestKeychain::Internal, 0),
indexer_b.spk_at_index(TestKeychain::Internal, 0)
);
assert_eq!(
indexer_a.last_revealed_indices(),
indexer_b.last_revealed_indices()
);
}
// When the same descriptor is associated with various keychains,
// index methods only return the highest keychain by Ord
#[test]
fn test_only_highest_ord_keychain_is_returned() {
let desc = parse_descriptor(DESCRIPTORS[0]);
let mut indexer = KeychainTxOutIndex::<TestKeychain>::new(0);
let _ = indexer.insert_descriptor(TestKeychain::Internal, desc.clone());
let _ = indexer.insert_descriptor(TestKeychain::External, desc);
// reveal_next_spk will work with either keychain
let spk0: ScriptBuf = indexer
.reveal_next_spk(&TestKeychain::External)
.unwrap()
.0
.1
.into();
let spk1: ScriptBuf = indexer
.reveal_next_spk(&TestKeychain::Internal)
.unwrap()
.0
.1
.into();
// index_of_spk will always return External
assert_eq!(
indexer.index_of_spk(&spk0),
Some((TestKeychain::External, 0))
);
assert_eq!(
indexer.index_of_spk(&spk1),
Some((TestKeychain::External, 1))
);
}

View File

@@ -1,3 +1,5 @@
#![cfg(feature = "miniscript")]
use std::ops::{Bound, RangeBounds};
use bdk_chain::{

View File

@@ -1,5 +1,7 @@
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
use bitcoin::{absolute, transaction, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
};
#[test]
fn spk_txout_sent_and_received() {
@@ -20,14 +22,23 @@ fn spk_txout_sent_and_received() {
}],
};
assert_eq!(index.sent_and_received(&tx1, ..), (0, 42_000));
assert_eq!(index.sent_and_received(&tx1, ..1), (0, 42_000));
assert_eq!(index.sent_and_received(&tx1, 1..), (0, 0));
assert_eq!(index.net_value(&tx1, ..), 42_000);
assert_eq!(
index.sent_and_received(&tx1, ..),
(Amount::from_sat(0), Amount::from_sat(42_000))
);
assert_eq!(
index.sent_and_received(&tx1, ..1),
(Amount::from_sat(0), Amount::from_sat(42_000))
);
assert_eq!(
index.sent_and_received(&tx1, 1..),
(Amount::from_sat(0), Amount::from_sat(0))
);
assert_eq!(index.net_value(&tx1, ..), SignedAmount::from_sat(42_000));
index.index_tx(&tx1);
assert_eq!(
index.sent_and_received(&tx1, ..),
(0, 42_000),
(Amount::from_sat(0), Amount::from_sat(42_000)),
"shouldn't change after scanning"
);
@@ -53,10 +64,19 @@ fn spk_txout_sent_and_received() {
],
};
assert_eq!(index.sent_and_received(&tx2, ..), (42_000, 50_000));
assert_eq!(index.sent_and_received(&tx2, ..1), (42_000, 30_000));
assert_eq!(index.sent_and_received(&tx2, 1..), (0, 20_000));
assert_eq!(index.net_value(&tx2, ..), 8_000);
assert_eq!(
index.sent_and_received(&tx2, ..),
(Amount::from_sat(42_000), Amount::from_sat(50_000))
);
assert_eq!(
index.sent_and_received(&tx2, ..1),
(Amount::from_sat(42_000), Amount::from_sat(30_000))
);
assert_eq!(
index.sent_and_received(&tx2, 1..),
(Amount::from_sat(0), Amount::from_sat(20_000))
);
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
}
#[test]

View File

@@ -1,3 +1,5 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use bdk_chain::tx_graph::CalculateFeeError;

View File

@@ -1,10 +1,12 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use std::collections::{BTreeSet, HashSet};
use bdk_chain::{keychain::Balance, BlockId};
use bitcoin::{OutPoint, Script};
use bitcoin::{Amount, OutPoint, Script};
use common::*;
#[allow(dead_code)]
@@ -79,10 +81,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 20000,
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(20000),
},
},
Scenario {
@@ -115,10 +117,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -150,10 +152,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -192,10 +194,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 40000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(40000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -227,10 +229,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -262,10 +264,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 20000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(20000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -311,10 +313,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
},
},
Scenario {
@@ -356,10 +358,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -397,10 +399,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 20000,
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(20000),
},
},
Scenario {
@@ -442,10 +444,10 @@ fn test_tx_conflict_handling() {
]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -487,10 +489,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 30000,
untrusted_pending: 0,
confirmed: 0,
immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
untrusted_pending: Amount::ZERO,
confirmed: Amount::ZERO,
},
},
Scenario {
@@ -532,10 +534,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
},
},
Scenario {
@@ -583,10 +585,10 @@ fn test_tx_conflict_handling() {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 50000,
immature: Amount::ZERO,
trusted_pending: Amount::ZERO,
untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
},
},
];

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_electrum"
version = "0.12.0"
version = "0.14.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -12,11 +12,9 @@ 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.13.0", default-features = false }
bdk_chain = { path = "../chain", version = "0.15.0" }
electrum-client = { version = "0.19" }
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default-features = false }
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
anyhow = "1"
bdk_testenv = { path = "../testenv", default-features = false }

View File

@@ -1,164 +1,48 @@
use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
collections::{BTreeMap, HashMap, HashSet},
local_chain::CheckPoint,
tx_graph::{self, TxGraph},
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
};
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
str::FromStr,
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult, TxCache},
tx_graph::TxGraph,
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
};
use core::str::FromStr;
use electrum_client::{ElectrumApi, Error, HeaderNotification};
use std::sync::Arc;
/// We include a chain suffix of a certain length for the purpose of robustness.
const CHAIN_SUFFIX_LENGTH: u32 = 8;
/// Represents updates fetched from an Electrum server, but excludes full transactions.
///
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
/// fetch the full transactions from Electrum and finalize the update.
#[derive(Debug, Default, Clone)]
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
impl RelevantTxids {
/// Determine the full transactions that are missing from `graph`.
///
/// Refer to [`RelevantTxids`] for more details.
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
self.0
.keys()
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
.cloned()
.collect()
}
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
///
/// Refer to [`RelevantTxids`] for more details.
pub fn into_tx_graph(
self,
client: &Client,
missing: Vec<Txid>,
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
let new_txs = client.batch_transaction_get(&missing)?;
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
for (txid, anchors) in self.0 {
for anchor in anchors {
let _ = graph.insert_anchor(txid, anchor);
}
}
Ok(graph)
}
/// Finalizes the update by fetching `missing` txids from the `client`, where the
/// resulting [`TxGraph`] has anchors of type [`ConfirmationTimeHeightAnchor`].
///
/// Refer to [`RelevantTxids`] for more details.
///
/// **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 into_confirmation_time_tx_graph(
self,
client: &Client,
missing: Vec<Txid>,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let graph = self.into_tx_graph(client, missing)?;
let relevant_heights = {
let mut visited_heights = HashSet::new();
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_changeset = {
let old_changeset = TxGraph::default().apply_update(graph);
tx_graph::ChangeSet {
txs: old_changeset.txs,
txouts: old_changeset.txouts,
last_seen: old_changeset.last_seen,
anchors: old_changeset
.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 = ConfirmationTimeHeightAnchor {
anchor_block: height_anchor.anchor_block,
confirmation_height,
confirmation_time,
};
(time_anchor, txid)
})
.collect(),
}
};
let mut new_graph = TxGraph::default();
new_graph.apply_changeset(graph_changeset);
Ok(new_graph)
}
}
/// Combination of chain and transactions updates from electrum
///
/// We have to update the chain and the txids at the same time since we anchor the txids to
/// the same chain tip that we check before and after we gather the txids.
#[derive(Debug)]
pub struct ElectrumUpdate {
/// Chain update
pub chain_update: CheckPoint,
/// Transaction updates from electrum
pub relevant_txids: RelevantTxids,
}
/// Trait to extend [`Client`] functionality.
/// Trait to extend [`electrum_client::Client`] functionality.
pub trait ElectrumExt {
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
/// returns updates for [`bdk_chain`] data structures.
///
/// - `prev_tip`: the most recent blockchain tip present locally
/// - `keychain_spks`: keychains that we want to scan transactions for
///
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
/// single batch request.
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
/// see [`FullScanRequest`]
/// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
/// associated transactions
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
/// request
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
/// calculation
fn full_scan<K: Ord + Clone>(
&self,
prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
request: FullScanRequest<K>,
stop_gap: usize,
batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error>;
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
/// and returns updates for [`bdk_chain`] data structures.
///
/// - `prev_tip`: the most recent blockchain tip present locally
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
/// - `txids`: transactions for which we want updated [`Anchor`]s
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
///
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
/// request.
/// - `request`: struct with data required to perform a spk-based blockchain client sync,
/// see [`SyncRequest`]
/// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
/// request
/// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee
/// calculation
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
@@ -166,31 +50,33 @@ pub trait ElectrumExt {
/// [`full_scan`]: ElectrumExt::full_scan
fn sync(
&self,
prev_tip: CheckPoint,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
request: SyncRequest,
batch_size: usize,
) -> Result<ElectrumUpdate, Error>;
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error>;
}
impl<A: ElectrumApi> ElectrumExt for A {
impl<E: ElectrumApi> ElectrumExt for E {
fn full_scan<K: Ord + Clone>(
&self,
prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
mut request: FullScanRequest<K>,
stop_gap: usize,
batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
let mut request_spks = keychain_spks
.into_iter()
.map(|(k, s)| (k, s.into_iter()))
.collect::<BTreeMap<K, _>>();
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error> {
let mut request_spks = request.spks_by_keychain;
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
// cannot be collected. In addition, we keep track of whether an spk has an active tx
// history for determining the `last_active_index`.
// * key: (keychain, spk_index) that identifies the spk.
// * val: (script_pubkey, has_tx_history).
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
let (electrum_update, keychain_update) = loop {
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
let mut relevant_txids = RelevantTxids::default();
let update = loop {
let (tip, _) = construct_update_tip(self, request.chain_tip.clone())?;
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
let cps = tip
.iter()
.take(10)
@@ -202,7 +88,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
scanned_spks.append(&mut populate_with_spks(
self,
&cps,
&mut relevant_txids,
&mut request.tx_cache,
&mut graph_update,
&mut scanned_spks
.iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
@@ -215,7 +102,8 @@ impl<A: ElectrumApi> ElectrumExt for A {
populate_with_spks(
self,
&cps,
&mut relevant_txids,
&mut request.tx_cache,
&mut graph_update,
keychain_spks,
stop_gap,
batch_size,
@@ -232,6 +120,11 @@ impl<A: ElectrumApi> ElectrumExt for A {
continue; // reorg
}
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
fetch_prev_txout(self, &mut request.tx_cache, &mut graph_update)?;
}
let chain_update = tip;
let keychain_update = request_spks
@@ -245,54 +138,148 @@ impl<A: ElectrumApi> ElectrumExt for A {
})
.collect::<BTreeMap<_, _>>();
break (
ElectrumUpdate {
chain_update,
relevant_txids,
},
keychain_update,
);
break FullScanResult {
graph_update,
chain_update,
last_active_indices: keychain_update,
};
};
Ok((electrum_update, keychain_update))
Ok(ElectrumFullScanResult(update))
}
fn sync(
&self,
prev_tip: CheckPoint,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
request: SyncRequest,
batch_size: usize,
) -> Result<ElectrumUpdate, Error> {
let spk_iter = misc_spks
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk));
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error> {
let mut tx_cache = request.tx_cache.clone();
let (mut electrum_update, _) = self.full_scan(
prev_tip.clone(),
[((), spk_iter)].into(),
usize::MAX,
batch_size,
)?;
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
.cache_txs(request.tx_cache)
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
let mut full_scan_res = self
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
.with_confirmation_height_anchor();
let (tip, _) = construct_update_tip(self, prev_tip)?;
let (tip, _) = construct_update_tip(self, request.chain_tip)?;
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
populate_with_txids(self, &cps, &mut electrum_update.relevant_txids, txids)?;
populate_with_txids(
self,
&cps,
&mut tx_cache,
&mut full_scan_res.graph_update,
request.txids,
)?;
populate_with_outpoints(
self,
&cps,
&mut tx_cache,
&mut full_scan_res.graph_update,
request.outpoints,
)?;
let _txs =
populate_with_outpoints(self, &cps, &mut electrum_update.relevant_txids, outpoints)?;
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
fetch_prev_txout(self, &mut tx_cache, &mut full_scan_res.graph_update)?;
}
Ok(electrum_update)
Ok(ElectrumSyncResult(SyncResult {
chain_update: full_scan_res.chain_update,
graph_update: full_scan_res.graph_update,
}))
}
}
/// The result of [`ElectrumExt::full_scan`].
///
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
/// [`ConfirmationTimeHeightAnchor`] anchor types.
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
impl<K> ElectrumFullScanResult<K> {
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
self.0
}
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
///
/// This requires additional calls to the Electrum server.
pub fn with_confirmation_time_height_anchor(
self,
client: &impl ElectrumApi,
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(FullScanResult {
graph_update: try_into_confirmation_time_result(res.graph_update, client)?,
chain_update: res.chain_update,
last_active_indices: res.last_active_indices,
})
}
}
/// The result of [`ElectrumExt::sync`].
///
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
/// [`ConfirmationTimeHeightAnchor`] anchor types.
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
impl ElectrumSyncResult {
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
self.0
}
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
///
/// This requires additional calls to the Electrum server.
pub fn with_confirmation_time_height_anchor(
self,
client: &impl ElectrumApi,
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(SyncResult {
graph_update: try_into_confirmation_time_result(res.graph_update, client)?,
chain_update: res.chain_update,
})
}
}
fn try_into_confirmation_time_result(
graph_update: TxGraph<ConfirmationHeightAnchor>,
client: &impl ElectrumApi,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let relevant_heights = graph_update
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height)
.collect::<HashSet<_>>();
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>>();
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
anchor_block: a.anchor_block,
confirmation_height: a.confirmation_height,
confirmation_time: height_to_time[&a.confirmation_height],
}))
}
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip(
client: &impl ElectrumApi,
@@ -408,48 +395,48 @@ fn determine_tx_anchor(
}
}
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
///
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
/// included. Anchors of the aforementioned transactions are included.
///
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
fn populate_with_outpoints(
client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
outpoints: impl IntoIterator<Item = OutPoint>,
) -> Result<HashMap<Txid, Transaction>, Error> {
let mut full_txs = HashMap::new();
) -> Result<(), Error> {
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) {
let op_txid = outpoint.txid;
let op_tx = fetch_tx(client, tx_cache, op_txid)?;
let op_txout = match op_tx.output.get(outpoint.vout as usize) {
Some(txout) => txout,
None => continue,
};
debug_assert_eq!(op_tx.txid(), op_txid);
// 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)? {
for res in client.script_get_history(&op_txout.script_pubkey)? {
if has_residing && has_spending {
break;
}
if res.tx_hash == txid {
if has_residing {
continue;
}
if !has_residing && res.tx_hash == op_txid {
has_residing = true;
full_txs.insert(res.tx_hash, tx.clone());
} else {
if has_spending {
continue;
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
}
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")
}
};
}
if !has_spending && res.tx_hash != op_txid {
let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
// we exclude txs/anchors that do not spend our specified outpoint(s)
has_spending = res_tx
.input
.iter()
@@ -457,26 +444,26 @@ fn populate_with_outpoints(
if !has_spending {
continue;
}
};
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
}
}
}
}
Ok(full_txs)
Ok(())
}
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
fn populate_with_txids(
client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
txids: impl IntoIterator<Item = Txid>,
) -> Result<(), Error> {
for txid in txids {
let tx = match client.transaction_get(&txid) {
let tx = match fetch_tx(client, tx_cache, txid) {
Ok(tx) => tx,
Err(electrum_client::Error::Protocol(_)) => continue,
Err(other_err) => return Err(other_err),
@@ -488,6 +475,8 @@ fn populate_with_txids(
.map(|txo| &txo.script_pubkey)
.expect("tx must have an output");
// because of restrictions of the Electrum API, we have to use the `script_get_history`
// call to get confirmation status of our transaction
let anchor = match client
.script_get_history(spk)?
.into_iter()
@@ -497,18 +486,64 @@ fn populate_with_txids(
None => continue,
};
let tx_entry = relevant_txids.0.entry(txid).or_default();
let _ = graph_update.insert_tx(tx);
if let Some(anchor) = anchor {
tx_entry.insert(anchor);
let _ = graph_update.insert_anchor(txid, anchor);
}
}
Ok(())
}
/// Fetch transaction of given `txid`.
///
/// We maintain a `tx_cache` so that we won't need to fetch from Electrum with every call.
fn fetch_tx<C: ElectrumApi>(
client: &C,
tx_cache: &mut TxCache,
txid: Txid,
) -> Result<Arc<Transaction>, Error> {
use bdk_chain::collections::hash_map::Entry;
Ok(match tx_cache.entry(txid) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => entry
.insert(Arc::new(client.transaction_get(&txid)?))
.clone(),
})
}
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
// which we do not have by default. This data is needed to calculate the transaction fee.
fn fetch_prev_txout<C: ElectrumApi>(
client: &C,
tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
) -> Result<(), Error> {
let full_txs: Vec<Arc<Transaction>> =
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
for tx in full_txs {
for vin in &tx.input {
let outpoint = vin.previous_output;
let vout = outpoint.vout;
let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?;
let txout = prev_tx.output[vout as usize].clone();
let _ = graph_update.insert_txout(outpoint, txout);
}
}
Ok(())
}
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
///
/// Transactions that contains an output with requested spk, or spends form an output with
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
/// also included.
///
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
fn populate_with_spks<I: Ord + Clone>(
client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>,
relevant_txids: &mut RelevantTxids,
tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
stop_gap: usize,
batch_size: usize,
@@ -540,10 +575,10 @@ fn populate_with_spks<I: Ord + Clone>(
unused_spk_count = 0;
}
for tx in spk_history {
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
tx_entry.insert(anchor);
for tx_res in spk_history {
let _ = graph_update.insert_tx(fetch_tx(client, tx_cache, tx_res.tx_hash)?);
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
}
}
}

View File

@@ -7,19 +7,10 @@
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
//! sync or full scan the user receives relevant blockchain data and output updates for
//! [`bdk_chain`] including [`RelevantTxids`].
//!
//! The [`RelevantTxids`] only includes `txid`s and not full transactions. The caller is responsible
//! for obtaining full transactions before applying new data to their [`bdk_chain`]. This can be
//! done with these steps:
//!
//! 1. Determine which full transactions are missing. Use [`RelevantTxids::missing_full_txs`].
//!
//! 2. Obtaining the full transactions. To do this via electrum use [`ElectrumApi::batch_transaction_get`].
//! [`bdk_chain`].
//!
//! Refer to [`example_electrum`] for a complete example.
//!
//! [`ElectrumApi::batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
#![warn(missing_docs)]

View File

@@ -1,18 +1,17 @@
use anyhow::Result;
use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
keychain::Balance,
local_chain::LocalChain,
spk_client::SyncRequest,
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
};
use bdk_electrum::{ElectrumExt, ElectrumUpdate};
use bdk_testenv::TestEnv;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use bdk_electrum::ElectrumExt;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
fn get_balance(
recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
) -> Result<Balance> {
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
let balance = recv_graph
@@ -28,7 +27,7 @@ fn get_balance(
/// 3. Mine extra block to confirm sent tx.
/// 4. Check [`Balance`] to ensure tx is confirmed.
#[test]
fn scan_detects_confirmed_tx() -> Result<()> {
fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
let env = TestEnv::new()?;
@@ -62,27 +61,52 @@ fn scan_detects_confirmed_tx() -> Result<()> {
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
let ElectrumUpdate {
chain_update,
relevant_txids,
} = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip())
.chain_spks(core::iter::once(spk_to_track)),
5,
true,
)?
.with_confirmation_time_height_anchor(&client)?;
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
let _ = recv_chain
.apply_update(chain_update)
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(graph_update);
let _ = recv_graph.apply_update(update.graph_update);
// Check to see if tx is confirmed.
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT.to_sat(),
confirmed: SEND_AMOUNT,
..Balance::default()
},
);
for tx in recv_graph.graph().full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transaction's previous outputs.
let fee = recv_graph
.graph()
.calculate_fee(&tx.tx)
.expect("fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
.get_transaction(&tx.txid, None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
.abs()
.to_sat() as u64;
// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}
Ok(())
}
@@ -93,7 +117,7 @@ fn scan_detects_confirmed_tx() -> Result<()> {
/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
#[test]
fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
const REORG_COUNT: usize = 8;
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
@@ -128,26 +152,27 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
let ElectrumUpdate {
chain_update,
relevant_txids,
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
let _ = recv_chain
.apply_update(chain_update)
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(graph_update.clone());
let _ = recv_graph.apply_update(update.graph_update.clone());
// Retain a snapshot of all anchors before reorg process.
let initial_anchors = graph_update.all_anchors();
let initial_anchors = update.graph_update.all_anchors();
// Check if initial balance is correct.
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64,
confirmed: SEND_AMOUNT * REORG_COUNT as u64,
..Balance::default()
},
"initial balance must be correct",
@@ -158,28 +183,29 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
env.reorg_empty_blocks(depth)?;
env.wait_until_electrum_sees_block()?;
let ElectrumUpdate {
chain_update,
relevant_txids,
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
let update = client
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
let _ = recv_chain
.apply_update(chain_update)
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
// Check to see if a new anchor is added during current reorg.
if !initial_anchors.is_superset(graph_update.all_anchors()) {
if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
println!("New anchor added at reorg depth {}", depth);
}
let _ = recv_graph.apply_update(graph_update);
let _ = recv_graph.apply_update(update.graph_update);
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64,
trusted_pending: SEND_AMOUNT.to_sat() * depth as u64,
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
trusted_pending: SEND_AMOUNT * depth as u64,
..Balance::default()
},
"reorg_count: {}",

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_esplora"
version = "0.12.0"
version = "0.14.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -12,7 +12,7 @@ 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.13.0", default-features = false }
bdk_chain = { path = "../chain", version = "0.15.0", default-features = false }
esplora-client = { version = "0.7.0", default-features = false }
async-trait = { version = "0.1.66", optional = true }
futures = { version = "0.3.26", optional = true }
@@ -22,10 +22,8 @@ bitcoin = { version = "0.31.0", optional = true, default-features = false }
miniscript = { version = "11.0.0", optional = true, default-features = false }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default_features = false }
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
bdk_testenv = { path = "../testenv", default-features = false }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1"
[features]
default = ["std", "async-https", "blocking-https-rustls"]

View File

@@ -28,8 +28,8 @@ pub trait EsploraAsyncExt {
/// Scan keychain scripts for transactions against Esplora, returning an update that can be
/// applied to the receiving structures.
///
/// * `local_tip`: the previously seen tip from [`LocalChain::tip`].
/// * `keychain_spks`: keychains that we want to scan transactions for
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
/// see [`FullScanRequest`]
///
/// The full 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
@@ -47,8 +47,6 @@ pub trait EsploraAsyncExt {
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
///
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
///
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
async fn full_scan<K: Ord + Clone + Send>(
&self,
request: FullScanRequest<K>,
@@ -59,16 +57,12 @@ pub trait EsploraAsyncExt {
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
/// specified and return a [`TxGraph`].
///
/// * `local_tip`: the previously seen tip from [`LocalChain::tip`].
/// * `misc_spks`: scripts that we want to sync transactions for
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
/// - `request`: struct with data required to perform a spk-based blockchain client sync, see
/// [`SyncRequest`]
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
///
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
/// [`full_scan`]: EsploraAsyncExt::full_scan
async fn sync(
&self,
@@ -417,8 +411,7 @@ mod test {
local_chain::LocalChain,
BlockId,
};
use bdk_testenv::TestEnv;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use esplora_client::Builder;
use crate::async_ext::{chain_update, fetch_latest_blocks};

View File

@@ -26,8 +26,8 @@ pub trait EsploraExt {
/// Scan keychain scripts for transactions against Esplora, returning an update that can be
/// applied to the receiving structures.
///
/// * `local_tip`: the previously seen tip from [`LocalChain::tip`].
/// * `keychain_spks`: keychains that we want to scan transactions for
/// - `request`: struct with data required to perform a spk-based blockchain client full scan,
/// see [`FullScanRequest`]
///
/// The full 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
@@ -45,8 +45,6 @@ pub trait EsploraExt {
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
///
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
///
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
fn full_scan<K: Ord + Clone>(
&self,
request: FullScanRequest<K>,
@@ -57,16 +55,12 @@ pub trait EsploraExt {
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
/// specified and return a [`TxGraph`].
///
/// * `local_tip`: the previously seen tip from [`LocalChain::tip`].
/// * `misc_spks`: scripts that we want to sync transactions for
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
/// - `request`: struct with data required to perform a spk-based blockchain client sync, see
/// [`SyncRequest`]
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
///
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
/// [`full_scan`]: EsploraExt::full_scan
fn sync(&self, request: SyncRequest, parallel_requests: usize) -> Result<SyncResult, Error>;
}
@@ -407,8 +401,7 @@ mod test {
use bdk_chain::bitcoin::Txid;
use bdk_chain::local_chain::LocalChain;
use bdk_chain::BlockId;
use bdk_testenv::TestEnv;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use esplora_client::{BlockHash, Builder};
use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration;

View File

@@ -1,7 +1,5 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
use bdk_esplora::EsploraAsyncExt;
use electrsd::bitcoind::anyhow;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
use std::str::FromStr;
@@ -9,7 +7,7 @@ use std::thread::sleep;
use std::time::Duration;
use bdk_chain::bitcoin::{Address, Amount, Txid};
use bdk_testenv::TestEnv;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
#[tokio::test]
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {

View File

@@ -1,7 +1,5 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
use bdk_esplora::EsploraExt;
use electrsd::bitcoind::anyhow;
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
use std::str::FromStr;
@@ -9,7 +7,7 @@ use std::thread::sleep;
use std::time::Duration;
use bdk_chain::bitcoin::{Address, Amount, Txid};
use bdk_testenv::TestEnv;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
#[test]
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_file_store"
version = "0.10.0"
version = "0.12.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -12,8 +12,8 @@ readme = "README.md"
[dependencies]
anyhow = { version = "1", default-features = false }
bdk_chain = { path = "../chain", version = "0.13.0", features = [ "serde", "miniscript" ] }
bdk_persist = { path = "../persist", version = "0.1.0"}
bdk_chain = { path = "../chain", version = "0.15.0", features = [ "serde", "miniscript" ] }
bdk_persist = { path = "../persist", version = "0.3.0"}
bincode = { version = "1" }
serde = { version = "1", features = ["derive"] }

View File

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

View File

@@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0"
readme = "README.md"
[dependencies]
bdk = { path = "../bdk" }
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.12" }
hwi = { version = "0.8.0", features = [ "miniscript"] }

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

@@ -0,0 +1,3 @@
# BDK HWI Signer
This crate contains `HWISigner`, an implementation of a `TransactionSigner` to be used with hardware wallets.

View File

@@ -3,10 +3,10 @@
//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk::bitcoin::Network;
//! # use bdk::signer::SignerOrdering;
//! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_hwi::HWISigner;
//! # use bdk::{KeychainKind, SignOptions, Wallet};
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
@@ -35,7 +35,7 @@
//! # }
//! ```
//!
//! [`TransactionSigner`]: bdk::wallet::signer::TransactionSigner
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
mod signer;
pub use signer::*;

View File

@@ -1,12 +1,12 @@
use bdk::bitcoin::bip32::Fingerprint;
use bdk::bitcoin::secp256k1::{All, Secp256k1};
use bdk::bitcoin::Psbt;
use bdk_wallet::bitcoin::bip32::Fingerprint;
use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1};
use bdk_wallet::bitcoin::Psbt;
use hwi::error::Error;
use hwi::types::{HWIChain, HWIDevice};
use hwi::HWIClient;
use bdk::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
#[derive(Debug)]
/// Custom signer for Hardware Wallets
@@ -38,7 +38,7 @@ impl TransactionSigner for HWISigner {
fn sign_transaction(
&self,
psbt: &mut Psbt,
_sign_options: &bdk::SignOptions,
_sign_options: &bdk_wallet::SignOptions,
_secp: &Secp256k1<All>,
) -> Result<(), SignerError> {
psbt.combine(
@@ -61,9 +61,9 @@ impl TransactionSigner for HWISigner {
// fn test_hardware_signer() {
// use std::sync::Arc;
//
// use bdk::tests::get_funded_wallet;
// use bdk::signer::SignerOrdering;
// use bdk::bitcoin::Network;
// use bdk_wallet::tests::get_funded_wallet;
// use bdk_wallet::signer::SignerOrdering;
// use bdk_wallet::bitcoin::Network;
// use crate::HWISigner;
// use hwi::HWIClient;
//
@@ -78,12 +78,12 @@ impl TransactionSigner for HWISigner {
//
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
// wallet.add_signer(
// bdk::KeychainKind::External,
// bdk_wallet::KeychainKind::External,
// SignerOrdering(200),
// Arc::new(custom_signer),
// );
//
// let addr = wallet.get_address(bdk::wallet::AddressIndex::LastUnused);
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
// let mut builder = wallet.build_tx();
// builder.drain_to(addr.script_pubkey()).drain_wallet();
// let (mut psbt, _) = builder.finish().unwrap();

View File

@@ -1,7 +1,7 @@
[package]
name = "bdk_persist"
homepage = "https://bitcoindevkit.org"
version = "0.1.0"
version = "0.3.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_persist"
description = "Types that define data persistence of a BDK wallet"
@@ -14,6 +14,9 @@ rust-version = "1.63"
[dependencies]
anyhow = { version = "1", default-features = false }
bdk_chain = { path = "../chain", version = "0.13.0", default-features = false }
bdk_chain = { path = "../chain", version = "0.15.0", default-features = false }
[features]
default = ["bdk_chain/std", "miniscript"]
serde = ["bdk_chain/serde"]
miniscript = ["bdk_chain/miniscript"]

View File

@@ -1,3 +1,5 @@
# BDK Persist
This crate is home to the [`PersistBackend`](crate::PersistBackend) trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures. The [`Persist`](crate::Persist) type provides a convenient wrapper around a `PersistBackend` that allows staging changes before committing them.
This crate is home to the [`PersistBackend`] trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures.
The [`Persist`] type provides a convenient wrapper around a [`PersistBackend`] that allows staging changes before committing them.

View File

@@ -0,0 +1,73 @@
#![cfg(feature = "miniscript")]
use bdk_chain::{bitcoin::Network, indexed_tx_graph, keychain, local_chain, Anchor, Append};
/// Changes from a combination of [`bdk_chain`] structures.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(bdk_chain::serde::Deserialize, bdk_chain::serde::Serialize),
serde(
crate = "bdk_chain::serde",
bound(
deserialize = "A: Ord + bdk_chain::serde::Deserialize<'de>, K: Ord + bdk_chain::serde::Deserialize<'de>",
serialize = "A: Ord + bdk_chain::serde::Serialize, K: Ord + bdk_chain::serde::Serialize",
),
)
)]
pub struct CombinedChangeSet<K, A> {
/// Changes to the [`LocalChain`](local_chain::LocalChain).
pub chain: local_chain::ChangeSet,
/// Changes to [`IndexedTxGraph`](indexed_tx_graph::IndexedTxGraph).
pub indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
/// Stores the network type of the transaction data.
pub network: Option<Network>,
}
impl<K, A> Default for CombinedChangeSet<K, A> {
fn default() -> Self {
Self {
chain: Default::default(),
indexed_tx_graph: Default::default(),
network: None,
}
}
}
impl<K: Ord, A: Anchor> Append for CombinedChangeSet<K, A> {
fn append(&mut self, other: Self) {
Append::append(&mut self.chain, other.chain);
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
if other.network.is_some() {
debug_assert!(
self.network.is_none() || self.network == other.network,
"network type must either be just introduced or remain the same"
);
self.network = other.network;
}
}
fn is_empty(&self) -> bool {
self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
}
}
impl<K, A> From<local_chain::ChangeSet> for CombinedChangeSet<K, A> {
fn from(chain: local_chain::ChangeSet) -> Self {
Self {
chain,
..Default::default()
}
}
}
impl<K, A> From<indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>>
for CombinedChangeSet<K, A>
{
fn from(indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>) -> Self {
Self {
indexed_tx_graph,
..Default::default()
}
}
}

View File

@@ -1,5 +1,8 @@
#![doc = include_str!("../README.md")]
#![no_std]
#![warn(missing_docs)]
mod changeset;
mod persist;
pub use changeset::*;
pub use persist::*;

19
crates/sqlite/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "bdk_sqlite"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_sqlite"
description = "A simple SQLite based implementation of Persist for Bitcoin Dev Kit."
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
anyhow = { version = "1", default-features = false }
bdk_chain = { path = "../chain", version = "0.15.0", features = ["serde", "miniscript"] }
bdk_persist = { path = "../persist", version = "0.3.0", features = ["serde"] }
rusqlite = { version = "0.31.0", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

8
crates/sqlite/README.md Normal file
View File

@@ -0,0 +1,8 @@
# BDK SQLite
This is a simple [SQLite] relational database schema backed implementation of [`PersistBackend`](bdk_persist::PersistBackend).
The main structure is `Store` which persists [`bdk_persist`] `CombinedChangeSet` data into a SQLite database file.
[`bdk_persist`]:https://docs.rs/bdk_persist/latest/bdk_persist/
[SQLite]: https://www.sqlite.org/index.html

View File

@@ -0,0 +1,69 @@
-- schema version control
CREATE TABLE version
(
version INTEGER
) STRICT;
INSERT INTO version
VALUES (1);
-- network is the valid network for all other table data
CREATE TABLE network
(
name TEXT UNIQUE NOT NULL
) STRICT;
-- keychain is the json serialized keychain structure as JSONB,
-- descriptor is the complete descriptor string,
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
-- last revealed index is a u32
CREATE TABLE keychain
(
keychain BLOB PRIMARY KEY NOT NULL,
descriptor TEXT NOT NULL,
descriptor_id BLOB NOT NULL,
last_revealed INTEGER
) STRICT;
-- hash is block hash hex string,
-- block height is a u32,
CREATE TABLE block
(
hash TEXT PRIMARY KEY NOT NULL,
height INTEGER NOT NULL
) STRICT;
-- txid is transaction hash hex string (reversed)
-- whole_tx is a consensus encoded transaction,
-- last seen is a u64 unix epoch seconds
CREATE TABLE tx
(
txid TEXT PRIMARY KEY NOT NULL,
whole_tx BLOB,
last_seen INTEGER
) STRICT;
-- Outpoint txid hash hex string (reversed)
-- Outpoint vout
-- TxOut value as SATs
-- TxOut script consensus encoded
CREATE TABLE txout
(
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
value INTEGER NOT NULL,
script BLOB NOT NULL,
PRIMARY KEY (txid, vout)
) STRICT;
-- join table between anchor and tx
-- block hash hex string
-- anchor is a json serialized Anchor structure as JSONB,
-- txid is transaction hash hex string (reversed)
CREATE TABLE anchor_tx
(
block_hash TEXT NOT NULL,
anchor BLOB NOT NULL,
txid TEXT NOT NULL REFERENCES tx (txid),
UNIQUE (anchor, txid),
FOREIGN KEY (block_hash) REFERENCES block(hash)
) STRICT;

34
crates/sqlite/src/lib.rs Normal file
View File

@@ -0,0 +1,34 @@
#![doc = include_str!("../README.md")]
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
mod schema;
mod store;
use bdk_chain::bitcoin::Network;
pub use rusqlite;
pub use store::Store;
/// Error that occurs while reading or writing change sets with the SQLite database.
#[derive(Debug)]
pub enum Error {
/// Invalid network, cannot change the one already stored in the database.
Network { expected: Network, given: Network },
/// SQLite error.
Sqlite(rusqlite::Error),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Network { expected, given } => write!(
f,
"network error trying to read or write change set, expected {}, given {}",
expected, given
),
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
}
}
}
impl std::error::Error for Error {}

View File

@@ -0,0 +1,96 @@
use crate::Store;
use rusqlite::{named_params, Connection, Error};
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
const MIGRATIONS: &[&str] = &[SCHEMA_0];
/// Schema migration related functions.
impl<K, A> Store<K, A> {
/// Migrate sqlite db schema to latest version.
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
let stmts = &MIGRATIONS
.iter()
.flat_map(|stmt| {
// remove comment lines
let s = stmt
.split('\n')
.filter(|l| !l.starts_with("--") && !l.is_empty())
.collect::<Vec<_>>()
.join(" ");
// split into statements
s.split(';')
// remove extra spaces
.map(|s| {
s.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
})
// remove empty statements
.filter(|s| !s.is_empty())
.collect::<Vec<String>>();
let version = Self::get_schema_version(conn)?;
let stmts = &stmts[(version as usize)..];
// begin transaction, all migration statements and new schema version commit or rollback
let tx = conn.transaction()?;
// execute every statement and return `Some` new schema version
// if execution fails, return `Error::Rusqlite`
// if no statements executed returns `None`
let new_version = stmts
.iter()
.enumerate()
.map(|version_stmt| {
tx.execute(version_stmt.1.as_str(), [])
// map result value to next migration version
.map(|_| version_stmt.0 as i32 + version + 1)
})
.last()
.transpose()?;
// if `Some` new statement version, set new schema version
if let Some(version) = new_version {
Self::set_schema_version(&tx, version)?;
}
// commit transaction
tx.commit()?;
Ok(())
}
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
let statement = conn.prepare_cached("SELECT version FROM version");
match statement {
Err(Error::SqliteFailure(e, Some(msg))) => {
if msg == "no such table: version" {
Ok(0)
} else {
Err(Error::SqliteFailure(e, Some(msg)))
}
}
Ok(mut stmt) => {
let mut rows = stmt.query([])?;
match rows.next()? {
Some(row) => {
let version: i32 = row.get(0)?;
Ok(version)
}
None => Ok(0),
}
}
_ => Ok(0),
}
}
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
conn.execute(
"UPDATE version SET version=:version",
named_params! {":version": version},
)
}
}

779
crates/sqlite/src/store.rs Normal file
View File

@@ -0,0 +1,779 @@
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
use bdk_chain::bitcoin::hashes::Hash;
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
use bdk_chain::bitcoin::{BlockHash, Txid};
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use rusqlite::{named_params, Connection};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crate::Error;
use bdk_chain::{
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
};
use bdk_persist::CombinedChangeSet;
/// Persists data in to a relational schema based [SQLite] database file.
///
/// The changesets loaded or stored represent changes to keychain and blockchain data.
///
/// [SQLite]: https://www.sqlite.org/index.html
pub struct Store<K, A> {
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
conn: Mutex<Connection>,
keychain_marker: PhantomData<K>,
anchor_marker: PhantomData<A>,
}
impl<K, A> Debug for Store<K, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.conn, f)
}
}
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Creates a new store from a [`Connection`].
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
Self::migrate(&mut conn)?;
Ok(Self {
conn: Mutex::new(conn),
keychain_marker: Default::default(),
anchor_marker: Default::default(),
})
}
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
let connection = self.conn.get_mut().expect("unlocked connection mutex");
connection.transaction().map_err(Error::Sqlite)
}
}
impl<K, A, C> bdk_persist::PersistBackend<C> for Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
C: Clone + From<CombinedChangeSet<K, A>> + Into<CombinedChangeSet<K, A>>,
{
fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()> {
self.write(&changeset.clone().into())
.map_err(|e| anyhow::anyhow!(e).context("unable to write changes to sqlite database"))
}
fn load_from_persistence(&mut self) -> anyhow::Result<Option<C>> {
self.read()
.map(|c| c.map(Into::into))
.map_err(|e| anyhow::anyhow!(e).context("unable to read changes from sqlite database"))
}
}
/// Network table related functions.
impl<K, A> Store<K, A> {
/// Insert [`Network`] for which all other tables data is valid.
///
/// Error if trying to insert different network value.
fn insert_network(
current_network: &Option<Network>,
db_transaction: &rusqlite::Transaction,
network_changeset: &Option<Network>,
) -> Result<(), Error> {
if let Some(network) = network_changeset {
match current_network {
// if no network change do nothing
Some(current_network) if current_network == network => Ok(()),
// if new network not the same as current, error
Some(current_network) => Err(Error::Network {
expected: *current_network,
given: *network,
}),
// insert network if none exists
None => {
let insert_network_stmt = &mut db_transaction
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
.expect("insert network statement");
let name = network.to_string();
insert_network_stmt
.execute(named_params! {":name": name })
.map_err(Error::Sqlite)?;
Ok(())
}
}
} else {
Ok(())
}
}
/// Select the valid [`Network`] for this database, or `None` if not set.
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
let mut select_network_stmt = db_transaction
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
.expect("select network statement");
let network = select_network_stmt
.query_row([], |row| {
let network = row.get_unwrap::<usize, String>(0);
let network = Network::from_str(network.as_str()).expect("valid network");
Ok(network)
})
.map_err(Error::Sqlite);
match network {
Ok(network) => Ok(Some(network)),
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(e) => Err(e),
}
}
}
/// Block table related functions.
impl<K, A> Store<K, A> {
/// Insert or delete local chain blocks.
///
/// Error if trying to insert existing block hash.
fn insert_or_delete_blocks(
db_transaction: &rusqlite::Transaction,
chain_changeset: &local_chain::ChangeSet,
) -> Result<(), Error> {
for (height, hash) in chain_changeset.iter() {
match hash {
// add new hash at height
Some(hash) => {
let insert_block_stmt = &mut db_transaction
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
.expect("insert block statement");
let hash = hash.to_string();
insert_block_stmt
.execute(named_params! {":hash": hash, ":height": height })
.map_err(Error::Sqlite)?;
}
// delete block at height
None => {
let delete_block_stmt = &mut db_transaction
.prepare_cached("DELETE FROM block WHERE height IS :height")
.expect("delete block statement");
delete_block_stmt
.execute(named_params! {":height": height })
.map_err(Error::Sqlite)?;
}
}
}
Ok(())
}
/// Select all blocks.
fn select_blocks(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
let mut select_blocks_stmt = db_transaction
.prepare_cached("SELECT height, hash FROM block")
.expect("select blocks statement");
let blocks = select_blocks_stmt
.query_map([], |row| {
let height = row.get_unwrap::<usize, u32>(0);
let hash = row.get_unwrap::<usize, String>(1);
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
Ok((height, hash))
})
.map_err(Error::Sqlite)?;
blocks
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Keychain table related functions.
///
/// The keychain objects are stored as [`JSONB`] data.
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + Send,
{
/// Insert keychain with descriptor and last active index.
///
/// If keychain exists only update last active index.
fn insert_keychains(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
let insert_keychain_stmt = &mut db_transaction
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
.expect("insert keychain statement");
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
let descriptor_id = descriptor.descriptor_id().to_byte_array();
let descriptor = descriptor.to_string();
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Update descriptor last revealed index.
fn update_last_revealed(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
let update_last_revealed_stmt = &mut db_transaction
.prepare_cached(
"UPDATE keychain SET last_revealed = :last_revealed
WHERE descriptor_id = :descriptor_id",
)
.expect("update last revealed statement");
let descriptor_id = descriptor_id.to_byte_array();
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select keychains added.
fn select_keychains(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
let mut select_keychains_added_stmt = db_transaction
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
.expect("select keychains statement");
let keychains = select_keychains_added_stmt
.query_map([], |row| {
let keychain = row.get_unwrap::<usize, String>(0);
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
let descriptor = row.get_unwrap::<usize, String>(1);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
Ok((keychain, descriptor))
})
.map_err(Error::Sqlite)?;
keychains
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select descriptor last revealed indexes.
fn select_last_revealed(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
let mut select_last_revealed_stmt = db_transaction
.prepare_cached(
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
)
.expect("select last revealed statement");
let last_revealed = select_last_revealed_stmt
.query_map([], |row| {
let descriptor = row.get_unwrap::<usize, String>(0);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
let descriptor_id = descriptor.descriptor_id();
let last_revealed = row.get_unwrap::<usize, u32>(1);
Ok((descriptor_id, last_revealed))
})
.map_err(Error::Sqlite)?;
last_revealed
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Tx (transaction) and txout (transaction output) table related functions.
impl<K, A> Store<K, A> {
/// Insert transactions.
///
/// Error if trying to insert existing txid.
fn insert_txs(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for tx in tx_graph_changeset.graph.txs.iter() {
let insert_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
.expect("insert or update tx whole_tx statement");
let txid = tx.txid().to_string();
let whole_tx = serialize(&tx);
insert_tx_stmt
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transactions.
fn select_txs(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
let mut select_tx_stmt = db_transaction
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
.expect("select tx statement");
let txs = select_tx_stmt
.query_map([], |row| {
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
Ok(Arc::new(whole_tx))
})
.map_err(Error::Sqlite)?;
txs.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select all transactions with last_seen values.
fn select_last_seen(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<Txid, u64>, Error> {
// load tx last_seen
let mut select_last_seen_stmt = db_transaction
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
.expect("select tx last seen statement");
let last_seen = select_last_seen_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let last_seen = row.get_unwrap::<usize, u64>(1);
Ok((txid, last_seen))
})
.map_err(Error::Sqlite)?;
last_seen
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Insert txouts.
///
/// Error if trying to insert existing outpoint.
fn insert_txouts(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for txout in tx_graph_changeset.graph.txouts.iter() {
let insert_txout_stmt = &mut db_transaction
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
.expect("insert txout statement");
let txid = txout.0.txid.to_string();
let vout = txout.0.vout;
let value = txout.1.value.to_sat();
let script = txout.1.script_pubkey.as_bytes();
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transaction outputs.
fn select_txouts(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
// load tx outs
let mut select_txout_stmt = db_transaction
.prepare_cached("SELECT txid, vout, value, script FROM txout")
.expect("select txout statement");
let txouts = select_txout_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let vout = row.get_unwrap::<usize, u32>(1);
let outpoint = OutPoint::new(txid, vout);
let value = row.get_unwrap::<usize, u64>(2);
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
let txout = TxOut {
value: Amount::from_sat(value),
script_pubkey,
};
Ok((outpoint, txout))
})
.map_err(Error::Sqlite)?;
txouts
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Update transaction last seen times.
fn update_last_seen(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
let insert_or_update_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
.expect("insert or update tx last_seen statement");
let txid = tx_last_seen.0.to_string();
let last_seen = *tx_last_seen.1;
insert_or_update_tx_stmt
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
.map_err(Error::Sqlite)?;
}
Ok(())
}
}
/// Anchor table related functions.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Insert anchors.
fn insert_anchors(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
) -> Result<(), Error> {
// serde_json::to_string
for anchor in tx_graph_changeset.graph.anchors.iter() {
let insert_anchor_stmt = &mut db_transaction
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
.expect("insert anchor statement");
let block_hash = anchor.0.anchor_block().hash.to_string();
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
let txid = anchor.1.to_string();
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all anchors.
fn select_anchors(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<(A, Txid)>, Error> {
// serde_json::from_str
let mut select_anchor_stmt = db_transaction
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
.expect("select anchor statement");
let anchors = select_anchor_stmt
.query_map([], |row| {
let hash = row.get_unwrap::<usize, String>(0);
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
let anchor = row.get_unwrap::<usize, String>(1);
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
// double check anchor blob block hash matches
assert_eq!(hash, anchor.anchor_block().hash);
let txid = row.get_unwrap::<usize, String>(2);
let txid = Txid::from_str(&txid).expect("txid");
Ok((anchor, txid))
})
.map_err(Error::Sqlite)?;
anchors
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Functions to read and write all [`ChangeSet`] data.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
// no need to write anything if changeset is empty
if changeset.is_empty() {
return Ok(());
}
let db_transaction = self.db_transaction()?;
let network_changeset = &changeset.network;
let current_network = Self::select_network(&db_transaction)?;
Self::insert_network(&current_network, &db_transaction, network_changeset)?;
let chain_changeset = &changeset.chain;
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
let tx_graph_changeset = &changeset.indexed_tx_graph;
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
db_transaction.commit().map_err(Error::Sqlite)
}
fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
let db_transaction = self.db_transaction()?;
let network = Self::select_network(&db_transaction)?;
let chain = Self::select_blocks(&db_transaction)?;
let keychains_added = Self::select_keychains(&db_transaction)?;
let last_revealed = Self::select_last_revealed(&db_transaction)?;
let txs = Self::select_txs(&db_transaction)?;
let last_seen = Self::select_last_seen(&db_transaction)?;
let txouts = Self::select_txouts(&db_transaction)?;
let anchors = Self::select_anchors(&db_transaction)?;
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
txs,
txouts,
anchors,
last_seen,
};
let indexer: keychain::ChangeSet<K> = keychain::ChangeSet {
keychains_added,
last_revealed,
};
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>> =
indexed_tx_graph::ChangeSet { graph, indexer };
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
Ok(None)
} else {
Ok(Some(CombinedChangeSet {
chain,
indexed_tx_graph,
network,
}))
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::Append;
use bdk_chain::bitcoin::consensus::encode::deserialize;
use bdk_chain::bitcoin::constants::genesis_block;
use bdk_chain::bitcoin::hashes::hex::FromHex;
use bdk_chain::bitcoin::transaction::Transaction;
use bdk_chain::bitcoin::Network::Testnet;
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
use bdk_chain::miniscript::Descriptor;
use bdk_chain::{
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
ConfirmationTimeHeightAnchor, DescriptorExt,
};
use bdk_persist::PersistBackend;
use std::str::FromStr;
use std::sync::Arc;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
enum Keychain {
External { account: u32, name: String },
Internal { account: u32, name: String },
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor(
) -> anyhow::Result<()> {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
confirmation_height: height,
confirmation_time: time,
anchor_block: (height, hash).into(),
});
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
});
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
Ok(())
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() -> anyhow::Result<()>
{
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
confirmation_height: height,
anchor_block: (height, hash).into(),
});
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
});
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
Ok(())
}
#[test]
fn insert_and_load_aggregate_changesets_with_blockid_anchor() -> anyhow::Result<()> {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
});
let agg_changeset = store.load_from_persistence().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
Ok(())
}
fn create_test_changesets<A: Anchor + Copy>(
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
) -> (
Vec<CombinedChangeSet<Keychain, A>>,
CombinedChangeSet<Keychain, A>,
) {
let secp = &secp256k1::Secp256k1::signing_only();
let network_changeset = Some(Testnet);
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
let block_hash_1 =
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
.unwrap();
let block_hash_2 =
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
.unwrap();
let block_changeset = [
(0, Some(block_hash_0)),
(1, Some(block_hash_1)),
(2, Some(block_hash_2)),
]
.into();
let ext_keychain = Keychain::External {
account: 0,
name: "ext test".to_string(),
};
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
let ext_desc_id = ext_desc.descriptor_id();
let int_keychain = Keychain::Internal {
account: 0,
name: "int test".to_string(),
};
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
let int_desc_id = int_desc.descriptor_id();
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
let outpoint0_0 = OutPoint::new(tx0.txid(), 0);
let txout0_0 = tx0.output.first().unwrap().clone();
let outpoint1_0 = OutPoint::new(tx1.txid(), 0);
let txout1_0 = tx1.output.first().unwrap().clone();
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
txs: [tx0.clone(), tx1.clone()].into(),
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
anchors: [(anchor1, tx0.txid()), (anchor1, tx1.txid())].into(),
last_seen: [
(tx0.txid(), 1598918400),
(tx1.txid(), 1598919121),
(tx2.txid(), 1608919121),
]
.into(),
};
let keychain_changeset = keychain::ChangeSet {
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
};
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset,
indexer: keychain_changeset,
};
// test changesets to write to db
let mut changesets = Vec::new();
changesets.push(CombinedChangeSet {
chain: block_changeset,
indexed_tx_graph: graph_changeset,
network: network_changeset,
});
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
txs: [tx2.clone()].into(),
txouts: BTreeMap::default(),
anchors: BTreeSet::default(),
last_seen: [(tx2.txid(), 1708919121)].into(),
};
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset2,
indexer: keychain::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset2,
network: None,
});
// create changeset that adds a new anchor2 for tx0 and tx1
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
txs: BTreeSet::default(),
txouts: BTreeMap::default(),
anchors: [(anchor2, tx0.txid()), (anchor2, tx1.txid())].into(),
last_seen: BTreeMap::default(),
};
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset3,
indexer: keychain::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset3,
network: None,
});
// aggregated test changesets
let agg_test_changesets =
changesets
.iter()
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
i.append(cs.clone());
i
});
(changesets, agg_test_changesets)
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_testenv"
version = "0.3.0"
version = "0.5.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -13,10 +13,8 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitcoincore-rpc = { version = "0.18" }
bdk_chain = { path = "../chain", version = "0.13", default-features = false }
bdk_chain = { path = "../chain", version = "0.15", default-features = false }
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
anyhow = { version = "1" }
[features]
default = ["std"]

View File

@@ -11,6 +11,11 @@ use bitcoincore_rpc::{
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
RpcApi,
};
pub use electrsd;
pub use electrsd::bitcoind;
pub use electrsd::bitcoind::anyhow;
pub use electrsd::bitcoind::bitcoincore_rpc;
pub use electrsd::electrum_client;
use electrsd::electrum_client::ElectrumApi;
use std::time::Duration;
@@ -261,8 +266,7 @@ impl TestEnv {
#[cfg(test)]
mod test {
use crate::TestEnv;
use anyhow::Result;
use bitcoincore_rpc::RpcApi;
use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
#[test]

View File

@@ -1,7 +1,7 @@
[package]
name = "bdk"
name = "bdk_wallet"
homepage = "https://bitcoindevkit.org"
version = "1.0.0-alpha.10"
version = "1.0.0-alpha.12"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library"
@@ -19,8 +19,8 @@ miniscript = { version = "11.0.0", features = ["serde"], default-features = fals
bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.13.0", features = ["miniscript", "serde"], default-features = false }
bdk_persist = { path = "../persist", version = "0.1.0" }
bdk_chain = { path = "../chain", version = "0.15.0", features = ["miniscript", "serde"], default-features = false }
bdk_persist = { path = "../persist", version = "0.3.0", features = ["miniscript", "serde"], default-features = false }
# Optional dependencies
bip39 = { version = "2.0", optional = true }
@@ -45,6 +45,7 @@ dev-getrandom-wasm = ["getrandom/js"]
lazy_static = "1.4"
assert_matches = "1.5.0"
tempfile = "3"
bdk_sqlite = { path = "../sqlite" }
bdk_file_store = { path = "../file_store" }
anyhow = "1"

View File

@@ -8,11 +8,11 @@
</p>
<p>
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
<a href="https://crates.io/crates/bdk_wallet"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk_wallet.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://docs.rs/bdk_wallet"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk_wallet-green"/></a>
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.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>
@@ -20,13 +20,13 @@
<h4>
<a href="https://bitcoindevkit.org">Project Homepage</a>
<span> | </span>
<a href="https://docs.rs/bdk">Documentation</a>
<a href="https://docs.rs/bdk_wallet">Documentation</a>
</h4>
</div>
## `bdk`
# BDK Wallet
The `bdk` crate provides the [`Wallet`] type which is a simple, high-level
The `bdk_wallet` crate provides the [`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
@@ -36,7 +36,7 @@ can create and sign transactions.
For details about the API of `Wallet` see the [module-level documentation][`Wallet`].
### Blockchain data
## Blockchain data
In order to get blockchain data for `Wallet` to consume, you should configure a client from
an available chain source. Typically you make a request to the chain source and get a response
@@ -55,7 +55,7 @@ that the `Wallet` can use to update its view of the chain.
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
### Persistence
## Persistence
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
@@ -67,7 +67,7 @@ To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`
<!-- compile_fail because outpoint and txout are fake variables -->
```rust,compile_fail
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
use bdk_wallet::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
fn main() {
// Create a new file `Store`.
@@ -85,13 +85,13 @@ fn main() {
<!-- ### 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; -->
<!-- use bdk_wallet::Wallet; -->
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
<!-- use bdk_wallet::SyncOptions; -->
<!-- use bdk_wallet::electrum_client::Client; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
<!-- let wallet = Wallet::new( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
@@ -109,11 +109,11 @@ fn main() {
<!-- ### Generate a few addresses -->
<!-- ```rust -->
<!-- use bdk::Wallet; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- use bdk::bitcoin::Network; -->
<!-- use bdk_wallet::Wallet; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
@@ -131,17 +131,17 @@ fn main() {
<!-- ### Create a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
<!-- use bdk::blockchain::ElectrumBlockchain; -->
<!-- use bdk_wallet::{FeeRate, Wallet, SyncOptions}; -->
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
<!-- use bdk::electrum_client::Client; -->
<!-- use bdk::wallet::AddressIndex::New; -->
<!-- use bdk_wallet::electrum_client::Client; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bitcoin::base64; -->
<!-- use bdk::bitcoin::consensus::serialize; -->
<!-- use bdk::bitcoin::Network; -->
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- fn main() -> Result<(), bdk_wallet::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/*)", -->
@@ -172,13 +172,13 @@ fn main() {
<!-- ### Sign a transaction -->
<!-- ```rust,no_run -->
<!-- use bdk::{Wallet, SignOptions}; -->
<!-- use bdk_wallet::{Wallet, SignOptions}; -->
<!-- use bitcoin::base64; -->
<!-- use bdk::bitcoin::consensus::deserialize; -->
<!-- use bdk::bitcoin::Network; -->
<!-- use bdk_wallet::bitcoin::consensus::deserialize; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk::Error> { -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
<!-- let wallet = Wallet::new_no_persist( -->
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
@@ -202,7 +202,7 @@ fn main() {
cargo test
```
## License
# License
Licensed under either of
@@ -211,15 +211,15 @@ Licensed under either of
at your option.
### Contribution
# 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.
[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html
[`PersistBackend`]: https://docs.rs/bdk_persist/latest/bdk_persist/trait.PersistBackend.html
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
[`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

View File

@@ -9,7 +9,7 @@
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk;
extern crate bdk_wallet;
extern crate bitcoin;
extern crate miniscript;
extern crate serde_json;
@@ -21,7 +21,7 @@ use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
use bdk::{KeychainKind, Wallet};
use bdk_wallet::{KeychainKind, Wallet};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html

View File

@@ -7,14 +7,14 @@
// licenses.
use anyhow::anyhow;
use bdk::bitcoin::bip32::DerivationPath;
use bdk::bitcoin::secp256k1::Secp256k1;
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_wallet::bitcoin::bip32::DerivationPath;
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::descriptor;
use bdk_wallet::descriptor::IntoWalletDescriptor;
use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount};
use bdk_wallet::keys::{GeneratableKey, GeneratedKey};
use bdk_wallet::miniscript::Tap;
use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase

View File

@@ -9,14 +9,14 @@
// You may not use this file except in accordance with one or both of these
// licenses.
extern crate bdk;
extern crate bdk_wallet;
use std::error::Error;
use bdk::bitcoin::Network;
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk::wallet::signer::SignersContainer;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk_wallet::wallet::signer::SignersContainer;
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
///
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
/// This is useful to express complex miniscript spending conditions into more human readable form.
@@ -34,11 +34,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
// Use the descriptor string to derive the full descriptor and a keymap.
// The wallet descriptor can be used to create a new bdk::wallet.
// The wallet descriptor can be used to create a new bdk_wallet::wallet.
// While the `keymap` can be used to create a `SignerContainer`.
//
// The `SignerContainer` can sign for `PSBT`s.
// a bdk::wallet internally uses these to handle transaction signing.
// a bdk_wallet::wallet internally uses these to handle transaction signing.
// But they can be used as independent tools also.
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;

View File

@@ -423,7 +423,7 @@ macro_rules! apply_modifier {
///
/// ```
/// # use std::str::FromStr;
/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
/// let (my_descriptor, my_keys_map, networks) = bdk_wallet::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
@@ -444,7 +444,7 @@ macro_rules! apply_modifier {
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
/// let my_timelock = 50;
///
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
/// let (descriptor_a, key_map_a, networks) = bdk_wallet::descriptor! {
/// wsh (
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
/// )
@@ -452,11 +452,12 @@ macro_rules! apply_modifier {
///
/// #[rustfmt::skip]
/// let b_items = vec![
/// bdk::fragment!(pk(my_key_1))?,
/// bdk::fragment!(s:pk(my_key_2))?,
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
/// bdk_wallet::fragment!(pk(my_key_1))?,
/// bdk_wallet::fragment!(s:pk(my_key_2))?,
/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?,
/// ];
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
/// let (descriptor_b, mut key_map_b, networks) =
/// bdk_wallet::descriptor!(wsh(thresh_vec(2, b_items)))?;
///
/// assert_eq!(descriptor_a, descriptor_b);
/// assert_eq!(key_map_a.len(), key_map_b.len());
@@ -475,7 +476,7 @@ macro_rules! apply_modifier {
/// let my_key_2 =
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
///
/// let (descriptor, key_map, networks) = bdk::descriptor! {
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! {
/// wsh (
/// multi(2, my_key_1, my_key_2)
/// )
@@ -491,7 +492,7 @@ macro_rules! apply_modifier {
/// let my_key =
/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?;
///
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///

View File

@@ -20,10 +20,10 @@
//!
//! ```
//! # use std::sync::Arc;
//! # use bdk::descriptor::*;
//! # use bdk::wallet::signer::*;
//! # use bdk::bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::policy::BuildSatisfaction;
//! # use bdk_wallet::descriptor::*;
//! # use bdk_wallet::wallet::signer::*;
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new();
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
//!

View File

@@ -36,17 +36,17 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// ## Example
///
/// ```
/// use bdk::descriptor::error::Error as DescriptorError;
/// use bdk::keys::{IntoDescriptorKey, KeyError};
/// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
/// use bdk_wallet::descriptor::error::Error as DescriptorError;
/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError};
/// use bdk_wallet::miniscript::Legacy;
/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut};
/// use bitcoin::Network;
///
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
///
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
/// Ok(bdk::descriptor!(pkh(self.0))?)
/// Ok(bdk_wallet::descriptor!(pkh(self.0))?)
/// }
/// }
/// ```
@@ -72,10 +72,10 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// ## Example
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::KeychainKind;
/// use bdk::template::P2Pkh;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Pkh;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
@@ -102,10 +102,10 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// ## Example
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::KeychainKind;
/// use bdk::template::P2Wpkh_P2Sh;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh_P2Sh;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
@@ -133,10 +133,10 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// ## Example
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet};
/// # use bdk::KeychainKind;
/// use bdk::template::P2Wpkh;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet};
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
@@ -163,10 +163,10 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// ## Example
///
/// ```
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::Wallet;
/// # use bdk::KeychainKind;
/// use bdk::template::P2TR;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2TR;
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
@@ -198,9 +198,9 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip44;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip44;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
@@ -234,9 +234,9 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip44Public;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip44Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
@@ -271,9 +271,9 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip49;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip49;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
@@ -307,9 +307,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip49Public;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip49Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
@@ -344,9 +344,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip84;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip84;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
@@ -380,9 +380,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip84Public;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip84Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
@@ -417,9 +417,9 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip86;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip86;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new_no_persist(
@@ -453,9 +453,9 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
///
/// ```
/// # use std::str::FromStr;
/// # use bdk::bitcoin::{PrivateKey, Network};
/// # use bdk::{Wallet, KeychainKind};
/// use bdk::template::Bip86Public;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip86Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;

View File

@@ -97,7 +97,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
}
}
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
// This method is used internally by `bdk_wallet::fragment!` and `bdk_wallet::descriptor!`. It has to be
// 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)]
@@ -206,9 +206,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type valid in any context:
///
/// ```
/// use bdk::bitcoin::PublicKey;
/// use bdk_wallet::bitcoin::PublicKey;
///
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
///
/// pub struct MyKeyType {
/// pubkey: PublicKey,
@@ -224,9 +224,9 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type that is only valid on mainnet:
///
/// ```
/// use bdk::bitcoin::PublicKey;
/// use bdk_wallet::bitcoin::PublicKey;
///
/// use bdk::keys::{
/// use bdk_wallet::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
/// ScriptContext, SinglePub, SinglePubKey,
/// };
@@ -251,9 +251,11 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// Key type that internally encodes in which context it's valid. The context is checked at runtime:
///
/// ```
/// use bdk::bitcoin::PublicKey;
/// use bdk_wallet::bitcoin::PublicKey;
///
/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
/// use bdk_wallet::keys::{
/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext,
/// };
///
/// pub struct MyKeyType {
/// is_legacy: bool,
@@ -279,17 +281,17 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// makes the compiler (correctly) fail.
///
/// ```compile_fail
/// use bdk::bitcoin::PublicKey;
/// use bdk_wallet::bitcoin::PublicKey;
/// use core::str::FromStr;
///
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
///
/// pub struct MySegwitOnlyKeyType {
/// pubkey: PublicKey,
/// }
///
/// impl IntoDescriptorKey<bdk::miniscript::Segwitv0> for MySegwitOnlyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk::miniscript::Segwitv0>, KeyError> {
/// impl IntoDescriptorKey<bdk_wallet::miniscript::Segwitv0> for MySegwitOnlyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<bdk_wallet::miniscript::Segwitv0>, KeyError> {
/// self.pubkey.into_descriptor_key()
/// }
/// }
@@ -297,8 +299,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// let key = MySegwitOnlyKeyType {
/// pubkey: PublicKey::from_str("...")?,
/// };
/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?;
/// // ^^^^^ changing this to `wpkh` would make it compile
/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?;
/// // ^^^^^ changing this to `wpkh` would make it compile
///
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
@@ -387,9 +389,9 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
///
/// ```
/// use bdk::bitcoin;
/// use bdk::bitcoin::bip32;
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
/// use bdk_wallet::bitcoin;
/// use bdk_wallet::bitcoin::bip32;
/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
///
/// struct MyCustomKeyType {
/// key_data: bitcoin::PrivateKey,
@@ -418,9 +420,9 @@ impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
/// [`Xpriv`] or [`Xpub`] will be considered valid.
///
/// ```
/// use bdk::bitcoin;
/// use bdk::bitcoin::bip32;
/// use bdk::keys::{
/// use bdk_wallet::bitcoin;
/// use bdk_wallet::bitcoin::bip32;
/// use bdk_wallet::keys::{
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
/// };
///
@@ -469,9 +471,9 @@ pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
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};
use bdk::keys::bip39::{Mnemonic, Language};
use bdk_wallet::bitcoin::Network;
use bdk_wallet::keys::{DerivableKey, ExtendedKey};
use bdk_wallet::keys::bip39::{Mnemonic, Language};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let xkey: ExtendedKey =
@@ -764,7 +766,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((pks, key_map, valid_networks))
}
// Used internally by `bdk::fragment!` to build `pk_k()` fragments
// Used internally by `bdk_wallet::fragment!` to build `pk_k()` fragments
#[doc(hidden)]
pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
descriptor_key: Pk,
@@ -778,7 +780,7 @@ pub fn make_pk<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk::fragment!` to build `pk_h()` fragments
// Used internally by `bdk_wallet::fragment!` to build `pk_h()` fragments
#[doc(hidden)]
pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
descriptor_key: Pk,
@@ -792,7 +794,7 @@ pub fn make_pkh<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk::fragment!` to build `multi()` fragments
// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments
#[doc(hidden)]
pub fn make_multi<
Pk: IntoDescriptorKey<Ctx>,
@@ -812,7 +814,7 @@ pub fn make_multi<
Ok((minisc, key_map, valid_networks))
}
// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments
// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments
#[doc(hidden)]
pub fn make_sortedmulti<Pk, Ctx, F>(
thresh: usize,
@@ -834,7 +836,7 @@ where
Ok((descriptor, key_map, valid_networks))
}
/// The "identity" conversion is used internally by some `bdk::fragment`s
/// The "identity" conversion is used internally by some `bdk_wallet::fragment`s
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
Ok(self)

View File

@@ -26,11 +26,11 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk_persist::PersistBackend;
//! # use bdk::*;
//! # use bdk::wallet::coin_selection::decide_change;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::coin_selection::decide_change;
//! # use anyhow::Error;
//! #[derive(Debug)]
//! struct AlwaysSpendEverything;
@@ -92,7 +92,7 @@
//! .unwrap();
//! let psbt = {
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
//! builder.add_recipient(to_address.script_pubkey(), 50_000);
//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
//! builder.finish()?
//! };
//!

View File

@@ -20,8 +20,8 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
//! let import = r#"{
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
//! "blockheight":1782088,
@@ -40,8 +40,8 @@
//! ### Export a `Wallet`
//! ```
//! # use bitcoin::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
//! let wallet = Wallet::new_no_persist(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
@@ -189,6 +189,7 @@ impl FullyNodedExport {
WshInner::SortedMulti(_) => Ok(()),
WshInner::Ms(ms) => check_ms(&ms.node),
},
Descriptor::Tr(_) => Ok(()),
_ => Err("The descriptor is not compatible with Bitcoin Core"),
}
}
@@ -314,6 +315,18 @@ mod test {
assert_eq!(export.label, "Test Label");
}
#[test]
fn test_export_tr() {
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
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);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
#[test]
fn test_export_to_json() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";

View File

@@ -14,11 +14,11 @@
//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk::bitcoin::Network;
//! # use bdk::signer::SignerOrdering;
//! # use bdk::wallet::hardwaresigner::HWISigner;
//! # use bdk::wallet::AddressIndex::New;
//! # use bdk::{KeychainKind, SignOptions, Wallet};
//! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::wallet::AddressIndex::New;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #

View File

@@ -22,7 +22,7 @@ use alloc::{
pub use bdk_chain::keychain::Balance;
use bdk_chain::{
indexed_tx_graph,
keychain::{self, KeychainTxOutIndex},
keychain::KeychainTxOutIndex,
local_chain::{
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
},
@@ -32,14 +32,14 @@ use bdk_chain::{
IndexedTxGraph,
};
use bdk_persist::{Persist, PersistBackend};
use bitcoin::constants::genesis_block;
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{
absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence,
Transaction, TxOut, Txid, Witness,
};
use bitcoin::{consensus::encode::serialize, transaction, Amount, BlockHash, Psbt};
use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
use bitcoin::{constants::genesis_block, Amount};
use core::fmt;
use core::ops::Deref;
use descriptor::error::Error as DescriptorError;
@@ -54,11 +54,12 @@ pub mod tx_builder;
pub(crate) mod utils;
pub mod error;
pub use utils::IsDust;
use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
use tx_builder::{FeePolicy, TxBuilder, TxParams};
use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::descriptor::policy::BuildSatisfaction;
@@ -133,72 +134,7 @@ impl From<SyncResult> for Update {
}
/// The changes made to a wallet by applying an [`Update`].
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
pub struct ChangeSet {
/// Changes to the [`LocalChain`].
///
/// [`LocalChain`]: local_chain::LocalChain
pub chain: local_chain::ChangeSet,
/// Changes to [`IndexedTxGraph`].
///
/// [`IndexedTxGraph`]: bdk_chain::indexed_tx_graph::IndexedTxGraph
pub indexed_tx_graph: indexed_tx_graph::ChangeSet<
ConfirmationTimeHeightAnchor,
keychain::ChangeSet<KeychainKind>,
>,
/// Stores the network type of the wallet.
pub network: Option<Network>,
}
impl Append for ChangeSet {
fn append(&mut self, other: Self) {
Append::append(&mut self.chain, other.chain);
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
if other.network.is_some() {
debug_assert!(
self.network.is_none() || self.network == other.network,
"network type must be consistent"
);
self.network = other.network;
}
}
fn is_empty(&self) -> bool {
self.chain.is_empty() && self.indexed_tx_graph.is_empty()
}
}
impl From<local_chain::ChangeSet> for ChangeSet {
fn from(chain: local_chain::ChangeSet) -> Self {
Self {
chain,
..Default::default()
}
}
}
impl
From<
indexed_tx_graph::ChangeSet<
ConfirmationTimeHeightAnchor,
keychain::ChangeSet<KeychainKind>,
>,
> for ChangeSet
{
fn from(
indexed_tx_graph: indexed_tx_graph::ChangeSet<
ConfirmationTimeHeightAnchor,
keychain::ChangeSet<KeychainKind>,
>,
) -> Self {
Self {
indexed_tx_graph,
..Default::default()
}
}
}
pub type ChangeSet = bdk_persist::CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>;
/// A derived address and the index it was found at.
/// For convenience this automatically derefs to `Address`
@@ -305,6 +241,8 @@ pub enum LoadError {
MissingNetwork,
/// Data loaded from persistence is missing genesis hash.
MissingGenesis,
/// Data loaded from persistence is missing descriptor.
MissingDescriptor,
}
impl fmt::Display for LoadError {
@@ -317,6 +255,7 @@ impl fmt::Display for LoadError {
}
LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
LoadError::MissingDescriptor => write!(f, "loaded data is missing descriptor"),
}
}
}
@@ -352,6 +291,13 @@ pub enum NewOrLoadError {
/// The network type loaded from persistence.
got: Option<Network>,
},
/// The loaded desccriptor does not match what was provided.
LoadedDescriptorDoesNotMatch {
/// The descriptor loaded from persistence.
got: Option<ExtendedDescriptor>,
/// The keychain of the descriptor not matching
keychain: KeychainKind,
},
}
impl fmt::Display for NewOrLoadError {
@@ -372,6 +318,13 @@ impl fmt::Display for NewOrLoadError {
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
write!(f, "loaded network type is not {}, got {:?}", expected, got)
}
NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
write!(
f,
"loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
got, keychain
)
}
}
}
}
@@ -499,21 +452,58 @@ impl Wallet {
}
/// Load [`Wallet`] from the given persistence backend.
pub fn load<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
///
/// Note that the descriptor secret keys are not persisted to the db; this means that after
/// calling this method the [`Wallet`] **won't** know the secret keys, and as such, won't be
/// able to sign transactions.
///
/// If you wish to use the wallet to sign transactions, you need to add the secret keys
/// manually to the [`Wallet`]:
///
/// ```rust,no_run
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
/// # use bdk_wallet::descriptor::Descriptor;
/// # use bitcoin::key::Secp256k1;
/// # use bdk_wallet::KeychainKind;
/// # use bdk_sqlite::{Store, rusqlite::Connection};
/// #
/// # fn main() -> Result<(), anyhow::Error> {
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// # let conn = Connection::open(file_path).expect("must open connection");
/// # let db = Store::new(conn).expect("must create db");
/// let secp = Secp256k1::new();
///
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
/// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
///
/// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
/// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
///
/// let mut wallet = Wallet::load(db)?;
///
/// external_signer_container.signers().into_iter()
/// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
/// internal_signer_container.signers().into_iter()
/// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
/// # Ok(())
/// # }
/// ```
///
/// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
/// passed-in descriptors to the [`Wallet`].
pub fn load(
mut db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
) -> Result<Self, LoadError> {
let changeset = db
.load_from_persistence()
.map_err(LoadError::Persist)?
.ok_or(LoadError::NotInitialized)?;
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
Self::load_from_changeset(db, changeset)
}
fn load_from_changeset<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
fn load_from_changeset(
db: impl PersistBackend<ChangeSet> + Send + Sync + 'static,
changeset: ChangeSet,
) -> Result<Self, LoadError> {
@@ -522,10 +512,23 @@ impl Wallet {
let chain =
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
let descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::External)
.ok_or(LoadError::MissingDescriptor)?
.clone();
let change_descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::Internal)
.cloned();
let (signers, change_signers) =
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
.map_err(LoadError::Descriptor)?;
.expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
let mut indexed_graph = IndexedTxGraph::new(index);
indexed_graph.apply_changeset(changeset.indexed_tx_graph);
@@ -562,8 +565,8 @@ impl Wallet {
)
}
/// Either loads [`Wallet`] from persistence, or initializes it if it does not exist (with a
/// custom genesis hash).
/// Either loads [`Wallet`] from persistence, or initializes it if it does not exist, using the
/// provided descriptor, change descriptor, network, and custom genesis hash.
///
/// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
/// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
@@ -580,25 +583,23 @@ impl Wallet {
.map_err(NewOrLoadError::Persist)?;
match changeset {
Some(changeset) => {
let wallet =
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
.map_err(|e| match e {
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
LoadError::Persist(e) => NewOrLoadError::Persist(e),
LoadError::NotInitialized => NewOrLoadError::NotInitialized,
LoadError::MissingNetwork => {
NewOrLoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: None,
}
}
LoadError::MissingGenesis => {
NewOrLoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: None,
}
}
})?;
let mut wallet = Self::load_from_changeset(db, changeset).map_err(|e| match e {
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
LoadError::Persist(e) => NewOrLoadError::Persist(e),
LoadError::NotInitialized => NewOrLoadError::NotInitialized,
LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: None,
},
LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: None,
},
LoadError::MissingDescriptor => NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: None,
keychain: KeychainKind::External,
},
})?;
if wallet.network != network {
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
expected: network,
@@ -611,6 +612,73 @@ impl Wallet {
got: Some(wallet.chain.genesis_hash()),
});
}
let (expected_descriptor, expected_descriptor_keymap) = descriptor
.into_wallet_descriptor(&wallet.secp, network)
.map_err(NewOrLoadError::Descriptor)?;
let wallet_descriptor = wallet.public_descriptor(KeychainKind::External).cloned();
if wallet_descriptor != Some(expected_descriptor.clone()) {
return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: wallet_descriptor,
keychain: KeychainKind::External,
});
}
// if expected descriptor has private keys add them as new signers
if !expected_descriptor_keymap.is_empty() {
let signer_container = SignersContainer::build(
expected_descriptor_keymap,
&expected_descriptor,
&wallet.secp,
);
signer_container.signers().into_iter().for_each(|signer| {
wallet.add_signer(
KeychainKind::External,
SignerOrdering::default(),
signer.clone(),
)
});
}
let expected_change_descriptor = if let Some(c) = change_descriptor {
Some(
c.into_wallet_descriptor(&wallet.secp, network)
.map_err(NewOrLoadError::Descriptor)?,
)
} else {
None
};
let wallet_change_descriptor =
wallet.public_descriptor(KeychainKind::Internal).cloned();
match (expected_change_descriptor, wallet_change_descriptor) {
(Some((expected_descriptor, expected_keymap)), Some(wallet_descriptor))
if wallet_descriptor == expected_descriptor =>
{
// if expected change descriptor has private keys add them as new signers
if !expected_keymap.is_empty() {
let signer_container = SignersContainer::build(
expected_keymap,
&expected_descriptor,
&wallet.secp,
);
signer_container.signers().into_iter().for_each(|signer| {
wallet.add_signer(
KeychainKind::Internal,
SignerOrdering::default(),
signer.clone(),
)
});
}
}
(None, None) => (),
(_, wallet_descriptor) => {
return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: wallet_descriptor,
keychain: KeychainKind::Internal,
});
}
}
Ok(wallet)
}
None => Self::new_with_genesis_hash(
@@ -636,7 +704,7 @@ impl Wallet {
}
/// Iterator over all keychains in this wallet
pub fn keychains(&self) -> &BTreeMap<KeychainKind, ExtendedDescriptor> {
pub fn keychains(&self) -> impl Iterator<Item = (&KeychainKind, &ExtendedDescriptor)> {
self.indexed_graph.index.keychains()
}
@@ -650,7 +718,11 @@ impl Wallet {
/// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index.
pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo {
let keychain = self.map_keychain(keychain);
let mut spk_iter = self.indexed_graph.index.unbounded_spk_iter(&keychain);
let mut spk_iter = self
.indexed_graph
.index
.unbounded_spk_iter(&keychain)
.expect("Must exist (we called map_keychain)");
if !spk_iter.descriptor().has_wildcard() {
index = 0;
}
@@ -677,7 +749,11 @@ impl Wallet {
/// If writing to persistent storage fails.
pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
let keychain = self.map_keychain(keychain);
let ((index, spk), index_changeset) = self.indexed_graph.index.reveal_next_spk(&keychain);
let ((index, spk), index_changeset) = self
.indexed_graph
.index
.reveal_next_spk(&keychain)
.expect("Must exist (we called map_keychain)");
self.persist
.stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
@@ -705,8 +781,11 @@ impl Wallet {
index: u32,
) -> anyhow::Result<impl Iterator<Item = AddressInfo> + '_> {
let keychain = self.map_keychain(keychain);
let (spk_iter, index_changeset) =
self.indexed_graph.index.reveal_to_target(&keychain, index);
let (spk_iter, index_changeset) = self
.indexed_graph
.index
.reveal_to_target(&keychain, index)
.expect("must exist (we called map_keychain)");
self.persist
.stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
@@ -729,7 +808,11 @@ impl Wallet {
/// If writing to persistent storage fails.
pub fn next_unused_address(&mut self, keychain: KeychainKind) -> anyhow::Result<AddressInfo> {
let keychain = self.map_keychain(keychain);
let ((index, spk), index_changeset) = self.indexed_graph.index.next_unused_spk(&keychain);
let ((index, spk), index_changeset) = self
.indexed_graph
.index
.next_unused_spk(&keychain)
.expect("must exist (we called map_keychain)");
self.persist
.stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?;
@@ -799,7 +882,7 @@ impl Wallet {
.filter_chain_unspents(
&self.chain,
self.chain.tip().block_id(),
self.indexed_graph.index.outpoints().iter().cloned(),
self.indexed_graph.index.outpoints(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
}
@@ -813,7 +896,7 @@ impl Wallet {
.filter_chain_txouts(
&self.chain,
self.chain.tip().block_id(),
self.indexed_graph.index.outpoints().iter().cloned(),
self.indexed_graph.index.outpoints(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
}
@@ -851,7 +934,11 @@ impl Wallet {
&self,
keychain: KeychainKind,
) -> impl Iterator<Item = (u32, ScriptBuf)> + Clone {
self.indexed_graph.index.unbounded_spk_iter(&keychain)
let keychain = self.map_keychain(keychain);
self.indexed_graph
.index
.unbounded_spk_iter(&keychain)
.expect("Must exist (we called map_keychain)")
}
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
@@ -901,7 +988,7 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Txid;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
@@ -910,7 +997,7 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Psbt;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let mut psbt: Psbt = todo!();
/// let tx = &psbt.clone().extract_tx().expect("tx");
@@ -932,7 +1019,7 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Txid;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
@@ -941,7 +1028,7 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Psbt;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let mut psbt: Psbt = todo!();
/// let tx = &psbt.clone().extract_tx().expect("tx");
@@ -950,10 +1037,10 @@ impl Wallet {
/// [`insert_txout`]: Self::insert_txout
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
self.calculate_fee(tx)
.map(|fee| bitcoin::Amount::from_sat(fee) / tx.weight())
.map(|fee| Amount::from_sat(fee) / tx.weight())
}
/// Compute the `tx`'s sent and received amounts (in satoshis).
/// Compute the `tx`'s sent and received [`Amount`]s.
///
/// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
/// that spend from previous txouts tracked by this wallet. Received is the summation
@@ -963,7 +1050,7 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Txid;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx;
@@ -972,13 +1059,13 @@ impl Wallet {
///
/// ```rust, no_run
/// # use bitcoin::Psbt;
/// # use bdk::Wallet;
/// # use bdk_wallet::Wallet;
/// # let mut wallet: Wallet = todo!();
/// # let mut psbt: Psbt = todo!();
/// let tx = &psbt.clone().extract_tx().expect("tx");
/// let (sent, received) = wallet.sent_and_received(tx);
/// ```
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) {
self.indexed_graph.index.sent_and_received(tx, ..)
}
@@ -993,8 +1080,8 @@ impl Wallet {
/// the transaction was last seen in the mempool is provided.
///
/// ```rust, no_run
/// use bdk::{chain::ChainPosition, Wallet};
/// use bdk_chain::Anchor;
/// use bdk_wallet::{chain::ChainPosition, Wallet};
/// # let wallet: Wallet = todo!();
/// # let my_txid: bitcoin::Txid = todo!();
///
@@ -1133,7 +1220,7 @@ impl Wallet {
self.indexed_graph.graph().balance(
&self.chain,
self.chain.tip().block_id(),
self.indexed_graph.index.outpoints().iter().cloned(),
self.indexed_graph.index.outpoints(),
|&(k, _), _| k == KeychainKind::Internal,
)
}
@@ -1160,8 +1247,8 @@ impl Wallet {
/// ## Example
///
/// ```
/// # use bdk::{Wallet, KeychainKind};
/// # use bdk::bitcoin::Network;
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::bitcoin::Network;
/// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?;
/// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) {
/// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
@@ -1186,9 +1273,9 @@ impl Wallet {
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_persist::PersistBackend;
/// # use anyhow::Error;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
@@ -1197,7 +1284,7 @@ impl Wallet {
/// let psbt = {
/// let mut builder = wallet.build_tx();
/// builder
/// .add_recipient(to_address.script_pubkey(), 50_000);
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
/// builder.finish()?
/// };
///
@@ -1206,12 +1293,11 @@ impl Wallet {
/// ```
///
/// [`TxBuilder`]: crate::TxBuilder
pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> {
pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm> {
TxBuilder {
wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
params: TxParams::default(),
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
}
}
@@ -1220,17 +1306,9 @@ impl Wallet {
coin_selection: Cs,
params: TxParams,
) -> Result<Psbt, CreateTxError> {
let external_descriptor = self
.indexed_graph
.index
.keychains()
.get(&KeychainKind::External)
.expect("must exist");
let internal_descriptor = self
.indexed_graph
.index
.keychains()
.get(&KeychainKind::Internal);
let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect();
let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist");
let internal_descriptor = keychains.get(&KeychainKind::Internal);
let external_policy = external_descriptor
.extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
@@ -1464,8 +1542,11 @@ impl Wallet {
Some(ref drain_recipient) => drain_recipient.clone(),
None => {
let change_keychain = self.map_keychain(KeychainKind::Internal);
let ((index, spk), index_changeset) =
self.indexed_graph.index.next_unused_spk(&change_keychain);
let ((index, spk), index_changeset) = self
.indexed_graph
.index
.next_unused_spk(&change_keychain)
.expect("Keychain exists (we called map_keychain)");
let spk = spk.into();
self.indexed_graph.index.mark_used(change_keychain, index);
self.persist
@@ -1568,9 +1649,9 @@ impl Wallet {
/// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first.
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_persist::PersistBackend;
/// # use anyhow::Error;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
@@ -1579,7 +1660,7 @@ impl Wallet {
/// let mut psbt = {
/// let mut builder = wallet.build_tx();
/// builder
/// .add_recipient(to_address.script_pubkey(), 50_000)
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
/// .enable_rbf();
/// builder.finish()?
/// };
@@ -1602,7 +1683,7 @@ impl Wallet {
pub fn build_fee_bump(
&mut self,
txid: Txid,
) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
) -> Result<TxBuilder<'_, DefaultCoinSelectionAlgorithm>, BuildFeeBumpError> {
let graph = self.indexed_graph.graph();
let txout_index = &self.indexed_graph.index;
let chain_tip = self.chain.tip().block_id();
@@ -1726,7 +1807,6 @@ impl Wallet {
wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)),
params,
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
})
}
@@ -1743,16 +1823,16 @@ impl Wallet {
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_persist::PersistBackend;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
/// let mut psbt = {
/// let mut builder = wallet.build_tx();
/// builder.add_recipient(to_address.script_pubkey(), 50_000);
/// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000));
/// builder.finish()?
/// };
/// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
@@ -1825,7 +1905,11 @@ impl Wallet {
///
/// This can be used to build a watch-only version of a wallet
pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> {
self.indexed_graph.index.keychains().get(&keychain)
self.indexed_graph
.index
.keychains()
.find(|(k, _)| *k == &keychain)
.map(|(_, d)| d)
}
/// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass
@@ -1876,17 +1960,9 @@ impl Wallet {
.get_utxo_for(n)
.and_then(|txout| self.get_descriptor_for_txout(&txout))
.or_else(|| {
self.indexed_graph
.index
.keychains()
.iter()
.find_map(|(_, desc)| {
desc.derive_from_psbt_input(
psbt_input,
psbt.get_utxo_for(n),
&self.secp,
)
})
self.indexed_graph.index.keychains().find_map(|(_, desc)| {
desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp)
})
});
match desc {
@@ -1952,7 +2028,12 @@ impl Wallet {
/// The index of the next address that you would get if you were to ask the wallet for a new address
pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 {
self.indexed_graph.index.next_index(&keychain).0
let keychain = self.map_keychain(keychain);
self.indexed_graph
.index
.next_index(&keychain)
.expect("Keychain must exist (we called map_keychain)")
.0
}
/// Informs the wallet that you no longer intend to broadcast a tx that was built from it.
@@ -2119,7 +2200,6 @@ impl Wallet {
if params.add_global_xpubs {
let all_xpubs = self
.keychains()
.iter()
.flat_map(|(_, desc)| desc.get_extended_keys())
.collect::<Vec<_>>();
@@ -2419,6 +2499,7 @@ impl Wallet {
/// start a blockchain sync with a spk based blockchain client.
pub fn start_sync_with_revealed_spks(&self) -> SyncRequest {
SyncRequest::from_chain_tip(self.chain.tip())
.cache_graph_txs(self.tx_graph())
.populate_with_revealed_spks(&self.indexed_graph.index, ..)
}
@@ -2432,6 +2513,7 @@ impl Wallet {
/// in which the list of used scripts is not known.
pub fn start_full_scan(&self) -> FullScanRequest<KeychainKind> {
FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index)
.cache_graph_txs(self.tx_graph())
}
}
@@ -2496,13 +2578,13 @@ fn create_signers<E: IntoWalletDescriptor>(
) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), crate::descriptor::error::Error> {
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
index.add_keychain(KeychainKind::External, descriptor);
let _ = index.insert_descriptor(KeychainKind::External, descriptor);
let change_signers = match change_descriptor {
Some(descriptor) => {
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
index.add_keychain(KeychainKind::Internal, descriptor);
let _ = index.insert_descriptor(KeychainKind::Internal, descriptor);
signers
}
None => Arc::new(SignersContainer::new()),

View File

@@ -19,8 +19,8 @@
//! # use core::str::FromStr;
//! # use bitcoin::secp256k1::{Secp256k1, All};
//! # use bitcoin::*;
//! # use bdk::signer::*;
//! # use bdk::*;
//! # use bdk_wallet::signer::*;
//! # use bdk_wallet::*;
//! # #[derive(Debug)]
//! # struct CustomHSM;
//! # impl CustomHSM {

View File

@@ -16,10 +16,9 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk::*;
//! # use bdk::wallet::ChangeSet;
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk::wallet::tx_builder::CreateTx;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::ChangeSet;
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk_persist::PersistBackend;
//! # use anyhow::Error;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -29,7 +28,7 @@
//!
//! tx_builder
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
//! .add_recipient(to_address.script_pubkey(), 50_000)
//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
//! // With a custom fee rate of 5.0 satoshi/vbyte
//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
//! // Only spend non-change outputs
@@ -43,31 +42,16 @@
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use core::cell::RefCell;
use core::fmt;
use core::marker::PhantomData;
use bitcoin::psbt::{self, Psbt};
use bitcoin::script::PushBytes;
use bitcoin::{absolute, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::coin_selection::CoinSelectionAlgorithm;
use super::{CreateTxError, Wallet};
use crate::collections::{BTreeMap, HashSet};
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
/// Context in which the [`TxBuilder`] is valid
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).
#[derive(Debug, Default, Clone)]
pub struct CreateTx;
impl TxBuilderContext for CreateTx {}
/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction.
#[derive(Debug, Default, Clone)]
pub struct BumpFee;
impl TxBuilderContext for BumpFee {}
/// A transaction builder
///
/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After
@@ -78,12 +62,12 @@ impl TxBuilderContext for BumpFee {}
/// as in the following example:
///
/// ```
/// # use bdk::*;
/// # use bdk::wallet::tx_builder::*;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::tx_builder::*;
/// # use bitcoin::*;
/// # use core::str::FromStr;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_persist::PersistBackend;
/// # use anyhow::Error;
/// # let mut wallet = doctest_wallet!();
@@ -94,8 +78,8 @@ impl TxBuilderContext for BumpFee {}
/// let mut builder = wallet.build_tx();
/// builder
/// .ordering(TxOrdering::Untouched)
/// .add_recipient(addr1.script_pubkey(), 50_000)
/// .add_recipient(addr2.script_pubkey(), 50_000);
/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000))
/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000));
/// builder.finish()?
/// };
///
@@ -104,7 +88,7 @@ impl TxBuilderContext for BumpFee {}
/// let mut builder = wallet.build_tx();
/// builder.ordering(TxOrdering::Untouched);
/// for addr in &[addr1, addr2] {
/// builder.add_recipient(addr.script_pubkey(), 50_000);
/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000));
/// }
/// builder.finish()?
/// };
@@ -123,11 +107,10 @@ impl TxBuilderContext for BumpFee {}
/// [`finish`]: Self::finish
/// [`coin_selection`]: Self::coin_selection
#[derive(Debug)]
pub struct TxBuilder<'a, Cs, Ctx> {
pub struct TxBuilder<'a, Cs> {
pub(crate) wallet: Rc<RefCell<&'a mut Wallet>>,
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
}
/// The parameters for transaction creation sans coin selection algorithm.
@@ -175,19 +158,18 @@ impl Default for FeePolicy {
}
}
impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> {
impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> {
fn clone(&self) -> Self {
TxBuilder {
wallet: self.wallet.clone(),
params: self.params.clone(),
coin_selection: self.coin_selection.clone(),
phantom: PhantomData,
}
}
}
// methods supported by both contexts, for any CoinSelectionAlgorithm
impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
// Methods supported for any CoinSelectionAlgorithm.
impl<'a, Cs> TxBuilder<'a, Cs> {
/// Set a custom fee rate.
///
/// This method sets the mining fee paid by the transaction as a rate on its size.
@@ -212,8 +194,8 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
/// Note that this is really a minimum absolute fee -- it's possible to
/// overshoot it slightly since adding a change output to drain the remaining
/// excess might not be viable.
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
pub fn fee_absolute(&mut self, fee_amount: Amount) -> &mut Self {
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount.to_sat()));
self
}
@@ -263,7 +245,7 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
/// # use std::str::FromStr;
/// # use std::collections::BTreeMap;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk_wallet::*;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
@@ -274,7 +256,7 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
///
/// let builder = wallet
/// .build_tx()
/// .add_recipient(to_address.script_pubkey(), 50_000)
/// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000))
/// .policy_path(path, KeychainKind::External);
///
/// # Ok::<(), anyhow::Error>(())
@@ -553,18 +535,14 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
/// Choose the coin selection algorithm
///
/// Overrides the [`DefaultCoinSelectionAlgorithm`].
/// Overrides the [`CoinSelectionAlgorithm`].
///
/// 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>(
self,
coin_selection: P,
) -> TxBuilder<'a, P, Ctx> {
pub fn coin_selection<P: CoinSelectionAlgorithm>(self, coin_selection: P) -> TxBuilder<'a, P> {
TxBuilder {
wallet: self.wallet,
params: self.params,
coin_selection,
phantom: PhantomData,
}
}
@@ -612,9 +590,84 @@ impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> {
self.params.allow_dust = allow_dust;
self
}
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self {
self.params.recipients = recipients
.into_iter()
.map(|(script, amount)| (script, amount.to_sat()))
.collect();
self
}
/// Add a recipient to the internal list
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self {
self.params
.recipients
.push((script_pubkey, amount.to_sat()));
self
}
/// Add data as an output, using OP_RETURN
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
let script = ScriptBuf::new_op_return(data);
self.add_recipient(script, Amount::ZERO);
self
}
/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
/// coins are too small) it will not be included in the resulting transaction. The only
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should provide the utxos that the
/// transaction should spend via [`add_utxos`].
///
/// # Example
///
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
/// single address.
///
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_persist::PersistBackend;
/// # use anyhow::Error;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
/// .assume_checked();
/// # let mut wallet = doctest_wallet!();
/// let mut tx_builder = wallet.build_tx();
///
/// tx_builder
/// // Spend all outputs in this wallet.
/// .drain_wallet()
/// // Send the excess (which is all the coins minus the fee) to this address.
/// .drain_to(to_address.script_pubkey())
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
/// .enable_rbf();
/// let psbt = tx_builder.finish()?;
/// # Ok::<(), anyhow::Error>(())
/// ```
///
/// [`add_recipient`]: Self::add_recipient
/// [`add_utxos`]: Self::add_utxos
/// [`drain_wallet`]: Self::drain_wallet
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
self.params.drain_to = Some(script_pubkey);
self
}
}
impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> {
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
/// Finish building the transaction.
///
/// Returns a new [`Psbt`] per [`BIP174`].
@@ -689,137 +742,6 @@ impl fmt::Display for AddForeignUtxoError {
#[cfg(feature = "std")]
impl std::error::Error for AddForeignUtxoError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::allow_shrinking`]
pub enum AllowShrinkingError {
/// Script/PubKey was not in the original transaction
MissingScriptPubKey(ScriptBuf),
}
impl fmt::Display for AllowShrinkingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingScriptPubKey(script_buf) => write!(
f,
"Script/PubKey was not in the original transaction: {}",
script_buf,
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AllowShrinkingError {}
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> {
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
self.params.recipients = recipients;
self
}
/// Add a recipient to the internal list
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: u64) -> &mut Self {
self.params.recipients.push((script_pubkey, amount));
self
}
/// Add data as an output, using OP_RETURN
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
let script = ScriptBuf::new_op_return(data);
self.add_recipient(script, 0u64);
self
}
/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
/// coins are too small) it will not be included in the resulting transaction. The only
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should either provide the utxos that the
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
///
/// When bumping the fees of a transaction made with this option, you probably want to
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
///
/// # Example
///
/// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
/// single address.
///
/// ```
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk::wallet::tx_builder::CreateTx;
/// # use bdk_persist::PersistBackend;
/// # use anyhow::Error;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap()
/// .assume_checked();
/// # let mut wallet = doctest_wallet!();
/// let mut tx_builder = wallet.build_tx();
///
/// tx_builder
/// // Spend all outputs in this wallet.
/// .drain_wallet()
/// // Send the excess (which is all the coins minus the fee) to this address.
/// .drain_to(to_address.script_pubkey())
/// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
/// .enable_rbf();
/// let psbt = tx_builder.finish()?;
/// # Ok::<(), anyhow::Error>(())
/// ```
///
/// [`allow_shrinking`]: Self::allow_shrinking
/// [`add_recipient`]: Self::add_recipient
/// [`add_utxos`]: Self::add_utxos
/// [`drain_wallet`]: Self::drain_wallet
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
self.params.drain_to = Some(script_pubkey);
self
}
}
// methods supported only by bump_fee
impl<'a> TxBuilder<'a, 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.
///
/// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is
/// preserved then it is currently not guaranteed to be in the same position as it was
/// originally.
///
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
/// transaction we are bumping.
pub fn allow_shrinking(
&mut self,
script_pubkey: ScriptBuf,
) -> Result<&mut Self, AllowShrinkingError> {
match self
.params
.recipients
.iter()
.position(|(recipient_script, _)| *recipient_script == script_pubkey)
{
Some(position) => {
self.params.recipients.remove(position);
self.params.drain_to = Some(script_pubkey);
Ok(self)
}
None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
}
}
}
/// Ordering of the transaction's inputs and outputs
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum TxOrdering {

View File

@@ -1,8 +1,8 @@
#![allow(unused)]
use bdk::{KeychainKind, LocalOutput, Wallet};
use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bitcoin::{
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,

View File

@@ -1,5 +1,5 @@
use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
use bdk::{psbt, KeychainKind, SignOptions};
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
use bdk_wallet::{psbt, KeychainKind, SignOptions};
use core::str::FromStr;
mod common;
use common::*;
@@ -14,7 +14,7 @@ fn test_psbt_malformed_psbt_input_legacy() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
let mut psbt = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[0].clone());
let options = SignOptions {
@@ -31,7 +31,7 @@ fn test_psbt_malformed_psbt_input_segwit() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
let mut psbt = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[1].clone());
let options = SignOptions {
@@ -47,7 +47,7 @@ fn test_psbt_malformed_tx_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
let mut psbt = builder.finish().unwrap();
psbt.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
@@ -63,7 +63,7 @@ fn test_psbt_sign_with_finalized() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.peek_address(KeychainKind::External, 0);
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000));
let mut psbt = builder.finish().unwrap();
// add a finalized input
@@ -156,8 +156,8 @@ fn test_psbt_fee_rate_with_missing_txout() {
#[test]
fn test_psbt_multiple_internalkey_signers() {
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
use bdk::KeychainKind;
use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper};
use bdk_wallet::KeychainKind;
use bitcoin::key::TapTweak;
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
@@ -201,7 +201,7 @@ fn test_psbt_multiple_internalkey_signers() {
// the prevout we're spending
let prevouts = &[TxOut {
script_pubkey: send_to.script_pubkey(),
value: Amount::from_sat(to_spend),
value: to_spend,
}];
let prevouts = Prevouts::All(prevouts);
let input_index = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -212,7 +212,7 @@ fn main() -> anyhow::Result<()> {
graph.graph().balance(
&*chain,
synced_to.block_id(),
graph.index.outpoints().iter().cloned(),
graph.index.outpoints(),
|(k, _), _| k == &Keychain::Internal,
)
};
@@ -336,7 +336,7 @@ fn main() -> anyhow::Result<()> {
graph.graph().balance(
&*chain,
synced_to.block_id(),
graph.index.outpoints().iter().cloned(),
graph.index.outpoints(),
|(k, _), _| k == &Keychain::Internal,
)
};

View File

@@ -249,14 +249,20 @@ where
script_pubkey: address.script_pubkey(),
}];
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
let internal_keychain = if graph
.index
.keychains()
.any(|(k, _)| *k == Keychain::Internal)
{
Keychain::Internal
} else {
Keychain::External
};
let ((change_index, change_script), change_changeset) =
graph.index.next_unused_spk(&internal_keychain);
let ((change_index, change_script), change_changeset) = graph
.index
.next_unused_spk(&internal_keychain)
.expect("Must exist");
changeset.append(change_changeset);
// Clone to drop the immutable reference.
@@ -266,8 +272,9 @@ where
&graph
.index
.keychains()
.get(&internal_keychain)
.find(|(k, _)| *k == &internal_keychain)
.expect("must exist")
.1
.at_derivation_index(change_index)
.expect("change_index can't be hardened"),
&assets,
@@ -284,8 +291,9 @@ where
min_drain_value: graph
.index
.keychains()
.get(&internal_keychain)
.find(|(k, _)| *k == &internal_keychain)
.expect("must exist")
.1
.dust_value(),
..CoinSelectorOpt::fund_outputs(
&outputs,
@@ -416,7 +424,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
assets: &bdk_tmp_plan::Assets<K>,
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints().iter().cloned();
let outpoints = graph.index.outpoints();
graph
.graph()
.try_filter_chain_unspents(chain, chain_tip, outpoints)
@@ -428,8 +436,9 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
let desc = graph
.index
.keychains()
.get(&k)
.find(|(keychain, _)| *keychain == &k)
.expect("keychain must exist")
.1
.at_derivation_index(i)
.expect("i can't be hardened");
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
@@ -465,7 +474,8 @@ where
_ => unreachable!("only these two variants exist in match arm"),
};
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
let ((spk_i, spk), index_changeset) =
spk_chooser(index, &Keychain::External).expect("Must exist");
let db = &mut *db.lock().unwrap();
db.stage_and_commit(C::from((
local_chain::ChangeSet::default(),
@@ -506,18 +516,18 @@ where
let chain = &*chain.lock().unwrap();
fn print_balances<'a>(
title_str: &'a str,
items: impl IntoIterator<Item = (&'a str, u64)>,
items: impl IntoIterator<Item = (&'a str, Amount)>,
) {
println!("{}:", title_str);
for (name, amount) in items.into_iter() {
println!(" {:<10} {:>12} sats", name, amount)
println!(" {:<10} {:>12} sats", name, amount.to_sat())
}
}
let balance = graph.graph().try_balance(
chain,
chain.get_chain_tip()?,
graph.index.outpoints().iter().cloned(),
graph.index.outpoints(),
|(k, _), _| k == &Keychain::Internal,
)?;
@@ -547,7 +557,7 @@ where
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints().iter().cloned();
let outpoints = graph.index.outpoints();
match txout_cmd {
TxOutCmd::List {
@@ -695,9 +705,11 @@ where
let mut index = KeychainTxOutIndex::<Keychain>::default();
// TODO: descriptors are already stored in the db, so we shouldn't re-insert
// them in the index here. However, the keymap is not stored in the database.
let (descriptor, mut keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
index.add_keychain(Keychain::External, descriptor);
let _ = index.insert_descriptor(Keychain::External, descriptor);
if let Some((internal_descriptor, internal_keymap)) = args
.change_descriptor
@@ -706,7 +718,7 @@ where
.transpose()?
{
keymap.extend(internal_keymap);
index.add_keychain(Keychain::Internal, internal_descriptor);
let _ = index.insert_descriptor(Keychain::Internal, internal_descriptor);
}
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {

View File

@@ -1,19 +1,20 @@
use std::{
collections::BTreeMap,
io::{self, Write},
sync::Mutex,
};
use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid},
bitcoin::{constants::genesis_block, Address, Network, Txid},
collections::BTreeSet,
indexed_tx_graph::{self, IndexedTxGraph},
keychain,
local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationHeightAnchor,
};
use bdk_electrum::{
electrum_client::{self, Client, ElectrumApi},
ElectrumExt, ElectrumUpdate,
ElectrumExt,
};
use example_cli::{
anyhow::{self, Context},
@@ -147,42 +148,56 @@ fn main() -> anyhow::Result<()> {
let client = electrum_cmd.electrum_args().client(args.network)?;
let response = match electrum_cmd.clone() {
let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
ElectrumCommands::Scan {
stop_gap,
scan_options,
..
} => {
let (keychain_spks, tip) = {
let request = {
let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap();
let keychain_spks = graph
.index
.all_unbounded_spk_iters()
.into_iter()
.map(|(keychain, iter)| {
let mut first = true;
let spk_iter = iter.inspect(move |(i, _)| {
if first {
eprint!("\nscanning {}: ", keychain);
first = false;
FullScanRequest::from_chain_tip(chain.tip())
.cache_graph_txs(graph.graph())
.set_spks_for_keychain(
Keychain::External,
graph
.index
.unbounded_spk_iter(&Keychain::External)
.into_iter()
.flatten(),
)
.set_spks_for_keychain(
Keychain::Internal,
graph
.index
.unbounded_spk_iter(&Keychain::Internal)
.into_iter()
.flatten(),
)
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::new();
move |k, spk_i, _| {
if once.insert(k) {
eprint!("\nScanning {}: {} ", k, spk_i);
} else {
eprint!("{} ", spk_i);
}
eprint!("{} ", i);
let _ = io::stdout().flush();
});
(keychain, spk_iter)
io::stdout().flush().expect("must flush");
}
})
.collect::<BTreeMap<_, _>>();
let tip = chain.tip();
(keychain_spks, tip)
};
client
.full_scan(tip, keychain_spks, stop_gap, scan_options.batch_size)
let res = client
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
(
res.chain_update,
res.graph_update,
Some(res.last_active_indices),
)
}
ElectrumCommands::Sync {
mut unused_spks,
@@ -195,7 +210,6 @@ fn main() -> anyhow::Result<()> {
// 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().block_id();
if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true;
@@ -205,18 +219,20 @@ fn main() -> anyhow::Result<()> {
unused_spks = false;
}
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
Box::new(core::iter::empty());
let chain_tip = chain.tip();
let mut request =
SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(graph.graph());
if all_spks {
let all_spks = graph
.index
.revealed_spks(..)
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
eprintln!("scanning {}:{}", k, i);
request = request.chain_spks(all_spks.into_iter().map(|(k, spk_i, spk)| {
eprint!("Scanning {}: {}", k, spk_i);
spk
})));
}));
}
if unused_spks {
let unused_spks = graph
@@ -224,82 +240,88 @@ fn main() -> anyhow::Result<()> {
.unused_spks()
.map(|(k, i, spk)| (k, i, spk.to_owned()))
.collect::<Vec<_>>();
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
eprintln!(
"Checking if address {} {}:{} has been used",
Address::from_script(&spk, args.network).unwrap(),
k,
i,
);
spk
})));
request =
request.chain_spks(unused_spks.into_iter().map(move |(k, spk_i, spk)| {
eprint!(
"Checking if address {} {}:{} has been used",
Address::from_script(&spk, args.network).unwrap(),
k,
spk_i,
);
spk
}));
}
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
if utxos {
let init_outpoints = graph.index.outpoints().iter().cloned();
let init_outpoints = graph.index.outpoints();
let utxos = graph
.graph()
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
.filter_chain_unspents(&*chain, chain_tip.block_id(), 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),
);
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
eprint!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
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)
.list_chain_txs(&*chain, chain_tip.block_id())
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid)
.collect::<Vec<Txid>>();
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
eprintln!("Checking if {} is confirmed yet", txid);
}));
request = request.chain_txids(
unconfirmed_txids
.into_iter()
.inspect(|txid| eprint!("Checking if {} is confirmed yet", txid)),
);
}
let tip = chain.tip();
let total_spks = request.spks.len();
let total_txids = request.txids.len();
let total_ops = request.outpoints.len();
request = request
.inspect_spks({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_spks as f32)
}
})
.inspect_txids({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_txids as f32)
}
})
.inspect_outpoints({
let mut visited = 0;
move |_| {
visited += 1;
eprintln!(" [ {:>6.2}% ]", (visited * 100) as f32 / total_ops as f32)
}
});
let res = client
.sync(request, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
// drop lock on graph and chain
drop((graph, chain));
let electrum_update = client
.sync(tip, spks, txids, outpoints, scan_options.batch_size)
.context("scanning the blockchain")?;
(electrum_update, BTreeMap::new())
(res.chain_update, res.graph_update, None)
}
};
let (
ElectrumUpdate {
chain_update,
relevant_txids,
},
keychain_update,
) = response;
let missing_txids = {
let graph = &*graph.lock().unwrap();
relevant_txids.missing_full_txs(graph.graph())
};
let mut graph_update = relevant_txids.into_tx_graph(&client, missing_txids)?;
let now = std::time::UNIX_EPOCH
.elapsed()
.expect("must get time")
@@ -310,21 +332,17 @@ fn main() -> anyhow::Result<()> {
let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap();
let chain = chain.apply_update(chain_update)?;
let chain_changeset = chain.apply_update(chain_update)?;
let indexed_tx_graph = {
let mut changeset =
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
changeset.append(indexed_tx_graph::ChangeSet {
indexer,
..Default::default()
});
changeset.append(graph.apply_update(graph_update));
changeset
};
let mut indexed_tx_graph_changeset =
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
if let Some(keychain_update) = keychain_update {
let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update);
indexed_tx_graph_changeset.append(keychain_changeset.into());
}
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
(chain, indexed_tx_graph)
(chain_changeset, indexed_tx_graph_changeset)
};
let mut db = db.lock().unwrap();

View File

@@ -277,7 +277,7 @@ fn main() -> anyhow::Result<()> {
// We want to search for whether the UTXO is spent, and spent by which
// transaction. We provide the outpoint of the UTXO to
// `EsploraExt::update_tx_graph_without_keychain`.
let init_outpoints = graph.index.outpoints().iter().cloned();
let init_outpoints = graph.index.outpoints();
let utxos = graph
.graph()
.filter_chain_unspents(&*chain, local_tip.block_id(), init_outpoints)

View File

@@ -4,7 +4,7 @@ version = "0.2.0"
edition = "2021"
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_wallet = { path = "../../crates/wallet" }
bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@@ -1,24 +1,25 @@
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
const SEND_AMOUNT: u64 = 5000;
const SEND_AMOUNT: Amount = Amount::from_sat(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::wallet::Update;
use bdk::{bitcoin::Network, Wallet};
use bdk::{KeychainKind, SignOptions};
use bdk_electrum::{
electrum_client::{self, ElectrumApi},
ElectrumExt, ElectrumUpdate,
ElectrumExt,
};
use bdk_file_store::Store;
use bdk_wallet::bitcoin::{Address, Amount};
use bdk_wallet::chain::collections::HashSet;
use bdk_wallet::{bitcoin::Network, Wallet};
use bdk_wallet::{KeychainKind, SignOptions};
fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-electrum-example");
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let db =
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
@@ -38,44 +39,30 @@ fn main() -> Result<(), anyhow::Error> {
print!("Syncing...");
let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?;
let prev_tip = wallet.latest_checkpoint();
let keychain_spks = wallet
.all_unbounded_spk_iters()
.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)
let request = wallet
.start_full_scan()
.inspect_spks_for_all_keychains({
let mut once = HashSet::<KeychainKind>::new();
move |k, spk_i, _| {
if once.insert(k) {
print!("\nScanning keychain [{:?}]", k)
} else {
print!(" {:<3}", spk_i)
}
}
})
.collect();
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
let (
ElectrumUpdate {
chain_update,
relevant_txids,
},
keychain_update,
) = client.full_scan(prev_tip, keychain_spks, STOP_GAP, BATCH_SIZE)?;
let mut update = client
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
.with_confirmation_time_height_anchor(&client)?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
println!();
let missing = relevant_txids.missing_full_txs(wallet.as_ref());
let mut graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = graph_update.update_last_seen_unconfirmed(now);
let wallet_update = Update {
last_active_indices: keychain_update,
graph: graph_update,
chain: Some(chain_update),
};
wallet.apply_update(wallet_update)?;
wallet.apply_update(update)?;
wallet.commit()?;
let balance = wallet.get_balance();

View File

@@ -6,8 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_wallet = { path = "../../crates/wallet" }
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
bdk_file_store = { path = "../../crates/file_store" }
bdk_sqlite = { path = "../../crates/sqlite" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1"

View File

@@ -1,21 +1,22 @@
use std::{collections::BTreeSet, io::Write, str::FromStr};
use bdk::{
bitcoin::{Address, Network, Script},
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{
bitcoin::{Address, Amount, Network, Script},
KeychainKind, 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;
use bdk_sqlite::{rusqlite::Connection, Store};
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let db_path = "bdk-esplora-async-example.sqlite";
let conn = Connection::open(db_path)?;
let db = Store::new(conn)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
@@ -23,7 +24,7 @@ async fn main() -> Result<(), anyhow::Error> {
external_descriptor,
Some(internal_descriptor),
db,
Network::Testnet,
Network::Signet,
)?;
let address = wallet.next_unused_address(KeychainKind::External)?;
@@ -33,8 +34,7 @@ async fn main() -> Result<(), anyhow::Error> {
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client =
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_async()?;
let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
let mut once = Some(());
@@ -90,7 +90,7 @@ async fn main() -> Result<(), anyhow::Error> {
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Testnet)?;
.require_network(Network::Signet)?;
let mut tx_builder = wallet.build_tx();
tx_builder

View File

@@ -7,7 +7,7 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_wallet = { path = "../../crates/wallet" }
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@@ -1,20 +1,21 @@
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
const SEND_AMOUNT: u64 = 1000;
const SEND_AMOUNT: Amount = Amount::from_sat(1000);
const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 1;
use std::{collections::BTreeSet, io::Write, str::FromStr};
use bdk::{
bitcoin::{Address, Network},
KeychainKind, SignOptions, Wallet,
};
use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store;
use bdk_wallet::{
bitcoin::{Address, Amount, Network},
KeychainKind, SignOptions, Wallet,
};
fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-esplora-example");
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let db =
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";

View File

@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bdk = { path = "../../crates/bdk" }
bdk_wallet = { path = "../../crates/wallet" }
bdk_file_store = { path = "../../crates/file_store" }
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }

View File

@@ -4,7 +4,7 @@
$ cargo run --bin wallet_rpc -- --help
wallet_rpc 0.1.0
Bitcoind RPC example using `bdk::Wallet`
Bitcoind RPC example using `bdk_wallet::Wallet`
USAGE:
wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]

View File

@@ -1,18 +1,18 @@
use bdk::{
bitcoin::{Block, Network, Transaction},
wallet::Wallet,
};
use bdk_bitcoind_rpc::{
bitcoincore_rpc::{Auth, Client, RpcApi},
Emitter,
};
use bdk_file_store::Store;
use bdk_wallet::{
bitcoin::{Block, Network, Transaction},
wallet::Wallet,
};
use clap::{self, Parser};
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
const DB_MAGIC: &str = "bdk-rpc-wallet-example";
/// Bitcoind RPC example using `bdk::Wallet`.
/// Bitcoind RPC example using `bdk_wallet::Wallet`.
///
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
/// count.
@@ -89,7 +89,10 @@ fn main() -> anyhow::Result<()> {
let mut wallet = Wallet::new_or_load(
&args.descriptor,
args.change_descriptor.as_ref(),
Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?,
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(
DB_MAGIC.as_bytes(),
args.db_path,
)?,
args.network,
)?;
println!(

View File

@@ -305,341 +305,341 @@ where
}?
}
#[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() {}
}
// #[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() {}
// }