Compare commits

...

62 Commits

Author SHA1 Message Date
Steve Myers
785371e0a1 Merge bitcoindevkit/bdk#1530: chore: fix clippy lints
Some checks failed
Code Coverage / Code Coverage (push) Failing after 1m1s
CI / Build and test (--all-features, map[clippy:true version:stable]) (push) Failing after 7m39s
CI / Build and test (--all-features, map[version:1.63.0]) (push) Failing after 18m9s
CI / Build and test (--no-default-features, map[clippy:true version:stable]) (push) Failing after 8m41s
CI / Build and test (--no-default-features, map[version:1.63.0]) (push) Failing after 16m42s
CI / Check no_std (push) Successful in 1m37s
CI / Check WASM (push) Successful in 3m15s
CI / Rust fmt (push) Successful in 21s
CI / clippy_check (push) Failing after 16s
Publish Nightly Docs / Build docs (push) Failing after 7s
Publish Nightly Docs / Publish docs (push) Has been skipped
8bf8c7d080 chore: fix clippy lints (Jose Storopoli)

Pull request description:

  ### Description

  Fix some clippy CI lints

  ### Notes to the reviewers

  More caught by the Nix CI in #1320.

  ### Changelog notice

  chore: clippy lints

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

  #### Bugfixes:

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

ACKs for top commit:
  notmandatory:
    ACK 8bf8c7d080

Tree-SHA512: 6b53cb739e506d79106a2f42aa2b8fa28ef226543fbbf100225f10ed82257f6fd0218f09c0f1291803fbc9c1c88e32ba1c7e02fe3f601ccc9dc5be8a6efea6e1
2024-07-31 23:11:38 -05:00
Steve Myers
18626c66ac Merge bitcoindevkit/bdk#1529: fix: typos
a8efeaa0fb fix: typos (Jose Storopoli)

Pull request description:

  ### Description

  Needed for the typos nix check in #1320.

  ### Notes to the reviewers

  More typos that the Nix CI caught.

  ### Changelog notice

  - fix: typos

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

  #### Bugfixes:

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

ACKs for top commit:
  notmandatory:
    ACK a8efeaa0fb

Tree-SHA512: 44a625bcf538acb7a766316f1d512ef3fd7cc14aae03f09b129229946ef156be35dff73dab3dc24fbc21137764b5707a2bd632bfb84b38dc9fe4d061659ac766
2024-07-31 22:55:15 -05:00
Jose Storopoli
8bf8c7d080 chore: fix clippy lints 2024-07-30 14:43:32 -03:00
Jose Storopoli
a8efeaa0fb fix: typos
Needed for the typos nix check in #1320.
2024-07-30 13:35:23 -03:00
Steve Myers
82141a8201 Merge bitcoindevkit/bdk#1524: ci: pin tokio to 1.38.1 to support MSRV 1.63
28d75304e1 ci: pin tokio to 1.38.1 to support MSRV 1.63 (Steve Myers)

Pull request description:

  ### Description

  The latest tokio minor version update from 1.38.1 to 1.39.1 changed it's MSRV from 1.63.0 to 1.70.0, breaking our CI MSRV 1.63 testing. This PR pins `tokio` back to 1.38.1 for our CI MSRV testing.

  ### Notes to the reviewers

  https://github.com/tokio-rs/tokio/pull/6645

  https://crates.io/crates/tokio/versions

  ### 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:
  LagginTimes:
    ACK 28d75304e1
  ValuedMammal:
    ACK 28d75304e1
  oleonardolima:
    ACK 28d75304e1

Tree-SHA512: 9eef750422cb8c3faa15771438e790afae44362d7c77e1d36cf70816ac7dabdd2e408e14502836ec86f845a2f1fe82905000c681fb6b4e873b036a41a280abd9
2024-07-27 08:17:37 -05:00
Steve Myers
28d75304e1 ci: pin tokio to 1.38.1 to support MSRV 1.63 2024-07-25 22:16:25 -05:00
Steve Myers
17a9850cba Merge bitcoindevkit/bdk#1522: Bump bdk version to 1.0.0-beta.1
53bea0d902 Bump bdk version to 1.0.0-beta.1 (Steve Myers)

Pull request description:

  ### Description

  Bump bdk version to 1.0.0-beta.1

  bdk_chain to 0.17.0
  bdk_bitcoind_rpc to 0.13.0
  bdk_electrum to 0.16.0
  bdk_esplora to 0.16.0
  bdk_file_store to 0.14.0
  bdk_testenv to 0.7.0
  bdk_hwi to 0.4.0

  ### Notes to the reviewers

  This release completes the 1.0.0-alpha milestone and starts our new 1.0.0-beta milestone series of releases.

  The beta releases will maintain a stable `bdk_wallet` API and be focused on improving testing, docs, CI, and fixing any newly discovered bugs.

ACKs for top commit:
  LLFourn:
    ACK 53bea0d902
  LagginTimes:
    ACK 53bea0d902
  storopoli:
    ACK 53bea0d902

Tree-SHA512: cc2adbdeb7c2786659198da64e96fcb374833666e8d98c3f255e2de5f7283573efe3e3c53fd0c7b0e475fe3ac30d3c88169075e079d4d0b67496fc9a962b39db
2024-07-22 09:13:42 -05:00
Steve Myers
53bea0d902 Bump bdk version to 1.0.0-beta.1
bdk_chain to 0.17.0
bdk_bitcoind_rpc to 0.13.0
bdk_electrum to 0.16.0
bdk_esplora to 0.16.0
bdk_file_store to 0.14.0
bdk_testenv to 0.7.0
bdk_hwi to 0.4.0
2024-07-21 21:47:39 -05:00
Steve Myers
5478bb1ebb Merge bitcoindevkit/bdk#1506: Standardize API ownership in KeychainTxOutIndex
79262185d5 refactor(chain)!: update KeychainTxOutIndex methods to use owned ScriptBuf (Steve Myers)
7c07b9de02 refactor(chain)!: update KeychainTxOutIndex methods to use owned K (Rob N)

Pull request description:

  ### Description

  Make all method signatures of `KeychainTxOutIndex` take owned `K` and use `ScriptBuf` instead of its borrowed counterpart `&Script`. Fixes #1482

  ### Notes to the reviewers

  Steve also added a CI fix as well

  ### Changelog notice

  - Make all method signatures of `KeychainTxOutIndex` take owned `K`
  - Update `KeychainTxOutIndex` methods to use `ScriptBuf`

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

  #### Bugfixes:

  * [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: 3cb7d627ef6f38e1eaf6b88174f143c42dfc4d34e3d3d56cc843c256b2f32360fd00fa9ee328d0a41dac1f46771ccae797a96d9e3cee6f5ac4ef63e27cf6b7b7
2024-07-21 21:37:11 -05:00
Steve Myers
79262185d5 refactor(chain)!: update KeychainTxOutIndex methods to use owned ScriptBuf 2024-07-21 20:28:01 -05:00
Rob N
7c07b9de02 refactor(chain)!: update KeychainTxOutIndex methods to use owned K 2024-07-21 20:27:59 -05:00
Steve Myers
0c8ee1dfe2 Merge bitcoindevkit/bdk#1514: refactor(wallet)!: rework persistence, changeset, and construction
64eb576348 chore(wallet): Fix ChangeSet::merge (LLFourn)
8875c92ec1 chore(wallet): Fix descriptor mismatch error keychain (LLFourn)
2cf07d686b refactor(chain,wallet)!: move rusqlite things into it's own file (志宇)
93f9b83e27 chore(chain): rm unused `sqlite` types (志宇)
892b97d441 feat(chain,wallet)!: Change persist-traits to be "safer" (志宇)
3aed4cf179 test(wallet): ensure checks work when loading wallet (志宇)
af4ee0fa4b refactor(wallet)!: Make `bdk_wallet::ChangeSet` non-exhaustive (志宇)
22d02ed3d1 feat!: improve wallet building methods (志宇)
eb73f0659e refactor!: move `WalletChangeSet` to `bdk_wallet` and fix import paths (志宇)
6b43001951 feat!: Rework sqlite, changesets, persistence and wallet-construction (志宇)

Pull request description:

  Closes #1496
  Closes #1498
  Closes #1500

  ### Description

  Rework sqlite: Instead of only supported one schema (defined in `bdk_sqlite`), we have a schema per changeset type for more flexiblity.

  * rm `bdk_sqlite` crate (as we don't need `bdk_sqlite::Store` anymore).
  * add `sqlite` feature on `bdk_chain` which adds methods on each changeset type for initializing tables, loading the changeset and writing.

  Rework changesets: Some callers may want to use `KeychainTxOutIndex` where `K` may change per descriptor on every run. So we only want to persist the last revealed indices by `DescriptorId` (which uniquely-ish identifies the descriptor).

  * rm `keychain_added` field from `keychain_txout`'s changeset.
  * Add `keychain_added` to `CombinedChangeSet` (which is renamed to `WalletChangeSet`).

  Rework persistence: add back some safety and convenience when persisting our types. Working with changeset directly (as we were doing before) can be cumbersome.

  * Intoduce `struct Persisted<T>` which wraps a type `T` which stores staged changes to it. This adds safety when creating and or loading `T` from db.
  * `struct Persisted<T>` methods, `create`, `load` and `persist`, are available if `trait PersistWith<Db>` is implemented for `T`. `Db` represents the database connection and `PersistWith` should be implemented per database-type.
  * For async, we have `trait PersistedAsyncWith<Db>`.
  * `Wallet` has impls of `PersistedWith<rusqlite::Connection>`, `PersistedWith<rusqlite::Transaction>` and `PersistedWith<bdk_file_store::Store>` by default.

  Rework wallet-construction: Before, we had multiple methods for loading and creating with different input-counts so it would be unwieldly to add more parameters in the future. This also makes it difficult to impl `PersistWith` (which has a single method for `load` that takes in `PersistWith::LoadParams` and a single method for `create` that takes in `PersistWith::CreateParams`).

  * Introduce a builder pattern when constructing a `Wallet`. For loading from persistence or `ChangeSet`, we have `LoadParams`. For creating a new wallet, we have `CreateParams`.

  ### Notes to the reviewers

  TODO

  ### Changelog notice

  ```
  ### Added

  - Add `sqlite` feature to `bdk_chain` which introduces methods on changeset types that encode/decode changesets to SQLite database.
  * Introduce `PersistWith<Db>` and `PersistAsyncWith<Db>` traits and a `Persisted` wrapper. This ergonomically makes sure user inits the db before reading/writing to it.

  ### Changed

  - Moved `bdk_chain::CombinedChangeSet` to `bdk_wallet::ChangeSet` and added `keychain_added` field.
  - `bdk_wallet::Wallet` construction now uses a builder API using the newly introduced `CreateParams` and `LoadParams`.

  ### Removed

  - Remove `keychains_added` field from `bdk_chain::keychain_txout::ChangeSet`.

  ```
  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

ACKs for top commit:
  LLFourn:
    ACK: 64eb576348
  notmandatory:
    Re ACK 64eb576348

Tree-SHA512: b8a1d48aea26d9fa293a8387a3533cd16c8ddae890f94d61fb91efa492fb05ac5e0a66200d64d7c857774368d5f0f8991a98684307029c25f50a1d8fceee8e67
2024-07-21 20:26:30 -05:00
LLFourn
64eb576348 chore(wallet): Fix ChangeSet::merge
h/t @ValuedMammal
2024-07-22 03:01:52 +10:00
LLFourn
8875c92ec1 chore(wallet): Fix descriptor mismatch error keychain 2024-07-22 02:57:00 +10:00
志宇
2cf07d686b refactor(chain,wallet)!: move rusqlite things into it's own file
Also fix imports and rename `sqlite` module to `rusqlite_impl`.
2024-07-19 11:25:11 +00:00
志宇
93f9b83e27 chore(chain): rm unused sqlite types 2024-07-19 07:22:09 +00:00
志宇
892b97d441 feat(chain,wallet)!: Change persist-traits to be "safer"
Previously, `Persist{Async}With::persist` can be directly called as a
method on the type (i.e. `Wallet`). However, the `db: Db` (which is an
input) may not be initialized. We want a design which makes it harder
for the caller to make this mistake.

We change `Persist{Async}With::persist` to be an "associated function"
which takes two inputs: `db: &mut Db` and `changeset`. However, the
implementer cannot take directly from `Self` (as it's no longer an
input). So we introduce a new trait `Staged` which defines the staged
changeset type and a method that gives us a `&mut` of the staged
changes.
2024-07-19 07:05:38 +00:00
志宇
3aed4cf179 test(wallet): ensure checks work when loading wallet 2024-07-18 06:47:34 +00:00
志宇
af4ee0fa4b refactor(wallet)!: Make bdk_wallet::ChangeSet non-exhaustive 2024-07-18 05:47:12 +00:00
志宇
22d02ed3d1 feat!: improve wallet building methods
Remove returning `Result` for builder methods on `CreateParams` and
`LoadParams`.
2024-07-18 04:16:05 +00:00
志宇
eb73f0659e refactor!: move WalletChangeSet to bdk_wallet and fix import paths 2024-07-18 04:15:47 +00:00
志宇
6b43001951 feat!: Rework sqlite, changesets, persistence and wallet-construction
Rework sqlite: Instead of only supported one schema (defined in
`bdk_sqlite`), we have a schema per changeset type for more flexiblity.

* rm `bdk_sqlite` crate (as we don't need `bdk_sqlite::Store` anymore).
* add `sqlite` feature on `bdk_chain` which adds methods on each
  changeset type for initializing tables, loading the changeset and
  writing.

Rework changesets: Some callers may want to use `KeychainTxOutIndex`
where `K` may change per descriptor on every run. So we only want to
persist the last revealed indices by `DescriptorId` (which uniquely-ish
identifies the descriptor).

* rm `keychain_added` field from `keychain_txout`'s changeset.
* Add `keychain_added` to `CombinedChangeSet` (which is renamed to
  `WalletChangeSet`).

Rework persistence: add back some safety and convenience when persisting
our types. Working with changeset directly (as we were doing before) can
be cumbersome.

* Intoduce `struct Persisted<T>` which wraps a type `T` which stores
  staged changes to it. This adds safety when creating and or loading
  `T` from db.
* `struct Persisted<T>` methods, `create`, `load` and `persist`, are
  avaliable if `trait PersistWith<Db>` is implemented for `T`. `Db`
  represents the database connection and `PersistWith` should be
  implemented per database-type.
* For async, we have `trait PersistedAsyncWith<Db>`.
* `Wallet` has impls of `PersistedWith<rusqlite::Connection>`,
  `PersistedWith<rusqlite::Transaction>` and
  `PersistedWith<bdk_file_store::Store>` by default.

Rework wallet-construction: Before, we had multiple methods for loading
and creating with different input-counts so it would be unwieldly to add
more parameters in the future. This also makes it difficult to impl
`PersistWith` (which has a single method for `load` that takes in
`PersistWith::LoadParams` and a single method for `create` that takes in
`PersistWith::CreateParams`).

* Introduce a builder pattern when constructing a `Wallet`. For loading
  from persistence or `ChangeSet`, we have `LoadParams`. For creating a
  new wallet, we have `CreateParams`.
2024-07-18 03:25:41 +00:00
Steve Myers
d99b3ef4b4 Merge bitcoindevkit/bdk#1489: feat(electrum)!: Update bdk_electrum to use merkle proofs
1a62488abf feat(chain)!: Implement `ConfirmationBlockTime` (Wei Chen)
e761adf481 test(electrum): Imported `bdk_esplora` tests into `bdk_electrum` (Wei Chen)
d7f4ab71e2 feat(electrum)!: Update `bdk_electrum` to use merkle proofs (Wei Chen)

Pull request description:

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

  ### Description

  This PR is the first step in reworking `bdk_electrum` to use merkle proofs. When we fetch a transaction, we now also obtain the merkle proof and block header for verification. We then insert an anchor only after validation that the transaction exists in a confirmed block. The loop logic that previously existed in `full_scan` to account for re-orgs has also been removed as part of this rework.

  This is a breaking change because `graph_update`s now provide the full `ConfirmationTimeHeightAnchor` type. This removes the need for the `ElectrumFullScanResult` and `ElectrumSyncResult` structs that existed only to provide the option for converting the anchor type from `ConfirmationHeightAnchor` into `ConfirmationTimeHeightAnchor`.

  ### Notes to the reviewers

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

  ### Changelog notice

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
  * `ConfirmationTimeHeightAnchor` and `ConfirmationHeightAnchor` have been removed.
  * `ConfirmationBlockTime` has been introduced as a new anchor type.
  * `bdk_electrum`'s `full_scan` and `sync` now return `graph_update`s with `ConfirmationBlockTime`.

  ### Checklists

  #### All Submissions:

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

  #### New Features:

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

ACKs for top commit:
  ValuedMammal:
    ACK 1a62488abf
  notmandatory:
    ACK 1a62488abf

Tree-SHA512: 77af05bffcb9668ecb99b41abacc6b6aa503dc559226fa88c4cab6863e3af431b937706696ec765bb802c9c152333cd430c284d17a6cd190520e10b13d89e02f
2024-07-09 14:29:21 -05:00
Wei Chen
1a62488abf feat(chain)!: Implement ConfirmationBlockTime
Both `bdk_electrum` and `bdk_esplora` now report the exact block
that the transaction is in, which removes the need for having the
old `ConfirmationTimeHeightAnchor` and `ConfirmationHeightAnchor`.
This PR introduces a new, simpler anchor type that can be modified
to support additional data in the future.
2024-07-09 00:23:02 +08:00
Wei Chen
e761adf481 test(electrum): Imported bdk_esplora tests into bdk_electrum 2024-07-09 00:23:02 +08:00
Wei Chen
d7f4ab71e2 feat(electrum)!: Update bdk_electrum to use merkle proofs 2024-07-09 00:23:02 +08:00
Steve Myers
1a39821b88 Merge bitcoindevkit/bdk#1505: ci: pin cc dependency version to build with rust 1.63
3f9ed95e2e ci: pin cc dependency version to build with rust 1.63 (Wei Chen)

Pull request description:

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

  ### Description

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->
  `cc` version 1.0.106 raised `msrv` to 1.67.
  The previous working version 1.0.105 was pinned for CI to continue working.

  ### Notes to the reviewers

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

  ### Changelog notice

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
  * Pinned cc dependency version to build with rust 1.63.

  ### 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:
  storopoli:
    ACK 3f9ed95e2e
  notmandatory:
    ACK 3f9ed95e2e

Tree-SHA512: 995786f214c06db278daa07643489cdae557c14ffda4b873d2951ca6b52de64630e39dafcbc43cecaede0911fd6d137b992a868478d4bae674e73b4ecc18650b
2024-07-08 10:49:46 -05:00
Wei Chen
3f9ed95e2e ci: pin cc dependency version to build with rust 1.63 2024-07-08 23:20:26 +08:00
Steve Myers
8714e9d806 Merge bitcoindevkit/bdk#1503: feat(wallet): simplify public_descriptor fn and remove `get_descriptor_for_keych…
e7ec5a8733 refactor(wallet)!: remove redundant get_descriptor_for_keychain (Giovanni Napoli)

Pull request description:

  Fixes #1501

  ### Description

  Simplifies `public_descriptor` function by using `get_descriptor` and removes `get_descriptor_for_keychain`.

  ### Notes to the reviewers

  Tested with `cargo test --all-features`.

  ### Changelog notice

  - Simplifies `public_descriptor` function and removes `get_descriptor_for_keychain`

  ### Checklists

  #### All Submissions:

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

ACKs for top commit:
  notmandatory:
    ACK e7ec5a8733
  storopoli:
    ACK e7ec5a8733

Tree-SHA512: 5981190ac882e08e42b1be53c55afb70c4ba7e7a99a8ae2a4e05f0618d0eb23849ce544024bb406e6a6918d9e9757d9ff6ad5a701cd9814b686e36f1ea16b44a
2024-07-08 09:21:58 -05:00
Steve Myers
43f093d918 Merge bitcoindevkit/bdk#1502: chore(chain)!: Rename Append to Merge
962305f415 chore(chain)!: Rename `Append` to `Merge` (Wei Chen)

Pull request description:

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

  ### Description

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->
  Renames the `Append` trait to `Merge`.

  ### Notes to the reviewers

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

  ### Changelog notice

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
  * Rename `bdk_chain::Append` to `bdk_chain::Merge`.

  ### Checklists

  #### All Submissions:

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

ACKs for top commit:
  notmandatory:
    ACK 962305f415

Tree-SHA512: 2019d9ed631776850cda39c9a53bdd3660c7f4715d8c418824d8ad13718f2b2fd160159e03fd63743dbf0b9e91cfdfed7e4cd4a591636f67d2aaec419486d136
2024-07-07 21:02:39 -05:00
Wei Chen
962305f415 chore(chain)!: Rename Append to Merge 2024-07-07 16:46:10 +08:00
Lloyd Fournier
db8fbd729d Merge pull request #1493 from ValuedMammal/refactor/keychain-balance
[chain] Create module `indexer`
2024-07-06 22:37:02 +10:00
Giovanni Napoli
e7ec5a8733 refactor(wallet)!: remove redundant get_descriptor_for_keychain
Simplify Wallet::public_descriptor() and update Wallet internals to use
public_descriptor() instead of get_descriptor_for_keychain().
2024-07-06 13:39:49 +02:00
Steve Myers
139eec7da0 Merge bitcoindevkit/bdk#1487: Add support for custom sorting and deprecate BIP69
3bee563c81 refactor(wallet)!: remove TxOrdering::Bip69Lexicographic (nymius)
e5cb7b2066 feat(wallet): add TxOrdering::Custom (FadedCoder)

Pull request description:

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

  ### Description

  Resolves https://github.com/bitcoindevkit/bdk/issues/534.

  Resumes from the work in https://github.com/bitcoindevkit/bdk/pull/556.

  Add custom sorting function for inputs and outputs through `TxOrdering::Custom` and deprecates `TxOrdering::Bip69Lexicographic`.

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

  ### Notes to the reviewers

  I tried consider all discussions in https://github.com/bitcoindevkit/bdk/issues/534 while implementing some changes to the original PR. I created a summary of the considerations I had while implementing this:

  ##### Why use smart pointers?
  The size of enums and structs should be known at compilation time. A struct whose fields implements some kind of trait cannot be specified without using a smart pointer because the size of the implementations of the trait cannot be known beforehand.

  ##### Why `Arc` or `Rc` instead of `Box`?
  The majority of the useful smart pointers that I know (`Arc`, `Box`, `Rc`) for this case implement `Drop` which rules out the implementation of `Copy`, making harder to manipulate a simple enum like `TxOrdering`. `Clone` can be used instead, implemented by `Arc` and `Rc`, but not implemented by `Box`.

  #####  Why `Arc` instead of `Rc`?
  Multi threading I guess.

  ##### Why using a type alias like `TxVecSort`?
  cargo-clippy was accusing a too complex type if using the whole type inlined in the struct inside the enum.

  ##### Why `Fn` and not `FnMut`?
  `FnMut` is not allowed inside `Arc`. I think this is due to the `&mut self` ocupies the first parameter of the `call` method when desugared (https://rustyyato.github.io/rust/syntactic/sugar/2019/01/17/Closures-Magic-Functions.html), which doesn't respects `Arc` limitation of not having mutable references to data stored inside `Arc`:
  Quoting the [docs](https://doc.rust-lang.org/std/sync/struct.Arc.html):
  > you cannot generally obtain a mutable reference to something inside an `Arc`.

  `FnOnce` > `FnMut` > `Fn`, where `>` stands for "is supertrait of", so, `Fn` can be used everywhere `FnMut` is expected.

  ##### Why not `&'a dyn FnMut`?
  It needs to include a lifetime parameter in `TxOrdering`, which will force the addition of a lifetime parameter in `TxParams`, which will require the addition of a lifetime parameter in a lot of places more. **Which one is preferable?**

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

  ### Changelog notice

  - Adds new `TxOrdering` variant: `TxOrdering::Custom`. A structure that stores the ordering functions to sort the inputs and outputs of a transaction.
  - Deprecates `TxOrdering::Bip69Lexicographic`.

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

  ### Checklists

  #### All Submissions:

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

Top commit has no ACKs.

Tree-SHA512: 0d3e3ea9aee3a6c9e9d5e1ae93215be84bd1bd99907a319976517819aeda768a7166860a48a8d24abb30c516e0129decb6a6aebd8f24783ea2230143e6dcd72a
2024-07-05 15:31:03 -05:00
nymius
3bee563c81 refactor(wallet)!: remove TxOrdering::Bip69Lexicographic
BIP 69 proposed a deterministic way to sort transaction inputs and
outputs with the idea of enhancing privacy. It was later discovered
there was no such enhancement but rather a decrement in privacy due to
this sorting.
To avoid the promotion of bad practices, the
TxOrdering::Bip69Lexicographic variant which implemented this BIP for
BDK is removed with this change.
Notice that you can still produce a BIP 69 compliant transaction
defining order functions for TxOrdering::Custom.

Signed-off-by: Steve Myers <steve@notmandatory.org>
2024-07-05 15:03:08 -05:00
FadedCoder
e5cb7b2066 feat(wallet): add TxOrdering::Custom
The deterministic sorting of transaction inputs and outputs proposed in
BIP 69 doesn't improve the privacy of transactions as originally
intended. In the search of not spreading bad practices but still provide
flexibility for possible use cases depending on particular order of the
inputs/outpus of a transaction, a new TxOrdering variant has been added
to allow the implementation of these orders through the definition of
comparison functions.

Signed-off-by: Steve Myers <steve@notmandatory.org>
2024-07-05 15:03:05 -05:00
valued mammal
c3fc1dd123 ref(chain)!: create module indexer
and replace keychain module with `balance.rs`
2024-07-05 12:22:28 -04:00
Steve Myers
a112b4d97c Merge bitcoindevkit/bdk#1416: [chain] Change tx_last_seen to Option<u64>
af75817d4b ref(tx_graph): Change last_seen to `HashMap<Txid, u64>` (valued mammal)
6204d2c766 feat(tx_graph): Add method `txs_with_no_anchor_or_last_seen` (valued mammal)
496601b8b1 test(tx_graph): Add test for `list_canonical_txs` (valued mammal)
c4057297a9 wallet: delete method `insert_anchor` (valued mammal)
b34790c6b6 ref(tx_graph)!: Rename `list_chain_txs` to `list_canonical_txs` (valued mammal)
2ce4bb4dfc test(indexed_tx_graph): Add test_get_chain_position (valued mammal)
36f58870cb test(wallet): Add test_insert_tx_balance_and_utxos (valued mammal)
bbc19c3536 fix(tx_graph)!: Change tx_last_seen to `Option<u64>` (valued mammal)
324eeb3eb4 fix(wallet)!: Rework `Wallet::insert_tx` to no longer insert anchors (valued mammal)

Pull request description:

  The PR changes the type of last_seen to `Option<u64>` for `txs` member of `TxGraph`.

  This fixes an issue where unbroadcast and otherwise non-canonical transactions were returned from methods `list_chain_txs` and `Wallet::transactions` because every new tx inserted had a last_seen of 0 making it appear unconfirmed.

  fixes #1446
  fixes #1396

  ### Notes to the reviewers

  ### Changelog notice

  Changed
  - Member `last_seen_unconfirmed` of `TxNode` is changed to `Option<u64>`
  - Renamed `TxGraph` method `list_chain_txs` to `list_canonical_txs`
  - Changed `Wallet::insert_tx` to take a single `tx: Transaction` as parameter

  Added
  - Add method `txs_with_no_anchor_or_last_seen` for `TxGraph`
  - Add method `unbroadcast_transactions` for `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

  #### Bugfixes:

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

ACKs for top commit:
  notmandatory:
    Re ACK af75817d4b

Tree-SHA512: e664b3b49e2f547873923f15dffbbc7fa032b6240e5b856b180e9e26123ca141864d10448912dc4a31bbb200c75bef4251a910a4330dac17ee6841b564612d13
2024-07-02 16:50:26 -05:00
valued mammal
af75817d4b ref(tx_graph): Change last_seen to HashMap<Txid, u64> 2024-06-30 11:26:15 -04:00
valued mammal
6204d2c766 feat(tx_graph): Add method txs_with_no_anchor_or_last_seen 2024-06-30 10:08:54 -04:00
valued mammal
496601b8b1 test(tx_graph): Add test for list_canonical_txs 2024-06-30 10:08:54 -04:00
valued mammal
c4057297a9 wallet: delete method insert_anchor 2024-06-30 10:08:54 -04:00
valued mammal
b34790c6b6 ref(tx_graph)!: Rename list_chain_txs to list_canonical_txs 2024-06-30 10:08:54 -04:00
valued mammal
2ce4bb4dfc test(indexed_tx_graph): Add test_get_chain_position 2024-06-30 10:08:54 -04:00
valued mammal
36f58870cb test(wallet): Add test_insert_tx_balance_and_utxos 2024-06-30 10:08:54 -04:00
valued mammal
bbc19c3536 fix(tx_graph)!: Change tx_last_seen to Option<u64>
Also fixup `test_list_owned_txouts` to check that the right
outputs, utxos, and balance are returned at different local
chain heights.

This fixes an issue where unbroadcast and otherwise non-canonical
transactions were returned from methods `list_chain_txs` and
`Wallet::transactions` because every tx inserted had a last_seen
of 0 making it appear unconfirmed.

Note this commit changes the way `Balance` is represented due to
new logic in `try_get_chain_position` that no longer considers
txs with non-canonical anchors. Before this change, a tx anchored
to a block that is reorged out had a permanent effect on the
pending balance, and now only txs with a last_seen time or an
anchor confirmed in the best chain will return a `ChainPosition`.
2024-06-30 10:08:54 -04:00
Steve Myers
22368ab7b0 Merge bitcoindevkit/bdk#1486: refactor(chain): calculate DescriptorId as the sha256 hash of the spk at index 0
8f5b172e59 test(wallet): verify wallet panics in dev mode if using keychains with duplicate spks (Steve Myers)
46c6f18cc3 refactor(chain): calculate DescriptorId as sha256 hash of spk at index 0 (Steve Myers)

Pull request description:

  ### Description

  Rename `DescriptorId` to `KeychainId` and `descriptor_id()` to `keychain_id()`.

  Calculate keychain ids as the hash of the spk derived from its descriptor as index 0.

  Added docs to `Wallet` and `KeychainTxOutIndex::insert_descriptor()` explaining that it's the users responsibility not to use wildcard and non-wildcard descriptors that can derive the same script pubkey. I also recommended for `Wallet` that legacy non-wildcard descriptors be added to a temporary `Wallet` and swept into a modern wildcard descriptor.

  ### Notes to the reviewers

  fixes #1483

  ### Changelog notice

  changed

  - Renamed DescriptorId to KeychainId, DescriptorExt::descriptor_id() to keychain_id().

  ### Checklists

  #### All Submissions:

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

  #### Bugfixes:

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

ACKs for top commit:
  LLFourn:
    ACK 8f5b172e59
  oleonardolima:
    Concept ACK 8f5b172e59

Tree-SHA512: 07defa208d9cfcd61bc6e31ded06a287c392c51bcc4949f601ecfac635c3443e7d08c62d92618ed894dc5ef13cdcf784771a6bf8904a5397110bedb1563f52d4
2024-06-28 17:38:11 -05:00
Steve Myers
d75d9f94ce Merge bitcoindevkit/bdk#1490: Remove usage of blockdata:: from bitcoin paths
cf7aca84d1 Remove usage of blockdata:: from bitcoin paths (Tobin C. Harding)

Pull request description:

  ### Description

  In `rust-bitcoin` the `blockdata` module is a code organisation thing, it should never have been public. One day those guys would like to remove it, so as not to be a PITA for `bdk` when they do lets remove all usage of `blockdata::` now.

  ### Changelog notice

  Internal change only, no externally visible changes.

  ### 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: 4c5e54a2ac3f71835c25ce97ad0acd1859e5ae8d45ebfde087e19e9494754e0aa70a47ca5f3d6a3b509f27e28ef9d4a5269573c686250f43834d09378155681c
2024-06-28 16:41:06 -05:00
Steve Myers
8f5b172e59 test(wallet): verify wallet panics in dev mode if using keychains with duplicate spks 2024-06-27 07:56:09 -05:00
Steve Myers
46c6f18cc3 refactor(chain): calculate DescriptorId as sha256 hash of spk at index 0
Also update docs to explain how KeychainTxOutIndex handles descriptors that
generate the same spks.
2024-06-27 07:56:08 -05:00
Tobin C. Harding
cf7aca84d1 Remove usage of blockdata:: from bitcoin paths
In `rust-bitcoin` the `blockdata` module is a code organisation thing,
it should never have been public. One day those guys would like to
remove it, so as not to be a PITA for `bdk` when they do lets remove all
usage of `blockdata::` now.

Internal change only, no externally visible changes.
2024-06-27 10:27:54 +10:00
Steve Myers
5c7cc30978 Merge bitcoindevkit/bdk#1468: feat: use Weight type instead of usize
438cd4682d refactor(wallet)!: change WeightedUtxo to use Weight type (Leonardo Lima)

Pull request description:

  fixes #1466
  depends on #1448
  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  This PR is a follow-up on top of #1448, and should be rebased and merged after it, it uses the rust-bitcoin `Weight` type instead of the current `usize` usage.

  NOTE: ~~It also adds a new `MiniscriptError` variant, and remove the `.unwrap()` usage.~~ As suggested I'll address this on another issue #1485, trying to reproduce the error first.

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

  ### Notes to the reviewers
  It should be ready to review after #1448 gets merged.
  <!-- In this section you can include notes directed to the reviewers, like explaining why some parts
  of the PR were done in a specific way -->

  ### Changelog notice
  - Change `WeightedUtxo` `satisfaction_weight` has been changed to use `Weight` type.
  - Change `TxBuilder` methods `add_foreign_utxo` and `add_foreign_utxo_with_sequence` to expect `Weight` as `satisfaction_weight` type.

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

  ### Checklists

  #### All Submissions:

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

ACKs for top commit:
  storopoli:
    Anyways, ACK 438cd4682d
  notmandatory:
    ACK 438cd4682d

Tree-SHA512: 1998fe659833da890ce07aa746572ae24d035e636732c1a11b7828ffed48e753adb4108f42d00b7cd05e6f45831a7a9840faa26f94058fc13760497837af002f
2024-06-26 17:51:23 -05:00
Leonardo Lima
438cd4682d refactor(wallet)!: change WeightedUtxo to use Weight type 2024-06-26 09:09:23 -03:00
Steve Myers
275e069cf4 Merge bitcoindevkit/bdk#1424: Remove trait ComputeSighash
55a17293a4 ref(signer): Use `Psbt::sighash_ecdsa` for computing sighashes (valued mammal)
f2a2dae84c refactor(signer): Remove trait ComputeSighash (valued mammal)

Pull request description:

  This PR does some cleanup of the `bdk_wallet` signer module most notably by removing the internal trait `ComputeSighash` and replacing old code for computing the sighash (for legacy and segwit context) with a single method [`Psbt::sighash_ecdsa`](https://docs.rs/bitcoin/0.31.2/bitcoin/psbt/struct.Psbt.html#method.sighash_ecdsa). The logic for computing the taproot sighash is unchanged and extracted to a new helper function `compute_tap_sighash`.

  - [x] Unimplement `ComputeSighash`
  - [x] Try de-duplicating code by using `Psbt::sighash_ecdsa`. see https://github.com/bitcoindevkit/bdk/pull/1023#discussion_r1263140218
  - Not done in this PR: Consider removing unused `SignerError` variants

  fixes #1038

  ### Notes to the reviewers

  ### Changelog notice

  ### Checklists

  #### All Submissions:

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

Top commit has no ACKs.

Tree-SHA512: 56af3c9c463513ca3bae5480aa5b90d78de119c3c09c824a7220eb6832d5f403b172afc8168228918ea1adabb4bf8fca858790adfebf84fc334b4fc1cc99d3cd
2024-06-25 14:23:17 -05:00
valued mammal
55a17293a4 ref(signer): Use Psbt::sighash_ecdsa for computing sighashes
- Change param `hash` to `&Message` in `sign_psbt_ecdsa`
- Remove unused methods `compute_legacy_sighash`,
and `compute_segwitv0_sighash`.
- Match on `self.ctx` when signing for `SignerWrapper<PrivateKey>`
2024-06-24 09:06:32 -04:00
valued mammal
f2a2dae84c refactor(signer): Remove trait ComputeSighash 2024-06-24 09:04:57 -04:00
valued mammal
324eeb3eb4 fix(wallet)!: Rework Wallet::insert_tx to no longer insert anchors
since we'd be lacking context that should normally occur during
sync with a chain source. The logic for inserting a graph
anchor from a `ConfirmationTime` is moved to the wallet common
test module in order to simulate receiving new txs and
confirming them.
2024-06-23 13:15:23 -04:00
Steve Myers
6dab68d35b Merge bitcoindevkit/bdk#1395: Remove rand dependency from bdk
4bddb0de62 feat(wallet): add back TxBuilder finish() and sort_tx() with thread_rng() (Steve Myers)
45c0cae0a4 fix(bdk): remove rand dependency (rustaceanrob)

Pull request description:

  ### Description

  WIP towards removing `rand` fixes #871

  The `rand` dependency was imported explicitly, but `rand` is also implicitly used through the `rand-std` feature flag on `bitcoin`.

  ### Notes to he reviewers

  **Updated:**

  `rand` was used primarily in two parts of `bdk`. Particularly in signing and in building a transaction.

  Signing:
  - Used implicitly in [`sign_schnorr`](https://docs.rs/bitcoin/latest/bitcoin/key/struct.Secp256k1.html#method.sign_schnorr), but nowhere else within `signer`.

  Transaction ordering:
  - Used to shuffle the inputs and outputs of a transaction, the default
  - Used in the single random draw __as a fallback__ to branch and bound during coin selection. Branch and bound is the default coin selection option.

  See conversation for proposed solutions.

  ### Changelog notice

  - Remove the `rand` dependency from `bdk`

  ### Checklists

  #### All Submissions:

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

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

ACKs for top commit:
  ValuedMammal:
    ACK 4bddb0de62
  notmandatory:
    ACK 4bddb0de62

Tree-SHA512: 662d9bcb1e02f8195d73df16789b8c2aba8ccd7b37ba713ebb0bfd19c66163acbcb6f266b64f88347cbb1f96b88c8a150581012cbf818d1dc8b4437b3e53fc62
2024-06-22 21:28:37 -05:00
志宇
e406675f43 Merge bitcoindevkit/bdk#1476: fix(wallet)!: Simplify SignOptions and improve finalization logic
996605f2bf fix(wallet)!: Simplify `SignOptions` and improve finalization logic (valued mammal)

Pull request description:

  Rather than comingle various `SignOptions` with the finalization step, we simply clear all fields when finalizing as per the PSBT spec in BIPs 174 and 371 which is more in line with user expectations.

  ### Notes to the reviewers

  I chose to re-implement some parts of [`finalize_input`](https://docs.rs/miniscript/latest/src/miniscript/psbt/finalizer.rs.html#434) since it's fairly straightforward, see also https://github.com/bitcoindevkit/bdk/issues/1461#issuecomment-2171983426. I had to refactor some wallet tests but didn't go out of my way to add additional tests.

  closes #1461

  ### Changelog notice

  - Removed field `remove_partial_sigs` from `SignOptions`
  - Removed field `remove_taproot_extras` from `SignOptions`

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

ACKs for top commit:
  evanlinjin:
    re-ACK 996605f2bf

Tree-SHA512: 63e78e75c22031424e87fcc26cd6b0015c626cd57c02680256bad9d1783cef71f4048b2d7ce5d0425cd4239351e37dd0e2a626dda7e8417af7fc52cb0afe6933
2024-06-20 12:41:04 +08:00
Steve Myers
4bddb0de62 feat(wallet): add back TxBuilder finish() and sort_tx() with thread_rng() 2024-06-19 13:56:06 -10:00
valued mammal
996605f2bf fix(wallet)!: Simplify SignOptions and improve finalization logic
Rather than comingle various `SignOptions` with the finalization
step, we simply clear all fields when finalizing as per the PSBT
spec in BIPs 174 and 371 which is more in line with user
expectations.
2024-06-19 12:02:15 -04:00
rustaceanrob
45c0cae0a4 fix(bdk): remove rand dependency 2024-06-17 15:27:58 -10:00
83 changed files with 4452 additions and 3997 deletions

View File

@@ -35,6 +35,8 @@ jobs:
cargo update -p home --precise "0.5.5"
cargo update -p proptest --precise "1.2.0"
cargo update -p url --precise "2.5.0"
cargo update -p cc --precise "1.0.105"
cargo update -p tokio --precise "1.38.1"
- name: Build
run: cargo build ${{ matrix.features }}
- name: Test
@@ -92,7 +94,7 @@ jobs:
uses: Swatinem/rust-cache@v2.2.1
- name: Check bdk wallet
working-directory: ./crates/wallet
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
- name: Check esplora
working-directory: ./crates/esplora
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async

View File

@@ -4,7 +4,6 @@ members = [
"crates/wallet",
"crates/chain",
"crates/file_store",
"crates/sqlite",
"crates/electrum",
"crates/esplora",
"crates/bitcoind_rpc",

View File

@@ -73,6 +73,8 @@ cargo update -p time --precise "0.3.20"
cargo update -p home --precise "0.5.5"
cargo update -p proptest --precise "1.2.0"
cargo update -p url --precise "2.5.0"
cargo update -p cc --precise "1.0.105"
cargo update -p tokio --precise "1.38.1"
```
## License

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_bitcoind_rpc"
version = "0.12.0"
version = "0.13.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -15,7 +15,7 @@ readme = "README.md"
[dependencies]
bitcoin = { version = "0.32.0", default-features = false }
bitcoincore-rpc = { version = "0.19.0" }
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
bdk_chain = { path = "../chain", version = "0.17", default-features = false }
[dev-dependencies]
bdk_testenv = { path = "../testenv", default-features = false }

View File

@@ -3,9 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{
bitcoin::{Address, Amount, Txid},
keychain::Balance,
local_chain::{CheckPoint, LocalChain},
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
spk_txout::SpkTxOutIndex,
Balance, BlockId, IndexedTxGraph, Merge,
};
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
@@ -48,7 +48,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
BTreeMap::from([(height, Some(hash))]),
[(height, Some(hash))].into(),
"chain update changeset is unexpected",
);
}
@@ -94,11 +94,13 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
if exp_height == exp_hashes.len() - reorged_blocks.len() {
core::iter::once((height, Some(hash)))
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
.collect::<bdk_chain::local_chain::ChangeSet>()
bdk_chain::local_chain::ChangeSet {
blocks: core::iter::once((height, Some(hash)))
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
.collect(),
}
} else {
BTreeMap::from([(height, Some(hash))])
[(height, Some(hash))].into()
},
"chain update changeset is unexpected",
);
@@ -194,7 +196,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs);
assert_eq!(
indexed_additions
.graph
.tx_graph
.txs
.iter()
.map(|tx| tx.compute_txid())
@@ -202,7 +204,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
exp_txids,
"changeset should have the 3 mempool transactions",
);
assert!(indexed_additions.graph.anchors.is_empty());
assert!(indexed_additions.tx_graph.anchors.is_empty());
}
// mine a block that confirms the 3 txs
@@ -225,9 +227,9 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let height = emission.block_height();
let _ = chain.apply_update(emission.checkpoint)?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
assert!(indexed_additions.graph.txs.is_empty());
assert!(indexed_additions.graph.txouts.is_empty());
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
assert!(indexed_additions.tx_graph.txs.is_empty());
assert!(indexed_additions.tx_graph.txouts.is_empty());
assert_eq!(indexed_additions.tx_graph.anchors, exp_anchors);
}
Ok(())
@@ -392,7 +394,6 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
get_balance(&recv_chain, &recv_graph)?,
Balance {
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.16.0"
version = "0.17.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -20,6 +20,10 @@ serde_crate = { package = "serde", version = "1", optional = true, features = ["
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
miniscript = { version = "12.0.0", optional = true, default-features = false }
# Feature dependencies
rusqlite_crate = { package = "rusqlite", version = "0.31.0", features = ["bundled"], optional = true }
serde_json = {version = "1", optional = true }
[dev-dependencies]
rand = "0.8"
proptest = "1.2.0"
@@ -28,3 +32,4 @@ proptest = "1.2.0"
default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
rusqlite = ["std", "rusqlite_crate", "serde", "serde_json"]

View File

@@ -1,20 +1,4 @@
//! Module for keychain related structures.
//!
//! A keychain here is a set of application-defined indexes for a miniscript descriptor where we can
//! derive script pubkeys at a particular derivation index. The application's index is simply
//! anything that implements `Ord`.
//!
//! [`KeychainTxOutIndex`] indexes script pubkeys of keychains and scans in relevant outpoints (that
//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but
//! also maintains "revealed" and "lookahead" index counts per keychain.
//!
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
#[cfg(feature = "miniscript")]
mod txout_index;
use bitcoin::{Amount, ScriptBuf};
#[cfg(feature = "miniscript")]
pub use txout_index::*;
use bitcoin::Amount;
/// Balance, differentiated into various categories.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
@@ -49,11 +33,6 @@ impl Balance {
}
}
/// A tuple of keychain index and `T` representing the indexed value.
pub type Indexed<T> = (u32, T);
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
pub type KeychainIndexed<K, T> = ((K, u32), T);
impl core::fmt::Display for Balance {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(

View File

@@ -74,11 +74,11 @@ impl ConfirmationTime {
}
}
impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
fn from(observed_as: ChainPosition<ConfirmationTimeHeightAnchor>) -> Self {
impl From<ChainPosition<ConfirmationBlockTime>> for ConfirmationTime {
fn from(observed_as: ChainPosition<ConfirmationBlockTime>) -> Self {
match observed_as {
ChainPosition::Confirmed(a) => Self::Confirmed {
height: a.confirmation_height,
height: a.block_id.height,
time: a.confirmation_time,
},
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
@@ -145,9 +145,7 @@ impl From<(&u32, &BlockHash)> for BlockId {
}
}
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
///
/// Note that the confirmation block and the anchor block can be different here.
/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction.
///
/// Refer to [`Anchor`] for more details.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
@@ -156,70 +154,27 @@ impl From<(&u32, &BlockHash)> for BlockId {
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationHeightAnchor {
/// The exact confirmation height of the transaction.
///
/// It is assumed that this value is never larger than the height of the anchor block.
pub confirmation_height: u32,
pub struct ConfirmationBlockTime {
/// The anchor block.
pub anchor_block: BlockId,
}
impl Anchor for ConfirmationHeightAnchor {
fn anchor_block(&self) -> BlockId {
self.anchor_block
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
}
}
impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
Self {
anchor_block: block_id,
confirmation_height: block_id.height,
}
}
}
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
/// transaction.
///
/// Note that the confirmation block and the anchor block can be different here.
///
/// Refer to [`Anchor`] for more details.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationTimeHeightAnchor {
/// The confirmation height of the transaction being anchored.
pub confirmation_height: u32,
pub block_id: BlockId,
/// The confirmation time of the transaction being anchored.
pub confirmation_time: u64,
/// The anchor block.
pub anchor_block: BlockId,
}
impl Anchor for ConfirmationTimeHeightAnchor {
impl Anchor for ConfirmationBlockTime {
fn anchor_block(&self) -> BlockId {
self.anchor_block
self.block_id
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
self.block_id.height
}
}
impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
impl AnchorFromBlockPosition for ConfirmationBlockTime {
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
Self {
anchor_block: block_id,
confirmation_height: block_id.height,
block_id,
confirmation_time: block.header.time as _,
}
}
@@ -305,19 +260,19 @@ mod test {
#[test]
fn chain_position_ord() {
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
confirmation_height: 9,
anchor_block: BlockId {
height: 20,
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 20,
block_id: BlockId {
height: 9,
..Default::default()
},
});
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
confirmation_height: 12,
anchor_block: BlockId {
height: 15,
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
confirmation_time: 15,
block_id: BlockId {
height: 12,
..Default::default()
},
});

View File

@@ -1,89 +0,0 @@
/// A changeset containing [`crate`] structures typically persisted together.
#[cfg(feature = "miniscript")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(crate::serde::Deserialize, crate::serde::Serialize),
serde(
crate = "crate::serde",
bound(
deserialize = "A: Ord + crate::serde::Deserialize<'de>, K: Ord + crate::serde::Deserialize<'de>",
serialize = "A: Ord + crate::serde::Serialize, K: Ord + crate::serde::Serialize",
),
)
)]
pub struct CombinedChangeSet<K, A> {
/// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
pub chain: crate::local_chain::ChangeSet,
/// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph).
pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
}
#[cfg(feature = "miniscript")]
impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
fn default() -> Self {
Self {
chain: core::default::Default::default(),
indexed_tx_graph: core::default::Default::default(),
network: None,
}
}
}
#[cfg(feature = "miniscript")]
impl<K: Ord, A: crate::Anchor> crate::Append for CombinedChangeSet<K, A> {
fn append(&mut self, other: Self) {
crate::Append::append(&mut self.chain, other.chain);
crate::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()
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
fn from(chain: crate::local_chain::ChangeSet) -> Self {
Self {
chain,
..Default::default()
}
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>>
for CombinedChangeSet<K, A>
{
fn from(
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
) -> Self {
Self {
indexed_tx_graph,
..Default::default()
}
}
}
#[cfg(feature = "miniscript")]
impl<K, A> From<crate::keychain::ChangeSet<K>> for CombinedChangeSet<K, A> {
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
Self {
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
indexer,
..Default::default()
},
..Default::default()
}
}
}

View File

@@ -1,12 +1,8 @@
use crate::{
alloc::{string::ToString, vec::Vec},
miniscript::{Descriptor, DescriptorPublicKey},
};
use crate::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.
/// Represents the unique ID of a descriptor.
///
/// 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
@@ -21,8 +17,8 @@ pub trait DescriptorExt {
/// 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.
/// Returns the descriptor ID, calculated as the sha256 hash of the spk derived from the
/// descriptor at index 0.
fn descriptor_id(&self) -> DescriptorId;
}
@@ -36,9 +32,7 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
}
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))
let spk = self.at_derivation_index(0).unwrap().script_pubkey();
DescriptorId(sha256::Hash::hash(spk.as_bytes()))
}
}

