From: Rob N Date: Thu, 25 Apr 2024 01:01:17 +0000 (-1000) Subject: feat(bdk-persist): extract persistence traits to new crate X-Git-Tag: v1.0.0-alpha.10~3^2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.CommandStringError.html?a=commitdiff_plain;h=81de8f60516899fad2ec99cbd15d6b8d02385ebb;p=bdk feat(bdk-persist): extract persistence traits to new crate --- diff --git a/Cargo.toml b/Cargo.toml index 53ceb7cd..87428029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/esplora", "crates/bitcoind_rpc", "crates/hwi", + "crates/persist", "crates/testenv", "example-crates/example_cli", "example-crates/example_electrum", diff --git a/README.md b/README.md index d3222430..030ec2a4 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The project is split up into several crates in the `/crates` directory: - [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components - [`chain`](./crates/chain): Tools for storing and indexing chain data +- [`persist`](./crates/persist): Types that define data persistence of a BDK wallet - [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file. - [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume. - [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume. diff --git a/crates/bdk/Cargo.toml b/crates/bdk/Cargo.toml index 425199e1..982b2402 100644 --- a/crates/bdk/Cargo.toml +++ b/crates/bdk/Cargo.toml @@ -20,6 +20,7 @@ bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], defa serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } bdk_chain = { path = "../chain", version = "0.12.0", features = ["miniscript", "serde"], default-features = false } +bdk_persist = { path = "../persist", version = "0.1.0" } # Optional dependencies bip39 = { version = "2.0", optional = true } diff --git a/crates/bdk/README.md b/crates/bdk/README.md index a3d53f29..4722c584 100644 --- a/crates/bdk/README.md +++ b/crates/bdk/README.md @@ -219,7 +219,7 @@ license, shall be dual licensed as above, without any additional terms or conditions. [`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html -[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html +[`PersistBackend`]: https://docs.rs/bdk_persist/latest/bdk_persist/trait.PersistBackend.html [`bdk_chain`]: https://docs.rs/bdk_chain/latest [`bdk_file_store`]: https://docs.rs/bdk_file_store/latest [`bdk_electrum`]: https://docs.rs/bdk_electrum/latest diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 49cb56c0..f1897677 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -28,7 +28,7 @@ //! # use bitcoin::*; //! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection}; //! # use bdk::wallet::error::CreateTxError; -//! # use bdk_chain::PersistBackend; +//! # use bdk_persist::PersistBackend; //! # use bdk::*; //! # use bdk::wallet::coin_selection::decide_change; //! # use anyhow::Error; diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 1e24dd50..1fe37d55 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -28,8 +28,9 @@ use bdk_chain::{ }, tx_graph::{CanonicalTx, TxGraph}, Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, - IndexedTxGraph, Persist, PersistBackend, + IndexedTxGraph, }; +use bdk_persist::{Persist, PersistBackend}; use bitcoin::constants::genesis_block; use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; @@ -1167,7 +1168,7 @@ impl Wallet { /// # use bdk::*; /// # use bdk::wallet::ChangeSet; /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_chain::PersistBackend; + /// # use bdk_persist::PersistBackend; /// # use anyhow::Error; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); @@ -1549,7 +1550,7 @@ impl Wallet { /// # use bdk::*; /// # use bdk::wallet::ChangeSet; /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_chain::PersistBackend; + /// # use bdk_persist::PersistBackend; /// # use anyhow::Error; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); @@ -1724,7 +1725,7 @@ impl Wallet { /// # use bdk::*; /// # use bdk::wallet::ChangeSet; /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_chain::PersistBackend; + /// # use bdk_persist::PersistBackend; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index e7df8667..355d80d3 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -20,7 +20,7 @@ //! # use bdk::wallet::ChangeSet; //! # use bdk::wallet::error::CreateTxError; //! # use bdk::wallet::tx_builder::CreateTx; -//! # use bdk_chain::PersistBackend; +//! # use bdk_persist::PersistBackend; //! # use anyhow::Error; //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); //! # let mut wallet = doctest_wallet!(); @@ -84,7 +84,7 @@ impl TxBuilderContext for BumpFee {} /// # use core::str::FromStr; /// # use bdk::wallet::ChangeSet; /// # use bdk::wallet::error::CreateTxError; -/// # use bdk_chain::PersistBackend; +/// # use bdk_persist::PersistBackend; /// # use anyhow::Error; /// # let mut wallet = doctest_wallet!(); /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); @@ -758,7 +758,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> { /// # use bdk::wallet::ChangeSet; /// # use bdk::wallet::error::CreateTxError; /// # use bdk::wallet::tx_builder::CreateTx; - /// # use bdk_chain::PersistBackend; + /// # use bdk_persist::PersistBackend; /// # use anyhow::Error; /// # let to_address = /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 56cadd85..fda5924d 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -14,7 +14,6 @@ readme = "README.md" [dependencies] # For no-std, remember to enable the bitcoin/no-std feature -anyhow = { version = "1", default-features = false } bitcoin = { version = "0.31.0", default-features = false } serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 20656697..61c0b6d7 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -35,8 +35,6 @@ 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; diff --git a/crates/chain/src/persist.rs b/crates/chain/src/persist.rs deleted file mode 100644 index 64efa55b..00000000 --- a/crates/chain/src/persist.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::Append; -use alloc::boxed::Box; -use core::fmt; - -/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes (`C`) -/// before they are persisted. -/// -/// Not all changes to the in-memory representation needs to be written to disk right away, so -/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used -/// to write changes to disk. -pub struct Persist { - backend: Box + Send + Sync>, - stage: C, -} - -impl fmt::Debug for Persist { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - write!(fmt, "{:?}", self.stage)?; - Ok(()) - } -} - -impl Persist -where - C: Default + Append, -{ - /// Create a new [`Persist`] from [`PersistBackend`]. - pub fn new(backend: impl PersistBackend + Send + Sync + 'static) -> Self { - let backend = Box::new(backend); - Self { - backend, - stage: Default::default(), - } - } - - /// Stage a `changeset` to be committed later with [`commit`]. - /// - /// [`commit`]: Self::commit - pub fn stage(&mut self, changeset: C) { - self.stage.append(changeset) - } - - /// Get the changes that have not been committed yet. - pub fn staged(&self) -> &C { - &self.stage - } - - /// Commit the staged changes to the underlying persistence backend. - /// - /// Changes that are committed (if any) are returned. - /// - /// # Error - /// - /// Returns a backend-defined error if this fails. - pub fn commit(&mut self) -> anyhow::Result> { - if self.stage.is_empty() { - return Ok(None); - } - self.backend - .write_changes(&self.stage) - // if written successfully, take and return `self.stage` - .map(|_| Some(core::mem::take(&mut self.stage))) - } - - /// Stages a new changeset and commits it (along with any other previously staged changes) to - /// the persistence backend - /// - /// Convenience method for calling [`stage`] and then [`commit`]. - /// - /// [`stage`]: Self::stage - /// [`commit`]: Self::commit - pub fn stage_and_commit(&mut self, changeset: C) -> anyhow::Result> { - self.stage(changeset); - self.commit() - } -} - -/// A persistence backend for [`Persist`]. -/// -/// `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 { - /// 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_from_persistence - fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()>; - - /// Return the aggregate changeset `C` from persistence. - fn load_from_persistence(&mut self) -> anyhow::Result>; -} - -impl PersistBackend for () { - fn write_changes(&mut self, _changeset: &C) -> anyhow::Result<()> { - Ok(()) - } - - fn load_from_persistence(&mut self) -> anyhow::Result> { - Ok(None) - } -} diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 0d382b4b..1343effc 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" [dependencies] anyhow = { version = "1", default-features = false } bdk_chain = { path = "../chain", version = "0.12.0", features = [ "serde", "miniscript" ] } +bdk_persist = { path = "../persist", version = "0.1.0"} bincode = { version = "1" } serde = { version = "1", features = ["derive"] } diff --git a/crates/file_store/README.md b/crates/file_store/README.md index 4a334fcb..54e41e00 100644 --- a/crates/file_store/README.md +++ b/crates/file_store/README.md @@ -1,10 +1,10 @@ # BDK File Store This is a simple append-only flat file implementation of -[`Persist`](`bdk_chain::Persist`). +[`PersistBackend`](bdk_persist::PersistBackend). -The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s +The main structure is [`Store`](crate::Store), which can be used with [`bdk`]'s `Wallet` to persist wallet data into a flat file. [`bdk`]: https://docs.rs/bdk/latest -[`bdk_chain`]: https://docs.rs/bdk_chain/latest +[`bdk_persist`]: https://docs.rs/bdk_persist/latest diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index 16d8f7b7..6cea9276 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -1,6 +1,7 @@ use crate::{bincode_options, EntryIter, FileError, IterError}; use anyhow::anyhow; -use bdk_chain::{Append, PersistBackend}; +use bdk_chain::Append; +use bdk_persist::PersistBackend; use bincode::Options; use std::{ fmt::{self, Debug}, @@ -11,8 +12,6 @@ use std::{ }; /// Persists an append-only list of changesets (`C`) to a single file. -/// -/// The changesets are the results of altering a tracker implementation (`T`). #[derive(Debug)] pub struct Store where @@ -152,7 +151,7 @@ where /// /// You should usually check the error. In many applications, it may make sense to do a full /// wallet scan with a stop-gap after getting an error, since it is likely that one of the - /// changesets it was unable to read changed the derivation indices of the tracker. + /// changesets was unable to read changes of the derivation indices of a keychain. /// /// **WARNING**: This method changes the write position of the underlying file. The next /// changeset will be written over the erroring entry (or the end of the file if none existed). @@ -240,9 +239,6 @@ mod test { type TestChangeSet = BTreeSet; - #[derive(Debug)] - struct TestTracker; - /// Check behavior of [`Store::create_new`] and [`Store::open`]. #[test] fn construct_store() { diff --git a/crates/persist/Cargo.toml b/crates/persist/Cargo.toml new file mode 100644 index 00000000..257ab545 --- /dev/null +++ b/crates/persist/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bdk_persist" +homepage = "https://bitcoindevkit.org" +version = "0.1.0" +repository = "https://github.com/bitcoindevkit/bdk" +documentation = "https://docs.rs/bdk_persist" +description = "Types that define data persistence of a BDK wallet" +keywords = ["bitcoin", "wallet", "persistence", "database"] +readme = "README.md" +license = "MIT OR Apache-2.0" +authors = ["Bitcoin Dev Kit Developers"] +edition = "2021" +rust-version = "1.63" + +[dependencies] +anyhow = { version = "1", default-features = false } +bdk_chain = { path = "../chain", version = "0.12.0", default-features = false } + + diff --git a/crates/persist/README.md b/crates/persist/README.md new file mode 100644 index 00000000..1ed6ec8d --- /dev/null +++ b/crates/persist/README.md @@ -0,0 +1,3 @@ +# BDK Persist + +This crate is home to the [`PersistBackend`](crate::PersistBackend) trait which defines the behavior of a database to perform the task of persisting changes made to BDK data structures. The [`Persist`](crate::Persist) type provides a convenient wrapper around a `PersistBackend` that allows staging changes before committing them. \ No newline at end of file diff --git a/crates/persist/src/lib.rs b/crates/persist/src/lib.rs new file mode 100644 index 00000000..e055f2a4 --- /dev/null +++ b/crates/persist/src/lib.rs @@ -0,0 +1,5 @@ +#![doc = include_str!("../README.md")] +#![no_std] +#![warn(missing_docs)] +mod persist; +pub use persist::*; diff --git a/crates/persist/src/persist.rs b/crates/persist/src/persist.rs new file mode 100644 index 00000000..5d9df3bf --- /dev/null +++ b/crates/persist/src/persist.rs @@ -0,0 +1,106 @@ +extern crate alloc; +use alloc::boxed::Box; +use bdk_chain::Append; +use core::fmt; + +/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes (`C`) +/// before they are persisted. +/// +/// Not all changes to the in-memory representation needs to be written to disk right away, so +/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used +/// to write changes to disk. +pub struct Persist { + backend: Box + Send + Sync>, + stage: C, +} + +impl fmt::Debug for Persist { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + write!(fmt, "{:?}", self.stage)?; + Ok(()) + } +} + +impl Persist +where + C: Default + Append, +{ + /// Create a new [`Persist`] from [`PersistBackend`]. + pub fn new(backend: impl PersistBackend + Send + Sync + 'static) -> Self { + let backend = Box::new(backend); + Self { + backend, + stage: Default::default(), + } + } + + /// Stage a `changeset` to be committed later with [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn stage(&mut self, changeset: C) { + self.stage.append(changeset) + } + + /// Get the changes that have not been committed yet. + pub fn staged(&self) -> &C { + &self.stage + } + + /// Commit the staged changes to the underlying persistence backend. + /// + /// Changes that are committed (if any) are returned. + /// + /// # Error + /// + /// Returns a backend-defined error if this fails. + pub fn commit(&mut self) -> anyhow::Result> { + if self.stage.is_empty() { + return Ok(None); + } + self.backend + .write_changes(&self.stage) + // if written successfully, take and return `self.stage` + .map(|_| Some(core::mem::take(&mut self.stage))) + } + + /// Stages a new changeset and commits it (along with any other previously staged changes) to + /// the persistence backend + /// + /// Convenience method for calling [`stage`] and then [`commit`]. + /// + /// [`stage`]: Self::stage + /// [`commit`]: Self::commit + pub fn stage_and_commit(&mut self, changeset: C) -> anyhow::Result> { + self.stage(changeset); + self.commit() + } +} + +/// A persistence backend for [`Persist`]. +/// +/// `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 { + /// 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_from_persistence + fn write_changes(&mut self, changeset: &C) -> anyhow::Result<()>; + + /// Return the aggregate changeset `C` from persistence. + fn load_from_persistence(&mut self) -> anyhow::Result>; +} + +impl PersistBackend for () { + fn write_changes(&mut self, _changeset: &C) -> anyhow::Result<()> { + Ok(()) + } + + fn load_from_persistence(&mut self) -> anyhow::Result> { + Ok(None) + } +} diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml index c85d2e99..42a0b51b 100644 --- a/example-crates/example_cli/Cargo.toml +++ b/example-crates/example_cli/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]} +bdk_persist = { path = "../../crates/persist" } bdk_file_store = { path = "../../crates/file_store" } bdk_tmp_plan = { path = "../../nursery/tmp_plan" } bdk_coin_select = { path = "../../nursery/coin_select" } diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index e7c4efec..5671a6b8 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -19,9 +19,10 @@ use bdk_chain::{ descriptor::{DescriptorSecretKey, KeyMap}, Descriptor, DescriptorPublicKey, }, - Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend, + Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, }; pub use bdk_file_store; +use bdk_persist::{Persist, PersistBackend}; pub use clap; use clap::{Parser, Subcommand};