Merge bitcoindevkit/bdk#1473: Remove persist submodule

a0bf45bef1 docs: remove PersistBackend from docs, Cargo.toml and wallet README.md (Steve Myers)
feb27df180 feat(chain)!: add `take` convenience method to `Append` trait (志宇)
1eca568be5 feat!: rm `persist` submodule (志宇)

Pull request description:

  ### Description

  @LLFourn suggested these changes which greatly simplifies the amount of code we have to maintain and removes the `async-trait` dependency. We remove `PersistBackend`, `PersistBackendAsync`, `StageExt` and `StageExtAsync` completely. Instead, we introduce `Wallet::take_staged(&mut self) -> Option<ChangeSet>`.

  The original intention to keep a staging area (`StageExt`, `StageExtAsync`) is to enforce:
  1. The caller will not persist an empty changeset.
  2. The caller does not take the staged changeset if the database (`PersistBackend`) fails.

  We achieve `1.` by returning `None` if the staged changeset is empty.

  `2.` is not too important. The caller can try figure out what to do with the changeset if persisting to db fails.

  ### Notes to the reviewers

  I added a `take` convenience method to the `Append` trait. I thought it would be handy for the caller to do a staging area with this.

  ### Changelog notice

  * Remove `persist` submodule from `bdk_chain`.
  * Change `Wallet` to outsource it's persistence logic by introducing `Wallet::take_staged`.
  * Add `take` convenience method to `Append` trait.

  ### 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:
  notmandatory:
    ACK a0bf45bef1
  evanlinjin:
    self-ACK a0bf45bef1

Tree-SHA512: 38939ab446c84d9baecd4cd36a7b66add5a121212ad5e06ade04a60f7903133dd9a20219e230ab8a40404c47e07b946ccd43085572d71c3a2a80240a2223a500
This commit is contained in:
Steve Myers
2024-06-14 21:07:51 -05:00
23 changed files with 266 additions and 487 deletions

View File