View File

@@ -1,11 +1,13 @@
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
//! [`IndexedTxGraph`] documentation for more.
use core::fmt::Debug;
use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
use crate::{
tx_graph::{self, TxGraph},
Anchor, AnchorFromBlockPosition, Append, BlockId,
Anchor, AnchorFromBlockPosition, BlockId, Indexer, Merge,
};
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
@@ -47,27 +49,30 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
self.index.apply_changeset(changeset.indexer);
for tx in &changeset.graph.txs {
for tx in &changeset.tx_graph.txs {
self.index.index_tx(tx);
}
for (&outpoint, txout) in &changeset.graph.txouts {
for (&outpoint, txout) in &changeset.tx_graph.txouts {
self.index.index_txout(outpoint, txout);
}
self.graph.apply_changeset(changeset.graph);
self.graph.apply_changeset(changeset.tx_graph);
}
/// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.initial_changeset();
let indexer = self.index.initial_changeset();
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
}
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
where
I::ChangeSet: Default + Append,
I::ChangeSet: Default + Merge,
{
fn index_tx_graph_changeset(
&mut self,
@@ -75,10 +80,10 @@ where
) -> I::ChangeSet {
let mut changeset = I::ChangeSet::default();
for added_tx in &tx_graph_changeset.txs {
changeset.append(self.index.index_tx(added_tx));
changeset.merge(self.index.index_tx(added_tx));
}
for (&added_outpoint, added_txout) in &tx_graph_changeset.txouts {
changeset.append(self.index.index_txout(added_outpoint, added_txout));
changeset.merge(self.index.index_txout(added_outpoint, added_txout));
}
changeset
}
@@ -89,21 +94,30 @@ where
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.apply_update(update);
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
/// Insert a floating `txout` of given `outpoint`.
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_txout(outpoint, txout);
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
/// Insert and index a transaction into the graph.
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_tx(tx);
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
/// Insert an `anchor` for a given transaction.
@@ -137,21 +151,24 @@ where
let mut indexer = I::ChangeSet::default();
for (tx, _) in &txs {
indexer.append(self.index.index_tx(tx));
indexer.merge(self.index.index_tx(tx));
}
let mut graph = tx_graph::ChangeSet::default();
for (tx, anchors) in txs {
if self.index.is_tx_relevant(tx) {
let txid = tx.compute_txid();
graph.append(self.graph.insert_tx(tx.clone()));
graph.merge(self.graph.insert_tx(tx.clone()));
for anchor in anchors {
graph.append(self.graph.insert_anchor(txid, anchor));
graph.merge(self.graph.insert_anchor(txid, anchor));
}
}
}
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
@@ -176,7 +193,7 @@ where
let mut indexer = I::ChangeSet::default();
for (tx, _) in &txs {
indexer.append(self.index.index_tx(tx));
indexer.merge(self.index.index_tx(tx));
}
let graph = self.graph.batch_insert_unconfirmed(
@@ -185,7 +202,10 @@ where
.map(|(tx, seen_at)| (tx.clone(), seen_at)),
);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
/// Batch insert unconfirmed transactions.
@@ -203,14 +223,17 @@ where
) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.batch_insert_unconfirmed(txs);
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
}
/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`].
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
where
I::ChangeSet: Default + Append,
I::ChangeSet: Default + Merge,
A: AnchorFromBlockPosition,
{
/// Batch insert all transactions of the given `block` of `height`, filtering out those that are
@@ -232,14 +255,14 @@ where
};
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
for (tx_pos, tx) in block.txdata.iter().enumerate() {
changeset.indexer.append(self.index.index_tx(tx));
changeset.indexer.merge(self.index.index_tx(tx));
if self.index.is_tx_relevant(tx) {
let txid = tx.compute_txid();
let anchor = A::from_block_position(block, block_id, tx_pos);
changeset.graph.append(self.graph.insert_tx(tx.clone()));
changeset.tx_graph.merge(self.graph.insert_tx(tx.clone()));
changeset
.graph
.append(self.graph.insert_anchor(txid, anchor));
.tx_graph
.merge(self.graph.insert_anchor(txid, anchor));
}
}
changeset
@@ -261,11 +284,20 @@ where
let mut graph = tx_graph::ChangeSet::default();
for (tx_pos, tx) in block.txdata.iter().enumerate() {
let anchor = A::from_block_position(&block, block_id, tx_pos);
graph.append(self.graph.insert_anchor(tx.compute_txid(), anchor));
graph.append(self.graph.insert_tx(tx.clone()));
graph.merge(self.graph.insert_anchor(tx.compute_txid(), anchor));
graph.merge(self.graph.insert_tx(tx.clone()));
}
let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer }
ChangeSet {
tx_graph: graph,
indexer,
}
}
}
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
fn as_ref(&self) -> &TxGraph<A> {
&self.graph
}
}
@@ -285,7 +317,7 @@ where
#[must_use]
pub struct ChangeSet<A, IA> {
/// [`TxGraph`] changeset.
pub graph: tx_graph::ChangeSet<A>,
pub tx_graph: tx_graph::ChangeSet<A>,
/// [`Indexer`] changeset.
pub indexer: IA,
}
@@ -293,68 +325,38 @@ pub struct ChangeSet<A, IA> {
impl<A, IA: Default> Default for ChangeSet<A, IA> {
fn default() -> Self {
Self {
graph: Default::default(),
tx_graph: Default::default(),
indexer: Default::default(),
}
}
}
impl<A: Anchor, IA: Append> Append for ChangeSet<A, IA> {
fn append(&mut self, other: Self) {
self.graph.append(other.graph);
self.indexer.append(other.indexer);
impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> {
fn merge(&mut self, other: Self) {
self.tx_graph.merge(other.tx_graph);
self.indexer.merge(other.indexer);
}
fn is_empty(&self) -> bool {
self.graph.is_empty() && self.indexer.is_empty()
self.tx_graph.is_empty() && self.indexer.is_empty()
}
}
impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
fn from(graph: tx_graph::ChangeSet<A>) -> Self {
Self {
graph,
tx_graph: graph,
..Default::default()
}
}
}
#[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 {
impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
Self {
graph: Default::default(),
tx_graph: Default::default(),
indexer,
}
}
}
/// Utilities for indexing transaction data.
///
/// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
/// This trait's methods should rarely be called directly.
pub trait Indexer {
/// The resultant "changeset" when new transaction data is indexed.
type ChangeSet;
/// Scan and index the given `outpoint` and `txout`.
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet;
/// Scans a transaction for relevant outpoints, which are stored and indexed internally.
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
/// Apply changeset to itself.
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
/// Determines the [`ChangeSet`] between `self` and an empty [`Indexer`].
fn initial_changeset(&self) -> Self::ChangeSet;
/// Determines whether the transaction should be included in the index.
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
}
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
fn as_ref(&self) -> &TxGraph<A> {
&self.graph
}
}

View File

@@ -0,0 +1,33 @@
//! [`Indexer`] provides utilities for indexing transaction data.
use bitcoin::{OutPoint, Transaction, TxOut};
#[cfg(feature = "miniscript")]
pub mod keychain_txout;
pub mod spk_txout;
/// Utilities for indexing transaction data.
///
/// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
/// This trait's methods should rarely be called directly.
///
/// [`IndexedTxGraph`]: crate::IndexedTxGraph
pub trait Indexer {
/// The resultant "changeset" when new transaction data is indexed.
type ChangeSet;
/// Scan and index the given `outpoint` and `txout`.
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet;
/// Scans a transaction for relevant outpoints, which are stored and indexed internally.
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
/// Apply changeset to itself.
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
/// Determines the [`ChangeSet`](Indexer::ChangeSet) between `self` and an empty [`Indexer`].
fn initial_changeset(&self) -> Self::ChangeSet;
/// Determines whether the transaction should be included in the index.
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
}

View File

@@ -1,19 +1,21 @@
//! [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains and
//! indexes [`TxOut`]s with them.
use crate::{
collections::*,
indexed_tx_graph::Indexer,
miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::BIP32_MAX_INDEX,
DescriptorExt, DescriptorId, SpkIterator, SpkTxOutIndex,
spk_txout::SpkTxOutIndex,
DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
};
use alloc::{borrow::ToOwned, vec::Vec};
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use core::{
fmt::Debug,
ops::{Bound, RangeBounds},
};
use super::*;
use crate::Append;
use crate::Merge;
/// The default lookahead for a [`KeychainTxOutIndex`]
pub const DEFAULT_LOOKAHEAD: u32 = 25;
@@ -73,7 +75,7 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25;
/// ## Synopsis
///
/// ```
/// use bdk_chain::keychain::KeychainTxOutIndex;
/// use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} };
/// # use core::str::FromStr;
///
@@ -97,8 +99,8 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25;
/// 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 });
/// # Ok::<_, bdk_chain::keychain::InsertDescriptorError<_>>(())
/// let new_spk_for_user = txout_index.reveal_next_spk(MyKeychain::MyAppUser{ user_id: 42 });
/// # Ok::<_, bdk_chain::indexer::keychain_txout::InsertDescriptorError<_>>(())
/// ```
///
/// [`Ord`]: core::cmp::Ord
@@ -134,7 +136,7 @@ impl<K> Default for KeychainTxOutIndex<K> {
}
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
type ChangeSet = ChangeSet<K>;
type ChangeSet = ChangeSet;
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
let mut changeset = ChangeSet::default();
@@ -153,20 +155,16 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
}
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet {
let mut changeset = ChangeSet::<K>::default();
let mut changeset = ChangeSet::default();
let txid = tx.compute_txid();
for (op, txout) in tx.output.iter().enumerate() {
changeset.append(self.index_txout(OutPoint::new(txid, op as u32), txout));
changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout));
}
changeset
}
fn initial_changeset(&self) -> Self::ChangeSet {
ChangeSet {
keychains_added: self
.keychains()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
last_revealed: self.last_revealed.clone().into_iter().collect(),
}
}
@@ -253,14 +251,14 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 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> {
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<ScriptBuf> {
self.inner.spk_at_index(&(keychain.clone(), 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)> {
pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&(K, u32)> {
self.inner.index_of_spk(script)
}
@@ -337,11 +335,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Return all keychains and their corresponding descriptors.
pub fn keychains(
&self,
) -> impl DoubleEndedIterator<Item = (&K, &Descriptor<DescriptorPublicKey>)> + ExactSizeIterator + '_
) -> impl DoubleEndedIterator<Item = (K, &Descriptor<DescriptorPublicKey>)> + ExactSizeIterator + '_
{
self.keychain_to_descriptor_id
.iter()
.map(|(k, did)| (k, self.descriptors.get(did).expect("invariant")))
.map(|(k, did)| (k.clone(), self.descriptors.get(did).expect("invariant")))
}
/// Insert a descriptor with a keychain associated to it.
@@ -352,12 +350,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so
/// will return a [`InsertDescriptorError<K>`].
///
/// [`KeychainTxOutIndex`] will prevent you from inserting two descriptors which derive the same
/// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at
/// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level
/// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have
/// subtle implications up the application stack like one UTXO being missing from one keychain
/// because it has been assigned to another which produces the same script pubkey.
pub fn insert_descriptor(
&mut self,
keychain: K,
descriptor: Descriptor<DescriptorPublicKey>,
) -> Result<ChangeSet<K>, InsertDescriptorError<K>> {
let mut changeset = ChangeSet::<K>::default();
) -> Result<bool, InsertDescriptorError<K>> {
let did = descriptor.descriptor_id();
if !self.keychain_to_descriptor_id.contains_key(&keychain)
&& !self.descriptor_id_to_keychain.contains_key(&did)
@@ -366,39 +370,37 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
self.keychain_to_descriptor_id.insert(keychain.clone(), did);
self.descriptor_id_to_keychain.insert(did, keychain.clone());
self.replenish_inner_index(did, &keychain, self.lookahead);
changeset
.keychains_added
.insert(keychain.clone(), descriptor);
} else {
if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
if *existing_desc_id != did {
return Err(InsertDescriptorError::KeychainAlreadyAssigned {
existing_assignment: descriptor.clone(),
keychain,
});
}
}
return Ok(true);
}
if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
let descriptor = self.descriptors.get(&did).expect("invariant").clone();
if *existing_keychain != keychain {
return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
existing_assignment: existing_keychain.clone(),
descriptor,
});
}
if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
if *existing_desc_id != did {
return Err(InsertDescriptorError::KeychainAlreadyAssigned {
existing_assignment: descriptor.clone(),
keychain,
});
}
}
Ok(changeset)
if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
let descriptor = self.descriptors.get(&did).expect("invariant").clone();
if *existing_keychain != keychain {
return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
existing_assignment: existing_keychain.clone(),
descriptor,
});
}
}
Ok(false)
}
/// 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>> {
let did = self.keychain_to_descriptor_id.get(keychain)?;
pub fn get_descriptor(&self, keychain: K) -> Option<&Descriptor<DescriptorPublicKey>> {
let did = self.keychain_to_descriptor_id.get(&keychain)?;
self.descriptors.get(did)
}
@@ -414,8 +416,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Store lookahead scripts until `target_index` (inclusive).
///
/// This does not change the global `lookahead` setting.
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
if let Some((next_index, _)) = self.next_index(keychain) {
pub fn lookahead_to_target(&mut self, keychain: K, target_index: u32) {
if let Some((next_index, _)) = self.next_index(keychain.clone()) {
let temp_lookahead = (target_index + 1)
.checked_sub(next_index)
.filter(|&index| index > 0);
@@ -432,9 +434,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
}
}
fn replenish_inner_index_keychain(&mut self, keychain: &K, lookahead: u32) {
if let Some(did) = self.keychain_to_descriptor_id.get(keychain) {
self.replenish_inner_index(*did, keychain, lookahead);
fn replenish_inner_index_keychain(&mut self, keychain: K, lookahead: u32) {
if let Some(did) = self.keychain_to_descriptor_id.get(&keychain) {
self.replenish_inner_index(*did, &keychain, lookahead);
}
}
@@ -462,7 +464,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// keychain doesn't exist
pub fn unbounded_spk_iter(
&self,
keychain: &K,
keychain: K,
) -> Option<SpkIterator<Descriptor<DescriptorPublicKey>>> {
let descriptor = self.get_descriptor(keychain)?.clone();
Some(SpkIterator::new(descriptor))
@@ -487,7 +489,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
pub fn revealed_spks(
&self,
range: impl RangeBounds<K>,
) -> impl Iterator<Item = KeychainIndexed<K, &Script>> {
) -> impl Iterator<Item = KeychainIndexed<K, ScriptBuf>> + '_ {
let start = range.start_bound();
let end = range.end_bound();
let mut iter_last_revealed = self
@@ -514,7 +516,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
let (current_keychain, last_revealed) = current_keychain?;
if current_keychain == keychain && Some(*index) <= last_revealed {
break Some(((keychain.clone(), *index), spk.as_script()));
break Some(((keychain.clone(), *index), spk.clone()));
}
})
}
@@ -523,27 +525,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
///
/// This is a double ended iterator so you can easily reverse it to get an iterator where
/// the script pubkeys that were most recently revealed are first.
pub fn revealed_keychain_spks<'a>(
&'a self,
keychain: &'a K,
) -> impl DoubleEndedIterator<Item = Indexed<&Script>> + 'a {
pub fn revealed_keychain_spks(
&self,
keychain: K,
) -> impl DoubleEndedIterator<Item = Indexed<ScriptBuf>> + '_ {
let end = self
.last_revealed_index(keychain)
.last_revealed_index(keychain.clone())
.map(|v| v + 1)
.unwrap_or(0);
self.inner
.all_spks()
.range((keychain.clone(), 0)..(keychain.clone(), end))
.map(|((_, index), spk)| (*index, spk.as_script()))
.map(|((_, index), spk)| (*index, spk.clone()))
}
/// Iterate over revealed, but unused, spks of all keychains.
pub fn unused_spks(
&self,
) -> impl DoubleEndedIterator<Item = KeychainIndexed<K, &Script>> + Clone {
) -> impl DoubleEndedIterator<Item = KeychainIndexed<K, ScriptBuf>> + Clone + '_ {
self.keychain_to_descriptor_id.keys().flat_map(|keychain| {
self.unused_keychain_spks(keychain)
.map(|(i, spk)| ((keychain.clone(), i), spk))
self.unused_keychain_spks(keychain.clone())
.map(|(i, spk)| ((keychain.clone(), i), spk.clone()))
})
}
@@ -551,9 +553,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Returns an empty iterator if the provided keychain doesn't exist.
pub fn unused_keychain_spks(
&self,
keychain: &K,
) -> impl DoubleEndedIterator<Item = Indexed<&Script>> + Clone {
let end = match self.keychain_to_descriptor_id.get(keychain) {
keychain: K,
) -> impl DoubleEndedIterator<Item = Indexed<ScriptBuf>> + Clone + '_ {
let end = match self.keychain_to_descriptor_id.get(&keychain) {
Some(did) => self.last_revealed.get(did).map(|v| *v + 1).unwrap_or(0),
None => 0,
};
@@ -575,8 +577,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Not checking the second field of the tuple may result in address reuse.
///
/// Returns None if the provided `keychain` doesn't exist.
pub fn next_index(&self, keychain: &K) -> Option<(u32, bool)> {
let did = self.keychain_to_descriptor_id.get(keychain)?;
pub fn next_index(&self, keychain: K) -> Option<(u32, bool)> {
let did = self.keychain_to_descriptor_id.get(&keychain)?;
let last_index = self.last_revealed.get(did).cloned();
let descriptor = self.descriptors.get(did).expect("invariant");
@@ -613,18 +615,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 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> {
let descriptor_id = self.keychain_to_descriptor_id.get(keychain)?;
pub fn last_revealed_index(&self, keychain: K) -> Option<u32> {
let descriptor_id = self.keychain_to_descriptor_id.get(&keychain)?;
self.last_revealed.get(descriptor_id).cloned()
}
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet<K> {
pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet {
let mut changeset = ChangeSet::default();
for (keychain, &index) in keychains {
if let Some((_, new_changeset)) = self.reveal_to_target(keychain, index) {
changeset.append(new_changeset);
if let Some((_, new_changeset)) = self.reveal_to_target(keychain.clone(), index) {
changeset.merge(new_changeset);
}
}
@@ -646,19 +648,19 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
#[must_use]
pub fn reveal_to_target(
&mut self,
keychain: &K,
keychain: K,
target_index: u32,
) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet<K>)> {
) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet)> {
let mut changeset = ChangeSet::default();
let mut spks: Vec<Indexed<ScriptBuf>> = vec![];
while let Some((i, new)) = self.next_index(keychain) {
while let Some((i, new)) = self.next_index(keychain.clone()) {
if !new || i > target_index {
break;
}
match self.reveal_next_spk(keychain) {
match self.reveal_next_spk(keychain.clone()) {
Some(((i, spk), change)) => {
spks.push((i, spk));
changeset.append(change);
changeset.merge(change);
}
None => break,
}
@@ -679,21 +681,21 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 1. The descriptor has no wildcard and already has one script revealed.
/// 2. The descriptor has already revealed scripts up to the numeric bound.
/// 3. There is no descriptor associated with the given keychain.
pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
let (next_index, new) = self.next_index(keychain)?;
pub fn reveal_next_spk(&mut self, keychain: K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
let (next_index, new) = self.next_index(keychain.clone())?;
let mut changeset = ChangeSet::default();
if new {
let did = self.keychain_to_descriptor_id.get(keychain)?;
let did = self.keychain_to_descriptor_id.get(&keychain)?;
self.last_revealed.insert(*did, next_index);
changeset.last_revealed.insert(*did, next_index);
self.replenish_inner_index(*did, keychain, self.lookahead);
self.replenish_inner_index(*did, &keychain, self.lookahead);
}
let script = self
.inner
.spk_at_index(&(keychain.clone(), next_index))
.expect("we just inserted it");
Some(((next_index, script.into()), changeset))
Some(((next_index, script), changeset))
}
/// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest
@@ -709,9 +711,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// could be revealed (see [`reveal_next_spk`] for when this happens).
///
/// [`reveal_next_spk`]: Self::reveal_next_spk
pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
pub fn next_unused_spk(&mut self, keychain: K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
let next_unused = self
.unused_keychain_spks(keychain)
.unused_keychain_spks(keychain.clone())
.next()
.map(|(i, spk)| ((i, spk.to_owned()), ChangeSet::default()));
@@ -720,11 +722,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from
/// `keychain`.
pub fn keychain_outpoints<'a>(
&'a self,
keychain: &'a K,
) -> impl DoubleEndedIterator<Item = Indexed<OutPoint>> + 'a {
self.keychain_outpoints_in_range(keychain..=keychain)
pub fn keychain_outpoints(
&self,
keychain: K,
) -> impl DoubleEndedIterator<Item = Indexed<OutPoint>> + '_ {
self.keychain_outpoints_in_range(keychain.clone()..=keychain)
.map(|((_, i), op)| (i, op))
}
@@ -755,7 +757,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
/// found a [`TxOut`] with it's script pubkey.
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
pub fn last_used_index(&self, keychain: K) -> Option<u32> {
self.keychain_outpoints(keychain).last().map(|(i, _)| i)
}
@@ -765,33 +767,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
self.keychain_to_descriptor_id
.iter()
.filter_map(|(keychain, _)| {
self.last_used_index(keychain)
self.last_used_index(keychain.clone())
.map(|index| (keychain.clone(), index))
})
.collect()
}
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
///
/// Keychains added by the `keychains_added` field of `ChangeSet<K>` respect the one-to-one
/// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will
/// panic if `debug_assertions` are enabled).
pub fn apply_changeset(&mut self, changeset: ChangeSet<K>) {
let ChangeSet {
keychains_added,
last_revealed,
} = changeset;
for (keychain, descriptor) in keychains_added {
let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor);
}
for (&desc_id, &index) in &last_revealed {
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
for (&desc_id, &index) in &changeset.last_revealed {
let v = self.last_revealed.entry(desc_id).or_default();
*v = index.max(*v);
}
for did in last_revealed.keys() {
self.replenish_inner_index_did(*did, self.lookahead);
self.replenish_inner_index_did(desc_id, self.lookahead);
}
}
}
@@ -848,54 +835,28 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
///
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`].
///
/// The `last_revealed` field is monotone in that [`append`] will never decrease it.
/// The `last_revealed` field is monotone in that [`merge`] will never decrease it.
/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the
/// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself.
///
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
/// [`append`]: Self::append
#[derive(Clone, Debug, PartialEq)]
/// [`KeychainTxOutIndex`]: crate::keychain_txout::KeychainTxOutIndex
/// [`apply_changeset`]: crate::keychain_txout::KeychainTxOutIndex::apply_changeset
/// [`merge`]: Self::merge
#[derive(Clone, Debug, Default, 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"
)
)
serde(crate = "serde_crate")
)]
#[must_use]
pub struct ChangeSet<K> {
/// Contains the keychains that have been added and their respective descriptor
pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
pub struct ChangeSet {
/// Contains for each descriptor_id the last revealed index of derivation
pub last_revealed: BTreeMap<DescriptorId, u32>,
}
impl<K: Ord> Append for ChangeSet<K> {
/// Merge another [`ChangeSet<K>`] into self.
///
/// For the `keychains_added` field this method respects the invariants of
/// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two.
///
/// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor
fn append(&mut self, other: Self) {
for (new_keychain, new_descriptor) in other.keychains_added {
// enforce 1-to-1 invariance
if !self.keychains_added.contains_key(&new_keychain)
// FIXME: very inefficient
&& self
.keychains_added
.values()
.all(|descriptor| descriptor != &new_descriptor)
{
self.keychains_added.insert(new_keychain, new_descriptor);
}
}
impl Merge for ChangeSet {
/// Merge another [`ChangeSet`] into self.
fn merge(&mut self, other: Self) {
// 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 {
@@ -915,25 +876,6 @@ impl<K: Ord> Append for ChangeSet<K> {
/// 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(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`].
pub struct NoSuchKeychain<K>(K);
impl<K: Debug> core::fmt::Display for NoSuchKeychain<K> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "no such keychain {:?} exists", &self.0)
self.last_revealed.is_empty()
}
}

View File

@@ -1,10 +1,12 @@
//! [`SpkTxOutIndex`] is an index storing [`TxOut`]s that have a script pubkey that matches those in a list.
use core::ops::RangeBounds;
use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
indexed_tx_graph::Indexer,
Indexer,
};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
///
@@ -82,7 +84,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// Typically, this is used in two situations:
///
/// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
/// your txouts.
/// your txouts.
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
let mut scanned_indices = BTreeSet::new();
@@ -174,8 +176,8 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// Returns the script that has been inserted at the `index`.
///
/// If that index hasn't been inserted yet, it will return `None`.
pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
self.spks.get(index).map(|s| s.as_script())
pub fn spk_at_index(&self, index: &I) -> Option<ScriptBuf> {
self.spks.get(index).cloned()
}
/// The script pubkeys that are being tracked by the index.
@@ -206,7 +208,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// # Example
///
/// ```rust
/// # use bdk_chain::SpkTxOutIndex;
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
///
/// // imagine our spks are indexed like (keychain, derivation_index).
/// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
@@ -215,7 +217,10 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// let unused_change_spks =
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
/// ```
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
pub fn unused_spks<R>(
&self,
range: R,
) -> impl DoubleEndedIterator<Item = (&I, ScriptBuf)> + Clone + '_
where
R: RangeBounds<I>,
{
@@ -266,8 +271,8 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
}
/// Returns the index associated with the script pubkey.
pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
self.spk_indices.get(script)
pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&I> {
self.spk_indices.get(script.as_script())
}
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
@@ -291,7 +296,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
}
}
for txout in &tx.output {
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
if range.contains(index) {
received += txout.value;
}

View File

@@ -21,14 +21,15 @@
#![warn(missing_docs)]
pub use bitcoin;
mod spk_txout_index;
pub use spk_txout_index::*;
mod balance;
pub use balance::*;
mod chain_data;
pub use chain_data::*;
pub mod indexed_tx_graph;
pub use indexed_tx_graph::IndexedTxGraph;
pub mod keychain;
pub use keychain::{Indexed, KeychainIndexed};
pub mod indexer;
pub use indexer::spk_txout;
pub use indexer::Indexer;
pub mod local_chain;
mod tx_data_traits;
pub mod tx_graph;
@@ -36,6 +37,8 @@ pub use tx_data_traits::*;
pub use tx_graph::TxGraph;
mod chain_oracle;
pub use chain_oracle::*;
mod persist;
pub use persist::*;
#[doc(hidden)]
pub mod example_utils;
@@ -49,15 +52,18 @@ pub use descriptor_ext::{DescriptorExt, DescriptorId};
#[cfg(feature = "miniscript")]
mod spk_iter;
#[cfg(feature = "miniscript")]
pub use indexer::keychain_txout;
#[cfg(feature = "miniscript")]
pub use spk_iter::*;
mod changeset;
pub use changeset::*;
#[cfg(feature = "rusqlite")]
pub mod rusqlite_impl;
pub mod spk_client;
#[allow(unused_imports)]
#[macro_use]
extern crate alloc;
#[cfg(feature = "rusqlite")]
pub extern crate rusqlite_crate as rusqlite;
#[cfg(feature = "serde")]
pub extern crate serde_crate as serde;
@@ -98,3 +104,25 @@ pub mod collections {
/// How many confirmations are needed f or a coinbase output to be spent.
pub const COINBASE_MATURITY: u32 = 100;
/// A tuple of keychain index and `T` representing the indexed value.
pub type Indexed<T> = (u32, T);
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
pub type KeychainIndexed<K, T> = ((K, u32), T);
/// A wrapper that we use to impl remote traits for types in our crate or dependency crates.
pub struct Impl<T>(pub T);
impl<T> From<T> for Impl<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T> core::ops::Deref for Impl<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@@ -4,17 +4,11 @@ use core::convert::Infallible;
use core::ops::RangeBounds;
use crate::collections::BTreeMap;
use crate::{BlockId, ChainOracle};
use crate::{BlockId, ChainOracle, Merge};
use alloc::sync::Arc;
use bitcoin::block::Header;
use bitcoin::BlockHash;
/// The [`ChangeSet`] represents changes to [`LocalChain`].
///
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
/// transaction anchor.
///
@@ -216,7 +210,7 @@ impl CheckPoint {
/// Apply `changeset` to the checkpoint.
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
if let Some(start_height) = changeset.keys().next().cloned() {
if let Some(start_height) = changeset.blocks.keys().next().cloned() {
// changes after point of agreement
let mut extension = BTreeMap::default();
// point of agreement
@@ -231,7 +225,7 @@ impl CheckPoint {
}
}
for (&height, &hash) in changeset {
for (&height, &hash) in &changeset.blocks {
match hash {
Some(hash) => {
extension.insert(height, hash);
@@ -331,7 +325,7 @@ impl LocalChain {
/// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
let genesis_entry = changeset.get(&0).copied().flatten();
let genesis_entry = changeset.blocks.get(&0).copied().flatten();
let genesis_hash = match genesis_entry {
Some(hash) => hash,
None => return Err(MissingGenesisError),
@@ -521,12 +515,14 @@ impl LocalChain {
}
let mut changeset = ChangeSet::default();
changeset.insert(block_id.height, Some(block_id.hash));
changeset
.blocks
.insert(block_id.height, Some(block_id.hash));
self.apply_changeset(&changeset)
.map_err(|_| AlterCheckPointError {
height: 0,
original_hash: self.genesis_hash(),
update_hash: changeset.get(&0).cloned().flatten(),
update_hash: changeset.blocks.get(&0).cloned().flatten(),
})?;
Ok(changeset)
}
@@ -548,7 +544,7 @@ impl LocalChain {
if cp_id.height < block_id.height {
break;
}
changeset.insert(cp_id.height, None);
changeset.blocks.insert(cp_id.height, None);
if cp_id == block_id {
remove_from = Some(cp);
}
@@ -569,13 +565,16 @@ impl LocalChain {
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
/// recover the current chain.
pub fn initial_changeset(&self) -> ChangeSet {
self.tip
.iter()
.map(|cp| {
let block_id = cp.block_id();
(block_id.height, Some(block_id.hash))
})
.collect()
ChangeSet {
blocks: self
.tip
.iter()
.map(|cp| {
let block_id = cp.block_id();
(block_id.height, Some(block_id.hash))
})
.collect(),
}
}
/// Iterate over checkpoints in descending height order.
@@ -587,7 +586,7 @@ impl LocalChain {
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
let mut curr_cp = self.tip.clone();
for (height, exp_hash) in changeset.iter().rev() {
for (height, exp_hash) in changeset.blocks.iter().rev() {
match curr_cp.get(*height) {
Some(query_cp) => {
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
@@ -630,6 +629,58 @@ impl LocalChain {
}
}
/// The [`ChangeSet`] represents changes to [`LocalChain`].
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ChangeSet {
/// Changes to the [`LocalChain`] blocks.
///
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
pub blocks: BTreeMap<u32, Option<BlockHash>>,
}
impl Merge for ChangeSet {
fn merge(&mut self, other: Self) {
Merge::merge(&mut self.blocks, other.blocks)
}
fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
}
impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
fn from(blocks: B) -> Self {
Self {
blocks: blocks.into_iter().collect(),
}
}
}
impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
Self {
blocks: iter.into_iter().collect(),
}
}
}
impl FromIterator<(u32, BlockHash)> for ChangeSet {
fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
Self {
blocks: iter
.into_iter()
.map(|(height, hash)| (height, Some(hash)))
.collect(),
}
}
}
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
#[derive(Clone, Debug, PartialEq)]
pub struct MissingGenesisError;
@@ -761,7 +812,7 @@ fn merge_chains(
match (curr_orig.as_ref(), curr_update.as_ref()) {
// Update block that doesn't exist in the original chain
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
changeset.insert(u.height(), Some(u.hash()));
changeset.blocks.insert(u.height(), Some(u.hash()));
prev_update = curr_update.take();
}
// Original block that isn't in the update
@@ -813,9 +864,9 @@ fn merge_chains(
} else {
// We have an invalidation height so we set the height to the updated hash and
// also purge all the original chain block hashes above this block.
changeset.insert(u.height(), Some(u.hash()));
changeset.blocks.insert(u.height(), Some(u.hash()));
for invalidated_height in potentially_invalidated_heights.drain(..) {
changeset.insert(invalidated_height, None);
changeset.blocks.insert(invalidated_height, None);
}
prev_orig_was_invalidated = true;
}

169
crates/chain/src/persist.rs Normal file
View File

@@ -0,0 +1,169 @@
use core::{
future::Future,
ops::{Deref, DerefMut},
pin::Pin,
};
use alloc::boxed::Box;
use crate::Merge;
/// Represents a type that contains staged changes.
pub trait Staged {
/// Type for staged changes.
type ChangeSet: Merge;
/// Get mutable reference of staged changes.
fn staged(&mut self) -> &mut Self::ChangeSet;
}
/// Trait that persists the type with `Db`.
///
/// Methods of this trait should not be called directly.
pub trait PersistWith<Db>: Staged + Sized {
/// Parameters for [`PersistWith::create`].
type CreateParams;
/// Parameters for [`PersistWith::load`].
type LoadParams;
/// Error type of [`PersistWith::create`].
type CreateError;
/// Error type of [`PersistWith::load`].
type LoadError;
/// Error type of [`PersistWith::persist`].
type PersistError;
/// Initialize the `Db` and create `Self`.
fn create(db: &mut Db, params: Self::CreateParams) -> Result<Self, Self::CreateError>;
/// Initialize the `Db` and load a previously-persisted `Self`.
fn load(db: &mut Db, params: Self::LoadParams) -> Result<Option<Self>, Self::LoadError>;
/// Persist changes to the `Db`.
fn persist(
db: &mut Db,
changeset: &<Self as Staged>::ChangeSet,
) -> Result<(), Self::PersistError>;
}
type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
/// Trait that persists the type with an async `Db`.
pub trait PersistAsyncWith<Db>: Staged + Sized {
/// Parameters for [`PersistAsyncWith::create`].
type CreateParams;
/// Parameters for [`PersistAsyncWith::load`].
type LoadParams;
/// Error type of [`PersistAsyncWith::create`].
type CreateError;
/// Error type of [`PersistAsyncWith::load`].
type LoadError;
/// Error type of [`PersistAsyncWith::persist`].
type PersistError;
/// Initialize the `Db` and create `Self`.
fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult<Self, Self::CreateError>;
/// Initialize the `Db` and load a previously-persisted `Self`.
fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult<Option<Self>, Self::LoadError>;
/// Persist changes to the `Db`.
fn persist<'a>(
db: &'a mut Db,
changeset: &'a <Self as Staged>::ChangeSet,
) -> FutureResult<'a, (), Self::PersistError>;
}
/// Represents a persisted `T`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Persisted<T> {
inner: T,
}
impl<T> Persisted<T> {
/// Create a new persisted `T`.
pub fn create<Db>(db: &mut Db, params: T::CreateParams) -> Result<Self, T::CreateError>
where
T: PersistWith<Db>,
{
T::create(db, params).map(|inner| Self { inner })
}
/// Create a new persisted `T` with async `Db`.
pub async fn create_async<Db>(
db: &mut Db,
params: T::CreateParams,
) -> Result<Self, T::CreateError>
where
T: PersistAsyncWith<Db>,
{
T::create(db, params).await.map(|inner| Self { inner })
}
/// Construct a persisted `T` from `Db`.
pub fn load<Db>(db: &mut Db, params: T::LoadParams) -> Result<Option<Self>, T::LoadError>
where
T: PersistWith<Db>,
{
Ok(T::load(db, params)?.map(|inner| Self { inner }))
}
/// Construct a persisted `T` from an async `Db`.
pub async fn load_async<Db>(
db: &mut Db,
params: T::LoadParams,
) -> Result<Option<Self>, T::LoadError>
where
T: PersistAsyncWith<Db>,
{
Ok(T::load(db, params).await?.map(|inner| Self { inner }))
}
/// Persist staged changes of `T` into `Db`.
///
/// If the database errors, the staged changes will not be cleared.
pub fn persist<Db>(&mut self, db: &mut Db) -> Result<bool, T::PersistError>
where
T: PersistWith<Db>,
{
let stage = T::staged(&mut self.inner);
if stage.is_empty() {
return Ok(false);
}
T::persist(db, &*stage)?;
stage.take();
Ok(true)
}
/// Persist staged changes of `T` into an async `Db`.
///
/// If the database errors, the staged changes will not be cleared.
pub async fn persist_async<'a, Db>(
&'a mut self,
db: &'a mut Db,
) -> Result<bool, T::PersistError>
where
T: PersistAsyncWith<Db>,
{
let stage = T::staged(&mut self.inner);
if stage.is_empty() {
return Ok(false);
}
T::persist(db, &*stage).await?;
stage.take();
Ok(true)
}
}
impl<T> Deref for Persisted<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for Persisted<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}

View File

@@ -0,0 +1,530 @@
//! Module for stuff
use crate::*;
use core::str::FromStr;
use alloc::{borrow::ToOwned, boxed::Box, string::ToString, sync::Arc, vec::Vec};
use bitcoin::consensus::{Decodable, Encodable};
use rusqlite;
use rusqlite::named_params;
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
use rusqlite::OptionalExtension;
use rusqlite::Transaction;
/// Table name for schemas.
pub const SCHEMAS_TABLE_NAME: &str = "bdk_schemas";
/// Initialize the schema table.
fn init_schemas_table(db_tx: &Transaction) -> rusqlite::Result<()> {
let sql = format!("CREATE TABLE IF NOT EXISTS {}( name TEXT PRIMARY KEY NOT NULL, version INTEGER NOT NULL ) STRICT", SCHEMAS_TABLE_NAME);
db_tx.execute(&sql, ())?;
Ok(())
}
/// Get schema version of `schema_name`.
fn schema_version(db_tx: &Transaction, schema_name: &str) -> rusqlite::Result<Option<u32>> {
let sql = format!(
"SELECT version FROM {} WHERE name=:name",
SCHEMAS_TABLE_NAME
);
db_tx
.query_row(&sql, named_params! { ":name": schema_name }, |row| {
row.get::<_, u32>("version")
})
.optional()
}
/// Set the `schema_version` of `schema_name`.
fn set_schema_version(
db_tx: &Transaction,
schema_name: &str,
schema_version: u32,
) -> rusqlite::Result<()> {
let sql = format!(
"REPLACE INTO {}(name, version) VALUES(:name, :version)",
SCHEMAS_TABLE_NAME,
);
db_tx.execute(
&sql,
named_params! { ":name": schema_name, ":version": schema_version },
)?;
Ok(())
}
/// Runs logic that initializes/migrates the table schemas.
pub fn migrate_schema(
db_tx: &Transaction,
schema_name: &str,
versioned_scripts: &[&[&str]],
) -> rusqlite::Result<()> {
init_schemas_table(db_tx)?;
let current_version = schema_version(db_tx, schema_name)?;
let exec_from = current_version.map_or(0_usize, |v| v as usize + 1);
let scripts_to_exec = versioned_scripts.iter().enumerate().skip(exec_from);
for (version, &script) in scripts_to_exec {
set_schema_version(db_tx, schema_name, version as u32)?;
for statement in script {
db_tx.execute(statement, ())?;
}
}
Ok(())
}
impl FromSql for Impl<bitcoin::Txid> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Txid::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Impl<bitcoin::Txid> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Impl<bitcoin::BlockHash> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::BlockHash::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Impl<bitcoin::BlockHash> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
#[cfg(feature = "miniscript")]
impl FromSql for Impl<DescriptorId> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
DescriptorId::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl ToSql for Impl<DescriptorId> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Impl<bitcoin::Transaction> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Impl<bitcoin::Transaction> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let mut bytes = Vec::<u8>::new();
self.consensus_encode(&mut bytes).map_err(to_sql_error)?;
Ok(bytes.into())
}
}
impl FromSql for Impl<bitcoin::ScriptBuf> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(bitcoin::Script::from_bytes(value.as_bytes()?)
.to_owned()
.into())
}
}
impl ToSql for Impl<bitcoin::ScriptBuf> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.as_bytes().into())
}
}
impl FromSql for Impl<bitcoin::Amount> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into())
}
}
impl ToSql for Impl<bitcoin::Amount> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?;
Ok(amount.into())
}
}
impl<A: Anchor + serde_crate::de::DeserializeOwned> FromSql for Impl<A> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
serde_json::from_str(value.as_str()?)
.map(Impl)
.map_err(from_sql_error)
}
}
impl<A: Anchor + serde_crate::Serialize> ToSql for Impl<A> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
serde_json::to_string(&self.0)
.map(Into::into)
.map_err(to_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl FromSql for Impl<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
miniscript::Descriptor::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl ToSql for Impl<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Impl<bitcoin::Network> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Network::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Impl<bitcoin::Network> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> FromSqlError {
FromSqlError::Other(Box::new(err))
}
fn to_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> rusqlite::Error {
rusqlite::Error::ToSqlConversionFailure(Box::new(err))
}
impl<A> tx_graph::ChangeSet<A>
where
A: Anchor + Clone + Ord + serde::Serialize + serde::de::DeserializeOwned,
{
/// Schema name for [`tx_graph::ChangeSet`].
pub const SCHEMA_NAME: &'static str = "bdk_txgraph";
/// Name of table that stores full transactions and `last_seen` timestamps.
pub const TXS_TABLE_NAME: &'static str = "bdk_txs";
/// Name of table that stores floating txouts.
pub const TXOUTS_TABLE_NAME: &'static str = "bdk_txouts";
/// Name of table that stores [`Anchor`]s.
pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors";
/// Initialize sqlite tables.
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
let schema_v0: &[&str] = &[
// full transactions
&format!(
"CREATE TABLE {} ( \
txid TEXT PRIMARY KEY NOT NULL, \
raw_tx BLOB, \
last_seen INTEGER \
) STRICT",
Self::TXS_TABLE_NAME,
),
// floating txouts
&format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL, \
vout INTEGER NOT NULL, \
value INTEGER NOT NULL, \
script BLOB NOT NULL, \
PRIMARY KEY (txid, vout) \
) STRICT",
Self::TXOUTS_TABLE_NAME,
),
// anchors
&format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL REFERENCES {} (txid), \
block_height INTEGER NOT NULL, \
block_hash TEXT NOT NULL, \
anchor BLOB NOT NULL, \
PRIMARY KEY (txid, block_height, block_hash) \
) STRICT",
Self::ANCHORS_TABLE_NAME,
Self::TXS_TABLE_NAME,
),
];
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
}
/// Construct a [`TxGraph`] from an sqlite database.
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT txid, raw_tx, last_seen FROM {}",
Self::TXS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
row.get::<_, Option<Impl<bitcoin::Transaction>>>("raw_tx")?,
row.get::<_, Option<u64>>("last_seen")?,
))
})?;
for row in row_iter {
let (Impl(txid), tx, last_seen) = row?;
if let Some(Impl(tx)) = tx {
changeset.txs.insert(Arc::new(tx));
}
if let Some(last_seen) = last_seen {
changeset.last_seen.insert(txid, last_seen);
}
}
let mut statement = db_tx.prepare(&format!(
"SELECT txid, vout, value, script FROM {}",
Self::TXOUTS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
row.get::<_, u32>("vout")?,
row.get::<_, Impl<bitcoin::Amount>>("value")?,
row.get::<_, Impl<bitcoin::ScriptBuf>>("script")?,
))
})?;
for row in row_iter {
let (Impl(txid), vout, Impl(value), Impl(script_pubkey)) = row?;
changeset.txouts.insert(
bitcoin::OutPoint { txid, vout },
bitcoin::TxOut {
value,
script_pubkey,
},
);
}
let mut statement = db_tx.prepare(&format!(
"SELECT json(anchor), txid FROM {}",
Self::ANCHORS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Impl<A>>("json(anchor)")?,
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
))
})?;
for row in row_iter {
let (Impl(anchor), Impl(txid)) = row?;
changeset.anchors.insert((anchor, txid));
}
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
let mut statement = db_tx.prepare_cached(&format!(
"INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx",
Self::TXS_TABLE_NAME,
))?;
for tx in &self.txs {
statement.execute(named_params! {
":txid": Impl(tx.compute_txid()),
":raw_tx": Impl(tx.as_ref().clone()),
})?;
}
let mut statement = db_tx
.prepare_cached(&format!(
"INSERT INTO {}(txid, last_seen) VALUES(:txid, :last_seen) ON CONFLICT(txid) DO UPDATE SET last_seen=:last_seen",
Self::TXS_TABLE_NAME,
))?;
for (&txid, &last_seen) in &self.last_seen {
statement.execute(named_params! {
":txid": Impl(txid),
":last_seen": Some(last_seen),
})?;
}
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, vout, value, script) VALUES(:txid, :vout, :value, :script)",
Self::TXOUTS_TABLE_NAME,
))?;
for (op, txo) in &self.txouts {
statement.execute(named_params! {
":txid": Impl(op.txid),
":vout": op.vout,
":value": Impl(txo.value),
":script": Impl(txo.script_pubkey.clone()),
})?;
}
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))",
Self::ANCHORS_TABLE_NAME,
))?;
for (anchor, txid) in &self.anchors {
let anchor_block = anchor.anchor_block();
statement.execute(named_params! {
":txid": Impl(*txid),
":block_height": anchor_block.height,
":block_hash": Impl(anchor_block.hash),
":anchor": Impl(anchor.clone()),
})?;
}
Ok(())
}
}
impl local_chain::ChangeSet {
/// Schema name for the changeset.
pub const SCHEMA_NAME: &'static str = "bdk_localchain";
/// Name of sqlite table that stores blocks of [`LocalChain`](local_chain::LocalChain).
pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks";
/// Initialize sqlite tables for persisting [`local_chain::LocalChain`].
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
let schema_v0: &[&str] = &[
// blocks
&format!(
"CREATE TABLE {} ( \
block_height INTEGER PRIMARY KEY NOT NULL, \
block_hash TEXT NOT NULL \
) STRICT",
Self::BLOCKS_TABLE_NAME,
),
];
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
}
/// Construct a [`LocalChain`](local_chain::LocalChain) from sqlite database.
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT block_height, block_hash FROM {}",
Self::BLOCKS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, u32>("block_height")?,
row.get::<_, Impl<bitcoin::BlockHash>>("block_hash")?,
))
})?;
for row in row_iter {
let (height, Impl(hash)) = row?;
changeset.blocks.insert(height, Some(hash));
}
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
let mut replace_statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)",
Self::BLOCKS_TABLE_NAME,
))?;
let mut delete_statement = db_tx.prepare_cached(&format!(
"DELETE FROM {} WHERE block_height=:block_height",
Self::BLOCKS_TABLE_NAME,
))?;
for (&height, &hash) in &self.blocks {
match hash {
Some(hash) => replace_statement.execute(named_params! {
":block_height": height,
":block_hash": Impl(hash),
})?,
None => delete_statement.execute(named_params! {
":block_height": height,
})?,
};
}
Ok(())
}
}
#[cfg(feature = "miniscript")]
impl keychain_txout::ChangeSet {
/// Schema name for the changeset.
pub const SCHEMA_NAME: &'static str = "bdk_keychaintxout";
/// Name for table that stores last revealed indices per descriptor id.
pub const LAST_REVEALED_TABLE_NAME: &'static str = "bdk_descriptor_last_revealed";
/// Initialize sqlite tables for persisting
/// [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
let schema_v0: &[&str] = &[
// last revealed
&format!(
"CREATE TABLE {} ( \
descriptor_id TEXT PRIMARY KEY NOT NULL, \
last_revealed INTEGER NOT NULL \
) STRICT",
Self::LAST_REVEALED_TABLE_NAME,
),
];
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
}
/// Construct [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex) from sqlite database
/// and given parameters.
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT descriptor_id, last_revealed FROM {}",
Self::LAST_REVEALED_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Impl<DescriptorId>>("descriptor_id")?,
row.get::<_, u32>("last_revealed")?,
))
})?;
for row in row_iter {
let (Impl(descriptor_id), last_revealed) = row?;
changeset.last_revealed.insert(descriptor_id, last_revealed);
}
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)",
Self::LAST_REVEALED_TABLE_NAME,
))?;
for (&descriptor_id, &last_revealed) in &self.last_revealed {
statement.execute(named_params! {
":descriptor_id": Impl(descriptor_id),
":last_revealed": last_revealed,
})?;
}
Ok(())
}
}

