From: 志宇 Date: Fri, 9 Aug 2024 16:14:15 +0000 (+0000) Subject: feat(wallet)!: introduce `WalletPersister` X-Git-Tag: v1.0.0-beta.2~7^2~5 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.SegwitCodeLengthError.html?a=commitdiff_plain;h=039622fd1de7ee331eceb7a4c77751bd8ecccda0;p=bdk feat(wallet)!: introduce `WalletPersister` This replaces `bdk_chain::PersistWith` which wanted to persist anything (not only `Wallet`), hence, requiring a whole bunch of generic parameters. Having `WalletPersister` dedicated to persisting `Wallet` simplifies the trait by a lot. In addition, `AsyncWalletPersister` has proper lifetime bounds whereas `bdk_chain::PersistAsyncWith` did not. --- diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 9b079539..22e7a5b7 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -1,12 +1,13 @@ 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}; @@ -109,25 +110,25 @@ impl CreateParams { } /// Create [`PersistedWallet`] with the given `Db`. - pub fn create_wallet( + pub fn create_wallet

( self, - db: &mut Db, - ) -> Result>::CreateError> + persister: &mut P, + ) -> Result> where - Wallet: PersistWith, + P: WalletPersister, { - PersistedWallet::create(db, self) + PersistedWallet::create(persister, self) } /// Create [`PersistedWallet`] with the given async `Db`. - pub async fn create_wallet_async( + pub async fn create_wallet_async

( self, - db: &mut Db, - ) -> Result>::CreateError> + persister: &mut P, + ) -> Result> where - Wallet: PersistAsyncWith, + P: AsyncWalletPersister, { - PersistedWallet::create_async(db, self).await + PersistedWallet::create_async(persister, self).await } /// Create [`Wallet`] without persistence. @@ -220,25 +221,25 @@ impl LoadParams { } /// Load [`PersistedWallet`] with the given `Db`. - pub fn load_wallet( + pub fn load_wallet

( self, - db: &mut Db, - ) -> Result, >::LoadError> + persister: &mut P, + ) -> Result, LoadWithPersistError> where - Wallet: PersistWith, + P: WalletPersister, { - PersistedWallet::load(db, self) + PersistedWallet::load(persister, self) } /// Load [`PersistedWallet`] with the given async `Db`. - pub async fn load_wallet_async( + pub async fn load_wallet_async

( self, - db: &mut Db, - ) -> Result, >::LoadError> + persister: &mut P, + ) -> Result, LoadWithPersistError> where - Wallet: PersistAsyncWith, + P: AsyncWalletPersister, { - PersistedWallet::load_async(db, self).await + PersistedWallet::load_async(persister, self).await } /// Load [`Wallet`] without persistence. diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index cc9f267f..a7181a3c 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -1,130 +1,305 @@ -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; + + /// 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> + 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; +/// +/// 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> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - - type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; - type PersistError = bdk_chain::rusqlite::Error; - - fn create( - db: &mut bdk_chain::rusqlite::Transaction<'c>, - params: Self::CreateParams, - ) -> Result { - 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

( + persister: &mut P, + params: CreateParams, + ) -> Result> + 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

( + persister: &mut P, + params: CreateParams, + ) -> Result> + 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

( + persister: &mut P, + params: LoadParams, + ) -> Result, LoadWithPersistError> + 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, 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

( + persister: &mut P, + params: LoadParams, + ) -> Result, LoadWithPersistError> + 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

(&mut self, persister: &mut P) -> Result + 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: &::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 + 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 for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - - type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; - type PersistError = bdk_chain::rusqlite::Error; - - fn create( - db: &mut bdk_chain::rusqlite::Connection, - params: Self::CreateParams, - ) -> Result { - 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, 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: &::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::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 { + 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> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - type CreateError = CreateWithPersistError; - type LoadError = - LoadWithPersistError>; - type PersistError = std::io::Error; - - fn create( - db: &mut bdk_file_store::Store, - params: Self::CreateParams, - ) -> Result { - 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), + /// 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 { + type Error = FileStoreError; - fn load( - db: &mut bdk_file_store::Store, - params: Self::LoadParams, - ) -> Result, Self::LoadError> { - let changeset = db + fn initialize(persister: &mut Self) -> Result { + 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, - changeset: &::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) } } @@ -154,6 +329,8 @@ impl std::error::Error for LoadWithPersistError pub enum CreateWithPersistError { /// Error from persistence. Persist(E), + /// Persister already has wallet data. + DataAlreadyExists(ChangeSet), /// Occurs when the loaded changeset cannot construct [`Wallet`]. Descriptor(DescriptorError), } @@ -162,6 +339,11 @@ impl fmt::Display for CreateWithPersistError { 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), } } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index d41544a1..32b7a0f7 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -5,15 +5,15 @@ use std::str::FromStr; 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; @@ -111,10 +111,8 @@ fn wallet_is_persisted() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Wallet: PersistWith, - >::CreateError: std::error::Error + Send + Sync + 'static, - >::LoadError: std::error::Error + Send + Sync + 'static, - >::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); @@ -188,7 +186,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> { #[test] fn wallet_load_checks() -> anyhow::Result<()> { - fn run( + fn run( filename: &str, create_db: CreateDb, open_db: OpenDb, @@ -196,15 +194,8 @@ fn wallet_load_checks() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Wallet: PersistWith< - Db, - CreateParams = CreateParams, - LoadParams = LoadParams, - LoadError = LoadWithPersistError, - >, - >::CreateError: std::error::Error + Send + Sync + 'static, - >::LoadError: std::error::Error + Send + Sync + 'static, - >::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); @@ -258,8 +249,8 @@ fn wallet_load_checks() -> anyhow::Result<()> { 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::::create_new(DB_MAGIC, path)?), + |path| Ok(bdk_file_store::Store::::open(DB_MAGIC, path)?), )?; run( "store.sqlite", @@ -280,7 +271,7 @@ fn single_descriptor_wallet_persist_and_recover() { 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(); @@ -4174,7 +4165,7 @@ fn test_insert_tx_balance_and_utxos() { #[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();