@@ -19,7 +19,6 @@ serde_crate = { package = "serde", version = "1", optional = true, features = ["
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
miniscript = { version = "12.0.0", optional = true, default-features = false }
async-trait = { version = "0.1.80", optional = true }
[dev-dependencies]
rand = "0.8"
@@ -29,4 +28,3 @@ proptest = "1.2.0"
default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
async = ["async-trait"]

View File

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

@@ -50,7 +50,8 @@ pub use descriptor_ext::{DescriptorExt, DescriptorId};
mod spk_iter;
#[cfg(feature = "miniscript")]
pub use spk_iter::*;
pub mod persist;
mod changeset;
pub use changeset::*;
pub mod spk_client;
#[allow(unused_imports)]

View File

@@ -1,279 +0,0 @@
//! This module is home to the [`PersistBackend`] trait which defines the behavior of a data store
//! required to persist changes made to BDK data structures.
//!
//! The [`CombinedChangeSet`] type encapsulates a combination of [`crate`] structures that are
//! typically persisted together.
#[cfg(feature = "async")]
use alloc::boxed::Box;
#[cfg(feature = "async")]
use async_trait::async_trait;
use core::convert::Infallible;
use core::fmt::{Debug, Display};
use crate::Append;
/// A changeset containing [`crate`] structures typically persisted together.
#[derive(Debug, Clone, PartialEq)]
#[cfg(feature = "miniscript")]
#[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()
}
}
}
/// A persistence backend for writing and loading changesets.
///
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
/// that are to be persisted, or retrieved from persistence.
pub trait PersistBackend<C> {
/// The error the backend returns when it fails to write.
type WriteError: Debug + Display;
/// The error the backend returns when it fails to load changesets `C`.
type LoadError: Debug + Display;
/// Writes a changeset to the persistence backend.
///
/// It is up to the backend what it does with this. It could store every changeset in a list or
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
/// changesets had been applied sequentially.
///
/// [`load_from_persistence`]: Self::load_changes
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
/// Return the aggregate changeset `C` from persistence.
fn load_changes(&mut self) -> Result<Option<C>, Self::LoadError>;
}
impl<C> PersistBackend<C> for () {
type WriteError = Infallible;
type LoadError = Infallible;
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
Ok(())
}
fn load_changes(&mut self) -> Result<Option<C>, Self::LoadError> {
Ok(None)
}
}
#[cfg(feature = "async")]
/// An async persistence backend for writing and loading changesets.
///
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
/// that are to be persisted, or retrieved from persistence.
#[async_trait]
pub trait PersistBackendAsync<C> {
/// The error the backend returns when it fails to write.
type WriteError: Debug + Display;
/// The error the backend returns when it fails to load changesets `C`.
type LoadError: Debug + Display;
/// Writes a changeset to the persistence backend.
///
/// It is up to the backend what it does with this. It could store every changeset in a list or
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
/// changesets had been applied sequentially.
///
/// [`load_from_persistence`]: Self::load_changes
async fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
/// Return the aggregate changeset `C` from persistence.
async fn load_changes(&mut self) -> Result<Option<C>, Self::LoadError>;
}
#[cfg(feature = "async")]
#[async_trait]
impl<C> PersistBackendAsync<C> for () {
type WriteError = Infallible;
type LoadError = Infallible;
async fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
Ok(())
}
async fn load_changes(&mut self) -> Result<Option<C>, Self::LoadError> {
Ok(None)
}
}
/// Extends a changeset so that it acts as a convenient staging area for any [`PersistBackend`].
///
/// Not all changes to the in-memory representation needs to be written to disk right away.
/// [`Append::append`] can be used to *stage* changes first and then [`StageExt::commit_to`] can be
/// used to write changes to disk.
pub trait StageExt: Append + Default + Sized {
/// Commit the staged changes to the persistence `backend`.
///
/// Changes that are committed (if any) are returned.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
fn commit_to<B>(&mut self, backend: &mut B) -> Result<Option<Self>, B::WriteError>
where
B: PersistBackend<Self>,
{
// do not do anything if changeset is empty
if self.is_empty() {
return Ok(None);
}
backend.write_changes(&*self)?;
// only clear if changes are written successfully to backend
Ok(Some(core::mem::take(self)))
}
/// Stages a new `changeset` and commits it (alongside any other previously staged changes) to
/// the persistence `backend`.
///
/// Convenience method for calling [`Append::append`] and then [`StageExt::commit_to`].
fn append_and_commit_to<B>(
&mut self,
changeset: Self,
backend: &mut B,
) -> Result<Option<Self>, B::WriteError>
where
B: PersistBackend<Self>,
{
Append::append(self, changeset);
self.commit_to(backend)
}
}
impl<C: Append + Default> StageExt for C {}
/// Extends a changeset so that it acts as a convenient staging area for any
/// [`PersistBackendAsync`].
///
/// Not all changes to the in-memory representation needs to be written to disk right away.
/// [`Append::append`] can be used to *stage* changes first and then [`StageExtAsync::commit_to`]
/// can be used to write changes to disk.
#[cfg(feature = "async")]
#[async_trait]
pub trait StageExtAsync: Append + Default + Sized + Send + Sync {
/// Commit the staged changes to the persistence `backend`.
///
/// Changes that are committed (if any) are returned.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
async fn commit_to<B>(&mut self, backend: &mut B) -> Result<Option<Self>, B::WriteError>
where
B: PersistBackendAsync<Self> + Send + Sync,
{
// do not do anything if changeset is empty
if self.is_empty() {
return Ok(None);
}
backend.write_changes(&*self).await?;
// only clear if changes are written successfully to backend
Ok(Some(core::mem::take(self)))
}
/// Stages a new `changeset` and commits it (alongside any other previously staged changes) to
/// the persistence `backend`.
///
/// Convenience method for calling [`Append::append`] and then [`StageExtAsync::commit_to`].
async fn append_and_commit_to<B>(
&mut self,
changeset: Self,
backend: &mut B,
) -> Result<Option<Self>, B::WriteError>
where
B: PersistBackendAsync<Self> + Send + Sync,
{
Append::append(self, changeset);
self.commit_to(backend).await
}
}
#[cfg(feature = "async")]
#[async_trait]
impl<C: Append + Default + Send + Sync> StageExtAsync for C {}

View File