View File

@@ -1,8 +1,7 @@
//! Helper types for spk-based blockchain clients.
use crate::{
collections::BTreeMap, keychain::Indexed, local_chain::CheckPoint,
ConfirmationTimeHeightAnchor, TxGraph,
collections::BTreeMap, local_chain::CheckPoint, ConfirmationBlockTime, Indexed, TxGraph,
};
use alloc::boxed::Box;
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
@@ -160,7 +159,7 @@ impl SyncRequest {
#[must_use]
pub fn populate_with_revealed_spks<K: Clone + Ord + core::fmt::Debug + Send + Sync>(
self,
index: &crate::keychain::KeychainTxOutIndex<K>,
index: &crate::indexer::keychain_txout::KeychainTxOutIndex<K>,
spk_range: impl core::ops::RangeBounds<K>,
) -> Self {
use alloc::borrow::ToOwned;
@@ -177,7 +176,7 @@ impl SyncRequest {
/// Data returned from a spk-based blockchain client sync.
///
/// See also [`SyncRequest`].
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
pub struct SyncResult<A = ConfirmationBlockTime> {
/// The update to apply to the receiving [`TxGraph`].
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
@@ -216,12 +215,12 @@ impl<K: Ord + Clone> FullScanRequest<K> {
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the
/// [`FullScanRequest`].
///
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::indexer::keychain_txout::KeychainTxOutIndex::all_unbounded_spk_iters
#[cfg(feature = "miniscript")]
#[must_use]
pub fn from_keychain_txout_index(
chain_tip: CheckPoint,
index: &crate::keychain::KeychainTxOutIndex<K>,
index: &crate::indexer::keychain_txout::KeychainTxOutIndex<K>,
) -> Self
where
K: core::fmt::Debug,
@@ -318,7 +317,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
/// Data returned from a spk-based blockchain client full scan.
///
/// See also [`FullScanRequest`].
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
pub struct FullScanResult<K, A = ConfirmationBlockTime> {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub graph_update: TxGraph<A>,
/// The update to apply to the receiving [`TxGraph`].

View File

@@ -1,7 +1,7 @@
use crate::{
bitcoin::{secp256k1::Secp256k1, ScriptBuf},
keychain::Indexed,
miniscript::{Descriptor, DescriptorPublicKey},
Indexed,
};
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
@@ -137,7 +137,7 @@ where
mod test {
use crate::{
bitcoin::secp256k1::Secp256k1,
keychain::KeychainTxOutIndex,
indexer::keychain_txout::KeychainTxOutIndex,
miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::{SpkIterator, BIP32_MAX_INDEX},
};

View File

@@ -20,8 +20,7 @@ use alloc::vec::Vec;
/// # use bdk_chain::local_chain::LocalChain;
/// # use bdk_chain::tx_graph::TxGraph;
/// # use bdk_chain::BlockId;
/// # use bdk_chain::ConfirmationHeightAnchor;
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
/// # use bdk_chain::ConfirmationBlockTime;
/// # use bdk_chain::example_utils::*;
/// # use bitcoin::hashes::Hash;
/// // Initialize the local chain with two blocks.
@@ -50,39 +49,19 @@ use alloc::vec::Vec;
/// },
/// );
///
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
/// // This anchor records the anchor block and the confirmation height of the transaction.
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
/// // confirmation block can be different. However, the confirmation block cannot be higher than
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
/// let _ = graph_b.insert_tx(tx.clone());
/// graph_b.insert_anchor(
/// tx.compute_txid(),
/// ConfirmationHeightAnchor {
/// anchor_block: BlockId {
/// height: 2,
/// hash: Hash::hash("second".as_bytes()),
/// },
/// confirmation_height: 1,
/// },
/// );
///
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
/// // confirmation block can be different. However, the confirmation block cannot be higher than
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationBlockTime` as the anchor type.
/// // This anchor records the anchor block and the confirmation time of the transaction. When a
/// // transaction is anchored with `ConfirmationBlockTime`, the anchor block and confirmation block
/// // of the transaction is the same block.
/// let mut graph_c = TxGraph::<ConfirmationBlockTime>::default();
/// let _ = graph_c.insert_tx(tx.clone());
/// graph_c.insert_anchor(
/// tx.compute_txid(),
/// ConfirmationTimeHeightAnchor {
/// anchor_block: BlockId {
/// ConfirmationBlockTime {
/// block_id: BlockId {
/// height: 2,
/// hash: Hash::hash("third".as_bytes()),
/// },
/// confirmation_height: 1,
/// confirmation_time: 123,
/// },
/// );
@@ -113,10 +92,10 @@ pub trait AnchorFromBlockPosition: Anchor {
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self;
}
/// Trait that makes an object appendable.
pub trait Append: Default {
/// Append another object of the same type onto `self`.
fn append(&mut self, other: Self);
/// Trait that makes an object mergeable.
pub trait Merge: Default {
/// Merge another object of the same type onto `self`.
fn merge(&mut self, other: Self);
/// Returns whether the structure is considered empty.
fn is_empty(&self) -> bool;
@@ -131,8 +110,8 @@ pub trait Append: Default {
}
}
impl<K: Ord, V> Append for BTreeMap<K, V> {
fn append(&mut self, other: Self) {
impl<K: Ord, V> Merge for BTreeMap<K, V> {
fn merge(&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
BTreeMap::extend(self, other)
@@ -143,8 +122,8 @@ impl<K: Ord, V> Append for BTreeMap<K, V> {
}
}
impl<T: Ord> Append for BTreeSet<T> {
fn append(&mut self, other: Self) {
impl<T: Ord> Merge for BTreeSet<T> {
fn merge(&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
BTreeSet::extend(self, other)
@@ -155,8 +134,8 @@ impl<T: Ord> Append for BTreeSet<T> {
}
}
impl<T> Append for Vec<T> {
fn append(&mut self, mut other: Self) {
impl<T> Merge for Vec<T> {
fn merge(&mut self, mut other: Self) {
Vec::append(self, &mut other)
}
@@ -165,30 +144,30 @@ impl<T> Append for Vec<T> {
}
}
macro_rules! impl_append_for_tuple {
macro_rules! impl_merge_for_tuple {
($($a:ident $b:tt)*) => {
impl<$($a),*> Append for ($($a,)*) where $($a: Append),* {
impl<$($a),*> Merge for ($($a,)*) where $($a: Merge),* {
fn append(&mut self, _other: Self) {
$(Append::append(&mut self.$b, _other.$b) );*
fn merge(&mut self, _other: Self) {
$(Merge::merge(&mut self.$b, _other.$b) );*
}
fn is_empty(&self) -> bool {
$(Append::is_empty(&self.$b) && )* true
$(Merge::is_empty(&self.$b) && )* true
}
}
}
}
impl_append_for_tuple!();
impl_append_for_tuple!(T0 0);
impl_append_for_tuple!(T0 0 T1 1);
impl_append_for_tuple!(T0 0 T1 1 T2 2);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9);
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10);
impl_merge_for_tuple!();
impl_merge_for_tuple!(T0 0);
impl_merge_for_tuple!(T0 0 T1 1);
impl_merge_for_tuple!(T0 0 T1 1 T2 2);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9);
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10);

View File

@@ -69,7 +69,7 @@
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
//!
//! ```
//! # use bdk_chain::{Append, BlockId};
//! # use bdk_chain::{Merge, BlockId};
//! # use bdk_chain::tx_graph::TxGraph;
//! # use bdk_chain::example_utils::*;
//! # use bitcoin::Transaction;
@@ -89,13 +89,12 @@
//! [`insert_txout`]: TxGraph::insert_txout
use crate::{
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
FullTxOut,
collections::*, Anchor, Balance, BlockId, ChainOracle, ChainPosition, FullTxOut, Merge,
};
use alloc::collections::vec_deque::VecDeque;
use alloc::sync::Arc;
use alloc::vec::Vec;
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
use core::fmt::{self, Formatter};
use core::{
convert::Infallible,
@@ -109,10 +108,11 @@ use core::{
/// [module-level documentation]: crate::tx_graph
#[derive(Clone, Debug, PartialEq)]
pub struct TxGraph<A = ()> {
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)`
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors)`
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>)>,
spends: BTreeMap<OutPoint, HashSet<Txid>>,
anchors: BTreeSet<(A, Txid)>,
last_seen: HashMap<Txid, u64>,
// This atrocity exists so that `TxGraph::outspends()` can return a reference.
// FIXME: This can be removed once `HashSet::new` is a const fn.
@@ -125,6 +125,7 @@ impl<A> Default for TxGraph<A> {
txs: Default::default(),
spends: Default::default(),
anchors: Default::default(),
last_seen: Default::default(),
empty_outspends: Default::default(),
}
}
@@ -140,7 +141,7 @@ pub struct TxNode<'a, T, A> {
/// The blocks that the transaction is "anchored" in.
pub anchors: &'a BTreeSet<A>,
/// The last-seen unix timestamp of the transaction as unconfirmed.
pub last_seen_unconfirmed: u64,
pub last_seen_unconfirmed: Option<u64>,
}
impl<'a, T, A> Deref for TxNode<'a, T, A> {
@@ -210,7 +211,7 @@ impl<A> TxGraph<A> {
///
/// This includes txouts of both full transactions as well as floating transactions.
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
self.txs.iter().flat_map(|(txid, (tx, _))| match tx {
TxNodeInternal::Whole(tx) => tx
.as_ref()
.output
@@ -232,7 +233,7 @@ impl<A> TxGraph<A> {
pub fn floating_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs
.iter()
.filter_map(|(txid, (tx_node, _, _))| match tx_node {
.filter_map(|(txid, (tx_node, _))| match tx_node {
TxNodeInternal::Whole(_) => None,
TxNodeInternal::Partial(txouts) => Some(
txouts
@@ -247,17 +248,30 @@ impl<A> TxGraph<A> {
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
.filter_map(|(&txid, (tx, anchors))| match tx {
TxNodeInternal::Whole(tx) => Some(TxNode {
txid,
tx: tx.clone(),
anchors,
last_seen_unconfirmed: *last_seen,
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
}),
TxNodeInternal::Partial(_) => None,
})
}
/// Iterate over graph transactions with no anchors or last-seen.
pub fn txs_with_no_anchor_or_last_seen(
&self,
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
self.full_txs().filter_map(|tx| {
if tx.anchors.is_empty() && tx.last_seen_unconfirmed.is_none() {
Some(tx)
} else {
None
}
})
}
/// Get a transaction by txid. This only returns `Some` for full transactions.
///
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
@@ -270,11 +284,11 @@ impl<A> TxGraph<A> {
/// Get a transaction node by txid. This only returns `Some` for full transactions.
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
match &self.txs.get(&txid)? {
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
(TxNodeInternal::Whole(tx), anchors) => Some(TxNode {
txid,
tx: tx.clone(),
anchors,
last_seen_unconfirmed: *last_seen,
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
}),
_ => None,
}
@@ -504,7 +518,6 @@ impl<A: Clone + Ord> TxGraph<A> {
(
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
BTreeSet::new(),
0,
),
);
self.apply_update(update)
@@ -518,7 +531,7 @@ impl<A: Clone + Ord> TxGraph<A> {
let mut update = Self::default();
update.txs.insert(
tx.compute_txid(),
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
(TxNodeInternal::Whole(tx), BTreeSet::new()),
);
self.apply_update(update)
}
@@ -534,8 +547,8 @@ impl<A: Clone + Ord> TxGraph<A> {
) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
for (tx, seen_at) in txs {
changeset.append(self.insert_seen_at(tx.compute_txid(), seen_at));
changeset.append(self.insert_tx(tx));
changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at));
changeset.merge(self.insert_tx(tx));
}
changeset
}
@@ -559,8 +572,7 @@ impl<A: Clone + Ord> TxGraph<A> {
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
let mut update = Self::default();
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
*update_last_seen = seen_at;
update.last_seen.insert(txid, seen_at);
self.apply_update(update)
}
@@ -607,7 +619,7 @@ impl<A: Clone + Ord> TxGraph<A> {
.txs
.iter()
.filter_map(
|(&txid, (_, anchors, _))| {
|(&txid, (_, anchors))| {
if anchors.is_empty() {
Some(txid)
} else {
@@ -618,7 +630,7 @@ impl<A: Clone + Ord> TxGraph<A> {
.collect();
for txid in unanchored_txs {
changeset.append(self.insert_seen_at(txid, seen_at));
changeset.merge(self.insert_seen_at(txid, seen_at));
}
changeset
}
@@ -656,10 +668,10 @@ impl<A: Clone + Ord> TxGraph<A> {
});
match self.txs.get_mut(&txid) {
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
Some((tx_node @ TxNodeInternal::Partial(_), _)) => {
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
}
Some((TxNodeInternal::Whole(tx), _, _)) => {
Some((TxNodeInternal::Whole(tx), _)) => {
debug_assert_eq!(
tx.as_ref().compute_txid(),
txid,
@@ -667,10 +679,8 @@ impl<A: Clone + Ord> TxGraph<A> {
);
}
None => {
self.txs.insert(
txid,
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
);
self.txs
.insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new()));
}
}
}
@@ -679,9 +689,8 @@ impl<A: Clone + Ord> TxGraph<A> {
let tx_entry = self.txs.entry(outpoint.txid).or_default();
match tx_entry {
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
}
(TxNodeInternal::Partial(txouts), _, _) => {
(TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ }
(TxNodeInternal::Partial(txouts), _) => {
txouts.insert(outpoint.vout, txout);
}
}
@@ -689,13 +698,13 @@ impl<A: Clone + Ord> TxGraph<A> {
for (anchor, txid) in changeset.anchors {
if self.anchors.insert((anchor.clone(), txid)) {
let (_, anchors, _) = self.txs.entry(txid).or_default();
let (_, anchors) = self.txs.entry(txid).or_default();
anchors.insert(anchor);
}
}
for (txid, new_last_seen) in changeset.last_seen {
let (_, _, last_seen) = self.txs.entry(txid).or_default();
let last_seen = self.last_seen.entry(txid).or_default();
if new_last_seen > *last_seen {
*last_seen = new_last_seen;
}
@@ -709,11 +718,10 @@ impl<A: Clone + Ord> TxGraph<A> {
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
for (&txid, (update_tx_node, _)) in &update.txs {
match (self.txs.get(&txid), update_tx_node) {
(None, TxNodeInternal::Whole(update_tx)) => {
changeset.txs.insert(update_tx.clone());
0
}
(None, TxNodeInternal::Partial(update_txos)) => {
changeset.txouts.extend(
@@ -721,18 +729,13 @@ impl<A: Clone + Ord> TxGraph<A> {
.iter()
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
);
0
}
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
(
Some((TxNodeInternal::Partial(_), _, last_seen)),
TxNodeInternal::Whole(update_tx),
) => {
(Some((TxNodeInternal::Whole(_), _)), _) => {}
(Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => {
changeset.txs.insert(update_tx.clone());
*last_seen
}
(
Some((TxNodeInternal::Partial(txos), _, last_seen)),
Some((TxNodeInternal::Partial(txos), _)),
TxNodeInternal::Partial(update_txos),
) => {
changeset.txouts.extend(
@@ -741,12 +744,14 @@ impl<A: Clone + Ord> TxGraph<A> {
.filter(|(vout, _)| !txos.contains_key(*vout))
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
);
*last_seen
}
};
}
}
if *update_last_seen > prev_last_seen {
changeset.last_seen.insert(txid, *update_last_seen);
for (txid, update_last_seen) in update.last_seen {
let prev_last_seen = self.last_seen.get(&txid).copied();
if Some(update_last_seen) > prev_last_seen {
changeset.last_seen.insert(txid, update_last_seen);
}
}
@@ -786,7 +791,7 @@ impl<A: Anchor> TxGraph<A> {
chain_tip: BlockId,
txid: Txid,
) -> Result<Option<ChainPosition<&A>>, C::Error> {
let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
let (tx_node, anchors) = match self.txs.get(&txid) {
Some(v) => v,
None => return Ok(None),
};
@@ -798,6 +803,13 @@ impl<A: Anchor> TxGraph<A> {
}
}
// If no anchors are in best chain and we don't have a last_seen, we can return
// early because by definition the tx doesn't have a chain position.
let last_seen = match self.last_seen.get(&txid) {
Some(t) => *t,
None => return Ok(None),
};
// The tx is not anchored to a block in the best chain, which means that it
// might be in mempool, or it might have been dropped already.
// Let's check conflicts to find out!
@@ -884,7 +896,7 @@ impl<A: Anchor> TxGraph<A> {
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
return Ok(None);
}
if conflicting_tx.last_seen_unconfirmed == *last_seen
if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
{
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
@@ -893,7 +905,7 @@ impl<A: Anchor> TxGraph<A> {
}
}
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
Ok(Some(ChainPosition::Unconfirmed(last_seen)))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
@@ -971,10 +983,10 @@ impl<A: Anchor> TxGraph<A> {
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
/// returned item.
///
/// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead.
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
///
/// [`list_chain_txs`]: Self::list_chain_txs
pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>(
/// [`list_canonical_txs`]: Self::list_canonical_txs
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
&'a self,
chain: &'a C,
chain_tip: BlockId,
@@ -993,15 +1005,15 @@ impl<A: Anchor> TxGraph<A> {
/// List graph transactions that are in `chain` with `chain_tip`.
///
/// This is the infallible version of [`try_list_chain_txs`].
/// This is the infallible version of [`try_list_canonical_txs`].
///
/// [`try_list_chain_txs`]: Self::try_list_chain_txs
pub fn list_chain_txs<'a, C: ChainOracle + 'a>(
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
pub fn list_canonical_txs<'a, C: ChainOracle + 'a>(
&'a self,
chain: &'a C,
chain_tip: BlockId,
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
self.try_list_chain_txs(chain, chain_tip)
self.try_list_canonical_txs(chain, chain_tip)
.map(|r| r.expect("oracle is infallible"))
}
@@ -1151,7 +1163,7 @@ impl<A: Anchor> TxGraph<A> {
chain: &C,
chain_tip: BlockId,
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
) -> Result<Balance, C::Error> {
let mut immature = Amount::ZERO;
let mut trusted_pending = Amount::ZERO;
@@ -1170,7 +1182,7 @@ impl<A: Anchor> TxGraph<A> {
}
}
ChainPosition::Unconfirmed(_) => {
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
trusted_pending += txout.txout.value;
} else {
untrusted_pending += txout.txout.value;
@@ -1197,7 +1209,7 @@ impl<A: Anchor> TxGraph<A> {
chain: &C,
chain_tip: BlockId,
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
trust_predicate: impl FnMut(&OI, &Script) -> bool,
trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
) -> Balance {
self.try_balance(chain, chain_tip, outpoints, trust_predicate)
.expect("oracle is infallible")
@@ -1281,8 +1293,8 @@ impl<A> ChangeSet<A> {
}
}
impl<A: Ord> Append for ChangeSet<A> {
fn append(&mut self, other: Self) {
impl<A: Ord> Merge for ChangeSet<A> {
fn merge(&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.txs.extend(other.txs);

View File

@@ -3,7 +3,7 @@
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor};
use bitcoin::{
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
@@ -119,7 +119,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
},
Some(index) => TxOut {
value: Amount::from_sat(output.value),
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
script_pubkey: spk_index.spk_at_index(index).unwrap(),
},
})
.collect(),
@@ -131,9 +131,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
for anchor in tx_tmp.anchors.iter() {
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
}
if let Some(seen_at) = tx_tmp.last_seen {
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
}
let _ = graph.insert_seen_at(tx.compute_txid(), tx_tmp.last_seen.unwrap_or(0));
}
(graph, spk_index, tx_ids)
}

View File

@@ -8,13 +8,11 @@ use std::{collections::BTreeSet, sync::Arc};
use crate::common::DESCRIPTORS;
use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph},
keychain::{self, Balance, KeychainTxOutIndex},
indexer::keychain_txout::KeychainTxOutIndex,
local_chain::LocalChain,
tx_graph, Append, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
};
use bitcoin::{
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt,
};
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@@ -26,12 +24,13 @@ use miniscript::Descriptor;
/// agnostic.
#[test]
fn insert_relevant_txs() {
use bdk_chain::indexer::keychain_txout;
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();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
KeychainTxOutIndex::new(10),
);
let _ = graph
@@ -72,13 +71,12 @@ fn insert_relevant_txs() {
let txs = [tx_c, tx_b, tx_a];
let changeset = indexed_tx_graph::ChangeSet {
graph: tx_graph::ChangeSet {
tx_graph: tx_graph::ChangeSet {
txs: txs.iter().cloned().map(Arc::new).collect(),
..Default::default()
},
indexer: keychain::ChangeSet {
indexer: keychain_txout::ChangeSet {
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
keychains_added: [].into(),
},
};
@@ -89,10 +87,9 @@ fn insert_relevant_txs() {
// 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 {
tx_graph: changeset.tx_graph,
indexer: keychain_txout::ChangeSet {
last_revealed: changeset.indexer.last_revealed,
keychains_added: [((), descriptor)].into(),
},
};
@@ -116,8 +113,8 @@ fn insert_relevant_txs() {
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
/// tx4: Mempool tx, sending 20000 sats to "untrusted" address.
/// tx5: Mempool tx, sending 15000 sats to "trusted" address.
/// tx6: Complete unrelated tx. [Block 3]
///
/// Different transactions are added via `insert_relevant_txs`.
@@ -139,20 +136,18 @@ fn test_list_owned_txouts() {
let (desc_2, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::new(
KeychainTxOutIndex::new(10),
);
assert!(!graph
assert!(graph
.index
.insert_descriptor("keychain_1".into(), desc_1)
.unwrap()
.is_empty());
assert!(!graph
.unwrap());
assert!(graph
.index
.insert_descriptor("keychain_2".into(), desc_2)
.unwrap()
.is_empty());
.unwrap());
// Get trusted and untrusted addresses
@@ -160,11 +155,11 @@ fn test_list_owned_txouts() {
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
{
// we need to scope here to take immutanble reference of the graph
// we need to scope here to take immutable reference of the graph
for _ in 0..10 {
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_1".to_string())
.reveal_next_spk("keychain_1".to_string())
.unwrap();
// TODO Assert indexes
trusted_spks.push(script.to_owned());
@@ -174,7 +169,7 @@ fn test_list_owned_txouts() {
for _ in 0..10 {
let ((_, script), _) = graph
.index
.reveal_next_spk(&"keychain_2".to_string())
.reveal_next_spk("keychain_2".to_string())
.unwrap();
untrusted_spks.push(script.to_owned());
}
@@ -226,7 +221,7 @@ fn test_list_owned_txouts() {
..common::new_tx(0)
};
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
// tx5 is an external transaction receiving at trusted keychain, unconfirmed.
let tx5 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(15000),
@@ -239,7 +234,7 @@ fn test_list_owned_txouts() {
let tx6 = common::new_tx(0);
// Insert transactions into graph with respective anchors
// For unconfirmed txs we pass in `None`.
// Insert unconfirmed txs with a last_seen timestamp
let _ =
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
@@ -249,9 +244,9 @@ fn test_list_owned_txouts() {
local_chain
.get(height)
.map(|cp| cp.block_id())
.map(|anchor_block| ConfirmationHeightAnchor {
anchor_block,
confirmation_height: anchor_block.height,
.map(|block_id| ConfirmationBlockTime {
block_id,
confirmation_time: 100,
}),
)
}));
@@ -260,8 +255,7 @@ fn test_list_owned_txouts() {
// A helper lambda to extract and filter data from the graph.
let fetch =
|height: u32,
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|height: u32, graph: &IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<String>>| {
let chain_tip = local_chain
.get(height)
.map(|cp| cp.block_id())
@@ -288,12 +282,9 @@ fn test_list_owned_txouts() {
&local_chain,
chain_tip,
graph.index.outpoints().iter().cloned(),
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|_, spk: ScriptBuf| trusted_spks.contains(&spk),
);
assert_eq!(txouts.len(), 5);
assert_eq!(utxos.len(), 4);
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
@@ -359,29 +350,25 @@ fn test_list_owned_txouts() {
balance,
) = fetch(0, &graph);
// tx1 is a confirmed txout and is unspent
// tx4, tx5 are unconfirmed
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[
tx2.compute_txid(),
tx3.compute_txid(),
tx4.compute_txid(),
tx5.compute_txid()
]
.into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
}
@@ -405,23 +392,26 @@ fn test_list_owned_txouts() {
);
assert_eq!(
unconfirmed_txouts_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
// tx2 doesn't get into confirmed utxos set
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
// tx2 gets into confirmed utxos set
assert_eq!(
confirmed_utxos_txid,
[tx1.compute_txid(), tx2.compute_txid()].into()
);
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
confirmed: Amount::from_sat(30_000) // tx2 got confirmed
}
);
}
@@ -477,6 +467,7 @@ fn test_list_owned_txouts() {
balance,
) = fetch(98, &graph);
// no change compared to block 2
assert_eq!(
confirmed_txouts_txid,
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
@@ -502,14 +493,14 @@ fn test_list_owned_txouts() {
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
confirmed: Amount::from_sat(10000) // tx3 is confirmed
}
);
}
// AT Block 99
{
let (_, _, _, _, balance) = fetch(100, &graph);
let (_, _, _, _, balance) = fetch(99, &graph);
// Coinbase maturity hits
assert_eq!(
@@ -523,3 +514,147 @@ fn test_list_owned_txouts() {
);
}
}
/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor
/// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the
/// result of `get_chain_position` in these cases:
///
/// - tx with no anchors or last_seen has no `ChainPosition`
/// - tx with any last_seen will be `Unconfirmed`
/// - tx with an anchor in best chain will be `Confirmed`
/// - tx with an anchor not in best chain (no last_seen) has no `ChainPosition`
#[test]
fn test_get_chain_position() {
use bdk_chain::local_chain::CheckPoint;
use bdk_chain::spk_txout::SpkTxOutIndex;
use bdk_chain::BlockId;
struct TestCase<A> {
name: &'static str,
tx: Transaction,
anchor: Option<A>,
last_seen: Option<u64>,
exp_pos: Option<ChainPosition<A>>,
}
// addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm
let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
let mut graph = IndexedTxGraph::new({
let mut index = SpkTxOutIndex::default();
let _ = index.insert_spk(0u32, spk.clone());
index
});
// Anchors to test
let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap();
let chain = LocalChain::from_tip(cp).unwrap();
// The test will insert a transaction into the indexed tx graph
// along with any anchors and timestamps, then check the value
// returned by `get_chain_position`.
fn run(
chain: &LocalChain,
graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<u32>>,
test: TestCase<BlockId>,
) {
let TestCase {
name,
tx,
anchor,
last_seen,
exp_pos,
} = test;
// add data to graph
let txid = tx.compute_txid();
let _ = graph.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = graph.insert_anchor(txid, anchor);
}
if let Some(seen_at) = last_seen {
let _ = graph.insert_seen_at(txid, seen_at);
}
// check chain position
let res = graph
.graph()
.get_chain_position(chain, chain.tip().block_id(), txid);
assert_eq!(
res.map(ChainPosition::cloned),
exp_pos,
"failed test case: {name}"
);
}
[
TestCase {
name: "tx no anchors or last_seen - no chain pos",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(0)
},
anchor: None,
last_seen: None,
exp_pos: None,
},
TestCase {
name: "tx last_seen - unconfirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(1)
},
anchor: None,
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
},
TestCase {
name: "tx anchor in best chain - confirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(2)
},
anchor: Some(blocks[1]),
last_seen: None,
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
},
TestCase {
name: "tx unknown anchor with last_seen - unconfirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(3)
},
anchor: Some(block_id!(2, "B'")),
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
},
TestCase {
name: "tx unknown anchor - no chain pos",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(4)
},
anchor: Some(block_id!(2, "B'")),
last_seen: None,
exp_pos: None,
},
]
.into_iter()
.for_each(|t| run(&chain, &mut graph, t));
}

View File

@@ -4,9 +4,8 @@
mod common;
use bdk_chain::{
collections::BTreeMap,
indexed_tx_graph::Indexer,
keychain::{self, ChangeSet, KeychainTxOutIndex},
Append, DescriptorExt, DescriptorId,
indexer::keychain_txout::{ChangeSet, KeychainTxOutIndex},
DescriptorExt, DescriptorId, Indexer, Merge,
};
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
@@ -31,8 +30,8 @@ fn init_txout_index(
external_descriptor: Descriptor<DescriptorPublicKey>,
internal_descriptor: Descriptor<DescriptorPublicKey>,
lookahead: u32,
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
) -> KeychainTxOutIndex<TestKeychain> {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(lookahead);
let _ = txout_index
.insert_descriptor(TestKeychain::External, external_descriptor)
@@ -52,13 +51,13 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
}
// 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:
// last_revealed, merge 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() {
fn merge_changesets_check_last_revealed() {
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let descriptor_ids: Vec<_> = DESCRIPTORS
.iter()
@@ -82,14 +81,12 @@ fn append_changesets_check_last_revealed() {
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);
lhs.merge(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));
@@ -101,53 +98,8 @@ fn append_changesets_check_last_revealed() {
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
}
#[test]
fn when_apply_contradictory_changesets_they_are_ignored() {
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, &external_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, &external_descriptor),
(&TestKeychain::Internal, &internal_descriptor)
]
);
}
#[test]
fn test_set_all_derivation_indices() {
use bdk_chain::indexed_tx_graph::Indexer;
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
let mut txout_index =
@@ -162,14 +114,13 @@ fn test_set_all_derivation_indices() {
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to),
ChangeSet {
keychains_added: BTreeMap::new(),
last_revealed: last_revealed.clone()
}
);
assert_eq!(txout_index.last_revealed_indices(), derive_to);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to),
keychain::ChangeSet::default(),
ChangeSet::default(),
"no changes if we set to the same thing"
);
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
@@ -191,7 +142,7 @@ fn test_lookahead() {
// - 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)
.reveal_to_target(TestKeychain::External, index)
.unwrap();
assert_eq!(
revealed_spks,
@@ -210,25 +161,25 @@ fn test_lookahead() {
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.revealed_keychain_spks(TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_keychain_spks(TestKeychain::Internal)
.count(),
0,
);
assert_eq!(
txout_index
.unused_keychain_spks(&TestKeychain::External)
.unused_keychain_spks(TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.unused_keychain_spks(&TestKeychain::Internal)
.unused_keychain_spks(TestKeychain::Internal)
.count(),
0,
);
@@ -242,7 +193,7 @@ fn test_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)
.reveal_to_target(TestKeychain::Internal, 24)
.unwrap();
assert_eq!(
revealed_spks,
@@ -263,17 +214,17 @@ fn test_lookahead() {
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_keychain_spks(TestKeychain::Internal)
.count(),
25,
);
// ensure derivation indices are expected for each keychain
let last_external_index = txout_index
.last_revealed_index(&TestKeychain::External)
.last_revealed_index(TestKeychain::External)
.expect("already derived");
let last_internal_index = txout_index
.last_revealed_index(&TestKeychain::Internal)
.last_revealed_index(TestKeychain::Internal)
.expect("already derived");
assert_eq!(last_external_index, 19);
assert_eq!(last_internal_index, 24);
@@ -304,24 +255,24 @@ fn test_lookahead() {
],
..common::new_tx(external_index)
};
assert_eq!(txout_index.index_tx(&tx), keychain::ChangeSet::default());
assert_eq!(txout_index.index_tx(&tx), ChangeSet::default());
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
txout_index.last_revealed_index(TestKeychain::External),
Some(last_external_index)
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::Internal),
txout_index.last_revealed_index(TestKeychain::Internal),
Some(last_internal_index)
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.revealed_keychain_spks(TestKeychain::External)
.count(),
last_external_index as usize + 1,
);
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::Internal)
.revealed_keychain_spks(TestKeychain::Internal)
.count(),
last_internal_index as usize + 1,
);
@@ -366,11 +317,11 @@ fn test_scan_with_lookahead() {
&[(external_descriptor.descriptor_id(), spk_i)].into()
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
txout_index.last_revealed_index(TestKeychain::External),
Some(spk_i)
);
assert_eq!(
txout_index.last_used_index(&TestKeychain::External),
txout_index.last_used_index(TestKeychain::External),
Some(spk_i)
);
}
@@ -406,11 +357,11 @@ 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).unwrap(), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
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.clone()));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (0_u32, external_spk_0.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
@@ -422,20 +373,20 @@ fn test_wildcard_derivations() {
// - next_derivation_index() = (26, true)
// - derive_new() = ((26, <spk>), keychain::ChangeSet)
// - next_unused() == ((16, <spk>), keychain::ChangeSet::is_empty())
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
let _ = txout_index.reveal_to_target(TestKeychain::External, 25);
(0..=15)
.chain([17, 20, 23])
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
assert_eq!(txout_index.next_index(TestKeychain::External).unwrap(), (26, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (26, external_spk_26));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (16, external_spk_16));
assert_eq!(&changeset.last_revealed, &[].into());
@@ -445,7 +396,7 @@ fn test_wildcard_derivations() {
txout_index.mark_used(TestKeychain::External, index);
});
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (27, external_spk_27));
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
}
@@ -473,21 +424,17 @@ fn test_non_wildcard_derivations() {
// - 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).unwrap(),
txout_index.next_index(TestKeychain::External).unwrap(),
(0, true)
);
let (spk, changeset) = txout_index
.reveal_next_spk(&TestKeychain::External)
.unwrap();
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(
&changeset.last_revealed,
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
);
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
@@ -498,24 +445,20 @@ fn test_non_wildcard_derivations() {
// - 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).unwrap(),
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)
.unwrap();
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
let (spk, changeset) = txout_index
.next_unused_spk(&TestKeychain::External)
.unwrap();
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
assert_eq!(spk, (0, external_spk.clone()));
assert_eq!(&changeset.last_revealed, &[].into());
let (revealed_spks, revealed_changeset) = txout_index
.reveal_to_target(&TestKeychain::External, 200)
.reveal_to_target(TestKeychain::External, 200)
.unwrap();
assert_eq!(revealed_spks.len(), 0);
assert!(revealed_changeset.is_empty());
@@ -523,7 +466,7 @@ fn test_non_wildcard_derivations() {
// we check that spks_of_keychain returns a SpkIterator with just one element
assert_eq!(
txout_index
.revealed_keychain_spks(&TestKeychain::External)
.revealed_keychain_spks(TestKeychain::External)
.count(),
1,
);
@@ -589,10 +532,10 @@ fn lookahead_to_target() {
);
if let Some(last_revealed) = t.external_last_revealed {
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
let _ = index.reveal_to_target(TestKeychain::External, last_revealed);
}
if let Some(last_revealed) = t.internal_last_revealed {
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
let _ = index.reveal_to_target(TestKeychain::Internal, last_revealed);
}
let keychain_test_cases = [
@@ -619,7 +562,7 @@ fn lookahead_to_target() {
}
None => target,
};
index.lookahead_to_target(&keychain, target);
index.lookahead_to_target(keychain.clone(), target);
let keys = index
.inner()
.all_spks()
@@ -636,51 +579,34 @@ fn lookahead_to_target() {
}
#[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()),
Ok(keychain::ChangeSet {
keychains_added: [((), desc.clone())].into(),
last_revealed: Default::default()
}),
);
assert_eq!(
txout_index.insert_descriptor((), desc.clone()),
Ok(keychain::ChangeSet::default()),
"inserting the same descriptor for keychain should return an empty changeset",
);
}
#[test]
#[cfg(not(debug_assertions))]
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
let desc = parse_descriptor(DESCRIPTORS[0]);
let changesets: &[ChangeSet<TestKeychain>] = &[
let changesets: &[ChangeSet] = &[
ChangeSet {
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
last_revealed: [].into(),
last_revealed: [(desc.descriptor_id(), 10)].into(),
},
ChangeSet {
keychains_added: [(TestKeychain::External, desc.clone())].into(),
last_revealed: [(desc.descriptor_id(), 12)].into(),
},
];
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
indexer_a
.insert_descriptor(TestKeychain::External, desc.clone())
.expect("must insert keychain");
for changeset in changesets {
indexer_a.apply_changeset(changeset.clone());
}
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
indexer_b
.insert_descriptor(TestKeychain::External, desc.clone())
.expect("must insert keychain");
let aggregate_changesets = changesets
.iter()
.cloned()
.reduce(|mut agg, cs| {
agg.append(cs);
agg.merge(cs);
agg
})
.expect("must aggregate changesets");
@@ -737,7 +663,7 @@ fn when_querying_over_a_range_of_keychains_the_utxos_should_show_up() {
let _ = indexer.insert_descriptor(i, descriptor.clone()).unwrap();
if i != 4 {
// skip one in the middle to see if uncovers any bugs
indexer.reveal_next_spk(&i);
indexer.reveal_next_spk(i);
}
tx.output.push(TxOut {
script_pubkey: descriptor.at_derivation_index(0).unwrap().script_pubkey(),

View File

@@ -1,4 +1,4 @@
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
};

View File

@@ -7,7 +7,7 @@ use bdk_chain::{
collections::*,
local_chain::LocalChain,
tx_graph::{ChangeSet, TxGraph},
Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor,
Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge,
};
use bitcoin::{
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount,
@@ -935,7 +935,7 @@ fn test_chain_spends() {
..common::new_tx(0)
};
let mut graph = TxGraph::<ConfirmationHeightAnchor>::default();
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
let _ = graph.insert_tx(tx_0.clone());
let _ = graph.insert_tx(tx_1.clone());
@@ -944,9 +944,9 @@ fn test_chain_spends() {
for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
let _ = graph.insert_anchor(
tx.compute_txid(),
ConfirmationHeightAnchor {
anchor_block: tip.block_id(),
confirmation_height: ht,
ConfirmationBlockTime {
block_id: tip.get(ht).unwrap().block_id(),
confirmation_time: 100,
},
);
}
@@ -959,9 +959,12 @@ fn test_chain_spends() {
OutPoint::new(tx_0.compute_txid(), 0)
),
Some((
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip.block_id(),
confirmation_height: 98
ChainPosition::Confirmed(&ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(98).unwrap().hash(),
height: 98,
},
confirmation_time: 100
}),
tx_1.compute_txid(),
)),
@@ -971,22 +974,15 @@ fn test_chain_spends() {
assert_eq!(
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
anchor_block: tip.block_id(),
confirmation_height: 95
Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
block_id: BlockId {
hash: tip.get(95).unwrap().hash(),
height: 95,
},
confirmation_time: 100
}))
);
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
assert_eq!(
graph.get_chain_spend(
&local_chain,
tip.block_id(),
OutPoint::new(tx_0.compute_txid(), 1)
),
Some((ChainPosition::Unconfirmed(0), tx_2.compute_txid())),
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
@@ -1059,9 +1055,9 @@ fn test_chain_spends() {
.is_none());
}
/// Ensure that `last_seen` values only increase during [`Append::append`].
/// Ensure that `last_seen` values only increase during [`Merge::merge`].
#[test]
fn test_changeset_last_seen_append() {
fn test_changeset_last_seen_merge() {
let txid: Txid = h!("test txid");
let test_cases: &[(Option<u64>, Option<u64>)] = &[
@@ -1084,7 +1080,7 @@ fn test_changeset_last_seen_append() {
};
assert!(!update.is_empty() || update_ls.is_none());
original.append(update);
original.merge(update);
assert_eq!(
&original.last_seen.get(&txid).cloned(),
Ord::max(original_ls, update_ls),
@@ -1099,10 +1095,10 @@ fn update_last_seen_unconfirmed() {
let txid = tx.compute_txid();
// insert a new tx
// initially we have a last_seen of 0, and no anchors
// initially we have a last_seen of None and no anchors
let _ = graph.insert_tx(tx);
let tx = graph.full_txs().next().unwrap();
assert_eq!(tx.last_seen_unconfirmed, 0);
assert_eq!(tx.last_seen_unconfirmed, None);
assert!(tx.anchors.is_empty());
// higher timestamp should update last seen
@@ -1117,7 +1113,56 @@ fn update_last_seen_unconfirmed() {
let _ = graph.insert_anchor(txid, ());
let changeset = graph.update_last_seen_unconfirmed(4);
assert!(changeset.is_empty());
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
assert_eq!(
graph
.full_txs()
.next()
.unwrap()
.last_seen_unconfirmed
.unwrap(),
2
);
}
#[test]
fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() {
let txs = vec![new_tx(0), new_tx(1)];
let txids: Vec<Txid> = txs.iter().map(Transaction::compute_txid).collect();
// graph
let mut graph = TxGraph::<BlockId>::new(txs);
let full_txs: Vec<_> = graph.full_txs().collect();
assert_eq!(full_txs.len(), 2);
let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect();
assert_eq!(unseen_txs.len(), 2);
// chain
let blocks: BTreeMap<u32, BlockHash> = [(0, h!("g")), (1, h!("A")), (2, h!("B"))]
.into_iter()
.collect();
let chain = LocalChain::from_blocks(blocks).unwrap();
let canonical_txs: Vec<_> = graph
.list_canonical_txs(&chain, chain.tip().block_id())
.collect();
assert!(canonical_txs.is_empty());
// tx0 with seen_at should be returned by canonical txs
let _ = graph.insert_seen_at(txids[0], 2);
let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id());
assert_eq!(
canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
txids[0]
);
drop(canonical_txs);
// tx1 with anchor is also canonical
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
let canonical_txids: Vec<_> = graph
.list_canonical_txs(&chain, chain.tip().block_id())
.map(|tx| tx.tx_node.txid)
.collect();
assert!(canonical_txids.contains(&txids[1]));
assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none());
}
#[test]

View File

@@ -5,8 +5,8 @@ mod common;
use std::collections::{BTreeSet, HashSet};
use bdk_chain::{keychain::Balance, BlockId};
use bitcoin::{Amount, OutPoint, Script};
use bdk_chain::{Balance, BlockId};
use bitcoin::{Amount, OutPoint, ScriptBuf};
use common::*;
#[allow(dead_code)]
@@ -15,7 +15,7 @@ struct Scenario<'a> {
name: &'a str,
/// Transaction templates
tx_templates: &'a [TxTemplate<'a, BlockId>],
/// Names of txs that must exist in the output of `list_chain_txs`
/// Names of txs that must exist in the output of `list_canonical_txs`
exp_chain_txs: HashSet<&'a str>,
/// Outpoints that must exist in the output of `filter_chain_txouts`
exp_chain_txouts: HashSet<(&'a str, u32)>,
@@ -27,7 +27,7 @@ struct Scenario<'a> {
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
/// This test also checks that [`TxGraph::list_canonical_txs`], [`TxGraph::filter_chain_txouts`],
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
#[test]
fn test_tx_conflict_handling() {
@@ -597,7 +597,7 @@ fn test_tx_conflict_handling() {
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
let txs = tx_graph
.list_chain_txs(&local_chain, chain_tip)
.list_canonical_txs(&local_chain, chain_tip)
.map(|tx| tx.tx_node.txid)
.collect::<BTreeSet<_>>();
let exp_txs = scenario
@@ -607,7 +607,7 @@ fn test_tx_conflict_handling() {
.collect::<BTreeSet<_>>();
assert_eq!(
txs, exp_txs,
"\n[{}] 'list_chain_txs' failed",
"\n[{}] 'list_canonical_txs' failed",
scenario.name
);
@@ -659,7 +659,7 @@ fn test_tx_conflict_handling() {
&local_chain,
chain_tip,
spk_index.outpoints().iter().cloned(),
|_, spk: &Script| spk_index.index_of_spk(spk).is_some(),
|_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(),
);
assert_eq!(
balance, scenario.exp_balance,

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_electrum"
version = "0.15.0"
version = "0.16.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.16.0" }
bdk_chain = { path = "../chain", version = "0.17.0" }
electrum-client = { version = "0.20" }
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }

View File

@@ -1,14 +1,16 @@
use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
collections::{BTreeMap, HashMap, HashSet},
bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid},
collections::{BTreeMap, HashMap},
local_chain::CheckPoint,
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::TxGraph,
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
Anchor, BlockId, ConfirmationBlockTime,
};
use core::str::FromStr;
use electrum_client::{ElectrumApi, Error, HeaderNotification};
use std::sync::{Arc, Mutex};
use std::{
collections::BTreeSet,
sync::{Arc, Mutex},
};
/// We include a chain suffix of a certain length for the purpose of robustness.
const CHAIN_SUFFIX_LENGTH: u32 = 8;
@@ -21,6 +23,8 @@ pub struct BdkElectrumClient<E> {
pub inner: E,
/// The transaction cache
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
/// The header cache
block_header_cache: Mutex<HashMap<u32, Header>>,
}
impl<E: ElectrumApi> BdkElectrumClient<E> {
@@ -29,6 +33,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
Self {
inner: client,
tx_cache: Default::default(),
block_header_cache: Default::default(),
}
}
@@ -65,6 +70,33 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
Ok(tx)
}
/// Fetch block header of given `height`.
///
/// If it hits the cache it will return the cached version and avoid making the request.
fn fetch_header(&self, height: u32) -> Result<Header, Error> {
let block_header_cache = self.block_header_cache.lock().unwrap();
if let Some(header) = block_header_cache.get(&height) {
return Ok(*header);
}
drop(block_header_cache);
self.update_header(height)
}
/// Update a block header at given `height`. Returns the updated header.
fn update_header(&self, height: u32) -> Result<Header, Error> {
let header = self.inner.block_header(height as usize)?;
self.block_header_cache
.lock()
.unwrap()
.insert(height, header);
Ok(header)
}
/// Broadcasts a transaction to the network.
///
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
@@ -88,87 +120,32 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
stop_gap: usize,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error> {
let mut request_spks = request.spks_by_keychain;
) -> Result<FullScanResult<K>, Error> {
let (tip, latest_blocks) =
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
let mut last_active_indices = BTreeMap::<K, u32>::new();
// 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 update = loop {
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
if !request_spks.is_empty() {
if !scanned_spks.is_empty() {
scanned_spks.append(
&mut self.populate_with_spks(
&cps,
&mut graph_update,
&mut scanned_spks
.iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
stop_gap,
batch_size,
)?,
);
}
for (keychain, keychain_spks) in &mut request_spks {
scanned_spks.extend(
self.populate_with_spks(
&cps,
&mut graph_update,
keychain_spks,
stop_gap,
batch_size,
)?
.into_iter()
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
);
}
for (keychain, spks) in request.spks_by_keychain {
if let Some(last_active_index) =
self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)?
{
last_active_indices.insert(keychain, last_active_index);
}
}
// check for reorgs during scan process
let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
if tip.hash() != server_blockhash {
continue; // reorg
}
let chain_update = chain_update(tip, &latest_blocks, graph_update.all_anchors())?;
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
self.fetch_prev_txout(&mut graph_update)?;
}
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
self.fetch_prev_txout(&mut graph_update)?;
}
let chain_update = tip;
let keychain_update = request_spks
.into_keys()
.filter_map(|k| {
scanned_spks
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
.rev()
.find(|(_, (_, active))| *active)
.map(|((_, i), _)| (k, *i))
})
.collect::<BTreeMap<_, _>>();
break FullScanResult {
graph_update,
chain_update,
last_active_indices: keychain_update,
};
};
Ok(ElectrumFullScanResult(update))
Ok(FullScanResult {
graph_update,
chain_update,
last_active_indices,
})
}
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
@@ -190,32 +167,31 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
request: SyncRequest,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error> {
) -> Result<SyncResult, Error> {
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
.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 mut full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size, false)?;
let (tip, latest_blocks) =
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
let cps = tip
.iter()
.take(10)
.map(|cp| (cp.height(), cp))
.collect::<BTreeMap<u32, CheckPoint>>();
self.populate_with_txids(&mut full_scan_res.graph_update, request.txids)?;
self.populate_with_outpoints(&mut full_scan_res.graph_update, request.outpoints)?;
self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
let chain_update = chain_update(
tip,
&latest_blocks,
full_scan_res.graph_update.all_anchors(),
)?;
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
}
Ok(ElectrumSyncResult(SyncResult {
chain_update: full_scan_res.chain_update,
Ok(SyncResult {
chain_update,
graph_update: full_scan_res.graph_update,
}))
})
}
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
@@ -223,84 +199,55 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
/// 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>(
fn populate_with_spks(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
graph_update: &mut TxGraph<ConfirmationBlockTime>,
mut spks: impl Iterator<Item = (u32, ScriptBuf)>,
stop_gap: usize,
batch_size: usize,
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
) -> Result<Option<u32>, Error> {
let mut unused_spk_count = 0_usize;
let mut scanned_spks = BTreeMap::new();
let mut last_active_index = Option::<u32>::None;
loop {
let spks = (0..batch_size)
.map_while(|_| spks.next())
.collect::<Vec<_>>();
if spks.is_empty() {
return Ok(scanned_spks);
return Ok(last_active_index);
}
let spk_histories = self
.inner
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
for ((spk_index, _spk), spk_history) in spks.into_iter().zip(spk_histories) {
if spk_history.is_empty() {
scanned_spks.insert(spk_index, (spk, false));
unused_spk_count += 1;
if unused_spk_count > stop_gap {
return Ok(scanned_spks);
unused_spk_count = unused_spk_count.saturating_add(1);
if unused_spk_count >= stop_gap {
return Ok(last_active_index);
}
continue;
} else {
scanned_spks.insert(spk_index, (spk, true));
last_active_index = Some(spk_index);
unused_spk_count = 0;
}
for tx_res in spk_history {
let _ = graph_update.insert_tx(self.fetch_tx(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);
}
self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?;
}
}
}
}
// 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(
&self,
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 = self.fetch_tx(outpoint.txid)?;
let txout = prev_tx.output[vout as usize].clone();
let _ = graph_update.insert_txout(outpoint, txout);
}
}
Ok(())
}
/// 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(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
graph_update: &mut TxGraph<ConfirmationBlockTime>,
outpoints: impl IntoIterator<Item = OutPoint>,
) -> Result<(), Error> {
for outpoint in outpoints {
@@ -324,9 +271,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
if !has_residing && res.tx_hash == op_txid {
has_residing = true;
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);
}
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
}
if !has_spending && res.tx_hash != op_txid {
@@ -340,9 +285,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
continue;
}
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);
}
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
}
}
}
@@ -352,8 +295,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
fn populate_with_txids(
&self,
cps: &BTreeMap<u32, CheckPoint>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
graph_update: &mut TxGraph<ConfirmationBlockTime>,
txids: impl IntoIterator<Item = Txid>,
) -> Result<(), Error> {
for txid in txids {
@@ -371,120 +313,100 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
// 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 self
if let Some(r) = self
.inner
.script_get_history(spk)?
.into_iter()
.find(|r| r.tx_hash == txid)
{
Some(r) => determine_tx_anchor(cps, r.height, txid),
None => continue,
};
self.validate_merkle_for_anchor(graph_update, txid, r.height)?;
}
let _ = graph_update.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = graph_update.insert_anchor(txid, anchor);
}
Ok(())
}
// Helper function which checks if a transaction is confirmed by validating the merkle proof.
// An anchor is inserted if the transaction is validated to be in a confirmed block.
fn validate_merkle_for_anchor(
&self,
graph_update: &mut TxGraph<ConfirmationBlockTime>,
txid: Txid,
confirmation_height: i32,
) -> Result<(), Error> {
if let Ok(merkle_res) = self
.inner
.transaction_get_merkle(&txid, confirmation_height as usize)
{
let mut header = self.fetch_header(merkle_res.block_height as u32)?;
let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
&txid,
&header.merkle_root,
&merkle_res,
);
// Merkle validation will fail if the header in `block_header_cache` is outdated, so we
// want to check if there is a new header and validate against the new one.
if !is_confirmed_tx {
header = self.update_header(merkle_res.block_height as u32)?;
is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
&txid,
&header.merkle_root,
&merkle_res,
);
}
if is_confirmed_tx {
let _ = graph_update.insert_anchor(
txid,
ConfirmationBlockTime {
confirmation_time: header.time as u64,
block_id: BlockId {
height: merkle_res.block_height as u32,
hash: header.block_hash(),
},
},
);
}
}
Ok(())
}
// 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(
&self,
graph_update: &mut TxGraph<ConfirmationBlockTime>,
) -> 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 = self.fetch_tx(outpoint.txid)?;
let txout = prev_tx.output[vout as usize].clone();
let _ = graph_update.insert_txout(outpoint, txout);
}
}
Ok(())
}
}
/// The result of [`BdkElectrumClient::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: &BdkElectrumClient<impl ElectrumApi>,
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(FullScanResult {
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
chain_update: res.chain_update,
last_active_indices: res.last_active_indices,
})
}
}
/// The result of [`BdkElectrumClient::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: &BdkElectrumClient<impl ElectrumApi>,
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
let res = self.0;
Ok(SyncResult {
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
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(
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are
/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
fn fetch_tip_and_latest_blocks(
client: &impl ElectrumApi,
prev_tip: CheckPoint,
) -> Result<(CheckPoint, Option<u32>), Error> {
) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
let new_tip_height = height as u32;
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
// not need updating. We just return the previous tip and use that as the point of agreement.
if new_tip_height < prev_tip.height() {
return Ok((prev_tip.clone(), Some(prev_tip.height())));
return Ok((prev_tip, BTreeMap::new()));
}
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
@@ -527,10 +449,13 @@ fn construct_update_tip(
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
let new_tip = new_blocks
.into_iter()
.iter()
// Prune `new_blocks` to only include blocks that are actually new.
.filter(|(height, _)| Some(*height) > agreement_height)
.map(|(height, hash)| BlockId { height, hash })
.filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height)
.map(|(height, hash)| BlockId {
height: *height,
hash: *hash,
})
.fold(agreement_cp, |prev_cp, block| {
Some(match prev_cp {
Some(cp) => cp.push(block).expect("must extend checkpoint"),
@@ -539,51 +464,28 @@ fn construct_update_tip(
})
.expect("must have at least one checkpoint");
Ok((new_tip, agreement_height))
Ok((new_tip, new_blocks))
}
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
///
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
///
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
fn determine_tx_anchor(
cps: &BTreeMap<u32, CheckPoint>,
raw_height: i32,
txid: Txid,
) -> Option<ConfirmationHeightAnchor> {
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
// height of 0. To avoid invalid representation in our data structures, we manually set
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
// unconfirmed for all other transactions.
if txid
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
.expect("must deserialize genesis coinbase txid")
{
let anchor_block = cps.values().next()?.block_id();
return Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: 0,
});
}
match raw_height {
h if h <= 0 => {
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
None
}
h => {
let h = h as u32;
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
if h > anchor_block.height {
None
} else {
Some(ConfirmationHeightAnchor {
anchor_block,
confirmation_height: h,
})
}
// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not
// surpass `latest_blocks`.
fn chain_update<A: Anchor>(
mut tip: CheckPoint,
latest_blocks: &BTreeMap<u32, BlockHash>,
anchors: &BTreeSet<(A, Txid)>,
) -> Result<CheckPoint, Error> {
for anchor in anchors {
let height = anchor.0.anchor_block().height;
// Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent
// in case of a re-org.
if tip.get(height).is_none() && height <= tip.height() {
let hash = match latest_blocks.get(&height) {
Some(&hash) => hash,
None => anchor.0.anchor_block().hash,
};
tip = tip.insert(BlockId { hash, height });
}
}
Ok(tip)
}

View File

@@ -1,16 +1,18 @@
use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
keychain::Balance,
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
local_chain::LocalChain,
spk_client::SyncRequest,
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
spk_client::{FullScanRequest, SyncRequest},
spk_txout::SpkTxOutIndex,
Balance, ConfirmationBlockTime, IndexedTxGraph,
};
use bdk_electrum::BdkElectrumClient;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use std::collections::{BTreeSet, HashSet};
use std::str::FromStr;
fn get_balance(
recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
recv_graph: &IndexedTxGraph<ConfirmationBlockTime, SpkTxOutIndex<()>>,
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
@@ -20,6 +22,222 @@ fn get_balance(
Ok(balance)
}
#[test]
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
let client = BdkElectrumClient::new(electrum_client);
let receive_address0 =
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
let receive_address1 =
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
let misc_spks = [
receive_address0.script_pubkey(),
receive_address1.script_pubkey(),
];
let _block_hashes = env.mine_blocks(101, None)?;
let txid1 = env.bitcoind.client.send_to_address(
&receive_address1,
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
let txid2 = env.bitcoind.client.send_to_address(
&receive_address0,
Amount::from_sat(20000),
None,
None,
None,
None,
Some(1),
None,
)?;
env.mine_blocks(1, None)?;
env.wait_until_electrum_sees_block()?;
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
let sync_update = {
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
client.sync(request, 1, true)?
};
assert!(
{
let update_cps = sync_update
.chain_update
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
let superset_cps = cp_tip
.iter()
.map(|cp| cp.block_id())
.collect::<BTreeSet<_>>();
superset_cps.is_superset(&update_cps)
},
"update should not alter original checkpoint tip since we already started with all checkpoints",
);
let graph_update = sync_update.graph_update;
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
for tx in graph_update.full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
let fee = graph_update.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_unsigned()
.expect("valid `Amount`");
// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
graph_update_txids.sort();
let mut expected_txids = vec![txid1, txid2];
expected_txids.sort();
assert_eq!(graph_update_txids, expected_txids);
Ok(())
}
/// Test the bounds of the address scan depending on the `stop_gap`.
#[test]
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
let client = BdkElectrumClient::new(electrum_client);
let _block_hashes = env.mine_blocks(101, None)?;
// Now let's test the gap limit. First of all get a chain of 10 addresses.
let addresses = [
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
];
let addresses: Vec<_> = addresses
.into_iter()
.map(|s| Address::from_str(s).unwrap().assume_checked())
.collect();
let spks: Vec<_> = addresses
.iter()
.enumerate()
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
.collect();
// Then receive coins on the 4th address.
let txid_4th_addr = env.bitcoind.client.send_to_address(
&addresses[3],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
env.mine_blocks(1, None)?;
env.wait_until_electrum_sees_block()?;
// use a full checkpoint linked list (since this is not what we are testing)
let cp_tip = env.make_checkpoint_tip();
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
// will.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1, false)?
};
assert!(full_scan_update.graph_update.full_txs().next().is_none());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 4, 1, false)?
};
assert_eq!(
full_scan_update
.graph_update
.full_txs()
.next()
.unwrap()
.txid,
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
// Now receive a coin on the last address.
let txid_last_addr = env.bitcoind.client.send_to_address(
&addresses[addresses.len() - 1],
Amount::from_sat(10000),
None,
None,
None,
None,
Some(1),
None,
)?;
env.mine_blocks(1, None)?;
env.wait_until_electrum_sees_block()?;
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
// The last active indice won't be updated in the first case but will in the second one.
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 5, 1, false)?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 3);
let full_scan_update = {
let request =
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
client.full_scan(request, 6, 1, false)?
};
let txs: HashSet<_> = full_scan_update
.graph_update
.full_txs()
.map(|tx| tx.txid)
.collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
assert_eq!(full_scan_update.last_active_indices[&0], 9);
Ok(())
}
/// Ensure that [`ElectrumExt`] can sync properly.
///
/// 1. Mine 101 blocks.
@@ -45,7 +263,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
// Setup receiver.
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
recv_index
@@ -62,14 +280,11 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
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 update = client.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks(core::iter::once(spk_to_track)),
5,
true,
)?;
let _ = recv_chain
.apply_update(update.chain_update)
@@ -138,7 +353,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
// Setup receiver.
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone());
recv_index
@@ -148,20 +363,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
env.mine_blocks(101, Some(addr_to_mine))?;
// Create transactions that are tracked by our receiver.
let mut txids = vec![];
let mut hashes = vec![];
for _ in 0..REORG_COUNT {
env.send(&addr_to_track, SEND_AMOUNT)?;
env.mine_blocks(1, None)?;
txids.push(env.send(&addr_to_track, SEND_AMOUNT)?);
hashes.extend(env.mine_blocks(1, None)?);
}
// Sync up to tip.
env.wait_until_electrum_sees_block()?;
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 update = client.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?;
let _ = recv_chain
.apply_update(update.chain_update)
@@ -170,6 +385,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
// Retain a snapshot of all anchors before reorg process.
let initial_anchors = update.graph_update.all_anchors();
let anchors: Vec<_> = initial_anchors.iter().cloned().collect();
assert_eq!(anchors.len(), REORG_COUNT);
for i in 0..REORG_COUNT {
let (anchor, txid) = anchors[i];
assert_eq!(anchor.block_id.hash, hashes[i]);
assert_eq!(txid, txids[i]);
}
// Check if initial balance is correct.
assert_eq!(
@@ -186,29 +408,24 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
env.reorg_empty_blocks(depth)?;
env.wait_until_electrum_sees_block()?;
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 update = client.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?;
let _ = recv_chain
.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(update.graph_update.all_anchors()) {
println!("New anchor added at reorg depth {}", depth);
}
// Check that no new anchors are added during current reorg.
assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
let _ = recv_graph.apply_update(update.graph_update);
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
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.15.0"
version = "0.16.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.16.0", default-features = false }
bdk_chain = { path = "../chain", version = "0.17.0", default-features = false }
esplora-client = { version = "0.8.0", default-features = false }
async-trait = { version = "0.1.66", optional = true }
futures = { version = "0.3.26", optional = true }

