use alloc::boxed::Box;
-use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
+use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
use bitcoin::{BlockHash, Network};
use miniscript::descriptor::KeyMap;
use crate::{
descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
utils::SecpCtx,
- KeychainKind, Wallet,
+ AsyncWalletPersister, CreateWithPersistError, KeychainKind, LoadWithPersistError, Wallet,
+ WalletPersister,
};
use super::{ChangeSet, LoadError, PersistedWallet};
}
/// Create [`PersistedWallet`] with the given `Db`.
- pub fn create_wallet<Db>(
+ pub fn create_wallet<P>(
self,
- db: &mut Db,
- ) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
+ persister: &mut P,
+ ) -> Result<PersistedWallet, CreateWithPersistError<P::Error>>
where
- Wallet: PersistWith<Db, CreateParams = Self>,
+ P: WalletPersister,
{
- PersistedWallet::create(db, self)
+ PersistedWallet::create(persister, self)
}
/// Create [`PersistedWallet`] with the given async `Db`.
- pub async fn create_wallet_async<Db>(
+ pub async fn create_wallet_async<P>(
self,
- db: &mut Db,
- ) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
+ persister: &mut P,
+ ) -> Result<PersistedWallet, CreateWithPersistError<P::Error>>
where
- Wallet: PersistAsyncWith<Db, CreateParams = Self>,
+ P: AsyncWalletPersister,
{
- PersistedWallet::create_async(db, self).await
+ PersistedWallet::create_async(persister, self).await
}
/// Create [`Wallet`] without persistence.
}
/// Load [`PersistedWallet`] with the given `Db`.
- pub fn load_wallet<Db>(
+ pub fn load_wallet<P>(
self,
- db: &mut Db,
- ) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
+ persister: &mut P,
+ ) -> Result<Option<PersistedWallet>, LoadWithPersistError<P::Error>>
where
- Wallet: PersistWith<Db, LoadParams = Self>,
+ P: WalletPersister,
{
- PersistedWallet::load(db, self)
+ PersistedWallet::load(persister, self)
}
/// Load [`PersistedWallet`] with the given async `Db`.
- pub async fn load_wallet_async<Db>(
+ pub async fn load_wallet_async<P>(
self,
- db: &mut Db,
- ) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
+ persister: &mut P,
+ ) -> Result<Option<PersistedWallet>, LoadWithPersistError<P::Error>>
where
- Wallet: PersistAsyncWith<Db, LoadParams = Self>,
+ P: AsyncWalletPersister,
{
- PersistedWallet::load_async(db, self).await
+ PersistedWallet::load_async(persister, self).await
}
/// Load [`Wallet`] without persistence.
-use core::fmt;
+use core::{
+ fmt,
+ future::Future,
+ ops::{Deref, DerefMut},
+ pin::Pin,
+};
-use crate::{descriptor::DescriptorError, Wallet};
+use alloc::boxed::Box;
+use chain::{Merge, Staged};
+
+use crate::{descriptor::DescriptorError, ChangeSet, CreateParams, LoadParams, Wallet};
+
+/// Trait that persists [`Wallet`].
+///
+/// For an async version, use [`AsyncWalletPersister`].
+///
+/// Associated functions of this trait should not be called directly, and the trait is designed so
+/// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is
+/// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of
+/// safety. Refer to [`PersistedWallet`] for more about the safety checks.
+pub trait WalletPersister {
+ /// Error type of the persister.
+ type Error;
+
+ /// Initialize the `persister` and load all data.
+ ///
+ /// This is called by [`PersistedWallet::create`] and [`PersistedWallet::load`] to ensure
+ /// the [`WalletPersister`] is initialized and returns all data in the `persister`.
+ ///
+ /// # Implementation Details
+ ///
+ /// The database schema of the `persister` (if any), should be initialized and migrated here.
+ ///
+ /// The implementation must return all data currently stored in the `persister`. If there is no
+ /// data, return an empty changeset (using [`ChangeSet::default()`]).
+ ///
+ /// Error should only occur on database failure. Multiple calls to `initialize` should not
+ /// error. Calling [`persist`] before calling `initialize` should not error either.
+ ///
+ /// [`persist`]: WalletPersister::persist
+ fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error>;
+
+ /// Persist the given `changeset` to the `persister`.
+ ///
+ /// This method can fail if the `persister` is not [`initialize`]d.
+ ///
+ /// [`initialize`]: WalletPersister::initialize
+ fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error>;
+}
+
+type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
+
+/// Async trait that persists [`Wallet`].
+///
+/// For a blocking version, use [`WalletPersister`].
+///
+/// Associated functions of this trait should not be called directly, and the trait is designed so
+/// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is
+/// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of
+/// safety. Refer to [`PersistedWallet`] for more about the safety checks.
+pub trait AsyncWalletPersister {
+ /// Error type of the persister.
+ type Error;
+
+ /// Initialize the `persister` and load all data.
+ ///
+ /// This is called by [`PersistedWallet::create_async`] and [`PersistedWallet::load_async`] to
+ /// ensure the [`WalletPersister`] is initialized and returns all data in the `persister`.
+ ///
+ /// # Implementation Details
+ ///
+ /// The database schema of the `persister` (if any), should be initialized and migrated here.
+ ///
+ /// The implementation must return all data currently stored in the `persister`. If there is no
+ /// data, return an empty changeset (using [`ChangeSet::default()`]).
+ ///
+ /// Error should only occur on database failure. Multiple calls to `initialize` should not
+ /// error. Calling [`persist`] before calling `initialize` should not error either.
+ ///
+ /// [`persist`]: AsyncWalletPersister::persist
+ fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error>
+ where
+ Self: 'a;
+
+ /// Persist the given `changeset` to the `persister`.
+ ///
+ /// This method can fail if the `persister` is not [`initialize`]d.
+ ///
+ /// [`initialize`]: AsyncWalletPersister::initialize
+ fn persist<'a>(
+ persister: &'a mut Self,
+ changeset: &'a ChangeSet,
+ ) -> FutureResult<'a, (), Self::Error>
+ where
+ Self: 'a;
+}
/// Represents a persisted wallet.
-pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
+///
+/// This is a light wrapper around [`Wallet`] that enforces some level of safety-checking when used
+/// with a [`WalletPersister`] or [`AsyncWalletPersister`] implementation. Safety checks assume that
+/// [`WalletPersister`] and/or [`AsyncWalletPersister`] are implemented correctly.
+///
+/// Checks include:
+///
+/// * Ensure the persister is initialized before data is persisted.
+/// * Ensure there were no previously persisted wallet data before creating a fresh wallet and
+/// persisting it.
+/// * Only clear the staged changes of [`Wallet`] after persisting succeeds.
+#[derive(Debug)]
+pub struct PersistedWallet(pub(crate) 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)
+impl Deref for PersistedWallet {
+ type Target = Wallet;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for PersistedWallet {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl PersistedWallet {
+ /// Create a new [`PersistedWallet`] with the given `persister` and `params`.
+ pub fn create<P>(
+ persister: &mut P,
+ params: CreateParams,
+ ) -> Result<Self, CreateWithPersistError<P::Error>>
+ where
+ P: WalletPersister,
+ {
+ let existing = P::initialize(persister).map_err(CreateWithPersistError::Persist)?;
+ if !existing.is_empty() {
+ return Err(CreateWithPersistError::DataAlreadyExists(existing));
+ }
+ let mut inner =
+ Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+ if let Some(changeset) = inner.take_staged() {
+ P::persist(persister, &changeset).map_err(CreateWithPersistError::Persist)?;
+ }
+ Ok(Self(inner))
+ }
+
+ /// Create a new [`PersistedWallet`] witht the given async `persister` and `params`.
+ pub async fn create_async<P>(
+ persister: &mut P,
+ params: CreateParams,
+ ) -> Result<Self, CreateWithPersistError<P::Error>>
+ where
+ P: AsyncWalletPersister,
+ {
+ let existing = P::initialize(persister)
+ .await
+ .map_err(CreateWithPersistError::Persist)?;
+ if !existing.is_empty() {
+ return Err(CreateWithPersistError::DataAlreadyExists(existing));
+ }
+ let mut inner =
+ Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+ if let Some(changeset) = inner.take_staged() {
+ P::persist(persister, &changeset)
+ .await
.map_err(CreateWithPersistError::Persist)?;
}
- Ok(wallet)
+ Ok(Self(inner))
+ }
+
+ /// Load a previously [`PersistedWallet`] from the given `persister` and `params`.
+ pub fn load<P>(
+ persister: &mut P,
+ params: LoadParams,
+ ) -> Result<Option<Self>, LoadWithPersistError<P::Error>>
+ where
+ P: WalletPersister,
+ {
+ let changeset = P::initialize(persister).map_err(LoadWithPersistError::Persist)?;
+ Wallet::load_with_params(changeset, params)
+ .map(|opt| opt.map(PersistedWallet))
+ .map_err(LoadWithPersistError::InvalidChangeSet)
}
- 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);
+ /// Load a previously [`PersistedWallet`] from the given async `persister` and `params`.
+ pub async fn load_async<P>(
+ persister: &mut P,
+ params: LoadParams,
+ ) -> Result<Option<Self>, LoadWithPersistError<P::Error>>
+ where
+ P: AsyncWalletPersister,
+ {
+ let changeset = P::initialize(persister)
+ .await
+ .map_err(LoadWithPersistError::Persist)?;
+ Wallet::load_with_params(changeset, params)
+ .map(|opt| opt.map(PersistedWallet))
+ .map_err(LoadWithPersistError::InvalidChangeSet)
+ }
+
+ /// Persist staged changes of wallet into `persister`.
+ ///
+ /// If the `persister` errors, the staged changes will not be cleared.
+ pub fn persist<P>(&mut self, persister: &mut P) -> Result<bool, P::Error>
+ where
+ P: WalletPersister,
+ {
+ let stage = Staged::staged(&mut self.0);
+ if stage.is_empty() {
+ return Ok(false);
}
- Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+ P::persist(persister, &*stage)?;
+ stage.take();
+ Ok(true)
}
- fn persist(
- db: &mut bdk_chain::rusqlite::Transaction<'c>,
- changeset: &<Self as chain::Staged>::ChangeSet,
- ) -> Result<(), Self::PersistError> {
- changeset.persist_to_sqlite(db)
+ /// Persist staged changes of wallet into an async `persister`.
+ ///
+ /// If the `persister` errors, the staged changes will not be cleared.
+ pub async fn persist_async<'a, P>(&'a mut self, persister: &mut P) -> Result<bool, P::Error>
+ where
+ P: AsyncWalletPersister,
+ {
+ let stage = Staged::staged(&mut self.0);
+ if stage.is_empty() {
+ return Ok(false);
+ }
+ P::persist(persister, &*stage).await?;
+ stage.take();
+ Ok(true)
}
}
#[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()?;
+impl<'c> WalletPersister for bdk_chain::rusqlite::Transaction<'c> {
+ type Error = bdk_chain::rusqlite::Error;
+
+ fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error> {
+ ChangeSet::from_sqlite(persister)
+ }
+
+ fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> {
+ changeset.persist_to_sqlite(persister)
+ }
+}
+
+#[cfg(feature = "rusqlite")]
+impl WalletPersister for bdk_chain::rusqlite::Connection {
+ type Error = bdk_chain::rusqlite::Error;
+
+ fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error> {
+ let db_tx = persister.transaction()?;
+ let changeset = ChangeSet::from_sqlite(&db_tx)?;
+ db_tx.commit()?;
+ Ok(changeset)
+ }
+
+ fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> {
+ let db_tx = persister.transaction()?;
changeset.persist_to_sqlite(&db_tx)?;
db_tx.commit()
}
}
+/// Error for [`bdk_file_store`]'s implementation of [`WalletPersister`].
#[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)?;
+#[derive(Debug)]
+pub enum FileStoreError {
+ /// Error when loading from the store.
+ Load(bdk_file_store::AggregateChangesetsError<ChangeSet>),
+ /// Error when writing to the store.
+ Write(std::io::Error),
+}
+
+#[cfg(feature = "file_store")]
+impl core::fmt::Display for FileStoreError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ use core::fmt::Display;
+ match self {
+ FileStoreError::Load(e) => Display::fmt(e, f),
+ FileStoreError::Write(e) => Display::fmt(e, f),
}
- Ok(wallet)
}
+}
+
+#[cfg(feature = "file_store")]
+impl std::error::Error for FileStoreError {}
+
+#[cfg(feature = "file_store")]
+impl WalletPersister for bdk_file_store::Store<ChangeSet> {
+ type Error = FileStoreError;
- fn load(
- db: &mut bdk_file_store::Store<crate::ChangeSet>,
- params: Self::LoadParams,
- ) -> Result<Option<Self>, Self::LoadError> {
- let changeset = db
+ fn initialize(persister: &mut Self) -> Result<ChangeSet, Self::Error> {
+ persister
.aggregate_changesets()
- .map_err(LoadWithPersistError::Persist)?
- .unwrap_or_default();
- Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+ .map(Option::unwrap_or_default)
+ .map_err(FileStoreError::Load)
}
- fn persist(
- db: &mut bdk_file_store::Store<crate::ChangeSet>,
- changeset: &<Self as chain::Staged>::ChangeSet,
- ) -> Result<(), Self::PersistError> {
- db.append_changeset(changeset)
+ fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> {
+ persister.append_changeset(changeset).map_err(FileStoreError::Write)
}
}
pub enum CreateWithPersistError<E> {
/// Error from persistence.
Persist(E),
+ /// Persister already has wallet data.
+ DataAlreadyExists(ChangeSet),
/// Occurs when the loaded changeset cannot construct [`Wallet`].
Descriptor(DescriptorError),
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
+ Self::DataAlreadyExists(changeset) => write!(
+ f,
+ "Cannot create wallet in persister which already contains wallet data: {:?}",
+ changeset
+ ),
Self::Descriptor(err) => fmt::Display::fmt(&err, f),
}
}
use anyhow::Context;
use assert_matches::assert_matches;
+use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime};
-use bdk_chain::{PersistWith, COINBASE_MATURITY};
use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::error::CreateTxError;
use bdk_wallet::psbt::PsbtUtils;
use bdk_wallet::signer::{SignOptions, SignerError};
use bdk_wallet::tx_builder::AddForeignUtxoError;
-use bdk_wallet::{AddressInfo, Balance, CreateParams, LoadParams, Wallet};
+use bdk_wallet::{AddressInfo, Balance, ChangeSet, Wallet, WalletPersister};
use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError};
use bitcoin::constants::ChainHash;
use bitcoin::hashes::Hash;
where
CreateDb: Fn(&Path) -> anyhow::Result<Db>,
OpenDb: Fn(&Path) -> anyhow::Result<Db>,
- Wallet: PersistWith<Db, CreateParams = CreateParams, LoadParams = LoadParams>,
- <Wallet as PersistWith<Db>>::CreateError: std::error::Error + Send + Sync + 'static,
- <Wallet as PersistWith<Db>>::LoadError: std::error::Error + Send + Sync + 'static,
- <Wallet as PersistWith<Db>>::PersistError: std::error::Error + Send + Sync + 'static,
+ Db: WalletPersister,
+ Db::Error: std::error::Error + Send + Sync + 'static,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
#[test]
fn wallet_load_checks() -> anyhow::Result<()> {
- fn run<Db, CreateDb, OpenDb, LoadDbError>(
+ fn run<Db, CreateDb, OpenDb>(
filename: &str,
create_db: CreateDb,
open_db: OpenDb,
where
CreateDb: Fn(&Path) -> anyhow::Result<Db>,
OpenDb: Fn(&Path) -> anyhow::Result<Db>,
- Wallet: PersistWith<
- Db,
- CreateParams = CreateParams,
- LoadParams = LoadParams,
- LoadError = LoadWithPersistError<LoadDbError>,
- >,
- <Wallet as PersistWith<Db>>::CreateError: std::error::Error + Send + Sync + 'static,
- <Wallet as PersistWith<Db>>::LoadError: std::error::Error + Send + Sync + 'static,
- <Wallet as PersistWith<Db>>::PersistError: std::error::Error + Send + Sync + 'static,
+ Db: WalletPersister,
+ Db::Error: std::error::Error + Send + Sync + 'static,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
run(
"store.db",
- |path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
- |path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
+ |path| Ok(bdk_file_store::Store::<ChangeSet>::create_new(DB_MAGIC, path)?),
+ |path| Ok(bdk_file_store::Store::<ChangeSet>::open(DB_MAGIC, path)?),
)?;
run(
"store.sqlite",
let mut db = rusqlite::Connection::open(db_path).unwrap();
let desc = get_test_tr_single_sig_xprv();
- let mut wallet = CreateParams::new_single(desc)
+ let mut wallet = Wallet::create_single(desc)
.network(Network::Testnet)
.create_wallet(&mut db)
.unwrap();
#[test]
fn single_descriptor_wallet_can_create_tx_and_receive_change() {
// create single descriptor wallet and fund it
- let mut wallet = CreateParams::new_single(get_test_tr_single_sig_xprv())
+ let mut wallet = Wallet::create_single(get_test_tr_single_sig_xprv())
.network(Network::Testnet)
.create_wallet_no_persist()
.unwrap();