@@ -114,12 +114,21 @@ pub trait AnchorFromBlockPosition: Anchor {
}
/// Trait that makes an object appendable.
pub trait Append {
pub trait Append: Default {
/// Append another object of the same type onto `self`.
fn append(&mut self, other: Self);
/// Returns whether the structure is considered empty.
fn is_empty(&self) -> bool;
/// Take the value, replacing it with the default value.
fn take(&mut self) -> Option<Self> {
if self.is_empty() {
None
} else {
Some(core::mem::take(self))
}
}
}
impl<K: Ord, V> Append for BTreeMap<K, V> {

View File

@@ -5,7 +5,7 @@ edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_file_store"
description = "A simple append-only flat file implementation of PersistBackend for Bitcoin Dev Kit."
description = "A simple append-only flat file database for persisting bdk_chain data."
keywords = ["bitcoin", "persist", "persistence", "bdk", "file"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"

View File

@@ -1,6 +1,6 @@
# BDK File Store
This is a simple append-only flat file implementation of [`PersistBackend`](bdk_chain::persist::PersistBackend).
This is a simple append-only flat file database for persisting [`bdk_chain`] changesets.
The main structure is [`Store`] which works with any [`bdk_chain`] based changesets to persist data into a flat file.

View File

@@ -1,5 +1,4 @@
use crate::{bincode_options, EntryIter, FileError, IterError};
use bdk_chain::persist::PersistBackend;
use bdk_chain::Append;
use bincode::Options;
use std::{
@@ -21,27 +20,6 @@ where
marker: PhantomData<C>,
}
impl<C> PersistBackend<C> for Store<C>
where
C: Append
+ Debug
+ serde::Serialize
+ serde::de::DeserializeOwned
+ core::marker::Send
+ core::marker::Sync,
{
type WriteError = io::Error;
type LoadError = AggregateChangesetsError<C>;
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
self.append_changeset(changeset)
}
fn load_changes(&mut self) -> Result<Option<C>, Self::LoadError> {
self.aggregate_changesets()
}
}
impl<C> Store<C>
where
C: Append
@@ -448,7 +426,7 @@ mod test {
acc
});
// We write after a short read.
db.write_changes(&last_changeset)
db.append_changeset(&last_changeset)
.expect("last write must succeed");
Append::append(&mut exp_aggregation, last_changeset.clone());
drop(db);

View File

@@ -5,7 +5,7 @@ edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_sqlite"
description = "A simple SQLite based implementation of PersistBackend for Bitcoin Dev Kit."
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"

View File

@@ -1,8 +1,8 @@
# BDK SQLite
This is a simple [SQLite] relational database schema backed implementation of `PersistBackend`.
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.
<!-- [`PersistBackend`]: bdk_chain::persist::PersistBackend -->
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
[SQLite]: https://www.sqlite.org/index.html

View File

@@ -12,7 +12,7 @@ use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crate::Error;
use bdk_chain::persist::{CombinedChangeSet, PersistBackend};
use bdk_chain::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
};
@@ -57,26 +57,6 @@ where
}
}
impl<K, A> PersistBackend<CombinedChangeSet<K, A>> for Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
type WriteError = Error;
type LoadError = Error;
fn write_changes(
&mut self,
changeset: &CombinedChangeSet<K, A>,
) -> Result<(), Self::WriteError> {
self.write(changeset)
}
fn load_changes(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Self::LoadError> {
self.read()
}
}
/// Network table related functions.
impl<K, A> Store<K, A> {
/// Insert [`Network`] for which all other tables data is valid.
@@ -482,13 +462,14 @@ where
}
}
/// Functions to read and write all [`ChangeSet`] data.
/// 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,
{
fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
/// 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(());
@@ -513,7 +494,8 @@ where
db_transaction.commit().map_err(Error::Sqlite)
}
fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
/// 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)?;
@@ -563,7 +545,7 @@ mod test {
use bdk_chain::bitcoin::Network::Testnet;
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
use bdk_chain::miniscript::Descriptor;
use bdk_chain::persist::{CombinedChangeSet, PersistBackend};
use bdk_chain::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
ConfirmationTimeHeightAnchor, DescriptorExt,
@@ -591,10 +573,10 @@ mod test {
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.load_changes().expect("aggregated changeset");
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
@@ -612,10 +594,10 @@ mod test {
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.load_changes().expect("aggregated changeset");
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
@@ -629,10 +611,10 @@ mod test {
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write_changes(changeset).expect("write changeset");
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.load_changes().expect("aggregated changeset");
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}

View File

@@ -30,7 +30,6 @@ js-sys = "0.3"
[features]
default = ["std"]
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
async = ["bdk_chain/async"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]

View File

@@ -57,31 +57,42 @@ that the `Wallet` can use to update its view of the chain.
## Persistence
To persist `Wallet` state data on disk use an implementation of the [`PersistBackend`] trait.
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
**Implementations**
* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
* [`bdk_sqlite`]: A simple sqlite implementation of [`PersistBackend`].
* [`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,compile_fail
use bdk_wallet::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
```rust,no_run
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
fn main() {
// Create a new file `Store`.
let mut db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/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.load_changes().expect("changeset loaded");
let mut wallet = Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet).expect("create or load wallet");
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");
// Insert a single `TxOut` at `OutPoint` into the wallet.
let _ = wallet.insert_txout(outpoint, txout);
wallet.commit_to(&mut db).expect("must commit changes to database");
// 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);
}
```
@@ -222,7 +233,6 @@ license, shall be dual licensed as above, without any additional terms or
conditions.
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/persist/trait.PersistBackend.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

View File

@@ -26,7 +26,6 @@ use bdk_chain::{
local_chain::{
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
},
persist::{PersistBackend, StageExt},
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::{CanonicalTx, TxGraph},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
@@ -85,12 +84,12 @@ const COINBASE_MATURITY: u32 = 100;
/// 1. output *descriptors* from which it can derive addresses.
/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors.
///
/// The user is responsible for loading and writing wallet changes using an implementation of
/// [`PersistBackend`]. See individual functions and example for instructions on when [`Wallet`]
/// state needs to be persisted.
/// The user is responsible for loading and writing wallet changes which are represented as
/// [`ChangeSet`]s (see [`take_staged`]). Also see individual functions and example for instructions
/// on when [`Wallet`] state needs to be persisted.
///
/// [`PersistBackend`]: bdk_chain::persist::PersistBackend
/// [`signer`]: crate::signer
/// [`take_staged`]: Wallet::take_staged
#[derive(Debug)]
pub struct Wallet {
signers: Arc<SignersContainer>,
@@ -141,8 +140,7 @@ impl From<SyncResult> for Update {
}
/// The changes made to a wallet by applying an [`Update`].
pub type ChangeSet =
bdk_chain::persist::CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>;
pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>;
/// A derived address and the index it was found at.
/// For convenience this automatically derefs to `Address`
@@ -408,14 +406,13 @@ impl Wallet {
/// # use bdk_wallet::descriptor::Descriptor;
/// # use bitcoin::key::Secp256k1;
/// # use bdk_wallet::KeychainKind;
/// # use bdk_sqlite::{Store, rusqlite::Connection};
/// use bdk_sqlite::{Store, rusqlite::Connection};
/// #
/// # fn main() -> Result<(), anyhow::Error> {
/// # use bdk_chain::persist::PersistBackend;
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// # let conn = Connection::open(file_path).expect("must open connection");
/// # let mut db = Store::new(conn).expect("must create db");
/// let conn = Connection::open(file_path).expect("must open connection");
/// let mut db = Store::new(conn).expect("must create db");
/// let secp = Secp256k1::new();
///
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
@@ -423,7 +420,7 @@ impl Wallet {
///
/// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
/// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
/// let changeset = db.load_changes()?.expect("there must be an existing changeset");
/// let changeset = db.read()?.expect("there must be an existing changeset");
/// let mut wallet = Wallet::load_from_changeset(changeset)?;
///
/// external_signer_container.signers().into_iter()
@@ -482,13 +479,12 @@ impl Wallet {
/// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
///
/// ```rust,no_run
/// # use bdk_chain::persist::PersistBackend;
/// # use bdk_wallet::Wallet;
/// # use bdk_sqlite::{Store, rusqlite::Connection};
/// use bdk_sqlite::{Store, rusqlite::Connection};
/// # use bitcoin::Network::Testnet;
/// # let conn = Connection::open_in_memory().expect("must open connection");
/// let conn = Connection::open_in_memory().expect("must open connection");
/// let mut db = Store::new(conn).expect("must create db");
/// let changeset = db.load_changes()?;
/// let changeset = db.read()?;
///
/// let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
@@ -666,16 +662,17 @@ impl Wallet {
/// calls to this method before closing the wallet. For example:
///
/// ```rust,no_run
/// # use bdk_chain::persist::PersistBackend;
/// # use bdk_wallet::wallet::{Wallet, ChangeSet};
/// # use bdk_wallet::KeychainKind;
/// # use bdk_sqlite::{Store, rusqlite::Connection};
/// # let conn = Connection::open_in_memory().expect("must open connection");
/// # let mut db = Store::new(conn).expect("must create store");
/// use bdk_sqlite::{rusqlite::Connection, Store};
/// let conn = Connection::open_in_memory().expect("must open connection");
/// let mut db = Store::new(conn).expect("must create store");
/// # let changeset = ChangeSet::default();
/// # let mut wallet = Wallet::load_from_changeset(changeset).expect("load wallet");
/// let next_address = wallet.reveal_next_address(KeychainKind::External);
/// wallet.commit_to(&mut db)?;
/// if let Some(changeset) = wallet.take_staged() {
/// db.write(&changeset)?;
/// }
///
/// // Now it's safe to show the user their next address!
/// println!("Next address: {}", next_address.address);
@@ -2283,44 +2280,18 @@ impl Wallet {
Ok(())
}
/// Commits all currently [`staged`](Wallet::staged) changes to the `persist_backend`.
///
/// This returns whether anything was persisted.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
pub fn commit_to<B>(&mut self, persist_backend: &mut B) -> Result<bool, B::WriteError>
where
B: PersistBackend<ChangeSet>,
{
let committed = StageExt::commit_to(&mut self.stage, persist_backend)?;
Ok(committed.is_some())
/// Get a reference of the staged [`ChangeSet`] that are yet to be committed (if any).
pub fn staged(&self) -> Option<&ChangeSet> {
if self.stage.is_empty() {
None
} else {
Some(&self.stage)
}
}
/// Commits all currently [`staged`](Wallet::staged) changes to the async `persist_backend`.
///
/// This returns whether anything was persisted.
///
/// # Error
///
/// Returns a backend-defined error if this fails.
#[cfg(feature = "async")]
pub async fn commit_to_async<B>(
&mut self,
persist_backend: &mut B,
) -> Result<bool, B::WriteError>
where
B: bdk_chain::persist::PersistBackendAsync<ChangeSet> + Send + Sync,
{
let committed =
bdk_chain::persist::StageExtAsync::commit_to(&mut self.stage, persist_backend).await?;
Ok(committed.is_some())
}
/// Get the staged [`ChangeSet`] that is yet to be committed.
pub fn staged(&self) -> &ChangeSet {
&self.stage
/// Take the staged [`ChangeSet`] to be persisted now (if any).
pub fn take_staged(&mut self) -> Option<ChangeSet> {
self.stage.take()
}
/// Get a reference to the inner [`TxGraph`].

View File

@@ -1,11 +1,10 @@
use anyhow::anyhow;
use std::path::Path;
use std::str::FromStr;
use assert_matches::assert_matches;
use bdk_chain::collections::BTreeMap;
use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{persist::PersistBackend, BlockId, ConfirmationTime};
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_sqlite::rusqlite::Connection;
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::psbt::PsbtUtils;
@@ -13,7 +12,7 @@ use bdk_wallet::signer::{SignOptions, SignerError};
use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::wallet::error::CreateTxError;
use bdk_wallet::wallet::tx_builder::AddForeignUtxoError;
use bdk_wallet::wallet::{AddressInfo, Balance, NewError, Wallet};
use bdk_wallet::wallet::{AddressInfo, Balance, ChangeSet, NewError, Wallet};
use bdk_wallet::KeychainKind;
use bitcoin::hashes::Hash;
use bitcoin::key::Secp256k1;
@@ -72,11 +71,18 @@ const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test]
fn load_recovers_wallet() -> anyhow::Result<()> {
fn run<B, FN, FR>(filename: &str, create_new: FN, recover: FR) -> anyhow::Result<()>
fn run<Db, New, Recover, Read, Write>(
filename: &str,
create_new: New,
recover: Recover,
read: Read,
write: Write,
) -> anyhow::Result<()>
where
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
FN: Fn(&Path) -> anyhow::Result<B>,
FR: Fn(&Path) -> anyhow::Result<B>,
New: Fn(&Path) -> anyhow::Result<Db>,
Recover: Fn(&Path) -> anyhow::Result<Db>,
Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
@@ -91,9 +97,9 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
// persist new wallet changes
let mut db = create_new(&file_path).expect("must create db");
wallet
.commit_to(&mut db)
.map_err(|e| anyhow!("write changes error: {}", e))?;
if let Some(changeset) = wallet.take_staged() {
write(&mut db, &changeset)?;
}
wallet.spk_index().clone()
};
@@ -101,10 +107,7 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
{
// load persisted wallet changes
let db = &mut recover(&file_path).expect("must recover db");
let changeset = db
.load_changes()
.expect("must recover wallet")
.expect("changeset");
let changeset = read(db).expect("must recover wallet").expect("changeset");
let wallet = Wallet::load_from_changeset(changeset).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
@@ -132,11 +135,15 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
"store.db",
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
|db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
|db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
)?;
run(
"store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|db| Ok(bdk_sqlite::Store::read(db)?),
|db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
)?;
Ok(())
@@ -144,10 +151,16 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
#[test]
fn new_or_load() -> anyhow::Result<()> {
fn run<B, F>(filename: &str, new_or_load: F) -> anyhow::Result<()>
fn run<Db, NewOrRecover, Read, Write>(
filename: &str,
new_or_load: NewOrRecover,
read: Read,
write: Write,
) -> anyhow::Result<()>
where
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
F: Fn(&Path) -> anyhow::Result<B>,
NewOrRecover: Fn(&Path) -> anyhow::Result<Db>,
Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
@@ -158,18 +171,16 @@ fn new_or_load() -> anyhow::Result<()> {
let wallet = &mut Wallet::new_or_load(desc, change_desc, None, Network::Testnet)
.expect("must init wallet");
let mut db = new_or_load(&file_path).expect("must create db");
wallet
.commit_to(&mut db)
.map_err(|e| anyhow!("write changes error: {}", e))?;
if let Some(changeset) = wallet.take_staged() {
write(&mut db, &changeset)?;
}
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
};
// wrong network
{
let mut db = new_or_load(&file_path).expect("must create db");
let changeset = db
.load_changes()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let changeset = read(&mut db)?;
let err = Wallet::new_or_load(desc, change_desc, changeset, Network::Bitcoin)
.expect_err("wrong network");
assert!(
@@ -192,9 +203,7 @@ fn new_or_load() -> anyhow::Result<()> {
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = db
.load_changes()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let changeset = read(db)?;
let err = Wallet::new_or_load_with_genesis_hash(
desc,
change_desc,
@@ -223,9 +232,7 @@ fn new_or_load() -> anyhow::Result<()> {
.0;
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = db
.load_changes()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let changeset = read(db)?;
let err =
Wallet::new_or_load(exp_descriptor, exp_change_desc, changeset, Network::Testnet)
.expect_err("wrong external descriptor");
@@ -249,9 +256,7 @@ fn new_or_load() -> anyhow::Result<()> {
.0;
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = db
.load_changes()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let changeset = read(db)?;
let err = Wallet::new_or_load(desc, exp_descriptor, changeset, Network::Testnet)
.expect_err("wrong internal descriptor");
assert!(
@@ -268,9 +273,7 @@ fn new_or_load() -> anyhow::Result<()> {
// all parameters match
{
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = db
.load_changes()
.map_err(|e| anyhow!("load changes error: {}", e))?;
let changeset = read(db)?;
let wallet = Wallet::new_or_load(desc, change_desc, changeset, Network::Testnet)
.expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
@@ -282,12 +285,18 @@ fn new_or_load() -> anyhow::Result<()> {
Ok(())
}
run("store.db", |path| {
Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?)
})?;
run("store.sqlite", |path| {
Ok(bdk_sqlite::Store::new(Connection::open(path)?)?)
})?;
run(
"store.db",
|path| Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?),
|db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
|db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
)?;
run(
"store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|db| Ok(bdk_sqlite::Store::read(db)?),
|db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
)?;
Ok(())
}