View File

@@ -6,7 +6,7 @@ use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
collections::BTreeMap,
local_chain::CheckPoint,
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
BlockId, ConfirmationBlockTime, TxGraph,
};
use bdk_chain::{Anchor, Indexed};
use esplora_client::{Amount, TxStatus};
@@ -231,7 +231,7 @@ async fn chain_update<A: Anchor>(
}
/// This performs a full scan to get an update for the [`TxGraph`] and
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex).
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
client: &esplora_client::AsyncClient,
keychain_spks: BTreeMap<
@@ -240,10 +240,10 @@ async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
let mut last_active_indexes = BTreeMap::<K, u32>::new();
for (keychain, spks) in keychain_spks {
@@ -333,7 +333,7 @@ async fn sync_for_index_and_graph(
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
let mut graph = full_scan_for_index_and_graph(
client,
[(

View File

@@ -1,13 +1,12 @@
use std::collections::BTreeSet;
use std::thread::JoinHandle;
use std::usize;
use bdk_chain::collections::BTreeMap;
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
use bdk_chain::{
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
local_chain::CheckPoint,
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
BlockId, ConfirmationBlockTime, TxGraph,
};
use bdk_chain::{Anchor, Indexed};
use esplora_client::TxStatus;
@@ -214,16 +213,16 @@ fn chain_update<A: Anchor>(
}
/// This performs a full scan to get an update for the [`TxGraph`] and
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex).
fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
client: &esplora_client::BlockingClient,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = Indexed<ScriptBuf>>>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
let mut tx_graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
let mut last_active_indices = BTreeMap::<K, u32>::new();
for (keychain, spks) in keychain_spks {
@@ -316,7 +315,7 @@ fn sync_for_index_and_graph_blocking(
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking(
client,
{

View File

@@ -16,7 +16,7 @@
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
use bdk_chain::{BlockId, ConfirmationBlockTime};
use esplora_client::TxStatus;
pub use esplora_client;
@@ -31,7 +31,7 @@ mod async_ext;
#[cfg(feature = "async")]
pub use async_ext::*;
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationBlockTime> {
if let TxStatus {
block_height: Some(height),
block_hash: Some(hash),
@@ -39,9 +39,8 @@ fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor>
..
} = status.clone()
{
Some(ConfirmationTimeHeightAnchor {
anchor_block: BlockId { height, hash },
confirmation_height: height,
Some(ConfirmationBlockTime {
block_id: BlockId { height, hash },
confirmation_time: time,
})
} else {

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_file_store"
version = "0.13.0"
version = "0.14.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", features = [ "serde", "miniscript" ] }
bdk_chain = { path = "../chain", version = "0.17.0", features = [ "serde", "miniscript" ] }
bincode = { version = "1" }
serde = { version = "1", features = ["derive"] }

View File

@@ -1,5 +1,5 @@
use crate::{bincode_options, EntryIter, FileError, IterError};
use bdk_chain::Append;
use bdk_chain::Merge;
use bincode::Options;
use std::{
fmt::{self, Debug},
@@ -22,7 +22,7 @@ where
impl<C> Store<C>
where
C: Append
C: Merge
+ serde::Serialize
+ serde::de::DeserializeOwned
+ core::marker::Send
@@ -147,7 +147,7 @@ where
}
};
match &mut changeset {
Some(changeset) => changeset.append(next_changeset),
Some(changeset) => changeset.merge(next_changeset),
changeset => *changeset = Some(next_changeset),
}
}
@@ -365,7 +365,7 @@ mod test {
assert_eq!(
err.changeset,
changesets.iter().cloned().reduce(|mut acc, cs| {
Append::append(&mut acc, cs);
Merge::merge(&mut acc, cs);
acc
}),
"should recover all changesets that are written in full",
@@ -386,7 +386,7 @@ mod test {
.cloned()
.chain(core::iter::once(last_changeset.clone()))
.reduce(|mut acc, cs| {
Append::append(&mut acc, cs);
Merge::merge(&mut acc, cs);
acc
}),
"should recover all changesets",
@@ -422,13 +422,13 @@ mod test {
.take(read_count)
.map(|r| r.expect("must read valid changeset"))
.fold(TestChangeSet::default(), |mut acc, v| {
Append::append(&mut acc, v);
Merge::merge(&mut acc, v);
acc
});
// We write after a short read.
db.append_changeset(&last_changeset)
.expect("last write must succeed");
Append::append(&mut exp_aggregation, last_changeset.clone());
Merge::merge(&mut exp_aggregation, last_changeset.clone());
drop(db);
// We open the file again and check whether aggregate changeset is expected.

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk_hwi"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
@@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0"
readme = "README.md"
[dependencies]
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13" }
bdk_wallet = { path = "../wallet", version = "1.0.0-beta.1" }
hwi = { version = "0.9.0", features = [ "miniscript"] }

View File

@@ -4,11 +4,13 @@
//! used with hardware wallets.
//! ```no_run
//! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::descriptor::Descriptor;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_hwi::HWISigner;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! # use std::str::FromStr;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut devices = HWIClient::enumerate()?;
@@ -18,11 +20,7 @@
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # "",
//! # Network::Testnet,
//! # )?;
//! # let mut wallet = Wallet::create("", "").network(Network::Testnet).create_wallet_no_persist()?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
@@ -35,7 +33,7 @@
//! # }
//! ```
//!
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
//! [`TransactionSigner`]: bdk_wallet::signer::TransactionSigner
mod signer;
pub use signer::*;

View File

@@ -83,7 +83,7 @@ impl TransactionSigner for HWISigner {
// Arc::new(custom_signer),
// );
//
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
// let addr = wallet.get_address(bdk_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,17 +0,0 @@
[package]
name = "bdk_sqlite"
version = "0.2.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 relational database client for persisting bdk_chain data."
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
rusqlite = { version = "0.31.0", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -1,8 +0,0 @@
# BDK SQLite
This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
[SQLite]: https://www.sqlite.org/index.html

View File

@@ -1,69 +0,0 @@
-- 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;

View File

@@ -1,34 +0,0 @@
#![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

@@ -1,96 +0,0 @@
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},
)
}
}

View File

@@ -1,758 +0,0 @@
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::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
};
/// 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)
}
}
/// 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.compute_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 [`CombinedChangeSet`] data.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Write the given `changeset` atomically.
pub 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)
}
/// Read the entire database and return the aggregate [`CombinedChangeSet`].
pub 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::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
ConfirmationTimeHeightAnchor, DescriptorExt,
};
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() {
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(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() {
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(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
#[test]
fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
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(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
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.compute_txid(), 0);
let txout0_0 = tx0.output.first().unwrap().clone();
let outpoint1_0 = OutPoint::new(tx1.compute_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.compute_txid()), (anchor1, tx1.compute_txid())].into(),
last_seen: [
(tx0.compute_txid(), 1598918400),
(tx1.compute_txid(), 1598919121),
(tx2.compute_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.compute_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.compute_txid()), (anchor2, tx1.compute_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.6.0"
version = "0.7.0"
edition = "2021"
rust-version = "1.63"
homepage = "https://bitcoindevkit.org"
@@ -13,7 +13,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.16", default-features = false }
bdk_chain = { path = "../chain", version = "0.17", default-features = false }
electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
[features]

View File

@@ -1,7 +1,7 @@
[package]
name = "bdk_wallet"
homepage = "https://bitcoindevkit.org"
version = "1.0.0-alpha.13"
version = "1.0.0-beta.1"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk"
description = "A modern, lightweight, descriptor-based wallet library"
@@ -13,39 +13,35 @@ edition = "2021"
rust-version = "1.63"
[dependencies]
rand = "^0.8"
rand_core = { version = "0.6.0" }
miniscript = { version = "12.0.0", features = ["serde"], default-features = false }
bitcoin = { version = "0.32.0", features = ["serde", "base64", "rand-std"], default-features = false }
bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
bdk_chain = { path = "../chain", version = "0.17.0", features = ["miniscript", "serde"], default-features = false }
bdk_file_store = { path = "../file_store", version = "0.14.0", optional = true }
# Optional dependencies
bip39 = { version = "2.0", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
js-sys = "0.3"
[features]
default = ["std"]
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
dev-getrandom-wasm = ["getrandom/js"]
rusqlite = ["bdk_chain/rusqlite"]
file_store = ["bdk_file_store"]
[dev-dependencies]
lazy_static = "1.4"
assert_matches = "1.5.0"
tempfile = "3"
bdk_sqlite = { path = "../sqlite" }
bdk_chain = { path = "../chain", features = ["rusqlite"] }
bdk_wallet = { path = ".", features = ["rusqlite", "file_store"] }
bdk_file_store = { path = "../file_store" }
anyhow = "1"
rand = "^0.8"
[package.metadata.docs.rs]
all-features = true

View File

@@ -57,43 +57,45 @@ that the `Wallet` can use to update its view of the chain.
## Persistence
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
To persist `Wallet` state data use a data store crate that reads and writes [`ChangeSet`].
**Implementations**
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
**Example**
<!-- compile_fail because outpoint and txout are fake variables -->
```rust,no_run
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
use bdk_wallet::{bitcoin::Network, KeychainKind, ChangeSet, Wallet};
fn main() {
// Open or create a new file store for wallet data.
let mut db =
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
.expect("create store");
// Open or create a new file store for wallet data.
let mut db =
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
.expect("create store");
// Create a wallet with initial wallet data read from the file store.
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets().expect("changeset loaded");
let mut wallet =
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
.expect("create or load wallet");
// Create a wallet with initial wallet data read from the file store.
let network = Network::Testnet;
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let wallet_opt = Wallet::load()
.descriptors(descriptor, change_descriptor)
.network(network)
.load_wallet(&mut db)
.expect("wallet");
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(descriptor, change_descriptor)
.network(network)
.create_wallet(&mut db)
.expect("wallet"),
};
// Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store.
let staged_changeset = wallet.take_staged();
if let Some(changeset) = staged_changeset {
db.append_changeset(&changeset)
.expect("must commit changes to database");
}
println!("Your new receive address is: {}", receive_address.address);
}
// Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store.
wallet.persist(&mut db).expect("persist");
println!("Your new receive address is: {}", receive_address.address);
```
<!-- ### Sync the balance of a descriptor -->
@@ -124,7 +126,7 @@ fn main() {
<!-- ```rust -->
<!-- use bdk_wallet::Wallet; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bdk_wallet::AddressIndex::New; -->
<!-- use bdk_wallet::bitcoin::Network; -->
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
@@ -149,7 +151,7 @@ fn main() {
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
<!-- use bdk_wallet::electrum_client::Client; -->
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
<!-- use bdk_wallet::AddressIndex::New; -->
<!-- use bitcoin::base64; -->
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
@@ -235,7 +237,6 @@ conditions.
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest

View File

@@ -77,7 +77,9 @@ fn main() -> Result<(), Box<dyn Error>> {
);
// Create a new wallet from descriptors
let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
let mut wallet = Wallet::create(descriptor, internal_descriptor)
.network(Network::Regtest)
.create_wallet_no_persist()?;
println!(
"First derived address from the descriptor: \n{}",

View File

@@ -14,7 +14,7 @@ use std::error::Error;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
use bdk_wallet::wallet::signer::SignersContainer;
use bdk_wallet::signer::SignersContainer;
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
///
@@ -38,7 +38,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// While the `keymap` can be used to create a `SignerContainer`.
//
// The `SignerContainer` can sign for `PSBT`s.
// a bdk_wallet::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

@@ -13,7 +13,7 @@
use core::fmt;
/// Errors related to the parsing and usage of descriptors
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Error {
/// Invalid HD Key path, such as having a wildcard but a length != 1
InvalidHdKeyPath,

View File

@@ -112,6 +112,16 @@ impl IntoWalletDescriptor for &String {
}
}
impl IntoWalletDescriptor for String {
fn into_wallet_descriptor(
self,
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
self.as_str().into_wallet_descriptor(secp, network)
}
}
impl IntoWalletDescriptor for ExtendedDescriptor {
fn into_wallet_descriptor(
self,
@@ -281,15 +291,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
}
}
/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
/// descriptor
pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
inner: T,
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
/// Extra checks for [`ExtendedDescriptor`].
pub(crate) fn check_wallet_descriptor(
descriptor: &Descriptor<DescriptorPublicKey>,
) -> Result<(), DescriptorError> {
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(DescriptorXKey {
@@ -316,7 +321,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
// issues
descriptor.sanity_check()?;
Ok((descriptor, keymap))
Ok(())
}
#[doc(hidden)]
@@ -855,22 +860,31 @@ mod test {
}
#[test]
fn test_into_wallet_descriptor_checked() {
fn test_check_wallet_descriptor() {
let secp = Secp256k1::new();
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert_matches!(result, Err(DescriptorError::MultiPath));
// repeated pubkeys
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert!(result.is_err());
}
@@ -882,8 +896,10 @@ mod test {
let secp = Secp256k1::new();
let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
let (descriptor, _) =
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
check_wallet_descriptor(&descriptor).expect("descriptor");
let descriptor = descriptor.at_derivation_index(0).unwrap();

View File

@@ -21,7 +21,7 @@
//! ```
//! # use std::sync::Arc;
//! # use bdk_wallet::descriptor::*;
//! # use bdk_wallet::wallet::signer::*;
//! # use bdk_wallet::signer::*;
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new();

View File

@@ -81,7 +81,9 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::create(P2Pkh(key_external), P2Pkh(key_internal))
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -91,6 +93,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
@@ -113,11 +116,9 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(
/// P2Wpkh_P2Sh(key_external),
/// P2Wpkh_P2Sh(key_internal),
/// Network::Testnet,
/// )?;
/// let mut wallet = Wallet::create(P2Wpkh_P2Sh(key_external), P2Wpkh_P2Sh(key_internal))
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -128,6 +129,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[allow(non_camel_case_types)]
#[derive(Debug, Clone)]
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
@@ -142,7 +144,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh;
///
@@ -150,7 +152,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::create(P2Wpkh(key_external), P2Wpkh(key_internal))
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -160,6 +164,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
@@ -182,7 +187,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
/// let mut wallet = Wallet::create(P2TR(key_external), P2TR(key_internal))
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -192,6 +199,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// );
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
@@ -208,23 +216,22 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
///
/// ## Example
///
/// ```
/// ```rust
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip44;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// Bip44(key.clone(), KeychainKind::External),
/// Bip44(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// let mut wallet = Wallet::create(Bip44(key.clone(), KeychainKind::External), Bip44(key, KeychainKind::Internal))
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
@@ -247,21 +254,23 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{KeychainKind, Wallet};
/// use bdk_wallet::template::Bip44Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip44Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
@@ -284,20 +293,22 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip49;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip49(key.clone(), KeychainKind::External),
/// Bip49(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
@@ -320,21 +331,23 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # 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")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip49Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
@@ -357,20 +370,22 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip84;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip84(key.clone(), KeychainKind::External),
/// Bip84(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
@@ -393,21 +408,23 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # 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")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip84Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
@@ -430,20 +447,22 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// use bdk_wallet::template::Bip86;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip86(key.clone(), KeychainKind::External),
/// Bip86(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
@@ -466,21 +485,23 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # 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")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = Wallet::create(
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip86Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )
/// .network(Network::Testnet)
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {

View File

@@ -20,6 +20,8 @@ use core::marker::PhantomData;
use core::ops::Deref;
use core::str::FromStr;
use rand_core::{CryptoRng, RngCore};
use bitcoin::secp256k1::{self, Secp256k1, Signing};
use bitcoin::bip32;
@@ -631,12 +633,23 @@ pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
entropy: Self::Entropy,
) -> Result<GeneratedKey<Self, Ctx>, Self::Error>;
/// Generate a key given the options with a random entropy
/// Generate a key given the options with random entropy.
///
/// Uses the thread-local random number generator.
#[cfg(feature = "std")]
fn generate(options: Self::Options) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
use rand::{thread_rng, Rng};
Self::generate_with_aux_rand(options, &mut bitcoin::key::rand::thread_rng())
}
/// Generate a key given the options with random entropy.
///
/// Uses a provided random number generator (rng).
fn generate_with_aux_rand(
options: Self::Options,
rng: &mut (impl CryptoRng + RngCore),
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
let mut entropy = Self::Entropy::default();
thread_rng().fill(entropy.as_mut());
rng.fill_bytes(entropy.as_mut());
Self::generate_with_entropy(options, entropy)
}
}
@@ -657,8 +670,20 @@ where
}
/// Generate a key with the default options and a random entropy
///
/// Uses the thread-local random number generator.
#[cfg(feature = "std")]
fn generate_default() -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
Self::generate(Default::default())
Self::generate_with_aux_rand(Default::default(), &mut bitcoin::key::rand::thread_rng())
}
/// Generate a key with the default options and a random entropy
///
/// Uses a provided random number generator (rng).
fn generate_default_with_aux_rand(
rng: &mut (impl CryptoRng + RngCore),
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
Self::generate_with_aux_rand(Default::default(), rng)
}
}
@@ -910,7 +935,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
}
/// Errors thrown while working with [`keys`](crate::keys)
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum KeyError {
/// The key cannot exist in the given script context
InvalidScriptContext,

View File

@@ -15,33 +15,36 @@ extern crate std;
#[doc(hidden)]
#[macro_use]
pub extern crate alloc;
pub extern crate bdk_chain as chain;
#[cfg(feature = "file_store")]
pub extern crate bdk_file_store as file_store;
#[cfg(feature = "keys-bip39")]
pub extern crate bip39;
pub extern crate bitcoin;
pub extern crate miniscript;
extern crate serde;
extern crate serde_json;
#[cfg(feature = "keys-bip39")]
extern crate bip39;
pub extern crate serde;
pub extern crate serde_json;
pub mod descriptor;
pub mod keys;
pub mod psbt;
pub(crate) mod types;
pub mod wallet;
mod types;
mod wallet;
pub(crate) use bdk_chain::collections;
#[cfg(feature = "rusqlite")]
pub use bdk_chain::rusqlite;
#[cfg(feature = "rusqlite")]
pub use bdk_chain::rusqlite_impl;
pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use signer;
pub use signer::SignOptions;
pub use tx_builder::*;
pub use types::*;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::Wallet;
pub use wallet::*;
/// Get the version of BDK at runtime
/// Get the version of [`bdk_wallet`](crate) at runtime.
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown")
}
pub use bdk_chain as chain;
pub(crate) use bdk_chain::collections;

View File

@@ -13,8 +13,8 @@ use alloc::boxed::Box;
use core::convert::AsRef;
use bdk_chain::ConfirmationTime;
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::psbt;
use bitcoin::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::{psbt, Weight};
use serde::{Deserialize, Serialize};
@@ -72,7 +72,7 @@ pub struct WeightedUtxo {
/// properly maintain the feerate when adding this input to a transaction during coin selection.
///
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
pub satisfaction_weight: usize,
pub satisfaction_weight: Weight,
/// The UTXO
pub utxo: Utxo,
}

View File

@@ -0,0 +1,209 @@
use bdk_chain::{
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge,
};
use miniscript::{Descriptor, DescriptorPublicKey};
type IndexedTxGraphChangeSet =
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>;
/// A changeset for [`Wallet`](crate::Wallet).
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub struct ChangeSet {
/// Descriptor for recipient addresses.
pub descriptor: Option<Descriptor<DescriptorPublicKey>>,
/// Descriptor for change addresses.
pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
/// Changes to the [`LocalChain`](local_chain::LocalChain).
pub local_chain: local_chain::ChangeSet,
/// Changes to [`TxGraph`](tx_graph::TxGraph).
pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
/// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
pub indexer: keychain_txout::ChangeSet,
}
impl Merge for ChangeSet {
/// Merge another [`ChangeSet`] into itself.
fn merge(&mut self, other: Self) {
if other.descriptor.is_some() {
debug_assert!(
self.descriptor.is_none() || self.descriptor == other.descriptor,
"descriptor must never change"
);
self.descriptor = other.descriptor;
}
if other.change_descriptor.is_some() {
debug_assert!(
self.change_descriptor.is_none()
|| self.change_descriptor == other.change_descriptor,
"change descriptor must never change"
);
self.change_descriptor = other.change_descriptor;
}
if other.network.is_some() {
debug_assert!(
self.network.is_none() || self.network == other.network,
"network must never change"
);
self.network = other.network;
}
Merge::merge(&mut self.local_chain, other.local_chain);
Merge::merge(&mut self.tx_graph, other.tx_graph);
Merge::merge(&mut self.indexer, other.indexer);
}
fn is_empty(&self) -> bool {
self.descriptor.is_none()
&& self.change_descriptor.is_none()
&& self.network.is_none()
&& self.local_chain.is_empty()
&& self.tx_graph.is_empty()
&& self.indexer.is_empty()
}
}
#[cfg(feature = "rusqlite")]
impl ChangeSet {
/// Schema name for wallet.
pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet descriptors and network.
pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
/// Initialize sqlite tables for wallet schema & table.
fn init_wallet_sqlite_tables(
db_tx: &chain::rusqlite::Transaction,
) -> chain::rusqlite::Result<()> {
let schema_v0: &[&str] = &[&format!(
"CREATE TABLE {} ( \
id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \
descriptor TEXT, \
change_descriptor TEXT, \
network TEXT \
) STRICT;",
Self::WALLET_TABLE_NAME,
)];
crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])
}
/// Recover a [`ChangeSet`] from sqlite database.
pub fn from_sqlite(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<Self> {
Self::init_wallet_sqlite_tables(db_tx)?;
use chain::rusqlite::OptionalExtension;
use chain::Impl;
let mut changeset = Self::default();
let mut wallet_statement = db_tx.prepare(&format!(
"SELECT descriptor, change_descriptor, network FROM {}",
Self::WALLET_TABLE_NAME,
))?;
let row = wallet_statement
.query_row([], |row| {
Ok((
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("descriptor")?,
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("change_descriptor")?,
row.get::<_, Impl<bitcoin::Network>>("network")?,
))
})
.optional()?;
if let Some((Impl(desc), Impl(change_desc), Impl(network))) = row {
changeset.descriptor = Some(desc);
changeset.change_descriptor = Some(change_desc);
changeset.network = Some(network);
}
changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?;
Ok(changeset)
}
/// Persist [`ChangeSet`] to sqlite database.
pub fn persist_to_sqlite(
&self,
db_tx: &chain::rusqlite::Transaction,
) -> chain::rusqlite::Result<()> {
Self::init_wallet_sqlite_tables(db_tx)?;
use chain::rusqlite::named_params;
use chain::Impl;
let mut descriptor_statement = db_tx.prepare_cached(&format!(
"INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor",
Self::WALLET_TABLE_NAME,
))?;
if let Some(descriptor) = &self.descriptor {
descriptor_statement.execute(named_params! {
":id": 0,
":descriptor": Impl(descriptor.clone()),
})?;
}
let mut change_descriptor_statement = db_tx.prepare_cached(&format!(
"INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor",
Self::WALLET_TABLE_NAME,
))?;
if let Some(change_descriptor) = &self.change_descriptor {
change_descriptor_statement.execute(named_params! {
":id": 0,
":change_descriptor": Impl(change_descriptor.clone()),
})?;
}
let mut network_statement = db_tx.prepare_cached(&format!(
"INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network",
Self::WALLET_TABLE_NAME,
))?;
if let Some(network) = self.network {
network_statement.execute(named_params! {
":id": 0,
":network": Impl(network),
})?;
}
self.local_chain.persist_to_sqlite(db_tx)?;
self.tx_graph.persist_to_sqlite(db_tx)?;
self.indexer.persist_to_sqlite(db_tx)?;
Ok(())
}
}
impl From<local_chain::ChangeSet> for ChangeSet {
fn from(chain: local_chain::ChangeSet) -> Self {
Self {
local_chain: chain,
..Default::default()
}
}
}
impl From<IndexedTxGraphChangeSet> for ChangeSet {
fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self {
Self {
tx_graph: indexed_tx_graph.tx_graph,
indexer: indexed_tx_graph.indexer,
..Default::default()
}
}
}
impl From<tx_graph::ChangeSet<ConfirmationBlockTime>> for ChangeSet {
fn from(tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>) -> Self {
Self {
tx_graph,
..Default::default()
}
}
}
impl From<keychain_txout::ChangeSet> for ChangeSet {
fn from(indexer: keychain_txout::ChangeSet) -> Self {
Self {
indexer,
..Default::default()
}
}
}

View File

@@ -26,10 +26,10 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk_wallet::{self, ChangeSet, coin_selection::*, coin_selection};
//! # use bdk_wallet::error::CreateTxError;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::coin_selection::decide_change;
//! # use bdk_wallet::coin_selection::decide_change;
//! # use anyhow::Error;
//! #[derive(Debug)]
//! struct AlwaysSpendEverything;
@@ -52,11 +52,10 @@
//! (&mut selected_amount, &mut additional_weight),
//! |(selected_amount, additional_weight), weighted_utxo| {
//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
//! **additional_weight += Weight::from_wu(
//! (TxIn::default().segwit_weight().to_wu()
//! + weighted_utxo.satisfaction_weight as u64)
//! as u64,
//! );
//! **additional_weight += TxIn::default()
//! .segwit_weight()
//! .checked_add(weighted_utxo.satisfaction_weight)
//! .expect("`Weight` addition should not cause an integer overflow");
//! Some(weighted_utxo.utxo)
//! },
//! )
@@ -114,8 +113,9 @@ use bitcoin::{Script, Weight};
use core::convert::TryInto;
use core::fmt::{self, Formatter};
use rand::seq::SliceRandom;
use rand_core::RngCore;
use super::utils::shuffle_slice;
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
/// overridden
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
@@ -343,10 +343,10 @@ fn select_sorted_utxos(
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
if must_use || **selected_amount < target_amount + **fee_amount {
**fee_amount += (fee_rate
* Weight::from_wu(
TxIn::default().segwit_weight().to_wu()
+ weighted_utxo.satisfaction_weight as u64,
))
* (TxIn::default()
.segwit_weight()
.checked_add(weighted_utxo.satisfaction_weight)
.expect("`Weight` addition should not cause an integer overflow")))
.to_sat();
**selected_amount += weighted_utxo.utxo.txout().value.to_sat();
Some(weighted_utxo.utxo)
@@ -389,9 +389,10 @@ struct OutputGroup {
impl OutputGroup {
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
let fee = (fee_rate
* Weight::from_wu(
TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
))
* (TxIn::default()
.segwit_weight()
.checked_add(weighted_utxo.satisfaction_weight)
.expect("`Weight` addition should not cause an integer overflow")))
.to_sat();
let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
OutputGroup {
@@ -516,27 +517,16 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
));
}
Ok(self
.bnb(
required_utxos.clone(),
optional_utxos.clone(),
curr_value,
curr_available_value,
target_amount,
cost_of_change,
drain_script,
fee_rate,
)
.unwrap_or_else(|_| {
self.single_random_draw(
required_utxos,
optional_utxos,
curr_value,
target_amount,
drain_script,
fee_rate,
)
}))
self.bnb(
required_utxos.clone(),
optional_utxos.clone(),
curr_value,
curr_available_value,
target_amount,
cost_of_change,
drain_script,
fee_rate,
)
}
}
@@ -663,40 +653,6 @@ impl BranchAndBoundCoinSelection {
))
}
#[allow(clippy::too_many_arguments)]
fn single_random_draw(
&self,
required_utxos: Vec<OutputGroup>,
mut optional_utxos: Vec<OutputGroup>,
curr_value: i64,
target_amount: i64,
drain_script: &Script,
fee_rate: FeeRate,
) -> CoinSelectionResult {
optional_utxos.shuffle(&mut rand::thread_rng());
let selected_utxos = optional_utxos.into_iter().fold(
(curr_value, vec![]),
|(mut amount, mut utxos), utxo| {
if amount >= target_amount {
(amount, utxos)
} else {
amount += utxo.effective_value;
utxos.push(utxo);
(amount, utxos)
}
},
);
// remaining_amount can't be negative as that would mean the
// selection wasn't successful
// target_amount = amount_needed + (fee_amount - vin_fees)
let remaining_amount = (selected_utxos.0 - target_amount) as u64;
let excess = decide_change(remaining_amount, fee_rate, drain_script);
BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
}
fn calculate_cs_result(
mut selected_utxos: Vec<OutputGroup>,
mut required_utxos: Vec<OutputGroup>,
@@ -717,6 +673,58 @@ impl BranchAndBoundCoinSelection {
}
}
// Pull UTXOs at random until we have enough to meet the target
pub(crate) fn single_random_draw(
required_utxos: Vec<WeightedUtxo>,
optional_utxos: Vec<WeightedUtxo>,
target_amount: u64,
drain_script: &Script,
fee_rate: FeeRate,
rng: &mut impl RngCore,
) -> CoinSelectionResult {
let target_amount = target_amount
.try_into()
.expect("Bitcoin amount to fit into i64");
let required_utxos: Vec<OutputGroup> = required_utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
.collect();
let mut optional_utxos: Vec<OutputGroup> = optional_utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
.collect();
let curr_value = required_utxos
.iter()
.fold(0, |acc, x| acc + x.effective_value);
shuffle_slice(&mut optional_utxos, rng);
let selected_utxos =
optional_utxos
.into_iter()
.fold((curr_value, vec![]), |(mut amount, mut utxos), utxo| {
if amount >= target_amount {
(amount, utxos)
} else {
amount += utxo.effective_value;
utxos.push(utxo);
(amount, utxos)
}
});
// remaining_amount can't be negative as that would mean the
// selection wasn't successful
// target_amount = amount_needed + (fee_amount - vin_fees)
let remaining_amount = (selected_utxos.0 - target_amount) as u64;
let excess = decide_change(remaining_amount, fee_rate, drain_script);
BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
}
/// Remove duplicate UTXOs.
///
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
@@ -740,6 +748,7 @@ where
mod test {
use assert_matches::assert_matches;
use core::str::FromStr;
use rand::rngs::StdRng;
use bdk_chain::ConfirmationTime;
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
@@ -748,8 +757,7 @@ mod test {
use crate::types::*;
use crate::wallet::coin_selection::filter_duplicates;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rand::prelude::SliceRandom;
use rand::{Rng, RngCore, SeedableRng};
// signature len (1WU) + signature and sighash (72WU)
@@ -766,7 +774,7 @@ mod test {
))
.unwrap();
WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
utxo: Utxo::Local(LocalOutput {
outpoint,
txout: TxOut {
@@ -826,7 +834,7 @@ mod test {
let mut res = Vec::new();
for i in 0..utxos_number {
res.push(WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str(&format!(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
@@ -857,7 +865,7 @@ mod test {
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
(0..utxos_number)
.map(|i| WeightedUtxo {
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::from_str(&format!(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
@@ -1090,13 +1098,12 @@ mod test {
}
#[test]
#[ignore = "SRD fn was moved out of BnB"]
fn test_bnb_coin_selection_success() {
// In this case bnb won't find a suitable match and single random draw will
// select three outputs
let utxos = generate_same_value_utxos(100_000, 20);
let drain_script = ScriptBuf::default();
let target_amount = 250_000 + FEE_AMOUNT;
let result = BranchAndBoundCoinSelection::default()
@@ -1136,6 +1143,7 @@ mod test {
}
#[test]
#[ignore = "no exact match for bnb, previously fell back to SRD"]
fn test_bnb_coin_selection_optional_are_enough() {
let utxos = get_test_utxos();
let drain_script = ScriptBuf::default();
@@ -1156,6 +1164,26 @@ mod test {
assert_eq!(result.fee_amount, 136);
}
#[test]
fn test_single_random_draw_function_success() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut utxos = generate_random_utxos(&mut rng, 300);
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
let drain_script = ScriptBuf::default();
let result = single_random_draw(
vec![],
utxos,
target_amount,
&drain_script,
fee_rate,
&mut rng,
);
assert!(result.selected_amount() > target_amount);
assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
}
#[test]
#[ignore]
fn test_bnb_coin_selection_required_not_enough() {
@@ -1410,34 +1438,6 @@ mod test {
}
}
#[test]
fn test_single_random_draw_function_success() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut utxos = generate_random_utxos(&mut rng, 300);
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
let utxos: Vec<OutputGroup> = utxos
.into_iter()
.map(|u| OutputGroup::new(u, fee_rate))
.collect();
let drain_script = ScriptBuf::default();
let result = BranchAndBoundCoinSelection::default().single_random_draw(
vec![],
utxos,
0,
target_amount as i64,
&drain_script,
fee_rate,
);
assert!(result.selected_amount() > target_amount);
assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
}
#[test]
fn test_bnb_exclude_negative_effective_value() {
let utxos = get_test_utxos();
@@ -1512,7 +1512,7 @@ mod test {
fn test_filter_duplicates() {
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
WeightedUtxo {
satisfaction_weight: 0,
satisfaction_weight: Weight::ZERO,
utxo: Utxo::Local(LocalOutput {
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
txout: TxOut {

View File

@@ -20,7 +20,7 @@
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::export::*;
//! # use bdk_wallet::*;
//! let import = r#"{
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
@@ -29,24 +29,26 @@
//! }"#;
//!
//! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new(
//! &import.descriptor(),
//! &import.change_descriptor().expect("change descriptor"),
//! Network::Testnet,
//! )?;
//! let wallet = Wallet::create(
//! import.descriptor(),
//! import.change_descriptor().expect("change descriptor"),
//! )
//! .network(Network::Testnet)
//! .create_wallet_no_persist()?;
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
//!
//! ### Export a `Wallet`
//! ```
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::export::*;
//! # use bdk_wallet::*;
//! let wallet = Wallet::new(
//! let wallet = Wallet::create(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
//! Network::Testnet,
//! )?;
//! )
//! .network(Network::Testnet)
//! .create_wallet_no_persist()?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
//!
//! println!("Exported: {}", export.to_string());
@@ -116,7 +118,7 @@ impl FullyNodedExport {
include_blockheight: bool,
) -> Result<Self, &'static str> {
let descriptor = wallet
.get_descriptor_for_keychain(KeychainKind::External)
.public_descriptor(KeychainKind::External)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::External)
@@ -128,7 +130,7 @@ impl FullyNodedExport {
let blockheight = if include_blockheight {
wallet.transactions().next().map_or(0, |canonical_tx| {
match canonical_tx.chain_position {
bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height,
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
}
})
@@ -144,7 +146,7 @@ impl FullyNodedExport {
let change_descriptor = {
let descriptor = wallet
.get_descriptor_for_keychain(KeychainKind::Internal)
.public_descriptor(KeychainKind::Internal)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::Internal)
@@ -214,35 +216,50 @@ mod test {
use core::str::FromStr;
use crate::std::string::ToString;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_chain::{BlockId, ConfirmationBlockTime};
use bitcoin::hashes::Hash;
use bitcoin::{transaction, BlockHash, Network, Transaction};
use super::*;
use crate::wallet::Wallet;
use crate::Wallet;
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
use crate::wallet::Update;
use bdk_chain::TxGraph;
let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string())
.network(network)
.create_wallet_no_persist()
.expect("must create wallet");
let transaction = Transaction {
input: vec![],
output: vec![],
version: transaction::Version::non_standard(0),
lock_time: bitcoin::absolute::LockTime::ZERO,
};
let txid = transaction.compute_txid();
let block_id = BlockId {
height: 5000,
hash: BlockHash::all_zeros(),
};
wallet.insert_checkpoint(block_id).unwrap();
wallet
.insert_checkpoint(BlockId {
height: 5001,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet.insert_tx(transaction);
let anchor = ConfirmationBlockTime {
confirmation_time: 0,
block_id,
};
let mut graph = TxGraph::default();
let _ = graph.insert_anchor(txid, anchor);
wallet
.insert_tx(
transaction,
ConfirmationTime::Confirmed {
height: 5000,
time: 0,
},
)
.apply_update(Update {
graph,
..Default::default()
})
.unwrap();
wallet
}

View File

@@ -16,9 +16,9 @@
//! ```no_run
//! # 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 bdk_wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::AddressIndex::New;
//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
@@ -30,11 +30,7 @@
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # None,
//! # Network::Testnet,
//! # )?;
//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,213 @@
use alloc::boxed::Box;
use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
use bitcoin::{BlockHash, Network};
use miniscript::descriptor::KeyMap;
use crate::{
descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
utils::SecpCtx,
KeychainKind, Wallet,
};
use super::{ChangeSet, LoadError, PersistedWallet};
/// This atrocity is to avoid having type parameters on [`CreateParams`] and [`LoadParams`].
///
/// The better option would be to do `Box<dyn IntoWalletDescriptor>`, but we cannot due to Rust's
/// [object safety rules](https://doc.rust-lang.org/reference/items/traits.html#object-safety).
type DescriptorToExtract = Box<
dyn FnOnce(&SecpCtx, Network) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>
+ 'static,
>;
fn make_descriptor_to_extract<D>(descriptor: D) -> DescriptorToExtract
where
D: IntoWalletDescriptor + 'static,
{
Box::new(|secp, network| descriptor.into_wallet_descriptor(secp, network))
}
/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
#[must_use]
pub struct CreateParams {
pub(crate) descriptor: DescriptorToExtract,
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor: DescriptorToExtract,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) network: Network,
pub(crate) genesis_hash: Option<BlockHash>,
pub(crate) lookahead: u32,
}
impl CreateParams {
/// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
///
/// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new<D: IntoWalletDescriptor + 'static>(descriptor: D, change_descriptor: D) -> Self {
Self {
descriptor: make_descriptor_to_extract(descriptor),
descriptor_keymap: KeyMap::default(),
change_descriptor: make_descriptor_to_extract(change_descriptor),
change_descriptor_keymap: KeyMap::default(),
network: Network::Bitcoin,
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
}
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Set `network`.
pub fn network(mut self, network: Network) -> Self {
self.network = network;
self
}
/// Use a custom `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Create [`PersistedWallet`] with the given `Db`.
pub fn create_wallet<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
where
Wallet: PersistWith<Db, CreateParams = Self>,
{
PersistedWallet::create(db, self)
}
/// Create [`PersistedWallet`] with the given async `Db`.
pub async fn create_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
where
Wallet: PersistAsyncWith<Db, CreateParams = Self>,
{
PersistedWallet::create_async(db, self).await
}
/// Create [`Wallet`] without persistence.
pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
Wallet::create_with_params(self)
}
}
/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
#[must_use]
pub struct LoadParams {
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) lookahead: u32,
pub(crate) check_network: Option<Network>,
pub(crate) check_genesis_hash: Option<BlockHash>,
pub(crate) check_descriptor: Option<DescriptorToExtract>,
pub(crate) check_change_descriptor: Option<DescriptorToExtract>,
}
impl LoadParams {
/// Construct parameters with default values.
///
/// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new() -> Self {
Self {
descriptor_keymap: KeyMap::default(),
change_descriptor_keymap: KeyMap::default(),
lookahead: DEFAULT_LOOKAHEAD,
check_network: None,
check_genesis_hash: None,
check_descriptor: None,
check_change_descriptor: None,
}
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Checks that `descriptor` of `keychain` matches this, and extracts private keys (if
/// available).
pub fn descriptors<D>(mut self, descriptor: D, change_descriptor: D) -> Self
where
D: IntoWalletDescriptor + 'static,
{
self.check_descriptor = Some(make_descriptor_to_extract(descriptor));
self.check_change_descriptor = Some(make_descriptor_to_extract(change_descriptor));
self
}
/// Check for `network`.
pub fn network(mut self, network: Network) -> Self {
self.check_network = Some(network);
self
}
/// Check for a `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.check_genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Load [`PersistedWallet`] with the given `Db`.
pub fn load_wallet<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
where
Wallet: PersistWith<Db, LoadParams = Self>,
{
PersistedWallet::load(db, self)
}
/// Load [`PersistedWallet`] with the given async `Db`.
pub async fn load_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
where
Wallet: PersistAsyncWith<Db, LoadParams = Self>,
{
PersistedWallet::load_async(db, self).await
}
/// Load [`Wallet`] without persistence.
pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
Wallet::load_with_params(changeset, self)
}
}
impl Default for LoadParams {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,171 @@
use core::fmt;
use crate::{descriptor::DescriptorError, Wallet};
/// Represents a persisted wallet.
pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
#[cfg(feature = "rusqlite")]
impl<'c> chain::PersistWith<bdk_chain::rusqlite::Transaction<'c>> for Wallet {
type CreateParams = crate::CreateParams;
type LoadParams = crate::LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::rusqlite::Transaction<'c>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
changeset
.persist_to_sqlite(db)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
conn: &mut bdk_chain::rusqlite::Transaction<'c>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset =
crate::ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
if chain::Merge::is_empty(&changeset) {
return Ok(None);
}
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
db: &mut bdk_chain::rusqlite::Transaction<'c>,
changeset: &<Self as chain::Staged>::ChangeSet,
) -> Result<(), Self::PersistError> {
changeset.persist_to_sqlite(db)
}
}
#[cfg(feature = "rusqlite")]
impl chain::PersistWith<bdk_chain::rusqlite::Connection> for Wallet {
type CreateParams = crate::CreateParams;
type LoadParams = crate::LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::rusqlite::Connection,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
let wallet = chain::PersistWith::create(&mut db_tx, params)?;
db_tx.commit().map_err(CreateWithPersistError::Persist)?;
Ok(wallet)
}
fn load(
db: &mut bdk_chain::rusqlite::Connection,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
let wallet_opt = chain::PersistWith::load(&mut db_tx, params)?;
db_tx.commit().map_err(LoadWithPersistError::Persist)?;
Ok(wallet_opt)
}
fn persist(
db: &mut bdk_chain::rusqlite::Connection,
changeset: &<Self as chain::Staged>::ChangeSet,
) -> Result<(), Self::PersistError> {
let db_tx = db.transaction()?;
changeset.persist_to_sqlite(&db_tx)?;
db_tx.commit()
}
}
#[cfg(feature = "file_store")]
impl chain::PersistWith<bdk_file_store::Store<crate::ChangeSet>> for Wallet {
type CreateParams = crate::CreateParams;
type LoadParams = crate::LoadParams;
type CreateError = CreateWithPersistError<std::io::Error>;
type LoadError =
LoadWithPersistError<bdk_file_store::AggregateChangesetsError<crate::ChangeSet>>;
type PersistError = std::io::Error;
fn create(
db: &mut bdk_file_store::Store<crate::ChangeSet>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
db: &mut bdk_file_store::Store<crate::ChangeSet>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset = db
.aggregate_changesets()
.map_err(LoadWithPersistError::Persist)?
.unwrap_or_default();
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
db: &mut bdk_file_store::Store<crate::ChangeSet>,
changeset: &<Self as chain::Staged>::ChangeSet,
) -> Result<(), Self::PersistError> {
db.append_changeset(changeset)
}
}
/// Error type for [`PersistedWallet::load`].
#[derive(Debug, PartialEq)]
pub enum LoadWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot construct [`Wallet`].
InvalidChangeSet(crate::LoadError),
}
impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
/// Error type for [`PersistedWallet::create`].
#[derive(Debug)]
pub enum CreateWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot construct [`Wallet`].
Descriptor(DescriptorError),
}
impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::Descriptor(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}

View File

@@ -69,7 +69,9 @@
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
//! let mut wallet = Wallet::create(descriptor, change_descriptor)
//! .network(Network::Testnet)
//! .create_wallet_no_persist()?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
@@ -91,7 +93,7 @@ use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
use bitcoin::hashes::hash160;
use bitcoin::secp256k1::Message;
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
use bitcoin::{ecdsa, psbt, sighash, taproot, transaction};
use bitcoin::{ecdsa, psbt, sighash, taproot};
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
use bitcoin::{PrivateKey, Psbt, PublicKey};
@@ -99,7 +101,7 @@ use miniscript::descriptor::{
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
InnerXKey, KeyMap, SinglePriv, SinglePubKey,
};
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use miniscript::{SigType, ToPublicKey};
use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils};
@@ -159,12 +161,10 @@ pub enum SignerError {
NonStandardSighash,
/// Invalid SIGHASH for the signing context in use
InvalidSighash,
/// Error while computing the hash to sign a P2WPKH input.
SighashP2wpkh(sighash::P2wpkhError),
/// Error while computing the hash to sign a Taproot input.
SighashTaproot(sighash::TaprootError),
/// Error while computing the hash, out of bounds access on the transaction inputs.
TxInputsIndexError(transaction::InputsIndexError),
/// PSBT sign error.
Psbt(psbt::SignError),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// To be used only by external libraries implementing [`InputSigner`] or
@@ -173,24 +173,6 @@ pub enum SignerError {
External(String),
}
impl From<transaction::InputsIndexError> for SignerError {
fn from(v: transaction::InputsIndexError) -> Self {
Self::TxInputsIndexError(v)
}
}
impl From<sighash::P2wpkhError> for SignerError {
fn from(e: sighash::P2wpkhError) -> Self {
Self::SighashP2wpkh(e)
}
}
impl From<sighash::TaprootError> for SignerError {
fn from(e: sighash::TaprootError) -> Self {
Self::SighashTaproot(e)
}
}
impl fmt::Display for SignerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -205,9 +187,8 @@ impl fmt::Display for SignerError {
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
Self::SighashP2wpkh(err) => write!(f, "Error while computing the hash to sign a P2WPKH input: {}", err),
Self::SighashTaproot(err) => write!(f, "Error while computing the hash to sign a Taproot input: {}", err),
Self::TxInputsIndexError(err) => write!(f, "Error while computing the hash, out of bounds access on the transaction inputs: {}", err),
Self::Psbt(err) => write!(f, "Error computing the sighash: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
Self::External(err) => write!(f, "{}", err),
}
@@ -472,93 +453,88 @@ impl InputSigner for SignerWrapper<PrivateKey> {
}
let pubkey = PublicKey::from_private_key(secp, self);
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
if let SignerContext::Tap { is_internal_key } = self.ctx {
if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
if is_internal_key
&& psbt.inputs[input_index].tap_key_sig.is_none()
&& sign_options.sign_with_tap_internal_key
&& x_only_pubkey == psbt_internal_key
match self.ctx {
SignerContext::Tap { is_internal_key } => {
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
if is_internal_key
&& psbt.inputs[input_index].tap_key_sig.is_none()
&& sign_options.sign_with_tap_internal_key
&& x_only_pubkey == psbt_internal_key
{
let (sighash, sighash_type) = compute_tap_sighash(psbt, input_index, None)?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
None,
&mut psbt.inputs[input_index],
sighash,
sighash_type,
secp,
);
}
}
if let Some((leaf_hashes, _)) =
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
{
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
None,
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
);
let leaf_hashes = leaf_hashes
.iter()
.filter(|lh| {
// Removing the leaves we shouldn't sign for
let should_sign = match &sign_options.tap_leaves_options {
TapLeavesOptions::All => true,
TapLeavesOptions::Include(v) => v.contains(lh),
TapLeavesOptions::Exclude(v) => !v.contains(lh),
TapLeavesOptions::None => false,
};
// Filtering out the leaves without our key
should_sign
&& !psbt.inputs[input_index]
.tap_script_sigs
.contains_key(&(x_only_pubkey, **lh))
})
.cloned()
.collect::<Vec<_>>();
for lh in leaf_hashes {
let (sighash, sighash_type) =
compute_tap_sighash(psbt, input_index, Some(lh))?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
Some(lh),
&mut psbt.inputs[input_index],
sighash,
sighash_type,
secp,
);
}
}
}
if let Some((leaf_hashes, _)) =
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
{
let leaf_hashes = leaf_hashes
.iter()
.filter(|lh| {
// Removing the leaves we shouldn't sign for
let should_sign = match &sign_options.tap_leaves_options {
TapLeavesOptions::All => true,
TapLeavesOptions::Include(v) => v.contains(lh),
TapLeavesOptions::Exclude(v) => !v.contains(lh),
TapLeavesOptions::None => false,
};
// Filtering out the leaves without our key
should_sign
&& !psbt.inputs[input_index]
.tap_script_sigs
.contains_key(&(x_only_pubkey, **lh))
})
.cloned()
.collect::<Vec<_>>();
for lh in leaf_hashes {
let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
sign_psbt_schnorr(
&self.inner,
x_only_pubkey,
Some(lh),
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
);
SignerContext::Segwitv0 | SignerContext::Legacy => {
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(());
}
}
return Ok(());
let mut sighasher = sighash::SighashCache::new(psbt.unsigned_tx.clone());
let (msg, sighash_type) = psbt
.sighash_ecdsa(input_index, &mut sighasher)
.map_err(SignerError::Psbt)?;
sign_psbt_ecdsa(
&self.inner,
pubkey,
&mut psbt.inputs[input_index],
&msg,
sighash_type,
secp,
sign_options.allow_grinding,
);
}
}
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(());
}
let (hash, hash_ty) = match self.ctx {
SignerContext::Segwitv0 => {
let (h, t) = Segwitv0::sighash(psbt, input_index, ())?;
let h = h.to_raw_hash();
(h, t)
}
SignerContext::Legacy => {
let (h, t) = Legacy::sighash(psbt, input_index, ())?;
let h = h.to_raw_hash();
(h, t)
}
_ => return Ok(()), // handled above
};
sign_psbt_ecdsa(
&self.inner,
pubkey,
&mut psbt.inputs[input_index],
hash,
hash_ty,
secp,
sign_options.allow_grinding,
);
Ok(())
}
}
@@ -567,12 +543,11 @@ fn sign_psbt_ecdsa(
secret_key: &secp256k1::SecretKey,
pubkey: PublicKey,
psbt_input: &mut psbt::Input,
hash: impl bitcoin::hashes::Hash<Bytes = [u8; 32]>,
msg: &Message,
sighash_type: EcdsaSighashType,
secp: &SecpCtx,
allow_grinding: bool,
) {
let msg = &Message::from_digest(hash.to_byte_array());
let signature = if allow_grinding {
secp.sign_ecdsa_low_r(msg, secret_key)
} else {
@@ -594,7 +569,7 @@ fn sign_psbt_schnorr(
pubkey: XOnlyPublicKey,
leaf_hash: Option<taproot::TapLeafHash>,
psbt_input: &mut psbt::Input,
hash: TapSighash,
sighash: TapSighash,
sighash_type: TapSighashType,
secp: &SecpCtx,
) {
@@ -606,8 +581,8 @@ fn sign_psbt_schnorr(
Some(_) => keypair, // no tweak for script spend
};
let msg = &Message::from(hash);
let signature = secp.sign_schnorr(msg, &keypair);
let msg = &Message::from(sighash);
let signature = secp.sign_schnorr_no_aux_rand(msg, &keypair);
secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
.expect("invalid or corrupted schnorr signature");
@@ -801,21 +776,6 @@ pub struct SignOptions {
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
pub allow_all_sighashes: bool,
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
///
/// Defaults to `true` which will remove partial signatures during finalization.
pub remove_partial_sigs: bool,
/// Whether to remove taproot specific fields from the PSBT on finalization.
///
/// For inputs this includes the taproot internal key, merkle root, and individual
/// scripts and signatures. For both inputs and outputs it includes key origin info.
///
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
///
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
pub remove_taproot_extras: bool,
/// Whether to try finalizing the PSBT after the inputs are signed.
///
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
@@ -860,8 +820,6 @@ impl Default for SignOptions {
trust_witness_utxo: false,
assume_height: None,
allow_all_sighashes: false,
remove_partial_sigs: true,
remove_taproot_extras: true,
try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true,
@@ -870,198 +828,53 @@ impl Default for SignOptions {
}
}
pub(crate) trait ComputeSighash {
type Extra;
type Sighash;
type SighashType;
fn sighash(
psbt: &Psbt,
input_index: usize,
extra: Self::Extra,
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
}
impl ComputeSighash for Legacy {
type Extra = ();
type Sighash = sighash::LegacySighash;
type SighashType = EcdsaSighashType;
fn sighash(
psbt: &Psbt,
input_index: usize,
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.unsigned_tx.input[input_index];
let sighash = psbt_input
.sighash_type
.unwrap_or_else(|| EcdsaSighashType::All.into())
.ecdsa_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let script = match psbt_input.redeem_script {
Some(ref redeem_script) => redeem_script.clone(),
None => {
let non_witness_utxo = psbt_input
.non_witness_utxo
.as_ref()
.ok_or(SignerError::MissingNonWitnessUtxo)?;
let prev_out = non_witness_utxo
.output
.get(tx_input.previous_output.vout as usize)
.ok_or(SignerError::InvalidNonWitnessUtxo)?;
prev_out.script_pubkey.clone()
}
};
Ok((
sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
input_index,
&script,
sighash.to_u32(),
)?,
sighash,
))
/// Computes the taproot sighash.
fn compute_tap_sighash(
psbt: &Psbt,
input_index: usize,
extra: Option<taproot::TapLeafHash>,
) -> Result<(sighash::TapSighash, TapSighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
}
impl ComputeSighash for Segwitv0 {
type Extra = ();
type Sighash = sighash::SegwitV0Sighash;
type SighashType = EcdsaSighashType;
let psbt_input = &psbt.inputs[input_index];
fn sighash(
psbt: &Psbt,
input_index: usize,
_extra: (),
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let sighash_type = psbt_input
.sighash_type
.unwrap_or_else(|| TapSighashType::Default.into())
.taproot_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let witness_utxos = (0..psbt.inputs.len())
.map(|i| psbt.get_utxo_for(i))
.collect::<Vec<_>>();
let mut all_witness_utxos = vec![];
let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.unsigned_tx.input[input_index];
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
let prevouts = if is_anyone_can_pay {
sighash::Prevouts::One(
input_index,
witness_utxos[input_index]
.as_ref()
.ok_or(SignerError::MissingWitnessUtxo)?,
)
} else if witness_utxos.iter().all(Option::is_some) {
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
sighash::Prevouts::All(&all_witness_utxos)
} else {
return Err(SignerError::MissingWitnessUtxo);
};
let sighash_type = psbt_input
.sighash_type
.unwrap_or_else(|| EcdsaSighashType::All.into())
.ecdsa_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
// Assume no OP_CODESEPARATOR
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
// Always try first with the non-witness utxo
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
// Check the provided prev-tx
if prev_tx.compute_txid() != tx_input.previous_output.txid {
return Err(SignerError::InvalidNonWitnessUtxo);
}
// The output should be present, if it's missing the `non_witness_utxo` is invalid
prev_tx
.output
.get(tx_input.previous_output.vout as usize)
.ok_or(SignerError::InvalidNonWitnessUtxo)?
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
// before we get to this point
witness_utxo
} else {
// Nothing has been provided
return Err(SignerError::MissingNonWitnessUtxo);
};
let value = utxo.value;
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
let sighash = match psbt_input.witness_script {
Some(ref witness_script) => {
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
}
None => {
if utxo.script_pubkey.is_p2wpkh() {
sighasher.p2wpkh_signature_hash(
input_index,
&utxo.script_pubkey,
value,
sighash_type,
)?
} else if psbt_input
.redeem_script
.as_ref()
.map(|s| s.is_p2wpkh())
.unwrap_or(false)
{
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
sighasher.p2wpkh_signature_hash(
input_index,
script_pubkey,
value,
sighash_type,
)?
} else {
return Err(SignerError::MissingWitnessScript);
}
}
};
Ok((sighash, sighash_type))
}
}
impl ComputeSighash for Tap {
type Extra = Option<taproot::TapLeafHash>;
type Sighash = TapSighash;
type SighashType = TapSighashType;
fn sighash(
psbt: &Psbt,
input_index: usize,
extra: Self::Extra,
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange);
}
let psbt_input = &psbt.inputs[input_index];
let sighash_type = psbt_input
.sighash_type
.unwrap_or_else(|| TapSighashType::Default.into())
.taproot_hash_ty()
.map_err(|_| SignerError::InvalidSighash)?;
let witness_utxos = (0..psbt.inputs.len())
.map(|i| psbt.get_utxo_for(i))
.collect::<Vec<_>>();
let mut all_witness_utxos = vec![];
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
let prevouts = if is_anyone_can_pay {
sighash::Prevouts::One(
input_index,
witness_utxos[input_index]
.as_ref()
.ok_or(SignerError::MissingWitnessUtxo)?,
)
} else if witness_utxos.iter().all(Option::is_some) {
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
sighash::Prevouts::All(&all_witness_utxos)
} else {
return Err(SignerError::MissingWitnessUtxo);
};
// Assume no OP_CODESEPARATOR
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
Ok((
cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
sighash_type,
))
}
Ok((
cache
.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)
.map_err(SignerError::SighashTaproot)?,
sighash_type,
))
}
impl PartialOrd for SignersContainerKey {

View File

@@ -17,8 +17,8 @@
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # use bdk_wallet::*;
//! # use bdk_wallet::wallet::ChangeSet;
//! # use bdk_wallet::wallet::error::CreateTxError;
//! # use bdk_wallet::ChangeSet;
//! # use bdk_wallet::error::CreateTxError;
//! # use anyhow::Error;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
//! # let mut wallet = doctest_wallet!();
@@ -42,11 +42,18 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use core::cell::RefCell;
use core::fmt;
use alloc::sync::Arc;
use bitcoin::psbt::{self, Psbt};
use bitcoin::script::PushBytes;
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
use bitcoin::{
absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
Weight,
};
use rand_core::RngCore;
use super::coin_selection::CoinSelectionAlgorithm;
use super::utils::shuffle_slice;
use super::{CreateTxError, Wallet};
use crate::collections::{BTreeMap, HashSet};
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
@@ -62,11 +69,11 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
///
/// ```
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::tx_builder::*;
/// # use bdk_wallet::tx_builder::*;
/// # use bitcoin::*;
/// # use core::str::FromStr;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_wallet::ChangeSet;
/// # use bdk_wallet::error::CreateTxError;
/// # use anyhow::Error;
/// # let mut wallet = doctest_wallet!();
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -292,10 +299,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
.collect::<Result<Vec<_>, _>>()?;
for utxo in utxos {
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
let satisfaction_weight =
descriptor.max_weight_to_satisfy().unwrap().to_wu() as usize;
let descriptor = wallet.public_descriptor(utxo.keychain);
let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(utxo),
@@ -364,7 +369,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
&mut self,
outpoint: OutPoint,
psbt_input: psbt::Input,
satisfaction_weight: usize,
satisfaction_weight: Weight,
) -> Result<&mut Self, AddForeignUtxoError> {
self.add_foreign_utxo_with_sequence(
outpoint,
@@ -379,7 +384,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
&mut self,
outpoint: OutPoint,
psbt_input: psbt::Input,
satisfaction_weight: usize,
satisfaction_weight: Weight,
sequence: Sequence,
) -> Result<&mut Self, AddForeignUtxoError> {
if psbt_input.witness_utxo.is_none() {
@@ -570,7 +575,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
///
/// This will be used to:
/// 1. Set the nLockTime for preventing fee sniping.
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
/// mature at `current_height`, we ignore them in the coin selection.
/// If you want to create a transaction that spends immature coinbase inputs, manually
@@ -636,8 +641,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
/// # use std::str::FromStr;
/// # use bitcoin::*;
/// # use bdk_wallet::*;
/// # use bdk_wallet::wallet::ChangeSet;
/// # use bdk_wallet::wallet::error::CreateTxError;
/// # use bdk_wallet::ChangeSet;
/// # use bdk_wallet::error::CreateTxError;
/// # use anyhow::Error;
/// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
@@ -669,16 +674,33 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
/// Finish building the transaction.
///
/// Uses the thread-local random number generator (rng).
///
/// Returns a new [`Psbt`] per [`BIP174`].
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
///
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
#[cfg(feature = "std")]
pub fn finish(self) -> Result<Psbt, CreateTxError> {
self.finish_with_aux_rand(&mut bitcoin::key::rand::thread_rng())
}
/// Finish building the transaction.
///
/// Uses a provided random number generator (rng).
///
/// Returns a new [`Psbt`] per [`BIP174`].
///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
///
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
pub fn finish_with_aux_rand(self, rng: &mut impl RngCore) -> Result<Psbt, CreateTxError> {
self.wallet
.borrow_mut()
.create_tx(self.coin_selection, self.params)
.create_tx(self.coin_selection, self.params, rng)
}
}
@@ -744,35 +766,60 @@ impl fmt::Display for AddForeignUtxoError {
#[cfg(feature = "std")]
impl std::error::Error for AddForeignUtxoError {}
type TxSort<T> = dyn Fn(&T, &T) -> core::cmp::Ordering;
/// Ordering of the transaction's inputs and outputs
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
#[derive(Clone, Default)]
pub enum TxOrdering {
/// Randomized (default)
#[default]
Shuffle,
/// Unchanged
Untouched,
/// BIP69 / Lexicographic
Bip69Lexicographic,
/// Provide custom comparison functions for sorting
Custom {
/// Transaction inputs sort function
input_sort: Arc<TxSort<TxIn>>,
/// Transaction outputs sort function
output_sort: Arc<TxSort<TxOut>>,
},
}
impl core::fmt::Debug for TxOrdering {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
TxOrdering::Shuffle => write!(f, "Shuffle"),
TxOrdering::Untouched => write!(f, "Untouched"),
TxOrdering::Custom { .. } => write!(f, "Custom"),
}
}
}
impl TxOrdering {
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
///
/// Uses the thread-local random number generator (rng).
#[cfg(feature = "std")]
pub fn sort_tx(&self, tx: &mut Transaction) {
self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng())
}
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
///
/// Uses a provided random number generator (rng).
pub fn sort_tx_with_aux_rand(&self, tx: &mut Transaction, rng: &mut impl RngCore) {
match self {
TxOrdering::Untouched => {}
TxOrdering::Shuffle => {
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
tx.input.shuffle(&mut rng);
tx.output.shuffle(&mut rng);
shuffle_slice(&mut tx.input, rng);
shuffle_slice(&mut tx.output, rng);
}
TxOrdering::Bip69Lexicographic => {
tx.input.sort_unstable_by_key(|txin| {
(txin.previous_output.txid, txin.previous_output.vout)
});
tx.output
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
TxOrdering::Custom {
input_sort,
output_sort,
} => {
tx.input.sort_unstable_by(|a, b| input_sort(a, b));
tx.output.sort_unstable_by(|a, b| output_sort(a, b));
}
}
}
@@ -851,12 +898,6 @@ mod test {
use bitcoin::TxOut;
use super::*;
#[test]
fn test_output_ordering_default_shuffle() {
assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
}
#[test]
fn test_output_ordering_untouched() {
let original_tx = ordering_test_tx!();
@@ -889,13 +930,28 @@ mod test {
}
#[test]
fn test_output_ordering_bip69() {
fn test_output_ordering_custom_but_bip69() {
use core::str::FromStr;
let original_tx = ordering_test_tx!();
let mut tx = original_tx;
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
};
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
project_utxo(tx_a).cmp(&project_utxo(tx_b))
};
let custom_bip69_ordering = TxOrdering::Custom {
input_sort: Arc::new(bip69_txin_cmp),
output_sort: Arc::new(bip69_txout_cmp),
};
custom_bip69_ordering.sort_tx(&mut tx);
assert_eq!(
tx.input[0].previous_output,
@@ -927,6 +983,63 @@ mod test {
);
}
#[test]
fn test_output_ordering_custom_with_sha256() {
use bitcoin::hashes::{sha256, Hash};
let original_tx = ordering_test_tx!();
let mut tx_1 = original_tx.clone();
let mut tx_2 = original_tx.clone();
let shared_secret = "secret_tweak";
let hash_txin_with_shared_secret_seed = Arc::new(|tx_a: &TxIn, tx_b: &TxIn| {
let secret_digest_from_txin = |txin: &TxIn| {
sha256::Hash::hash(
&[
&txin.previous_output.txid.to_raw_hash()[..],
&txin.previous_output.vout.to_be_bytes(),
shared_secret.as_bytes(),
]
.concat(),
)
};
secret_digest_from_txin(tx_a).cmp(&secret_digest_from_txin(tx_b))
});
let hash_txout_with_shared_secret_seed = Arc::new(|tx_a: &TxOut, tx_b: &TxOut| {
let secret_digest_from_txout = |txin: &TxOut| {
sha256::Hash::hash(
&[
&txin.value.to_sat().to_be_bytes(),
&txin.script_pubkey.clone().into_bytes()[..],
shared_secret.as_bytes(),
]
.concat(),
)
};
secret_digest_from_txout(tx_a).cmp(&secret_digest_from_txout(tx_b))
});
let custom_ordering_from_salted_sha256_1 = TxOrdering::Custom {
input_sort: hash_txin_with_shared_secret_seed.clone(),
output_sort: hash_txout_with_shared_secret_seed.clone(),
};
let custom_ordering_from_salted_sha256_2 = TxOrdering::Custom {
input_sort: hash_txin_with_shared_secret_seed,
output_sort: hash_txout_with_shared_secret_seed,
};
custom_ordering_from_salted_sha256_1.sort_tx(&mut tx_1);
custom_ordering_from_salted_sha256_2.sort_tx(&mut tx_2);
// Check the ordering is consistent between calls
assert_eq!(tx_1, tx_2);
// Check transaction order has changed
assert_ne!(tx_1, original_tx);
assert_ne!(tx_2, original_tx);
}
fn get_test_utxos() -> Vec<LocalOutput> {
use bitcoin::hashes::Hash;

View File

@@ -14,6 +14,8 @@ use bitcoin::{absolute, relative, Script, Sequence};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
use rand_core::RngCore;
/// Trait to check if a value is below the dust limit.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate
@@ -110,6 +112,19 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
}
}
// The Knuth shuffling algorithm based on the original [Fisher-Yates method](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
pub(crate) fn shuffle_slice<T>(list: &mut [T], rng: &mut impl RngCore) {
if list.is_empty() {
return;
}
let mut current_index = list.len() - 1;
while current_index > 0 {
let random_index = rng.next_u32() as usize % (current_index + 1);
list.swap(current_index, random_index);
current_index -= 1;
}
}
pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)]
@@ -118,9 +133,11 @@ mod test {
// otherwise it's time-based
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
use super::{check_nsequence_rbf, IsDust};
use super::{check_nsequence_rbf, shuffle_slice, IsDust};
use crate::bitcoin::{Address, Network, Sequence};
use alloc::vec::Vec;
use core::str::FromStr;
use rand::{rngs::StdRng, thread_rng, SeedableRng};
#[test]
fn test_is_dust() {
@@ -182,4 +199,46 @@ mod test {
);
assert!(result);
}
#[test]
#[cfg(feature = "std")]
fn test_shuffle_slice_empty_vec() {
let mut test: Vec<u8> = vec![];
shuffle_slice(&mut test, &mut thread_rng());
}
#[test]
#[cfg(feature = "std")]
fn test_shuffle_slice_single_vec() {
let mut test: Vec<u8> = vec![0];
shuffle_slice(&mut test, &mut thread_rng());
}
#[test]
fn test_shuffle_slice_duple_vec() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut test: Vec<u8> = vec![0, 1];
shuffle_slice(&mut test, &mut rng);
assert_eq!(test, &[0, 1]);
let seed = [6; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut test: Vec<u8> = vec![0, 1];
shuffle_slice(&mut test, &mut rng);
assert_eq!(test, &[1, 0]);
}
#[test]
fn test_shuffle_slice_multi_vec() {
let seed = [0; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut test: Vec<u8> = vec![0, 1, 2, 4, 5];
shuffle_slice(&mut test, &mut rng);
assert_eq!(test, &[2, 1, 0, 4, 5]);
let seed = [25; 32];
let mut rng: StdRng = SeedableRng::from_seed(seed);
let mut test: Vec<u8> = vec![0, 1, 2, 4, 5];
shuffle_slice(&mut test, &mut rng);
assert_eq!(test, &[0, 4, 1, 2, 5]);
}
}

View File

@@ -1,12 +1,9 @@
#![allow(unused)]
use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet};
use bitcoin::{
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
Txid,
hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction,
TxIn, TxOut, Txid,
};
use std::str::FromStr;
@@ -16,7 +13,11 @@ use std::str::FromStr;
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
/// sats are the transaction fee.
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
let mut wallet = Wallet::create(descriptor.to_string(), change.to_string())
.network(Network::Regtest)
.create_wallet_no_persist()
.expect("descriptors must be valid");
let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
.expect("address")
@@ -65,6 +66,12 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
],
};
wallet
.insert_checkpoint(BlockId {
height: 42,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_checkpoint(BlockId {
height: 1_000,
@@ -77,24 +84,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
tx0,
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
)
.unwrap();
wallet
.insert_tx(
tx1.clone(),
ConfirmationTime::Confirmed {
height: 2_000,
time: 200,
},
)
.unwrap();
wallet.insert_tx(tx0.clone());
insert_anchor_from_conf(
&mut wallet,
tx0.compute_txid(),
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
);
wallet.insert_tx(tx1.clone());
insert_anchor_from_conf(
&mut wallet,
tx1.compute_txid(),
ConfirmationTime::Confirmed {
height: 2_000,
time: 200,
},
);
(wallet, tx1.compute_txid())
}
@@ -192,3 +201,30 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
FeeRate::from_sat_per_kwu(sat_kwu)
}
/// Simulates confirming a tx with `txid` at the specified `position` by inserting an anchor
/// at the lowest height in local chain that is greater or equal to `position`'s height,
/// assuming the confirmation time matches `ConfirmationTime::Confirmed`.
pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: ConfirmationTime) {
if let ConfirmationTime::Confirmed { height, time } = position {
// anchor tx to checkpoint with lowest height that is >= position's height
let anchor = wallet
.local_chain()
.range(height..)
.last()
.map(|anchor_cp| ConfirmationBlockTime {
block_id: anchor_cp.block_id(),
confirmation_time: time,
})
.expect("confirmation height cannot be greater than tip");
let mut graph = TxGraph::default();
let _ = graph.insert_anchor(txid, anchor);
wallet
.apply_update(Update {
graph,
..Default::default()
})
.unwrap();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,10 @@ use bdk_bitcoind_rpc::{
};
use bdk_chain::{
bitcoin::{constants::genesis_block, Block, Transaction},
indexed_tx_graph, keychain,
indexed_tx_graph,
indexer::keychain_txout,
local_chain::{self, LocalChain},
Append, ConfirmationTimeHeightAnchor, IndexedTxGraph,
ConfirmationBlockTime, IndexedTxGraph, Merge,
};
use example_cli::{
anyhow,
@@ -37,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
type ChangeSet = (
local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
#[derive(Debug)]
@@ -190,7 +191,7 @@ fn main() -> anyhow::Result<()> {
.apply_update(emission.checkpoint)
.expect("must always apply as we receive blocks in order from emitter");
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
db_stage.append((chain_changeset, graph_changeset));
db_stage.merge((chain_changeset, graph_changeset));
// commit staged db changes in intervals
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
@@ -234,7 +235,7 @@ fn main() -> anyhow::Result<()> {
);
{
let db = &mut *db.lock().unwrap();
db_stage.append((local_chain::ChangeSet::default(), graph_changeset));
db_stage.merge((local_chain::ChangeSet::default(), graph_changeset));
if let Some(changeset) = db_stage.take() {
db.append_changeset(&changeset)?;
}
@@ -320,7 +321,7 @@ fn main() -> anyhow::Result<()> {
continue;
}
};
db_stage.append(changeset);
db_stage.merge(changeset);
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
let db = &mut *db.lock().unwrap();

View File

@@ -14,13 +14,13 @@ use bdk_chain::{
transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
},
indexed_tx_graph::{self, IndexedTxGraph},
keychain::{self, KeychainTxOutIndex},
indexer::keychain_txout::{self, KeychainTxOutIndex},
local_chain,
miniscript::{
descriptor::{DescriptorSecretKey, KeyMap},
Descriptor, DescriptorPublicKey,
},
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut,
Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge,
};
pub use bdk_file_store;
pub use clap;
@@ -30,7 +30,7 @@ use clap::{Parser, Subcommand};
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
pub type KeychainChangeSet<A> = (
local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
);
#[derive(Parser)]
@@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain {
}
pub struct CreateTxChange {
pub index_changeset: keychain::ChangeSet<Keychain>,
pub index_changeset: keychain_txout::ChangeSet,
pub change_keychain: Keychain,
pub index: u32,
}
@@ -207,7 +207,7 @@ pub fn create_tx<A: Anchor, O: ChainOracle>(
where
O::Error: std::error::Error + Send + Sync + 'static,
{
let mut changeset = keychain::ChangeSet::default();
let mut changeset = keychain_txout::ChangeSet::default();
let assets = bdk_tmp_plan::Assets {
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
@@ -252,7 +252,7 @@ where
let internal_keychain = if graph
.index
.keychains()
.any(|(k, _)| *k == Keychain::Internal)
.any(|(k, _)| k == Keychain::Internal)
{
Keychain::Internal
} else {
@@ -261,15 +261,15 @@ where
let ((change_index, change_script), change_changeset) = graph
.index
.next_unused_spk(&internal_keychain)
.next_unused_spk(internal_keychain)
.expect("Must exist");
changeset.append(change_changeset);
changeset.merge(change_changeset);
let change_plan = bdk_tmp_plan::plan_satisfaction(
&graph
.index
.keychains()
.find(|(k, _)| *k == &internal_keychain)
.find(|(k, _)| *k == internal_keychain)
.expect("must exist")
.1
.at_derivation_index(change_index)
@@ -288,7 +288,7 @@ where
min_drain_value: graph
.index
.keychains()
.find(|(k, _)| *k == &internal_keychain)
.find(|(k, _)| *k == internal_keychain)
.expect("must exist")
.1
.dust_value(),
@@ -433,7 +433,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
let desc = graph
.index
.keychains()
.find(|(keychain, _)| *keychain == &k)
.find(|(keychain, _)| *keychain == k)
.expect("keychain must exist")
.1
.at_derivation_index(i)
@@ -456,7 +456,7 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainO
where
O::Error: std::error::Error + Send + Sync + 'static,
C: Default
+ Append
+ Merge
+ DeserializeOwned
+ Serialize
+ From<KeychainChangeSet<A>>
@@ -479,7 +479,7 @@ where
};
let ((spk_i, spk), index_changeset) =
spk_chooser(index, &Keychain::External).expect("Must exist");
spk_chooser(index, Keychain::External).expect("Must exist");
let db = &mut *db.lock().unwrap();
db.append_changeset(&C::from((
local_chain::ChangeSet::default(),
@@ -501,8 +501,8 @@ where
true => Keychain::Internal,
false => Keychain::External,
};
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
let address = Address::from_script(spk, network)
for (spk_i, spk) in index.revealed_keychain_spks(target_keychain) {
let address = Address::from_script(spk.as_script(), network)
.expect("should always be able to derive address");
println!(
"{:?} {} used:{}",
@@ -675,7 +675,7 @@ where
/// The initial state returned by [`init`].
pub struct Init<CS: clap::Subcommand, S: clap::Args, C>
where
C: Default + Append + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
C: Default + Merge + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
{
/// Arguments parsed by the cli.
pub args: Args<CS, S>,
@@ -697,7 +697,7 @@ pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
) -> anyhow::Result<Init<CS, S, C>>
where
C: Default
+ Append
+ Merge
+ Serialize
+ DeserializeOwned
+ Debug

View File

@@ -7,10 +7,10 @@ use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, Txid},
collections::BTreeSet,
indexed_tx_graph::{self, IndexedTxGraph},
keychain,
indexer::keychain_txout,
local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationHeightAnchor,
ConfirmationBlockTime, Merge,
};
use bdk_electrum::{
electrum_client::{self, Client, ElectrumApi},
@@ -100,7 +100,7 @@ pub struct ScanOptions {
type ChangeSet = (
local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationHeightAnchor, keychain::ChangeSet<Keychain>>,
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
fn main() -> anyhow::Result<()> {
@@ -166,7 +166,7 @@ fn main() -> anyhow::Result<()> {
Keychain::External,
graph
.index
.unbounded_spk_iter(&Keychain::External)
.unbounded_spk_iter(Keychain::External)
.into_iter()
.flatten(),
)
@@ -174,7 +174,7 @@ fn main() -> anyhow::Result<()> {
Keychain::Internal,
graph
.index
.unbounded_spk_iter(&Keychain::Internal)
.unbounded_spk_iter(Keychain::Internal)
.into_iter()
.flatten(),
)
@@ -193,8 +193,7 @@ fn main() -> anyhow::Result<()> {
let res = client
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
.context("scanning the blockchain")?;
(
res.chain_update,
res.graph_update,
@@ -277,7 +276,7 @@ fn main() -> anyhow::Result<()> {
if unconfirmed {
let unconfirmed_txids = graph
.graph()
.list_chain_txs(&*chain, chain_tip.block_id())
.list_canonical_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>>();
@@ -317,8 +316,7 @@ fn main() -> anyhow::Result<()> {
let res = client
.sync(request, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
.context("scanning the blockchain")?;
// drop lock on graph and chain
drop((graph, chain));
@@ -340,12 +338,12 @@ fn main() -> anyhow::Result<()> {
let chain_changeset = chain.apply_update(chain_update)?;
let mut indexed_tx_graph_changeset =
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
indexed_tx_graph::ChangeSet::<ConfirmationBlockTime, _>::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.merge(keychain_changeset.into());
}
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
indexed_tx_graph_changeset.merge(graph.apply_update(graph_update));
(chain_changeset, indexed_tx_graph_changeset)
};

View File

@@ -7,10 +7,10 @@ use std::{
use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, Txid},
indexed_tx_graph::{self, IndexedTxGraph},
keychain,
indexer::keychain_txout,
local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationTimeHeightAnchor,
ConfirmationBlockTime, Merge,
};
use bdk_esplora::{esplora_client, EsploraExt};
@@ -22,11 +22,11 @@ use example_cli::{
};
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
const DB_PATH: &str = ".bdk_esplora_example.db";
const DB_PATH: &str = "bdk_example_esplora.db";
type ChangeSet = (
local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
#[derive(Subcommand, Debug, Clone)]
@@ -84,7 +84,7 @@ impl EsploraArgs {
Network::Bitcoin => "https://blockstream.info/api",
Network::Testnet => "https://blockstream.info/testnet/api",
Network::Regtest => "http://localhost:3002",
Network::Signet => "https://mempool.space/signet/api",
Network::Signet => "http://signet.bitcoindevkit.net",
_ => panic!("unsupported network"),
});
@@ -96,7 +96,7 @@ impl EsploraArgs {
#[derive(Parser, Debug, Clone, PartialEq)]
pub struct ScanOptions {
/// Max number of concurrent esplora server requests.
#[clap(long, default_value = "1")]
#[clap(long, default_value = "5")]
pub parallel_requests: usize,
}
@@ -208,7 +208,7 @@ fn main() -> anyhow::Result<()> {
.index
.reveal_to_target_multi(&update.last_active_indices);
let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update);
indexed_tx_graph_changeset.append(index_changeset.into());
indexed_tx_graph_changeset.merge(index_changeset.into());
indexed_tx_graph_changeset
})
}
@@ -307,7 +307,7 @@ fn main() -> anyhow::Result<()> {
// `EsploraExt::update_tx_graph_without_keychain`.
let unconfirmed_txids = graph
.graph()
.list_chain_txs(&*chain, local_tip.block_id())
.list_canonical_txs(&*chain, local_tip.block_id())
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid)
.collect::<Vec<Txid>>();

View File

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

View File

@@ -1,53 +1,54 @@
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5;
use anyhow::anyhow;
use bdk_wallet::file_store::Store;
use bdk_wallet::Wallet;
use std::io::Write;
use std::str::FromStr;
use bdk_electrum::electrum_client;
use bdk_electrum::BdkElectrumClient;
use bdk_file_store::Store;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::{Address, Amount};
use bdk_wallet::chain::collections::HashSet;
use bdk_wallet::{bitcoin::Network, Wallet};
use bdk_wallet::{KeychainKind, SignOptions};
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5;
const NETWORK: Network = Network::Testnet;
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002";
fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-electrum-example");
let mut 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/*)";
let changeset = db
.aggregate_changesets()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let mut wallet = Wallet::new_or_load(
external_descriptor,
internal_descriptor,
changeset,
Network::Testnet,
)?;
let db_path = "bdk-electrum-example.db";
let mut db = Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let wallet_opt = Wallet::load()
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.load_wallet(&mut db)?;
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.create_wallet(&mut db)?,
};
let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)?;
}
wallet.persist(&mut db)?;
println!("Generated Address: {}", address);
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client = BdkElectrumClient::new(electrum_client::Client::new(
"ssl://electrum.blockstream.info:60002",
)?);
let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
// Populate the electrum client's transaction cache so it doesn't redownload transaction we
// already have.
client.populate_tx_cache(&wallet);
client.populate_tx_cache(wallet.tx_graph());
let request = wallet
.start_full_scan()
@@ -63,9 +64,7 @@ fn main() -> Result<(), anyhow::Error> {
})
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
let mut update = client
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
.with_confirmation_time_height_anchor(&client)?;
let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
@@ -73,9 +72,7 @@ fn main() -> Result<(), anyhow::Error> {
println!();
wallet.apply_update(update)?;
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)?;
}
wallet.persist(&mut db)?;
let balance = wallet.balance();
println!("Wallet balance after syncing: {} sats", balance.total());

View File

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

View File

@@ -1,76 +1,58 @@
use std::{collections::BTreeSet, io::Write, str::FromStr};
use std::{collections::BTreeSet, io::Write};
use anyhow::Ok;
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{
bitcoin::{Address, Amount, Network, Script},
bitcoin::{Amount, Network},
rusqlite::Connection,
KeychainKind, SignOptions, Wallet,
};
use bdk_sqlite::{rusqlite::Connection, Store};
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50;
const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 5;
const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
const NETWORK: Network = Network::Signet;
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let db_path = "bdk-esplora-async-example.sqlite";
let conn = Connection::open(db_path)?;
let mut db = Store::new(conn)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let changeset = db.read()?;
let mut conn = Connection::open(DB_PATH)?;
let mut wallet = Wallet::new_or_load(
external_descriptor,
internal_descriptor,
changeset,
Network::Signet,
)?;
let wallet_opt = Wallet::load()
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.load_wallet(&mut conn)?;
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.create_wallet(&mut conn)?,
};
let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() {
db.write(&changeset)?;
}
println!("Generated Address: {}", address);
wallet.persist(&mut conn)?;
println!("Next unused address: ({}) {}", address.index, address);
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
let mut once = Some(());
let mut stdout = std::io::stdout();
move |spk_i, _| {
match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", kind),
None => print!(" {:<3}", spk_i),
};
stdout.flush().expect("must flush");
}
}
let request = wallet
.start_full_scan()
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
match once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain),
false => print!(" {:<3}", spk_i),
}
std::io::stdout().flush().expect("must flush")
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
if once.insert(keychain) {
print!("\nScanning keychain [{:?}] ", keychain);
}
})
.inspect_spks_for_keychain(
KeychainKind::External,
generate_inspect(KeychainKind::External),
)
.inspect_spks_for_keychain(
KeychainKind::Internal,
generate_inspect(KeychainKind::Internal),
);
print!(" {:<3}", spk_i);
std::io::stdout().flush().expect("must flush")
}
});
let mut update = client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
@@ -79,9 +61,7 @@ async fn main() -> Result<(), anyhow::Error> {
let _ = update.graph_update.update_last_seen_unconfirmed(now);
wallet.apply_update(update)?;
if let Some(changeset) = wallet.take_staged() {
db.write(&changeset)?;
}
wallet.persist(&mut conn)?;
println!();
let balance = wallet.balance();
@@ -95,12 +75,9 @@ async fn main() -> Result<(), anyhow::Error> {
std::process::exit(0);
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Signet)?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.add_recipient(address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();
let mut psbt = tx_builder.finish()?;

View File

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

View File

@@ -1,52 +1,57 @@
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
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 std::{collections::BTreeSet, io::Write};
use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store;
use bdk_wallet::{
bitcoin::{Address, Amount, Network},
bitcoin::{Amount, Network},
file_store::Store,
KeychainKind, SignOptions, Wallet,
};
fn main() -> Result<(), anyhow::Error> {
let db_path = std::env::temp_dir().join("bdk-esplora-example");
let mut 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/*)";
let changeset = db.aggregate_changesets()?;
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
const DB_PATH: &str = "bdk-example-esplora-blocking.db";
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 5;
let mut wallet = Wallet::new_or_load(
external_descriptor,
internal_descriptor,
changeset,
Network::Testnet,
)?;
const NETWORK: Network = Network::Signet;
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
fn main() -> Result<(), anyhow::Error> {
let mut db = Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?;
let wallet_opt = Wallet::load()
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.load_wallet(&mut db)?;
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.create_wallet(&mut db)?,
};
let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)?;
}
println!("Generated Address: {}", address);
wallet.persist(&mut db)?;
println!(
"Next unused address: ({}) {}",
address.index, address.address
);
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
let client =
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
match once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain),
false => print!(" {:<3}", spk_i),
};
if once.insert(keychain) {
print!("\nScanning keychain [{:?}] ", keychain);
}
print!(" {:<3}", spk_i);
std::io::stdout().flush().expect("must flush")
}
});
@@ -72,12 +77,9 @@ fn main() -> Result<(), anyhow::Error> {
std::process::exit(0);
}
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Testnet)?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.add_recipient(address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();
let mut psbt = tx_builder.finish()?;

View File

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

View File

@@ -2,10 +2,10 @@ use bdk_bitcoind_rpc::{
bitcoincore_rpc::{Auth, Client, RpcApi},
Emitter,
};
use bdk_file_store::Store;
use bdk_wallet::{
bitcoin::{Block, Network, Transaction},
wallet::Wallet,
file_store::Store,
Wallet,
};
use clap::{self, Parser};
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
@@ -86,18 +86,18 @@ fn main() -> anyhow::Result<()> {
);
let start_load_wallet = Instant::now();
let mut db = Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(
DB_MAGIC.as_bytes(),
args.db_path,
)?;
let changeset = db.aggregate_changesets()?;
let mut wallet = Wallet::new_or_load(
&args.descriptor,
&args.change_descriptor,
changeset,
args.network,
)?;
let mut db =
Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?;
let wallet_opt = Wallet::load()
.descriptors(args.descriptor.clone(), args.change_descriptor.clone())
.network(args.network)
.load_wallet(&mut db)?;
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(args.descriptor, args.change_descriptor)
.network(args.network)
.create_wallet(&mut db)?,
};
println!(
"Loaded wallet in {}s",
start_load_wallet.elapsed().as_secs_f32()
@@ -146,9 +146,7 @@ fn main() -> anyhow::Result<()> {
let connected_to = block_emission.connected_to();
let start_apply_block = Instant::now();
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)?;
}
wallet.persist(&mut db)?;
let elapsed = start_apply_block.elapsed().as_secs_f32();
println!(
"Applied block {} at height {} in {}s",
@@ -158,9 +156,7 @@ fn main() -> anyhow::Result<()> {
Emission::Mempool(mempool_emission) => {
let start_apply_mempool = Instant::now();
wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)?;
}
wallet.persist(&mut db)?;
println!(
"Applied unconfirmed transactions in {}s",
start_apply_mempool.elapsed().as_secs_f32()

View File

@@ -18,11 +18,11 @@ use bdk_chain::{bitcoin, collections::*, miniscript};
use bitcoin::{
absolute,
bip32::{DerivationPath, Fingerprint, KeySource},
blockdata::transaction::Sequence,
ecdsa,
hashes::{hash160, ripemd160, sha256},
secp256k1::Secp256k1,
taproot::{self, LeafVersion, TapLeafHash},
transaction::Sequence,
ScriptBuf, TxIn, Witness, WitnessVersion,
};
use miniscript::{