]> Untitled Git - bdk/commitdiff
feat!: Rework sqlite, changesets, persistence and wallet-construction
author志宇 <hello@evanlinjin.me>
Thu, 11 Jul 2024 04:49:01 +0000 (04:49 +0000)
committer志宇 <hello@evanlinjin.me>
Thu, 18 Jul 2024 03:25:41 +0000 (03:25 +0000)
Rework sqlite: Instead of only supported one schema (defined in
`bdk_sqlite`), we have a schema per changeset type for more flexiblity.

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

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

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

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

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

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

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

49 files changed:
Cargo.toml
crates/bitcoind_rpc/tests/test_emitter.rs
crates/chain/Cargo.toml
crates/chain/src/changeset.rs
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/indexer/keychain_txout.rs
crates/chain/src/indexer/spk_txout.rs
crates/chain/src/lib.rs
crates/chain/src/local_chain.rs
crates/chain/src/persist.rs [new file with mode: 0644]
crates/chain/src/sqlite.rs [new file with mode: 0644]
crates/chain/src/tx_graph.rs
crates/chain/tests/common/tx_template.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_keychain_txout_index.rs
crates/chain/tests/test_spk_txout_index.rs
crates/electrum/tests/test_electrum.rs
crates/hwi/src/lib.rs
crates/sqlite/Cargo.toml [deleted file]
crates/sqlite/README.md [deleted file]
crates/sqlite/schema/schema_0.sql [deleted file]
crates/sqlite/src/lib.rs [deleted file]
crates/sqlite/src/schema.rs [deleted file]
crates/sqlite/src/store.rs [deleted file]
crates/wallet/Cargo.toml
crates/wallet/README.md
crates/wallet/examples/compiler.rs
crates/wallet/src/descriptor/mod.rs
crates/wallet/src/descriptor/template.rs
crates/wallet/src/lib.rs
crates/wallet/src/wallet/export.rs
crates/wallet/src/wallet/hardwaresigner.rs
crates/wallet/src/wallet/mod.rs
crates/wallet/src/wallet/params.rs [new file with mode: 0644]
crates/wallet/src/wallet/persisted.rs [new file with mode: 0644]
crates/wallet/src/wallet/signer.rs
crates/wallet/tests/common.rs
crates/wallet/tests/wallet.rs
example-crates/example_bitcoind_rpc_polling/src/main.rs
example-crates/example_cli/src/lib.rs
example-crates/example_electrum/src/main.rs
example-crates/example_esplora/src/main.rs
example-crates/wallet_electrum/Cargo.toml
example-crates/wallet_electrum/src/main.rs
example-crates/wallet_esplora_async/Cargo.toml
example-crates/wallet_esplora_async/src/main.rs
example-crates/wallet_esplora_blocking/src/main.rs
example-crates/wallet_rpc/Cargo.toml
example-crates/wallet_rpc/src/main.rs

index 201bb21f3bcabe707939a800cfb54eb1cd5bae5e..f9dbbf885333db368316eafa79b035b9fd8919e3 100644 (file)
@@ -4,7 +4,6 @@ members = [
     "crates/wallet",
     "crates/chain",
     "crates/file_store",
-    "crates/sqlite",
     "crates/electrum",
     "crates/esplora",
     "crates/bitcoind_rpc",
index d7c8b60f75871c975fd935d5e0b295b7e3160601..3a5c67055db4f330503dd2975373caf16113d8b9 100644 (file)
@@ -4,7 +4,8 @@ use bdk_bitcoind_rpc::Emitter;
 use bdk_chain::{
     bitcoin::{Address, Amount, Txid},
     local_chain::{CheckPoint, LocalChain},
-    Balance, BlockId, IndexedTxGraph, Merge, SpkTxOutIndex,
+    spk_txout::SpkTxOutIndex,
+    Balance, BlockId, IndexedTxGraph, Merge,
 };
 use bdk_testenv::{anyhow, TestEnv};
 use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
@@ -47,7 +48,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
 
         assert_eq!(
             local_chain.apply_update(emission.checkpoint,)?,
-            BTreeMap::from([(height, Some(hash))]),
+            [(height, Some(hash))].into(),
             "chain update changeset is unexpected",
         );
     }
@@ -93,11 +94,13 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
         assert_eq!(
             local_chain.apply_update(emission.checkpoint,)?,
             if exp_height == exp_hashes.len() - reorged_blocks.len() {
-                core::iter::once((height, Some(hash)))
-                    .chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
-                    .collect::<bdk_chain::local_chain::ChangeSet>()
+                bdk_chain::local_chain::ChangeSet {
+                    blocks: core::iter::once((height, Some(hash)))
+                        .chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
+                        .collect(),
+                }
             } else {
-                BTreeMap::from([(height, Some(hash))])
+                [(height, Some(hash))].into()
             },
             "chain update changeset is unexpected",
         );
@@ -193,7 +196,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
         let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs);
         assert_eq!(
             indexed_additions
-                .graph
+                .tx_graph
                 .txs
                 .iter()
                 .map(|tx| tx.compute_txid())
@@ -201,7 +204,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
             exp_txids,
             "changeset should have the 3 mempool transactions",
         );
-        assert!(indexed_additions.graph.anchors.is_empty());
+        assert!(indexed_additions.tx_graph.anchors.is_empty());
     }
 
     // mine a block that confirms the 3 txs
@@ -224,9 +227,9 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
         let height = emission.block_height();
         let _ = chain.apply_update(emission.checkpoint)?;
         let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
-        assert!(indexed_additions.graph.txs.is_empty());
-        assert!(indexed_additions.graph.txouts.is_empty());
-        assert_eq!(indexed_additions.graph.anchors, exp_anchors);
+        assert!(indexed_additions.tx_graph.txs.is_empty());
+        assert!(indexed_additions.tx_graph.txouts.is_empty());
+        assert_eq!(indexed_additions.tx_graph.anchors, exp_anchors);
     }
 
     Ok(())
index df3fe41e9c16cd16bb9366477261f941f88fec71..2e704b1de4e4efc9f2b1071302b42b10ba4de041 100644 (file)
@@ -20,6 +20,10 @@ serde_crate = { package = "serde", version = "1", optional = true, features = ["
 hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
 miniscript = { version = "12.0.0", optional = true, default-features = false }
 
+# Feature dependencies
+rusqlite = { version = "0.31.0", features = ["bundled"], optional = true }
+serde_json = {version = "1", optional = true }
+
 [dev-dependencies]
 rand = "0.8"
 proptest = "1.2.0"
@@ -28,3 +32,4 @@ proptest = "1.2.0"
 default = ["std", "miniscript"]
 std = ["bitcoin/std", "miniscript?/std"]
 serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
+sqlite = ["std", "rusqlite", "serde", "serde_json"]
index e5777f46cb14c11dca33c97547ba074437002768..77a7ca233f344148ce3c8bbcf8a1585ffba4054f 100644 (file)
+use crate::{ConfirmationBlockTime, Merge};
+
+type IndexedTxGraphChangeSet =
+    crate::indexed_tx_graph::ChangeSet<ConfirmationBlockTime, crate::keychain_txout::ChangeSet>;
+
 /// A changeset containing [`crate`] structures typically persisted together.
-#[cfg(feature = "miniscript")]
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Default, 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",
-        ),
-    )
+    serde(crate = "crate::serde")
 )]
-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::indexer::keychain_txout::ChangeSet<K>>,
+pub struct WalletChangeSet {
+    /// Descriptor for recipient addresses.
+    pub descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
+    /// Descriptor for change addresses.
+    pub change_descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
     /// Stores the network type of the transaction data.
     pub network: Option<bitcoin::Network>,
+    /// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
+    pub local_chain: crate::local_chain::ChangeSet,
+    /// Changes to [`TxGraph`](crate::tx_graph::TxGraph).
+    pub tx_graph: crate::tx_graph::ChangeSet<crate::ConfirmationBlockTime>,
+    /// Changes to [`KeychainTxOutIndex`](crate::keychain_txout::KeychainTxOutIndex).
+    pub indexer: crate::keychain_txout::ChangeSet,
 }
 
-#[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::Merge for CombinedChangeSet<K, A> {
+impl Merge for WalletChangeSet {
+    /// Merge another [`WalletChangeSet`] into itself.
+    ///
+    /// The `keychains_added` field respects the invariants of... TODO: FINISH THIS!
     fn merge(&mut self, other: Self) {
-        crate::Merge::merge(&mut self.chain, other.chain);
-        crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph);
+        if other.descriptor.is_some() {
+            debug_assert!(
+                self.descriptor.is_none() || self.descriptor == other.descriptor,
+                "descriptor must never change"
+            );
+            self.descriptor = other.descriptor;
+        }
+        if other.change_descriptor.is_some() {
+            debug_assert!(
+                self.change_descriptor.is_none()
+                    || self.change_descriptor == other.change_descriptor,
+                "change descriptor must never change"
+            );
+        }
         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"
+                "network must never change"
             );
             self.network = other.network;
         }
+
+        crate::Merge::merge(&mut self.local_chain, other.local_chain);
+        crate::Merge::merge(&mut self.tx_graph, other.tx_graph);
+        crate::Merge::merge(&mut self.indexer, other.indexer);
     }
 
     fn is_empty(&self) -> bool {
-        self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
+        self.descriptor.is_none()
+            && self.change_descriptor.is_none()
+            && self.network.is_none()
+            && self.local_chain.is_empty()
+            && self.tx_graph.is_empty()
+            && self.indexer.is_empty()
+    }
+}
+
+#[cfg(feature = "sqlite")]
+impl WalletChangeSet {
+    /// Schema name for wallet.
+    pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
+    /// Name of table to store wallet descriptors and network.
+    pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
+
+    /// Initialize sqlite tables for wallet schema & table.
+    fn init_wallet_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        let schema_v0: &[&str] = &[&format!(
+            "CREATE TABLE {} ( \
+                id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \
+                descriptor TEXT, \
+                change_descriptor TEXT, \
+                network TEXT \
+                ) STRICT;",
+            Self::WALLET_TABLE_NAME,
+        )];
+        crate::sqlite::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])
+    }
+
+    /// Recover a [`WalletChangeSet`] from sqlite database.
+    pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+        Self::init_wallet_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+        use miniscript::{Descriptor, DescriptorPublicKey};
+        use rusqlite::OptionalExtension;
+
+        let mut changeset = Self::default();
+
+        let mut wallet_statement = db_tx.prepare(&format!(
+            "SELECT descriptor, change_descriptor, network FROM {}",
+            Self::WALLET_TABLE_NAME,
+        ))?;
+        let row = wallet_statement
+            .query_row([], |row| {
+                Ok((
+                    row.get::<_, Sql<Descriptor<DescriptorPublicKey>>>("descriptor")?,
+                    row.get::<_, Sql<Descriptor<DescriptorPublicKey>>>("change_descriptor")?,
+                    row.get::<_, Sql<bitcoin::Network>>("network")?,
+                ))
+            })
+            .optional()?;
+        if let Some((Sql(desc), Sql(change_desc), Sql(network))) = row {
+            changeset.descriptor = Some(desc);
+            changeset.change_descriptor = Some(change_desc);
+            changeset.network = Some(network);
+        }
+
+        changeset.local_chain = crate::local_chain::ChangeSet::from_sqlite(db_tx)?;
+        changeset.tx_graph = crate::tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
+        changeset.indexer = crate::indexer::keychain_txout::ChangeSet::from_sqlite(db_tx)?;
+
+        Ok(changeset)
+    }
+
+    /// Persist [`WalletChangeSet`] to sqlite database.
+    pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        Self::init_wallet_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+        use rusqlite::named_params;
+
+        let mut descriptor_statement = db_tx.prepare_cached(&format!(
+            "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor",
+            Self::WALLET_TABLE_NAME,
+        ))?;
+        if let Some(descriptor) = &self.descriptor {
+            descriptor_statement.execute(named_params! {
+                ":id": 0,
+                ":descriptor": Sql(descriptor.clone()),
+            })?;
+        }
+
+        let mut change_descriptor_statement = db_tx.prepare_cached(&format!(
+            "INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor",
+            Self::WALLET_TABLE_NAME,
+        ))?;
+        if let Some(change_descriptor) = &self.change_descriptor {
+            change_descriptor_statement.execute(named_params! {
+                ":id": 0,
+                ":change_descriptor": Sql(change_descriptor.clone()),
+            })?;
+        }
+
+        let mut network_statement = db_tx.prepare_cached(&format!(
+            "INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network",
+            Self::WALLET_TABLE_NAME,
+        ))?;
+        if let Some(network) = self.network {
+            network_statement.execute(named_params! {
+                ":id": 0,
+                ":network": Sql(network),
+            })?;
+        }
+
+        self.local_chain.persist_to_sqlite(db_tx)?;
+        self.tx_graph.persist_to_sqlite(db_tx)?;
+        self.indexer.persist_to_sqlite(db_tx)?;
+        Ok(())
     }
 }
 
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
+impl From<crate::local_chain::ChangeSet> for WalletChangeSet {
     fn from(chain: crate::local_chain::ChangeSet) -> Self {
         Self {
-            chain,
+            local_chain: chain,
+            ..Default::default()
+        }
+    }
+}
+
+impl From<IndexedTxGraphChangeSet> for WalletChangeSet {
+    fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self {
+        Self {
+            tx_graph: indexed_tx_graph.tx_graph,
+            indexer: indexed_tx_graph.indexer,
             ..Default::default()
         }
     }
 }
 
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>>
-    for CombinedChangeSet<K, A>
-{
-    fn from(
-        indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<
-            A,
-            crate::indexer::keychain_txout::ChangeSet<K>,
-        >,
-    ) -> Self {
+impl From<crate::tx_graph::ChangeSet<ConfirmationBlockTime>> for WalletChangeSet {
+    fn from(tx_graph: crate::tx_graph::ChangeSet<ConfirmationBlockTime>) -> Self {
         Self {
-            indexed_tx_graph,
+            tx_graph,
             ..Default::default()
         }
     }
 }
 
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::indexer::keychain_txout::ChangeSet<K>> for CombinedChangeSet<K, A> {
-    fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self {
+impl From<crate::keychain_txout::ChangeSet> for WalletChangeSet {
+    fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
         Self {
-            indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
-                indexer,
-                ..Default::default()
-            },
+            indexer,
             ..Default::default()
         }
     }
index 7181768a1d458190b0ddb0658a76860143e909cc..a8048d845622ba858e622d811d4db595c25ba952 100644 (file)
@@ -1,5 +1,7 @@
 //! Contains the [`IndexedTxGraph`] and associated types. Refer to the
 //! [`IndexedTxGraph`] documentation for more.
+use core::fmt::Debug;
+
 use alloc::vec::Vec;
 use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
 
@@ -47,21 +49,24 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
     pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
         self.index.apply_changeset(changeset.indexer);
 
-        for tx in &changeset.graph.txs {
+        for tx in &changeset.tx_graph.txs {
             self.index.index_tx(tx);
         }
-        for (&outpoint, txout) in &changeset.graph.txouts {
+        for (&outpoint, txout) in &changeset.tx_graph.txouts {
             self.index.index_txout(outpoint, txout);
         }
 
-        self.graph.apply_changeset(changeset.graph);
+        self.graph.apply_changeset(changeset.tx_graph);
     }
 
     /// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
     pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
         let graph = self.graph.initial_changeset();
         let indexer = self.index.initial_changeset();
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 }
 
@@ -89,21 +94,30 @@ where
     pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
         let graph = self.graph.apply_update(update);
         let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 
     /// Insert a floating `txout` of given `outpoint`.
     pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
         let graph = self.graph.insert_txout(outpoint, txout);
         let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 
     /// Insert and index a transaction into the graph.
     pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
         let graph = self.graph.insert_tx(tx);
         let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 
     /// Insert an `anchor` for a given transaction.
@@ -151,7 +165,10 @@ where
             }
         }
 
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 
     /// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
@@ -185,7 +202,10 @@ where
                 .map(|(tx, seen_at)| (tx.clone(), seen_at)),
         );
 
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 
     /// Batch insert unconfirmed transactions.
@@ -203,7 +223,10 @@ where
     ) -> ChangeSet<A, I::ChangeSet> {
         let graph = self.graph.batch_insert_unconfirmed(txs);
         let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
     }
 }
 
@@ -236,9 +259,9 @@ where
             if self.index.is_tx_relevant(tx) {
                 let txid = tx.compute_txid();
                 let anchor = A::from_block_position(block, block_id, tx_pos);
-                changeset.graph.merge(self.graph.insert_tx(tx.clone()));
+                changeset.tx_graph.merge(self.graph.insert_tx(tx.clone()));
                 changeset
-                    .graph
+                    .tx_graph
                     .merge(self.graph.insert_anchor(txid, anchor));
             }
         }
@@ -265,7 +288,16 @@ where
             graph.merge(self.graph.insert_tx(tx.clone()));
         }
         let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet { graph, indexer }
+        ChangeSet {
+            tx_graph: graph,
+            indexer,
+        }
+    }
+}
+
+impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
+    fn as_ref(&self) -> &TxGraph<A> {
+        &self.graph
     }
 }
 
@@ -285,7 +317,7 @@ where
 #[must_use]
 pub struct ChangeSet<A, IA> {
     /// [`TxGraph`] changeset.
-    pub graph: tx_graph::ChangeSet<A>,
+    pub tx_graph: tx_graph::ChangeSet<A>,
     /// [`Indexer`] changeset.
     pub indexer: IA,
 }
@@ -293,7 +325,7 @@ pub struct ChangeSet<A, IA> {
 impl<A, IA: Default> Default for ChangeSet<A, IA> {
     fn default() -> Self {
         Self {
-            graph: Default::default(),
+            tx_graph: Default::default(),
             indexer: Default::default(),
         }
     }
@@ -301,38 +333,30 @@ impl<A, IA: Default> Default for ChangeSet<A, IA> {
 
 impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> {
     fn merge(&mut self, other: Self) {
-        self.graph.merge(other.graph);
+        self.tx_graph.merge(other.tx_graph);
         self.indexer.merge(other.indexer);
     }
 
     fn is_empty(&self) -> bool {
-        self.graph.is_empty() && self.indexer.is_empty()
+        self.tx_graph.is_empty() && self.indexer.is_empty()
     }
 }
 
 impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
     fn from(graph: tx_graph::ChangeSet<A>) -> Self {
         Self {
-            graph,
+            tx_graph: graph,
             ..Default::default()
         }
     }
 }
 
 #[cfg(feature = "miniscript")]
-impl<A, K> From<crate::indexer::keychain_txout::ChangeSet<K>>
-    for ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>
-{
-    fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self {
+impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
+    fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
         Self {
-            graph: Default::default(),
+            tx_graph: Default::default(),
             indexer,
         }
     }
 }
-
-impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
-    fn as_ref(&self) -> &TxGraph<A> {
-        &self.graph
-    }
-}
index 4bbf026af3f02f5d2764414596b7b15c62b42ce2..4b81304f7bf984cb9700477090ee9ded795c32f5 100644 (file)
@@ -5,7 +5,8 @@ use crate::{
     collections::*,
     miniscript::{Descriptor, DescriptorPublicKey},
     spk_iter::BIP32_MAX_INDEX,
-    DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator, SpkTxOutIndex,
+    spk_txout::SpkTxOutIndex,
+    DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
 };
 use alloc::{borrow::ToOwned, vec::Vec};
 use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
@@ -135,7 +136,7 @@ impl<K> Default for KeychainTxOutIndex<K> {
 }
 
 impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
-    type ChangeSet = ChangeSet<K>;
+    type ChangeSet = ChangeSet;
 
     fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
         let mut changeset = ChangeSet::default();
@@ -154,7 +155,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
     }
 
     fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet {
-        let mut changeset = ChangeSet::<K>::default();
+        let mut changeset = ChangeSet::default();
         let txid = tx.compute_txid();
         for (op, txout) in tx.output.iter().enumerate() {
             changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout));
@@ -164,10 +165,6 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
 
     fn initial_changeset(&self) -> Self::ChangeSet {
         ChangeSet {
-            keychains_added: self
-                .keychains()
-                .map(|(k, v)| (k.clone(), v.clone()))
-                .collect(),
             last_revealed: self.last_revealed.clone().into_iter().collect(),
         }
     }
@@ -354,7 +351,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     /// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so
     /// will return a [`InsertDescriptorError<K>`].
     ///
-    /// `[KeychainTxOutIndex]` will prevent you from inserting two descriptors which derive the same
+    /// [`KeychainTxOutIndex`] will prevent you from inserting two descriptors which derive the same
     /// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at
     /// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level
     /// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have
@@ -364,8 +361,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
         &mut self,
         keychain: K,
         descriptor: Descriptor<DescriptorPublicKey>,
-    ) -> Result<ChangeSet<K>, InsertDescriptorError<K>> {
-        let mut changeset = ChangeSet::<K>::default();
+    ) -> Result<bool, InsertDescriptorError<K>> {
         let did = descriptor.descriptor_id();
         if !self.keychain_to_descriptor_id.contains_key(&keychain)
             && !self.descriptor_id_to_keychain.contains_key(&did)
@@ -374,33 +370,31 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
             self.keychain_to_descriptor_id.insert(keychain.clone(), did);
             self.descriptor_id_to_keychain.insert(did, keychain.clone());
             self.replenish_inner_index(did, &keychain, self.lookahead);
-            changeset
-                .keychains_added
-                .insert(keychain.clone(), descriptor);
-        } else {
-            if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
-                let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
-                if *existing_desc_id != did {
-                    return Err(InsertDescriptorError::KeychainAlreadyAssigned {
-                        existing_assignment: descriptor.clone(),
-                        keychain,
-                    });
-                }
+            return Ok(true);
+        }
+
+        if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
+            let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
+            if *existing_desc_id != did {
+                return Err(InsertDescriptorError::KeychainAlreadyAssigned {
+                    existing_assignment: descriptor.clone(),
+                    keychain,
+                });
             }
+        }
 
-            if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
-                let descriptor = self.descriptors.get(&did).expect("invariant").clone();
+        if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
+            let descriptor = self.descriptors.get(&did).expect("invariant").clone();
 
-                if *existing_keychain != keychain {
-                    return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
-                        existing_assignment: existing_keychain.clone(),
-                        descriptor,
-                    });
-                }
+            if *existing_keychain != keychain {
+                return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
+                    existing_assignment: existing_keychain.clone(),
+                    descriptor,
+                });
             }
         }
 
-        Ok(changeset)
+        Ok(false)
     }
 
     /// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't
@@ -627,7 +621,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     }
 
     /// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
-    pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet<K> {
+    pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet {
         let mut changeset = ChangeSet::default();
 
         for (keychain, &index) in keychains {
@@ -656,7 +650,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
         &mut self,
         keychain: &K,
         target_index: u32,
-    ) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet<K>)> {
+    ) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet)> {
         let mut changeset = ChangeSet::default();
         let mut spks: Vec<Indexed<ScriptBuf>> = vec![];
         while let Some((i, new)) = self.next_index(keychain) {
@@ -687,7 +681,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     ///  1. The descriptor has no wildcard and already has one script revealed.
     ///  2. The descriptor has already revealed scripts up to the numeric bound.
     ///  3. There is no descriptor associated with the given keychain.
-    pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
+    pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
         let (next_index, new) = self.next_index(keychain)?;
         let mut changeset = ChangeSet::default();
 
@@ -717,7 +711,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     /// could be revealed (see [`reveal_next_spk`] for when this happens).
     ///
     /// [`reveal_next_spk`]: Self::reveal_next_spk
-    pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
+    pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
         let next_unused = self
             .unused_keychain_spks(keychain)
             .next()
@@ -780,27 +774,80 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
     }
 
     /// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
-    ///
-    /// Keychains added by the `keychains_added` field of `ChangeSet<K>` respect the one-to-one
-    /// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will
-    /// panic if `debug_assertions` are enabled).
-    pub fn apply_changeset(&mut self, changeset: ChangeSet<K>) {
-        let ChangeSet {
-            keychains_added,
-            last_revealed,
-        } = changeset;
-        for (keychain, descriptor) in keychains_added {
-            let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor);
-        }
-
-        for (&desc_id, &index) in &last_revealed {
+    pub fn apply_changeset(&mut self, changeset: ChangeSet) {
+        for (&desc_id, &index) in &changeset.last_revealed {
             let v = self.last_revealed.entry(desc_id).or_default();
             *v = index.max(*v);
+            self.replenish_inner_index_did(desc_id, self.lookahead);
         }
+    }
+}
 
-        for did in last_revealed.keys() {
-            self.replenish_inner_index_did(*did, self.lookahead);
+#[cfg(feature = "sqlite")]
+impl ChangeSet {
+    /// Schema name for the changeset.
+    pub const SCHEMA_NAME: &'static str = "bdk_keychaintxout";
+    /// Name for table that stores last revealed indices per descriptor id.
+    pub const LAST_REVEALED_TABLE_NAME: &'static str = "bdk_descriptor_last_revealed";
+
+    /// Initialize sqlite tables for persisting [`KeychainTxOutIndex`].
+    fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        let schema_v0: &[&str] = &[
+            // last revealed
+            &format!(
+                "CREATE TABLE {} ( \
+                descriptor_id TEXT PRIMARY KEY NOT NULL, \
+                last_revealed INTEGER NOT NULL \
+                ) STRICT",
+                Self::LAST_REVEALED_TABLE_NAME,
+            ),
+        ];
+        crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+    }
+
+    /// Construct [`KeychainTxOutIndex`] from sqlite database and given parameters.
+    pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+
+        let mut changeset = Self::default();
+
+        let mut statement = db_tx.prepare(&format!(
+            "SELECT descriptor_id, last_revealed FROM {}",
+            Self::LAST_REVEALED_TABLE_NAME,
+        ))?;
+        let row_iter = statement.query_map([], |row| {
+            Ok((
+                row.get::<_, Sql<DescriptorId>>("descriptor_id")?,
+                row.get::<_, u32>("last_revealed")?,
+            ))
+        })?;
+        for row in row_iter {
+            let (Sql(descriptor_id), last_revealed) = row?;
+            changeset.last_revealed.insert(descriptor_id, last_revealed);
         }
+
+        Ok(changeset)
+    }
+
+    /// Persist `changeset` to the sqlite database.
+    pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::rusqlite::named_params;
+        use crate::sqlite::Sql;
+
+        let mut statement = db_tx.prepare_cached(&format!(
+            "REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)",
+            Self::LAST_REVEALED_TABLE_NAME,
+        ))?;
+        for (&descriptor_id, &last_revealed) in &self.last_revealed {
+            statement.execute(named_params! {
+                ":descriptor_id": Sql(descriptor_id),
+                ":last_revealed": last_revealed,
+            })?;
+        }
+
+        Ok(())
     }
 }
 
@@ -860,49 +907,24 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
 /// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the
 /// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself.
 ///
-/// [`apply_changeset`]: KeychainTxOutIndex::apply_changeset
-/// [`Merge`]: Self::merge
-#[derive(Clone, Debug, PartialEq)]
+/// [`KeychainTxOutIndex`]: crate::keychain_txout::KeychainTxOutIndex
+/// [`apply_changeset`]: crate::keychain_txout::KeychainTxOutIndex::apply_changeset
+/// [`merge`]: Self::merge
+#[derive(Clone, Debug, Default, PartialEq)]
 #[cfg_attr(
     feature = "serde",
     derive(serde::Deserialize, serde::Serialize),
-    serde(
-        crate = "serde_crate",
-        bound(
-            deserialize = "K: Ord + serde::Deserialize<'de>",
-            serialize = "K: Ord + serde::Serialize"
-        )
-    )
+    serde(crate = "serde_crate")
 )]
 #[must_use]
-pub struct ChangeSet<K> {
-    /// Contains the keychains that have been added and their respective descriptor
-    pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
+pub struct ChangeSet {
     /// Contains for each descriptor_id the last revealed index of derivation
     pub last_revealed: BTreeMap<DescriptorId, u32>,
 }
 
-impl<K: Ord> Merge for ChangeSet<K> {
-    /// Merge another [`ChangeSet<K>`] into self.
-    ///
-    /// For the `keychains_added` field this method respects the invariants of
-    /// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two.
-    ///
-    /// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor
+impl Merge for ChangeSet {
+    /// Merge another [`ChangeSet`] into self.
     fn merge(&mut self, other: Self) {
-        for (new_keychain, new_descriptor) in other.keychains_added {
-            // enforce 1-to-1 invariance
-            if !self.keychains_added.contains_key(&new_keychain)
-                // FIXME: very inefficient
-                && self
-                    .keychains_added
-                    .values()
-                    .all(|descriptor| descriptor != &new_descriptor)
-            {
-                self.keychains_added.insert(new_keychain, new_descriptor);
-            }
-        }
-
         // for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than
         // what was originally in `self`.
         for (desc_id, index) in other.last_revealed {
@@ -922,25 +944,6 @@ impl<K: Ord> Merge for ChangeSet<K> {
 
     /// Returns whether the changeset are empty.
     fn is_empty(&self) -> bool {
-        self.last_revealed.is_empty() && self.keychains_added.is_empty()
-    }
-}
-
-impl<K> Default for ChangeSet<K> {
-    fn default() -> Self {
-        Self {
-            last_revealed: BTreeMap::default(),
-            keychains_added: BTreeMap::default(),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`].
-pub struct NoSuchKeychain<K>(K);
-
-impl<K: Debug> core::fmt::Display for NoSuchKeychain<K> {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
-        write!(f, "no such keychain {:?} exists", &self.0)
+        self.last_revealed.is_empty()
     }
 }
index ead446a76ef7e186ae9c25ee6bb946acad4fd453..b3cd923eeeba42bb45983f3a4058153dc063ddb0 100644 (file)
@@ -208,7 +208,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
     /// # Example
     ///
     /// ```rust
-    /// # use bdk_chain::SpkTxOutIndex;
+    /// # use bdk_chain::spk_txout::SpkTxOutIndex;
     ///
     /// // imagine our spks are indexed like (keychain, derivation_index).
     /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
index ec0c61a35db3cf2c4c64f956f02e52c6e369e192..b95a1594e7b71f8891496843388ff566bf42fc54 100644 (file)
@@ -28,7 +28,7 @@ pub use chain_data::*;
 pub mod indexed_tx_graph;
 pub use indexed_tx_graph::IndexedTxGraph;
 pub mod indexer;
-pub use indexer::spk_txout::*;
+pub use indexer::spk_txout;
 pub use indexer::Indexer;
 pub mod local_chain;
 mod tx_data_traits;
@@ -37,6 +37,8 @@ pub use tx_data_traits::*;
 pub use tx_graph::TxGraph;
 mod chain_oracle;
 pub use chain_oracle::*;
+mod persist;
+pub use persist::*;
 
 #[doc(hidden)]
 pub mod example_utils;
@@ -51,8 +53,16 @@ pub use descriptor_ext::{DescriptorExt, DescriptorId};
 mod spk_iter;
 #[cfg(feature = "miniscript")]
 pub use spk_iter::*;
+#[cfg(feature = "miniscript")]
 mod changeset;
+#[cfg(feature = "miniscript")]
 pub use changeset::*;
+#[cfg(feature = "miniscript")]
+pub use indexer::keychain_txout;
+#[cfg(feature = "sqlite")]
+pub mod sqlite;
+#[cfg(feature = "sqlite")]
+pub use rusqlite;
 pub mod spk_client;
 
 #[allow(unused_imports)]
index 2c396cb33e3d07a00e4e9047e2eca96867c295a4..32394bc58121f590b1d66349c8efc4df8070e260 100644 (file)
@@ -4,17 +4,11 @@ use core::convert::Infallible;
 use core::ops::RangeBounds;
 
 use crate::collections::BTreeMap;
-use crate::{BlockId, ChainOracle};
+use crate::{BlockId, ChainOracle, Merge};
 use alloc::sync::Arc;
 use bitcoin::block::Header;
 use bitcoin::BlockHash;
 
-/// The [`ChangeSet`] represents changes to [`LocalChain`].
-///
-/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
-/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
-pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
-
 /// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
 /// transaction anchor.
 ///
@@ -216,7 +210,7 @@ impl CheckPoint {
 
     /// Apply `changeset` to the checkpoint.
     fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
-        if let Some(start_height) = changeset.keys().next().cloned() {
+        if let Some(start_height) = changeset.blocks.keys().next().cloned() {
             // changes after point of agreement
             let mut extension = BTreeMap::default();
             // point of agreement
@@ -231,7 +225,7 @@ impl CheckPoint {
                 }
             }
 
-            for (&height, &hash) in changeset {
+            for (&height, &hash) in &changeset.blocks {
                 match hash {
                     Some(hash) => {
                         extension.insert(height, hash);
@@ -331,7 +325,7 @@ impl LocalChain {
 
     /// Construct a [`LocalChain`] from an initial `changeset`.
     pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
-        let genesis_entry = changeset.get(&0).copied().flatten();
+        let genesis_entry = changeset.blocks.get(&0).copied().flatten();
         let genesis_hash = match genesis_entry {
             Some(hash) => hash,
             None => return Err(MissingGenesisError),
@@ -521,12 +515,14 @@ impl LocalChain {
         }
 
         let mut changeset = ChangeSet::default();
-        changeset.insert(block_id.height, Some(block_id.hash));
+        changeset
+            .blocks
+            .insert(block_id.height, Some(block_id.hash));
         self.apply_changeset(&changeset)
             .map_err(|_| AlterCheckPointError {
                 height: 0,
                 original_hash: self.genesis_hash(),
-                update_hash: changeset.get(&0).cloned().flatten(),
+                update_hash: changeset.blocks.get(&0).cloned().flatten(),
             })?;
         Ok(changeset)
     }
@@ -548,7 +544,7 @@ impl LocalChain {
             if cp_id.height < block_id.height {
                 break;
             }
-            changeset.insert(cp_id.height, None);
+            changeset.blocks.insert(cp_id.height, None);
             if cp_id == block_id {
                 remove_from = Some(cp);
             }
@@ -569,13 +565,16 @@ impl LocalChain {
     /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
     /// recover the current chain.
     pub fn initial_changeset(&self) -> ChangeSet {
-        self.tip
-            .iter()
-            .map(|cp| {
-                let block_id = cp.block_id();
-                (block_id.height, Some(block_id.hash))
-            })
-            .collect()
+        ChangeSet {
+            blocks: self
+                .tip
+                .iter()
+                .map(|cp| {
+                    let block_id = cp.block_id();
+                    (block_id.height, Some(block_id.hash))
+                })
+                .collect(),
+        }
     }
 
     /// Iterate over checkpoints in descending height order.
@@ -587,7 +586,7 @@ impl LocalChain {
 
     fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
         let mut curr_cp = self.tip.clone();
-        for (height, exp_hash) in changeset.iter().rev() {
+        for (height, exp_hash) in changeset.blocks.iter().rev() {
             match curr_cp.get(*height) {
                 Some(query_cp) => {
                     if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
@@ -630,6 +629,135 @@ impl LocalChain {
     }
 }
 
+/// The [`ChangeSet`] represents changes to [`LocalChain`].
+#[derive(Debug, Default, Clone, PartialEq)]
+#[cfg_attr(
+    feature = "serde",
+    derive(serde::Deserialize, serde::Serialize),
+    serde(crate = "serde_crate")
+)]
+pub struct ChangeSet {
+    /// Changes to the [`LocalChain`] blocks.
+    ///
+    /// The key represents the block height, and the value either represents added a new [`CheckPoint`]
+    /// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
+    pub blocks: BTreeMap<u32, Option<BlockHash>>,
+}
+
+impl Merge for ChangeSet {
+    fn merge(&mut self, other: Self) {
+        Merge::merge(&mut self.blocks, other.blocks)
+    }
+
+    fn is_empty(&self) -> bool {
+        self.blocks.is_empty()
+    }
+}
+
+impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
+    fn from(blocks: B) -> Self {
+        Self {
+            blocks: blocks.into_iter().collect(),
+        }
+    }
+}
+
+impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
+    fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
+        Self {
+            blocks: iter.into_iter().collect(),
+        }
+    }
+}
+
+impl FromIterator<(u32, BlockHash)> for ChangeSet {
+    fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
+        Self {
+            blocks: iter
+                .into_iter()
+                .map(|(height, hash)| (height, Some(hash)))
+                .collect(),
+        }
+    }
+}
+
+#[cfg(feature = "sqlite")]
+impl ChangeSet {
+    /// Schema name for the changeset.
+    pub const SCHEMA_NAME: &'static str = "bdk_localchain";
+    /// Name of sqlite table that stores blocks of [`LocalChain`].
+    pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks";
+
+    /// Initialize sqlite tables for persisting [`LocalChain`].
+    fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        let schema_v0: &[&str] = &[
+            // blocks
+            &format!(
+                "CREATE TABLE {} ( \
+                block_height INTEGER PRIMARY KEY NOT NULL, \
+                block_hash TEXT NOT NULL \
+                ) STRICT",
+                Self::BLOCKS_TABLE_NAME,
+            ),
+        ];
+        crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+    }
+
+    /// Construct a [`LocalChain`] from sqlite database.
+    pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+
+        let mut changeset = Self::default();
+
+        let mut statement = db_tx.prepare(&format!(
+            "SELECT block_height, block_hash FROM {}",
+            Self::BLOCKS_TABLE_NAME,
+        ))?;
+        let row_iter = statement.query_map([], |row| {
+            Ok((
+                row.get::<_, u32>("block_height")?,
+                row.get::<_, Sql<BlockHash>>("block_hash")?,
+            ))
+        })?;
+        for row in row_iter {
+            let (height, Sql(hash)) = row?;
+            changeset.blocks.insert(height, Some(hash));
+        }
+
+        Ok(changeset)
+    }
+
+    /// Persist `changeset` to the sqlite database.
+    pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+        use rusqlite::named_params;
+
+        let mut replace_statement = db_tx.prepare_cached(&format!(
+            "REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)",
+            Self::BLOCKS_TABLE_NAME,
+        ))?;
+        let mut delete_statement = db_tx.prepare_cached(&format!(
+            "DELETE FROM {} WHERE block_height=:block_height",
+            Self::BLOCKS_TABLE_NAME,
+        ))?;
+        for (&height, &hash) in &self.blocks {
+            match hash {
+                Some(hash) => replace_statement.execute(named_params! {
+                    ":block_height": height,
+                    ":block_hash": Sql(hash),
+                })?,
+                None => delete_statement.execute(named_params! {
+                    ":block_height": height,
+                })?,
+            };
+        }
+
+        Ok(())
+    }
+}
+
 /// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
 #[derive(Clone, Debug, PartialEq)]
 pub struct MissingGenesisError;
@@ -761,7 +889,7 @@ fn merge_chains(
         match (curr_orig.as_ref(), curr_update.as_ref()) {
             // Update block that doesn't exist in the original chain
             (o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
-                changeset.insert(u.height(), Some(u.hash()));
+                changeset.blocks.insert(u.height(), Some(u.hash()));
                 prev_update = curr_update.take();
             }
             // Original block that isn't in the update
@@ -813,9 +941,9 @@ fn merge_chains(
                 } else {
                     // We have an invalidation height so we set the height to the updated hash and
                     // also purge all the original chain block hashes above this block.
-                    changeset.insert(u.height(), Some(u.hash()));
+                    changeset.blocks.insert(u.height(), Some(u.hash()));
                     for invalidated_height in potentially_invalidated_heights.drain(..) {
-                        changeset.insert(invalidated_height, None);
+                        changeset.blocks.insert(invalidated_height, None);
                     }
                     prev_orig_was_invalidated = true;
                 }
diff --git a/crates/chain/src/persist.rs b/crates/chain/src/persist.rs
new file mode 100644 (file)
index 0000000..cdaf6d5
--- /dev/null
@@ -0,0 +1,135 @@
+use core::{
+    future::Future,
+    ops::{Deref, DerefMut},
+    pin::Pin,
+};
+
+use alloc::boxed::Box;
+
+/// Trait that persists the type with `Db`.
+///
+/// Methods of this trait should not be called directly.
+pub trait PersistWith<Db>: Sized {
+    /// Parameters for [`PersistWith::create`].
+    type CreateParams;
+    /// Parameters for [`PersistWith::load`].
+    type LoadParams;
+    /// Error type of [`PersistWith::create`].
+    type CreateError;
+    /// Error type of [`PersistWith::load`].
+    type LoadError;
+    /// Error type of [`PersistWith::persist`].
+    type PersistError;
+
+    /// Create the type and initialize the `Db`.
+    fn create(db: &mut Db, params: Self::CreateParams) -> Result<Self, Self::CreateError>;
+
+    /// Load the type from the `Db`.
+    fn load(db: &mut Db, params: Self::LoadParams) -> Result<Option<Self>, Self::LoadError>;
+
+    /// Persist staged changes into `Db`.
+    fn persist(&mut self, db: &mut Db) -> Result<bool, Self::PersistError>;
+}
+
+type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
+
+/// Trait that persists the type with an async `Db`.
+pub trait PersistAsyncWith<Db>: Sized {
+    /// Parameters for [`PersistAsyncWith::create`].
+    type CreateParams;
+    /// Parameters for [`PersistAsyncWith::load`].
+    type LoadParams;
+    /// Error type of [`PersistAsyncWith::create`].
+    type CreateError;
+    /// Error type of [`PersistAsyncWith::load`].
+    type LoadError;
+    /// Error type of [`PersistAsyncWith::persist`].
+    type PersistError;
+
+    /// Create the type and initialize the `Db`.
+    fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult<Self, Self::CreateError>;
+
+    /// Load the type from `Db`.
+    fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult<Option<Self>, Self::LoadError>;
+
+    /// Persist staged changes into `Db`.
+    fn persist<'a>(&'a mut self, db: &'a mut Db) -> FutureResult<'a, bool, Self::PersistError>;
+}
+
+/// Represents a persisted `T`.
+pub struct Persisted<T> {
+    inner: T,
+}
+
+impl<T> Persisted<T> {
+    /// Create a new persisted `T`.
+    pub fn create<Db>(db: &mut Db, params: T::CreateParams) -> Result<Self, T::CreateError>
+    where
+        T: PersistWith<Db>,
+    {
+        T::create(db, params).map(|inner| Self { inner })
+    }
+
+    /// Create a new persisted `T` with async `Db`.
+    pub async fn create_async<Db>(
+        db: &mut Db,
+        params: T::CreateParams,
+    ) -> Result<Self, T::CreateError>
+    where
+        T: PersistAsyncWith<Db>,
+    {
+        T::create(db, params).await.map(|inner| Self { inner })
+    }
+
+    /// Construct a persisted `T` from `Db`.
+    pub fn load<Db>(db: &mut Db, params: T::LoadParams) -> Result<Option<Self>, T::LoadError>
+    where
+        T: PersistWith<Db>,
+    {
+        Ok(T::load(db, params)?.map(|inner| Self { inner }))
+    }
+
+    /// Contruct a persisted `T` from an async `Db`.
+    pub async fn load_async<Db>(
+        db: &mut Db,
+        params: T::LoadParams,
+    ) -> Result<Option<Self>, T::LoadError>
+    where
+        T: PersistAsyncWith<Db>,
+    {
+        Ok(T::load(db, params).await?.map(|inner| Self { inner }))
+    }
+
+    /// Persist staged changes of `T` into `Db`.
+    pub fn persist<Db>(&mut self, db: &mut Db) -> Result<bool, T::PersistError>
+    where
+        T: PersistWith<Db>,
+    {
+        self.inner.persist(db)
+    }
+
+    /// Persist staged changes of `T` into an async `Db`.
+    pub async fn persist_async<'a, Db>(
+        &'a mut self,
+        db: &'a mut Db,
+    ) -> Result<bool, T::PersistError>
+    where
+        T: PersistAsyncWith<Db>,
+    {
+        self.inner.persist(db).await
+    }
+}
+
+impl<T> Deref for Persisted<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl<T> DerefMut for Persisted<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.inner
+    }
+}
diff --git a/crates/chain/src/sqlite.rs b/crates/chain/src/sqlite.rs
new file mode 100644 (file)
index 0000000..e8b1cb5
--- /dev/null
@@ -0,0 +1,332 @@
+//! Module for stuff
+
+use core::{fmt::Debug, ops::Deref, str::FromStr};
+
+use alloc::{borrow::ToOwned, boxed::Box, string::ToString, vec::Vec};
+use bitcoin::consensus::{Decodable, Encodable};
+pub use rusqlite;
+pub use rusqlite::Connection;
+use rusqlite::OptionalExtension;
+pub use rusqlite::Transaction;
+use rusqlite::{
+    named_params,
+    types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
+    ToSql,
+};
+
+use crate::{Anchor, Merge};
+
+/// Parameters for [`Persister`].
+pub trait PersistParams {
+    /// Data type that is loaded and written to the database.
+    type ChangeSet: Default + Merge;
+
+    /// Initialize SQL tables.
+    fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()>;
+
+    /// Load all data from tables.
+    fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>>;
+
+    /// Write data into table(s).
+    fn write_changeset(
+        &self,
+        db_tx: &Transaction,
+        changeset: &Self::ChangeSet,
+    ) -> rusqlite::Result<()>;
+}
+
+// TODO: Use macros
+impl<A: PersistParams, B: PersistParams> PersistParams for (A, B) {
+    type ChangeSet = (A::ChangeSet, B::ChangeSet);
+
+    fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()> {
+        self.0.initialize_tables(db_tx)?;
+        self.1.initialize_tables(db_tx)?;
+        Ok(())
+    }
+
+    fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>> {
+        let changeset = (
+            self.0.load_changeset(db_tx)?.unwrap_or_default(),
+            self.1.load_changeset(db_tx)?.unwrap_or_default(),
+        );
+        if changeset.is_empty() {
+            Ok(None)
+        } else {
+            Ok(Some(changeset))
+        }
+    }
+
+    fn write_changeset(
+        &self,
+        db_tx: &Transaction,
+        changeset: &Self::ChangeSet,
+    ) -> rusqlite::Result<()> {
+        self.0.write_changeset(db_tx, &changeset.0)?;
+        self.1.write_changeset(db_tx, &changeset.1)?;
+        Ok(())
+    }
+}
+
+/// Persists data in to a relational schema based [SQLite] database file.
+///
+/// The changesets loaded or stored represent changes to keychain and blockchain data.
+///
+/// [SQLite]: https://www.sqlite.org/index.html
+#[derive(Debug)]
+pub struct Persister<P> {
+    conn: rusqlite::Connection,
+    params: P,
+}
+
+impl<P: PersistParams> Persister<P> {
+    /// Persist changeset to the database connection.
+    pub fn persist(&mut self, changeset: &P::ChangeSet) -> rusqlite::Result<()> {
+        if !changeset.is_empty() {
+            let db_tx = self.conn.transaction()?;
+            self.params.write_changeset(&db_tx, changeset)?;
+            db_tx.commit()?;
+        }
+        Ok(())
+    }
+}
+
+/// Extends [`rusqlite::Connection`] to transform into a [`Persister`].
+pub trait ConnectionExt: Sized {
+    /// Transform into a [`Persister`].
+    fn into_persister<P: PersistParams>(
+        self,
+        params: P,
+    ) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)>;
+}
+
+impl ConnectionExt for rusqlite::Connection {
+    fn into_persister<P: PersistParams>(
+        mut self,
+        params: P,
+    ) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)> {
+        let db_tx = self.transaction()?;
+        params.initialize_tables(&db_tx)?;
+        let changeset = params.load_changeset(&db_tx)?;
+        db_tx.commit()?;
+        let persister = Persister { conn: self, params };
+        Ok((persister, changeset))
+    }
+}
+
+/// Table name for schemas.
+pub const SCHEMAS_TABLE_NAME: &str = "bdk_schemas";
+
+/// Initialize the schema table.
+fn init_schemas_table(db_tx: &Transaction) -> rusqlite::Result<()> {
+    let sql = format!("CREATE TABLE IF NOT EXISTS {}( name TEXT PRIMARY KEY NOT NULL, version INTEGER NOT NULL ) STRICT", SCHEMAS_TABLE_NAME);
+    db_tx.execute(&sql, ())?;
+    Ok(())
+}
+
+/// Get schema version of `schema_name`.
+fn schema_version(db_tx: &Transaction, schema_name: &str) -> rusqlite::Result<Option<u32>> {
+    let sql = format!(
+        "SELECT version FROM {} WHERE name=:name",
+        SCHEMAS_TABLE_NAME
+    );
+    db_tx
+        .query_row(&sql, named_params! { ":name": schema_name }, |row| {
+            row.get::<_, u32>("version")
+        })
+        .optional()
+}
+
+/// Set the `schema_version` of `schema_name`.
+fn set_schema_version(
+    db_tx: &Transaction,
+    schema_name: &str,
+    schema_version: u32,
+) -> rusqlite::Result<()> {
+    let sql = format!(
+        "REPLACE INTO {}(name, version) VALUES(:name, :version)",
+        SCHEMAS_TABLE_NAME,
+    );
+    db_tx.execute(
+        &sql,
+        named_params! { ":name": schema_name, ":version": schema_version },
+    )?;
+    Ok(())
+}
+
+/// Runs logic that initializes/migrates the table schemas.
+pub fn migrate_schema(
+    db_tx: &Transaction,
+    schema_name: &str,
+    versioned_scripts: &[&[&str]],
+) -> rusqlite::Result<()> {
+    init_schemas_table(db_tx)?;
+    let current_version = schema_version(db_tx, schema_name)?;
+    let exec_from = current_version.map_or(0_usize, |v| v as usize + 1);
+    let scripts_to_exec = versioned_scripts.iter().enumerate().skip(exec_from);
+    for (version, &script) in scripts_to_exec {
+        set_schema_version(db_tx, schema_name, version as u32)?;
+        for statement in script {
+            db_tx.execute(statement, ())?;
+        }
+    }
+    Ok(())
+}
+
+/// A wrapper so that we can impl [FromSql] and [ToSql] for multiple types.
+pub struct Sql<T>(pub T);
+
+impl<T> From<T> for Sql<T> {
+    fn from(value: T) -> Self {
+        Self(value)
+    }
+}
+
+impl<T> Deref for Sql<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl FromSql for Sql<bitcoin::Txid> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        bitcoin::Txid::from_str(value.as_str()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+impl ToSql for Sql<bitcoin::Txid> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.to_string().into())
+    }
+}
+
+impl FromSql for Sql<bitcoin::BlockHash> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        bitcoin::BlockHash::from_str(value.as_str()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+impl ToSql for Sql<bitcoin::BlockHash> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.to_string().into())
+    }
+}
+
+#[cfg(feature = "miniscript")]
+impl FromSql for Sql<crate::DescriptorId> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        crate::DescriptorId::from_str(value.as_str()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+#[cfg(feature = "miniscript")]
+impl ToSql for Sql<crate::DescriptorId> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.to_string().into())
+    }
+}
+
+impl FromSql for Sql<bitcoin::Transaction> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+impl ToSql for Sql<bitcoin::Transaction> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        let mut bytes = Vec::<u8>::new();
+        self.consensus_encode(&mut bytes).map_err(to_sql_error)?;
+        Ok(bytes.into())
+    }
+}
+
+impl FromSql for Sql<bitcoin::ScriptBuf> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        Ok(bitcoin::Script::from_bytes(value.as_bytes()?)
+            .to_owned()
+            .into())
+    }
+}
+
+impl ToSql for Sql<bitcoin::ScriptBuf> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.as_bytes().into())
+    }
+}
+
+impl FromSql for Sql<bitcoin::Amount> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into())
+    }
+}
+
+impl ToSql for Sql<bitcoin::Amount> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?;
+        Ok(amount.into())
+    }
+}
+
+impl<A: Anchor + serde_crate::de::DeserializeOwned> FromSql for Sql<A> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        serde_json::from_str(value.as_str()?)
+            .map(Sql)
+            .map_err(from_sql_error)
+    }
+}
+
+impl<A: Anchor + serde_crate::Serialize> ToSql for Sql<A> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        serde_json::to_string(&self.0)
+            .map(Into::into)
+            .map_err(to_sql_error)
+    }
+}
+
+#[cfg(feature = "miniscript")]
+impl FromSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        miniscript::Descriptor::from_str(value.as_str()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+#[cfg(feature = "miniscript")]
+impl ToSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.to_string().into())
+    }
+}
+
+impl FromSql for Sql<bitcoin::Network> {
+    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+        bitcoin::Network::from_str(value.as_str()?)
+            .map(Self)
+            .map_err(from_sql_error)
+    }
+}
+
+impl ToSql for Sql<bitcoin::Network> {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+        Ok(self.to_string().into())
+    }
+}
+
+fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> FromSqlError {
+    FromSqlError::Other(Box::new(err))
+}
+
+fn to_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> rusqlite::Error {
+    rusqlite::Error::ToSqlConversionFailure(Box::new(err))
+}
index 8c11e737a41a03dee8738194fee8cd681875db2c..67f6307afd91b8d70570b81272b680783797018b 100644 (file)
@@ -1293,6 +1293,188 @@ impl<A> ChangeSet<A> {
     }
 }
 
+#[cfg(feature = "sqlite")]
+impl<A> ChangeSet<A>
+where
+    A: Anchor + Clone + Ord + serde::Serialize + serde::de::DeserializeOwned,
+{
+    /// Schema name for the [`ChangeSet`].
+    pub const SCHEMA_NAME: &'static str = "bdk_txgraph";
+    /// Name of table that stores full transactions and `last_seen` timestamps.
+    pub const TXS_TABLE_NAME: &'static str = "bdk_txs";
+    /// Name of table that stores floating txouts.
+    pub const TXOUTS_TABLE_NAME: &'static str = "bdk_txouts";
+    /// Name of table that stores [`Anchor`]s.
+    pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors";
+
+    /// Initialize sqlite tables.
+    fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        let schema_v0: &[&str] = &[
+            // full transactions
+            &format!(
+                "CREATE TABLE {} ( \
+                txid TEXT PRIMARY KEY NOT NULL, \
+                raw_tx BLOB, \
+                last_seen INTEGER \
+                ) STRICT",
+                Self::TXS_TABLE_NAME,
+            ),
+            // floating txouts
+            &format!(
+                "CREATE TABLE {} ( \
+                txid TEXT NOT NULL, \
+                vout INTEGER NOT NULL, \
+                value INTEGER NOT NULL, \
+                script BLOB NOT NULL, \
+                PRIMARY KEY (txid, vout) \
+                ) STRICT",
+                Self::TXOUTS_TABLE_NAME,
+            ),
+            // anchors
+            &format!(
+                "CREATE TABLE {} ( \
+                txid TEXT NOT NULL REFERENCES {} (txid), \
+                block_height INTEGER NOT NULL, \
+                block_hash TEXT NOT NULL, \
+                anchor BLOB NOT NULL, \
+                PRIMARY KEY (txid, block_height, block_hash) \
+                ) STRICT",
+                Self::ANCHORS_TABLE_NAME,
+                Self::TXS_TABLE_NAME,
+            ),
+        ];
+        crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+    }
+
+    /// Construct a [`TxGraph`] from an sqlite database.
+    pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::sqlite::Sql;
+
+        let mut changeset = Self::default();
+
+        let mut statement = db_tx.prepare(&format!(
+            "SELECT txid, raw_tx, last_seen FROM {}",
+            Self::TXS_TABLE_NAME,
+        ))?;
+        let row_iter = statement.query_map([], |row| {
+            Ok((
+                row.get::<_, Sql<Txid>>("txid")?,
+                row.get::<_, Option<Sql<Transaction>>>("raw_tx")?,
+                row.get::<_, Option<u64>>("last_seen")?,
+            ))
+        })?;
+        for row in row_iter {
+            let (Sql(txid), tx, last_seen) = row?;
+            if let Some(Sql(tx)) = tx {
+                changeset.txs.insert(Arc::new(tx));
+            }
+            if let Some(last_seen) = last_seen {
+                changeset.last_seen.insert(txid, last_seen);
+            }
+        }
+
+        let mut statement = db_tx.prepare(&format!(
+            "SELECT txid, vout, value, script FROM {}",
+            Self::TXOUTS_TABLE_NAME,
+        ))?;
+        let row_iter = statement.query_map([], |row| {
+            Ok((
+                row.get::<_, Sql<Txid>>("txid")?,
+                row.get::<_, u32>("vout")?,
+                row.get::<_, Sql<Amount>>("value")?,
+                row.get::<_, Sql<bitcoin::ScriptBuf>>("script")?,
+            ))
+        })?;
+        for row in row_iter {
+            let (Sql(txid), vout, Sql(value), Sql(script_pubkey)) = row?;
+            changeset.txouts.insert(
+                OutPoint { txid, vout },
+                TxOut {
+                    value,
+                    script_pubkey,
+                },
+            );
+        }
+
+        let mut statement = db_tx.prepare(&format!(
+            "SELECT json(anchor), txid FROM {}",
+            Self::ANCHORS_TABLE_NAME,
+        ))?;
+        let row_iter = statement.query_map([], |row| {
+            Ok((
+                row.get::<_, Sql<A>>("json(anchor)")?,
+                row.get::<_, Sql<Txid>>("txid")?,
+            ))
+        })?;
+        for row in row_iter {
+            let (Sql(anchor), Sql(txid)) = row?;
+            changeset.anchors.insert((anchor, txid));
+        }
+
+        Ok(changeset)
+    }
+
+    /// Persist `changeset` to the sqlite database.
+    pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+        Self::init_sqlite_tables(db_tx)?;
+        use crate::rusqlite::named_params;
+        use crate::sqlite::Sql;
+
+        let mut statement = db_tx.prepare_cached(&format!(
+            "INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx",
+            Self::TXS_TABLE_NAME,
+        ))?;
+        for tx in &self.txs {
+            statement.execute(named_params! {
+                ":txid": Sql(tx.compute_txid()),
+                ":raw_tx": Sql(tx.as_ref().clone()),
+            })?;
+        }
+
+        let mut statement = db_tx
+            .prepare_cached(&format!(
+                "INSERT INTO {}(txid, last_seen) VALUES(:txid, :last_seen) ON CONFLICT(txid) DO UPDATE SET last_seen=:last_seen",
+                Self::TXS_TABLE_NAME,
+            ))?;
+        for (&txid, &last_seen) in &self.last_seen {
+            statement.execute(named_params! {
+                ":txid": Sql(txid),
+                ":last_seen": Some(last_seen),
+            })?;
+        }
+
+        let mut statement = db_tx.prepare_cached(&format!(
+            "REPLACE INTO {}(txid, vout, value, script) VALUES(:txid, :vout, :value, :script)",
+            Self::TXOUTS_TABLE_NAME,
+        ))?;
+        for (op, txo) in &self.txouts {
+            statement.execute(named_params! {
+                ":txid": Sql(op.txid),
+                ":vout": op.vout,
+                ":value": Sql(txo.value),
+                ":script": Sql(txo.script_pubkey.clone()),
+            })?;
+        }
+
+        let mut statement = db_tx.prepare_cached(&format!(
+            "REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))",
+            Self::ANCHORS_TABLE_NAME,
+        ))?;
+        for (anchor, txid) in &self.anchors {
+            let anchor_block = anchor.anchor_block();
+            statement.execute(named_params! {
+                ":txid": Sql(*txid),
+                ":block_height": anchor_block.height,
+                ":block_hash": Sql(anchor_block.hash),
+                ":anchor": Sql(anchor.clone()),
+            })?;
+        }
+
+        Ok(())
+    }
+}
+
 impl<A: Ord> Merge for ChangeSet<A> {
     fn merge(&mut self, other: Self) {
         // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
index 3337fb4368c3ff9f7d38c81d20b5e272eee2158a..6445fb63cafef5540481e49994141a49f7dfb697 100644 (file)
@@ -3,7 +3,7 @@
 use rand::distributions::{Alphanumeric, DistString};
 use std::collections::HashMap;
 
-use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
+use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor};
 use bitcoin::{
     locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
     Sequence, Transaction, TxIn, TxOut, Txid, Witness,
index 01d25c061a307d5d399dfaa32df24c64034bf869..e5e66a74b3be51d4d857122299897135a720e582 100644 (file)
@@ -10,7 +10,7 @@ use bdk_chain::{
     indexed_tx_graph::{self, IndexedTxGraph},
     indexer::keychain_txout::KeychainTxOutIndex,
     local_chain::LocalChain,
-    tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge,
+    tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt,
 };
 use bitcoin::{
     secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
@@ -73,13 +73,12 @@ fn insert_relevant_txs() {
     let txs = [tx_c, tx_b, tx_a];
 
     let changeset = indexed_tx_graph::ChangeSet {
-        graph: tx_graph::ChangeSet {
+        tx_graph: tx_graph::ChangeSet {
             txs: txs.iter().cloned().map(Arc::new).collect(),
             ..Default::default()
         },
         indexer: keychain_txout::ChangeSet {
             last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
-            keychains_added: [].into(),
         },
     };
 
@@ -90,10 +89,9 @@ fn insert_relevant_txs() {
 
     // The initial changeset will also contain info about the keychain we added
     let initial_changeset = indexed_tx_graph::ChangeSet {
-        graph: changeset.graph,
+        tx_graph: changeset.tx_graph,
         indexer: keychain_txout::ChangeSet {
             last_revealed: changeset.indexer.last_revealed,
-            keychains_added: [((), descriptor)].into(),
         },
     };
 
@@ -144,16 +142,14 @@ fn test_list_owned_txouts() {
         KeychainTxOutIndex::new(10),
     );
 
-    assert!(!graph
+    assert!(graph
         .index
         .insert_descriptor("keychain_1".into(), desc_1)
-        .unwrap()
-        .is_empty());
-    assert!(!graph
+        .unwrap());
+    assert!(graph
         .index
         .insert_descriptor("keychain_2".into(), desc_2)
-        .unwrap()
-        .is_empty());
+        .unwrap());
 
     // Get trusted and untrusted addresses
 
@@ -532,8 +528,8 @@ fn test_list_owned_txouts() {
 #[test]
 fn test_get_chain_position() {
     use bdk_chain::local_chain::CheckPoint;
+    use bdk_chain::spk_txout::SpkTxOutIndex;
     use bdk_chain::BlockId;
-    use bdk_chain::SpkTxOutIndex;
 
     struct TestCase<A> {
         name: &'static str,
index 517698c90a577cdb3e68075fe74fb06d49ddcc22..06e2e767cd9b785dc3473684288522da18b7186b 100644 (file)
@@ -81,11 +81,9 @@ fn merge_changesets_check_last_revealed() {
     lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
 
     let mut lhs = ChangeSet {
-        keychains_added: BTreeMap::<(), _>::new(),
         last_revealed: lhs_di,
     };
     let rhs = ChangeSet {
-        keychains_added: BTreeMap::<(), _>::new(),
         last_revealed: rhs_di,
     };
     lhs.merge(rhs);
@@ -100,49 +98,6 @@ fn merge_changesets_check_last_revealed() {
     assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
 }
 
-#[test]
-fn when_apply_contradictory_changesets_they_are_ignored() {
-    let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
-    let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
-    let mut txout_index =
-        init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
-    assert_eq!(
-        txout_index.keychains().collect::<Vec<_>>(),
-        vec![
-            (&TestKeychain::External, &external_descriptor),
-            (&TestKeychain::Internal, &internal_descriptor)
-        ]
-    );
-
-    let changeset = ChangeSet {
-        keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
-        last_revealed: [].into(),
-    };
-    txout_index.apply_changeset(changeset);
-
-    assert_eq!(
-        txout_index.keychains().collect::<Vec<_>>(),
-        vec![
-            (&TestKeychain::External, &external_descriptor),
-            (&TestKeychain::Internal, &internal_descriptor)
-        ]
-    );
-
-    let changeset = ChangeSet {
-        keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
-        last_revealed: [].into(),
-    };
-    txout_index.apply_changeset(changeset);
-
-    assert_eq!(
-        txout_index.keychains().collect::<Vec<_>>(),
-        vec![
-            (&TestKeychain::External, &external_descriptor),
-            (&TestKeychain::Internal, &internal_descriptor)
-        ]
-    );
-}
-
 #[test]
 fn test_set_all_derivation_indices() {
     let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
@@ -159,7 +114,6 @@ fn test_set_all_derivation_indices() {
     assert_eq!(
         txout_index.reveal_to_target_multi(&derive_to),
         ChangeSet {
-            keychains_added: BTreeMap::new(),
             last_revealed: last_revealed.clone()
         }
     );
@@ -633,46 +587,29 @@ fn lookahead_to_target() {
 }
 
 #[test]
-fn insert_descriptor_no_change() {
-    let secp = Secp256k1::signing_only();
-    let (desc, _) =
-        Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
-    let mut txout_index = KeychainTxOutIndex::<()>::default();
-    assert_eq!(
-        txout_index.insert_descriptor((), desc.clone()),
-        Ok(ChangeSet {
-            keychains_added: [((), desc.clone())].into(),
-            last_revealed: Default::default()
-        }),
-    );
-    assert_eq!(
-        txout_index.insert_descriptor((), desc.clone()),
-        Ok(ChangeSet::default()),
-        "inserting the same descriptor for keychain should return an empty changeset",
-    );
-}
-
-#[test]
-#[cfg(not(debug_assertions))]
 fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
     let desc = parse_descriptor(DESCRIPTORS[0]);
-    let changesets: &[ChangeSet<TestKeychain>] = &[
+    let changesets: &[ChangeSet] = &[
         ChangeSet {
-            keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
-            last_revealed: [].into(),
+            last_revealed: [(desc.descriptor_id(), 10)].into(),
         },
         ChangeSet {
-            keychains_added: [(TestKeychain::External, desc.clone())].into(),
             last_revealed: [(desc.descriptor_id(), 12)].into(),
         },
     ];
 
     let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
+    indexer_a
+        .insert_descriptor(TestKeychain::External, desc.clone())
+        .expect("must insert keychain");
     for changeset in changesets {
         indexer_a.apply_changeset(changeset.clone());
     }
 
     let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
+    indexer_b
+        .insert_descriptor(TestKeychain::External, desc.clone())
+        .expect("must insert keychain");
     let aggregate_changesets = changesets
         .iter()
         .cloned()
index ccad2af04f75f0041172cd7b22093a41cb9d9073..3d3b82e89d585f9bf1549ab9e7aaaa371f975129 100644 (file)
@@ -1,4 +1,4 @@
-use bdk_chain::{Indexer, SpkTxOutIndex};
+use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
 use bitcoin::{
     absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
 };
index 825454331333f8928cf78ad0332e653de53d7009..f0ff460b22a3e80e38d4a604991ef2c6687bb632 100644 (file)
@@ -2,7 +2,8 @@ use bdk_chain::{
     bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
     local_chain::LocalChain,
     spk_client::{FullScanRequest, SyncRequest},
-    Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex,
+    spk_txout::SpkTxOutIndex,
+    Balance, ConfirmationBlockTime, IndexedTxGraph,
 };
 use bdk_electrum::BdkElectrumClient;
 use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
index 73a8fc539698bfc1e67b5384e7d0d0791972a810..e06722755c460e7d831313959cc37f0fb7fae884 100644 (file)
@@ -4,11 +4,13 @@
 //! used with hardware wallets.
 //! ```no_run
 //! # use bdk_wallet::bitcoin::Network;
+//! # use bdk_wallet::descriptor::Descriptor;
 //! # use bdk_wallet::signer::SignerOrdering;
 //! # use bdk_hwi::HWISigner;
-//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+//! # use bdk_wallet::{KeychainKind, SignOptions};
 //! # use hwi::HWIClient;
 //! # use std::sync::Arc;
+//! # use std::str::FromStr;
 //! #
 //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
 //! let mut devices = HWIClient::enumerate()?;
 //! let first_device = devices.remove(0)?;
 //! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
 //!
-//! # let mut wallet = Wallet::new(
-//! #     "",
-//! #     "",
-//! #     Network::Testnet,
-//! # )?;
+//! # let mut wallet = bdk_wallet::CreateParams::new("", "", Network::Testnet)?
+//! #   .create_wallet_no_persist()?;
 //! #
 //! // Adding the hardware signer to the BDK wallet
 //! wallet.add_signer(
diff --git a/crates/sqlite/Cargo.toml b/crates/sqlite/Cargo.toml
deleted file mode 100644 (file)
index 8bd161a..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "bdk_sqlite"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
-repository = "https://github.com/bitcoindevkit/bdk"
-documentation = "https://docs.rs/bdk_sqlite"
-description = "A simple SQLite relational database client for persisting bdk_chain data."
-keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
-authors = ["Bitcoin Dev Kit Developers"]
-readme = "README.md"
-
-[dependencies]
-bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
-rusqlite = { version = "0.31.0", features = ["bundled"] }
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
\ No newline at end of file
diff --git a/crates/sqlite/README.md b/crates/sqlite/README.md
deleted file mode 100644 (file)
index ba612bd..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-# BDK SQLite
-
-This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
-
-The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
-
-[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
-[SQLite]: https://www.sqlite.org/index.html
diff --git a/crates/sqlite/schema/schema_0.sql b/crates/sqlite/schema/schema_0.sql
deleted file mode 100644 (file)
index 9b6d180..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
--- schema version control
-CREATE TABLE version
-(
-    version INTEGER
-) STRICT;
-INSERT INTO version
-VALUES (1);
-
--- network is the valid network for all other table data
-CREATE TABLE network
-(
-    name TEXT UNIQUE NOT NULL
-) STRICT;
-
--- keychain is the json serialized keychain structure as JSONB,
--- descriptor is the complete descriptor string,
--- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
--- last revealed index is a u32
-CREATE TABLE keychain
-(
-    keychain      BLOB PRIMARY KEY NOT NULL,
-    descriptor    TEXT             NOT NULL,
-    descriptor_id BLOB             NOT NULL,
-    last_revealed INTEGER
-) STRICT;
-
--- hash is block hash hex string,
--- block height is a u32,
-CREATE TABLE block
-(
-    hash   TEXT PRIMARY KEY NOT NULL,
-    height INTEGER          NOT NULL
-) STRICT;
-
--- txid is transaction hash hex string (reversed)
--- whole_tx is a consensus encoded transaction,
--- last seen is a u64 unix epoch seconds
-CREATE TABLE tx
-(
-    txid      TEXT PRIMARY KEY NOT NULL,
-    whole_tx  BLOB,
-    last_seen INTEGER
-) STRICT;
-
--- Outpoint txid hash hex string (reversed)
--- Outpoint vout
--- TxOut value as SATs
--- TxOut script consensus encoded
-CREATE TABLE txout
-(
-    txid   TEXT    NOT NULL,
-    vout   INTEGER NOT NULL,
-    value  INTEGER NOT NULL,
-    script BLOB    NOT NULL,
-    PRIMARY KEY (txid, vout)
-) STRICT;
-
--- join table between anchor and tx
--- block hash hex string
--- anchor is a json serialized Anchor structure as JSONB,
--- txid is transaction hash hex string (reversed)
-CREATE TABLE anchor_tx
-(
-    block_hash          TEXT NOT NULL,
-    anchor              BLOB NOT NULL,
-    txid                TEXT NOT NULL REFERENCES tx (txid),
-    UNIQUE (anchor, txid),
-    FOREIGN KEY (block_hash) REFERENCES block(hash)
-) STRICT;
\ No newline at end of file
diff --git a/crates/sqlite/src/lib.rs b/crates/sqlite/src/lib.rs
deleted file mode 100644 (file)
index ef81a4f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#![doc = include_str!("../README.md")]
-// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
-#![cfg_attr(docsrs, feature(doc_cfg))]
-
-mod schema;
-mod store;
-
-use bdk_chain::bitcoin::Network;
-pub use rusqlite;
-pub use store::Store;
-
-/// Error that occurs while reading or writing change sets with the SQLite database.
-#[derive(Debug)]
-pub enum Error {
-    /// Invalid network, cannot change the one already stored in the database.
-    Network { expected: Network, given: Network },
-    /// SQLite error.
-    Sqlite(rusqlite::Error),
-}
-
-impl core::fmt::Display for Error {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
-        match self {
-            Self::Network { expected, given } => write!(
-                f,
-                "network error trying to read or write change set, expected {}, given {}",
-                expected, given
-            ),
-            Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
-        }
-    }
-}
-
-impl std::error::Error for Error {}
diff --git a/crates/sqlite/src/schema.rs b/crates/sqlite/src/schema.rs
deleted file mode 100644 (file)
index ef7c4e0..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-use crate::Store;
-use rusqlite::{named_params, Connection, Error};
-
-const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
-const MIGRATIONS: &[&str] = &[SCHEMA_0];
-
-/// Schema migration related functions.
-impl<K, A> Store<K, A> {
-    /// Migrate sqlite db schema to latest version.
-    pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
-        let stmts = &MIGRATIONS
-            .iter()
-            .flat_map(|stmt| {
-                // remove comment lines
-                let s = stmt
-                    .split('\n')
-                    .filter(|l| !l.starts_with("--") && !l.is_empty())
-                    .collect::<Vec<_>>()
-                    .join(" ");
-                // split into statements
-                s.split(';')
-                    // remove extra spaces
-                    .map(|s| {
-                        s.trim()
-                            .split(' ')
-                            .filter(|s| !s.is_empty())
-                            .collect::<Vec<_>>()
-                            .join(" ")
-                    })
-                    .collect::<Vec<_>>()
-            })
-            // remove empty statements
-            .filter(|s| !s.is_empty())
-            .collect::<Vec<String>>();
-
-        let version = Self::get_schema_version(conn)?;
-        let stmts = &stmts[(version as usize)..];
-
-        // begin transaction, all migration statements and new schema version commit or rollback
-        let tx = conn.transaction()?;
-
-        // execute every statement and return `Some` new schema version
-        // if execution fails, return `Error::Rusqlite`
-        // if no statements executed returns `None`
-        let new_version = stmts
-            .iter()
-            .enumerate()
-            .map(|version_stmt| {
-                tx.execute(version_stmt.1.as_str(), [])
-                    // map result value to next migration version
-                    .map(|_| version_stmt.0 as i32 + version + 1)
-            })
-            .last()
-            .transpose()?;
-
-        // if `Some` new statement version, set new schema version
-        if let Some(version) = new_version {
-            Self::set_schema_version(&tx, version)?;
-        }
-
-        // commit transaction
-        tx.commit()?;
-        Ok(())
-    }
-
-    fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
-        let statement = conn.prepare_cached("SELECT version FROM version");
-        match statement {
-            Err(Error::SqliteFailure(e, Some(msg))) => {
-                if msg == "no such table: version" {
-                    Ok(0)
-                } else {
-                    Err(Error::SqliteFailure(e, Some(msg)))
-                }
-            }
-            Ok(mut stmt) => {
-                let mut rows = stmt.query([])?;
-                match rows.next()? {
-                    Some(row) => {
-                        let version: i32 = row.get(0)?;
-                        Ok(version)
-                    }
-                    None => Ok(0),
-                }
-            }
-            _ => Ok(0),
-        }
-    }
-
-    fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
-        conn.execute(
-            "UPDATE version SET version=:version",
-            named_params! {":version": version},
-        )
-    }
-}
diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs
deleted file mode 100644 (file)
index 5b79925..0000000
+++ /dev/null
@@ -1,734 +0,0 @@
-use bdk_chain::bitcoin::consensus::{deserialize, serialize};
-use bdk_chain::bitcoin::hashes::Hash;
-use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
-use bdk_chain::bitcoin::{BlockHash, Txid};
-use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
-use rusqlite::{named_params, Connection};
-use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Debug;
-use std::marker::PhantomData;
-use std::str::FromStr;
-use std::sync::{Arc, Mutex};
-
-use crate::Error;
-use bdk_chain::CombinedChangeSet;
-use bdk_chain::{
-    indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt,
-    DescriptorId, Merge,
-};
-
-/// Persists data in to a relational schema based [SQLite] database file.
-///
-/// The changesets loaded or stored represent changes to keychain and blockchain data.
-///
-/// [SQLite]: https://www.sqlite.org/index.html
-pub struct Store<K, A> {
-    // A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
-    conn: Mutex<Connection>,
-    keychain_marker: PhantomData<K>,
-    anchor_marker: PhantomData<A>,
-}
-
-impl<K, A> Debug for Store<K, A> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        Debug::fmt(&self.conn, f)
-    }
-}
-
-impl<K, A> Store<K, A>
-where
-    K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
-    A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
-    /// Creates a new store from a [`Connection`].
-    pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
-        Self::migrate(&mut conn)?;
-
-        Ok(Self {
-            conn: Mutex::new(conn),
-            keychain_marker: Default::default(),
-            anchor_marker: Default::default(),
-        })
-    }
-
-    pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
-        let connection = self.conn.get_mut().expect("unlocked connection mutex");
-        connection.transaction().map_err(Error::Sqlite)
-    }
-}
-
-/// Network table related functions.
-impl<K, A> Store<K, A> {
-    /// Insert [`Network`] for which all other tables data is valid.
-    ///
-    /// Error if trying to insert different network value.
-    fn insert_network(
-        current_network: &Option<Network>,
-        db_transaction: &rusqlite::Transaction,
-        network_changeset: &Option<Network>,
-    ) -> Result<(), Error> {
-        if let Some(network) = network_changeset {
-            match current_network {
-                // if no network change do nothing
-                Some(current_network) if current_network == network => Ok(()),
-                // if new network not the same as current, error
-                Some(current_network) => Err(Error::Network {
-                    expected: *current_network,
-                    given: *network,
-                }),
-                // insert network if none exists
-                None => {
-                    let insert_network_stmt = &mut db_transaction
-                        .prepare_cached("INSERT INTO network (name) VALUES (:name)")
-                        .expect("insert network statement");
-                    let name = network.to_string();
-                    insert_network_stmt
-                        .execute(named_params! {":name": name })
-                        .map_err(Error::Sqlite)?;
-                    Ok(())
-                }
-            }
-        } else {
-            Ok(())
-        }
-    }
-
-    /// Select the valid [`Network`] for this database, or `None` if not set.
-    fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
-        let mut select_network_stmt = db_transaction
-            .prepare_cached("SELECT name FROM network WHERE rowid = 1")
-            .expect("select network statement");
-
-        let network = select_network_stmt
-            .query_row([], |row| {
-                let network = row.get_unwrap::<usize, String>(0);
-                let network = Network::from_str(network.as_str()).expect("valid network");
-                Ok(network)
-            })
-            .map_err(Error::Sqlite);
-        match network {
-            Ok(network) => Ok(Some(network)),
-            Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
-            Err(e) => Err(e),
-        }
-    }
-}
-
-/// Block table related functions.
-impl<K, A> Store<K, A> {
-    /// Insert or delete local chain blocks.
-    ///
-    /// Error if trying to insert existing block hash.
-    fn insert_or_delete_blocks(
-        db_transaction: &rusqlite::Transaction,
-        chain_changeset: &local_chain::ChangeSet,
-    ) -> Result<(), Error> {
-        for (height, hash) in chain_changeset.iter() {
-            match hash {
-                // add new hash at height
-                Some(hash) => {
-                    let insert_block_stmt = &mut db_transaction
-                        .prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
-                        .expect("insert block statement");
-                    let hash = hash.to_string();
-                    insert_block_stmt
-                        .execute(named_params! {":hash": hash, ":height": height })
-                        .map_err(Error::Sqlite)?;
-                }
-                // delete block at height
-                None => {
-                    let delete_block_stmt = &mut db_transaction
-                        .prepare_cached("DELETE FROM block WHERE height IS :height")
-                        .expect("delete block statement");
-                    delete_block_stmt
-                        .execute(named_params! {":height": height })
-                        .map_err(Error::Sqlite)?;
-                }
-            }
-        }
-
-        Ok(())
-    }
-
-    /// Select all blocks.
-    fn select_blocks(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
-        let mut select_blocks_stmt = db_transaction
-            .prepare_cached("SELECT height, hash FROM block")
-            .expect("select blocks statement");
-
-        let blocks = select_blocks_stmt
-            .query_map([], |row| {
-                let height = row.get_unwrap::<usize, u32>(0);
-                let hash = row.get_unwrap::<usize, String>(1);
-                let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
-                Ok((height, hash))
-            })
-            .map_err(Error::Sqlite)?;
-        blocks
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-}
-
-/// Keychain table related functions.
-///
-/// The keychain objects are stored as [`JSONB`] data.
-/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
-impl<K, A> Store<K, A>
-where
-    K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
-    A: Anchor + Send,
-{
-    /// Insert keychain with descriptor and last active index.
-    ///
-    /// If keychain exists only update last active index.
-    fn insert_keychains(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        let keychain_changeset = &tx_graph_changeset.indexer;
-        for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
-            let insert_keychain_stmt = &mut db_transaction
-                .prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
-                .expect("insert keychain statement");
-            let keychain_json = serde_json::to_string(keychain).expect("keychain json");
-            let descriptor_id = descriptor.descriptor_id().to_byte_array();
-            let descriptor = descriptor.to_string();
-            insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-
-    /// Update descriptor last revealed index.
-    fn update_last_revealed(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        let keychain_changeset = &tx_graph_changeset.indexer;
-        for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
-            let update_last_revealed_stmt = &mut db_transaction
-                .prepare_cached(
-                    "UPDATE keychain SET last_revealed = :last_revealed
-                              WHERE descriptor_id = :descriptor_id",
-                )
-                .expect("update last revealed statement");
-            let descriptor_id = descriptor_id.to_byte_array();
-            update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-
-    /// Select keychains added.
-    fn select_keychains(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
-        let mut select_keychains_added_stmt = db_transaction
-            .prepare_cached("SELECT json(keychain), descriptor FROM keychain")
-            .expect("select keychains statement");
-
-        let keychains = select_keychains_added_stmt
-            .query_map([], |row| {
-                let keychain = row.get_unwrap::<usize, String>(0);
-                let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
-                let descriptor = row.get_unwrap::<usize, String>(1);
-                let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
-                Ok((keychain, descriptor))
-            })
-            .map_err(Error::Sqlite)?;
-        keychains
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-
-    /// Select descriptor last revealed indexes.
-    fn select_last_revealed(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeMap<DescriptorId, u32>, Error> {
-        let mut select_last_revealed_stmt = db_transaction
-            .prepare_cached(
-                "SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
-            )
-            .expect("select last revealed statement");
-
-        let last_revealed = select_last_revealed_stmt
-            .query_map([], |row| {
-                let descriptor = row.get_unwrap::<usize, String>(0);
-                let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
-                let descriptor_id = descriptor.descriptor_id();
-                let last_revealed = row.get_unwrap::<usize, u32>(1);
-                Ok((descriptor_id, last_revealed))
-            })
-            .map_err(Error::Sqlite)?;
-        last_revealed
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-}
-
-/// Tx (transaction) and txout (transaction output) table related functions.
-impl<K, A> Store<K, A> {
-    /// Insert transactions.
-    ///
-    /// Error if trying to insert existing txid.
-    fn insert_txs(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        for tx in tx_graph_changeset.graph.txs.iter() {
-            let insert_tx_stmt = &mut db_transaction
-                .prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
-                .expect("insert or update tx whole_tx statement");
-            let txid = tx.compute_txid().to_string();
-            let whole_tx = serialize(&tx);
-            insert_tx_stmt
-                .execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-
-    /// Select all transactions.
-    fn select_txs(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeSet<Arc<Transaction>>, Error> {
-        let mut select_tx_stmt = db_transaction
-            .prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
-            .expect("select tx statement");
-
-        let txs = select_tx_stmt
-            .query_map([], |row| {
-                let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
-                let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
-                Ok(Arc::new(whole_tx))
-            })
-            .map_err(Error::Sqlite)?;
-
-        txs.into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-
-    /// Select all transactions with last_seen values.
-    fn select_last_seen(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeMap<Txid, u64>, Error> {
-        // load tx last_seen
-        let mut select_last_seen_stmt = db_transaction
-            .prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
-            .expect("select tx last seen statement");
-
-        let last_seen = select_last_seen_stmt
-            .query_map([], |row| {
-                let txid = row.get_unwrap::<usize, String>(0);
-                let txid = Txid::from_str(&txid).expect("txid");
-                let last_seen = row.get_unwrap::<usize, u64>(1);
-                Ok((txid, last_seen))
-            })
-            .map_err(Error::Sqlite)?;
-        last_seen
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-
-    /// Insert txouts.
-    ///
-    /// Error if trying to insert existing outpoint.
-    fn insert_txouts(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        for txout in tx_graph_changeset.graph.txouts.iter() {
-            let insert_txout_stmt = &mut db_transaction
-                .prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
-                .expect("insert txout statement");
-            let txid = txout.0.txid.to_string();
-            let vout = txout.0.vout;
-            let value = txout.1.value.to_sat();
-            let script = txout.1.script_pubkey.as_bytes();
-            insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-
-    /// Select all transaction outputs.
-    fn select_txouts(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
-        // load tx outs
-        let mut select_txout_stmt = db_transaction
-            .prepare_cached("SELECT txid, vout, value, script FROM txout")
-            .expect("select txout statement");
-
-        let txouts = select_txout_stmt
-            .query_map([], |row| {
-                let txid = row.get_unwrap::<usize, String>(0);
-                let txid = Txid::from_str(&txid).expect("txid");
-                let vout = row.get_unwrap::<usize, u32>(1);
-                let outpoint = OutPoint::new(txid, vout);
-                let value = row.get_unwrap::<usize, u64>(2);
-                let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
-                let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
-                let txout = TxOut {
-                    value: Amount::from_sat(value),
-                    script_pubkey,
-                };
-                Ok((outpoint, txout))
-            })
-            .map_err(Error::Sqlite)?;
-        txouts
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-
-    /// Update transaction last seen times.
-    fn update_last_seen(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
-            let insert_or_update_tx_stmt = &mut db_transaction
-                .prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
-                .expect("insert or update tx last_seen statement");
-            let txid = tx_last_seen.0.to_string();
-            let last_seen = *tx_last_seen.1;
-            insert_or_update_tx_stmt
-                .execute(named_params! {":txid": txid, ":last_seen": last_seen })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-}
-
-/// Anchor table related functions.
-impl<K, A> Store<K, A>
-where
-    K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
-    A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
-    /// Insert anchors.
-    fn insert_anchors(
-        db_transaction: &rusqlite::Transaction,
-        tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
-    ) -> Result<(), Error> {
-        // serde_json::to_string
-        for anchor in tx_graph_changeset.graph.anchors.iter() {
-            let insert_anchor_stmt = &mut db_transaction
-                .prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
-                .expect("insert anchor statement");
-            let block_hash = anchor.0.anchor_block().hash.to_string();
-            let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
-            let txid = anchor.1.to_string();
-            insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
-                .map_err(Error::Sqlite)?;
-        }
-        Ok(())
-    }
-
-    /// Select all anchors.
-    fn select_anchors(
-        db_transaction: &rusqlite::Transaction,
-    ) -> Result<BTreeSet<(A, Txid)>, Error> {
-        // serde_json::from_str
-        let mut select_anchor_stmt = db_transaction
-            .prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
-            .expect("select anchor statement");
-        let anchors = select_anchor_stmt
-            .query_map([], |row| {
-                let hash = row.get_unwrap::<usize, String>(0);
-                let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
-                let anchor = row.get_unwrap::<usize, String>(1);
-                let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
-                // double check anchor blob block hash matches
-                assert_eq!(hash, anchor.anchor_block().hash);
-                let txid = row.get_unwrap::<usize, String>(2);
-                let txid = Txid::from_str(&txid).expect("txid");
-                Ok((anchor, txid))
-            })
-            .map_err(Error::Sqlite)?;
-        anchors
-            .into_iter()
-            .map(|row| row.map_err(Error::Sqlite))
-            .collect()
-    }
-}
-
-/// Functions to read and write all [`CombinedChangeSet`] data.
-impl<K, A> Store<K, A>
-where
-    K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
-    A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
-    /// Write the given `changeset` atomically.
-    pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
-        // no need to write anything if changeset is empty
-        if changeset.is_empty() {
-            return Ok(());
-        }
-
-        let db_transaction = self.db_transaction()?;
-
-        let network_changeset = &changeset.network;
-        let current_network = Self::select_network(&db_transaction)?;
-        Self::insert_network(&current_network, &db_transaction, network_changeset)?;
-
-        let chain_changeset = &changeset.chain;
-        Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
-
-        let tx_graph_changeset = &changeset.indexed_tx_graph;
-        Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
-        Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
-        Self::insert_txs(&db_transaction, tx_graph_changeset)?;
-        Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
-        Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
-        Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
-        db_transaction.commit().map_err(Error::Sqlite)
-    }
-
-    /// Read the entire database and return the aggregate [`CombinedChangeSet`].
-    pub fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
-        let db_transaction = self.db_transaction()?;
-
-        let network = Self::select_network(&db_transaction)?;
-        let chain = Self::select_blocks(&db_transaction)?;
-        let keychains_added = Self::select_keychains(&db_transaction)?;
-        let last_revealed = Self::select_last_revealed(&db_transaction)?;
-        let txs = Self::select_txs(&db_transaction)?;
-        let last_seen = Self::select_last_seen(&db_transaction)?;
-        let txouts = Self::select_txouts(&db_transaction)?;
-        let anchors = Self::select_anchors(&db_transaction)?;
-
-        let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
-            txs,
-            txouts,
-            anchors,
-            last_seen,
-        };
-
-        let indexer = keychain_txout::ChangeSet {
-            keychains_added,
-            last_revealed,
-        };
-
-        let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>> =
-            indexed_tx_graph::ChangeSet { graph, indexer };
-
-        if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
-            Ok(None)
-        } else {
-            Ok(Some(CombinedChangeSet {
-                chain,
-                indexed_tx_graph,
-                network,
-            }))
-        }
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use crate::store::Merge;
-    use bdk_chain::bitcoin::consensus::encode::deserialize;
-    use bdk_chain::bitcoin::constants::genesis_block;
-    use bdk_chain::bitcoin::hashes::hex::FromHex;
-    use bdk_chain::bitcoin::transaction::Transaction;
-    use bdk_chain::bitcoin::Network::Testnet;
-    use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
-    use bdk_chain::miniscript::Descriptor;
-    use bdk_chain::CombinedChangeSet;
-    use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt};
-    use std::str::FromStr;
-    use std::sync::Arc;
-
-    #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
-    enum Keychain {
-        External { account: u32, name: String },
-        Internal { account: u32, name: String },
-    }
-
-    #[test]
-    fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() {
-        let (test_changesets, agg_test_changesets) =
-            create_test_changesets(&|height, time, hash| ConfirmationBlockTime {
-                confirmation_time: time,
-                block_id: (height, hash).into(),
-            });
-
-        let conn = Connection::open_in_memory().expect("in memory connection");
-        let mut store = Store::<Keychain, ConfirmationBlockTime>::new(conn)
-            .expect("create new memory db store");
-
-        test_changesets.iter().for_each(|changeset| {
-            store.write(changeset).expect("write changeset");
-        });
-
-        let agg_changeset = store.read().expect("aggregated changeset");
-
-        assert_eq!(agg_changeset, Some(agg_test_changesets));
-    }
-
-    #[test]
-    fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
-        let (test_changesets, agg_test_changesets) =
-            create_test_changesets(&|height, _time, hash| BlockId { height, hash });
-
-        let conn = Connection::open_in_memory().expect("in memory connection");
-        let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
-
-        test_changesets.iter().for_each(|changeset| {
-            store.write(changeset).expect("write changeset");
-        });
-
-        let agg_changeset = store.read().expect("aggregated changeset");
-
-        assert_eq!(agg_changeset, Some(agg_test_changesets));
-    }
-
-    fn create_test_changesets<A: Anchor + Copy>(
-        anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
-    ) -> (
-        Vec<CombinedChangeSet<Keychain, A>>,
-        CombinedChangeSet<Keychain, A>,
-    ) {
-        let secp = &secp256k1::Secp256k1::signing_only();
-
-        let network_changeset = Some(Testnet);
-
-        let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
-        let block_hash_1 =
-            BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
-                .unwrap();
-        let block_hash_2 =
-            BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
-                .unwrap();
-
-        let block_changeset = [
-            (0, Some(block_hash_0)),
-            (1, Some(block_hash_1)),
-            (2, Some(block_hash_2)),
-        ]
-        .into();
-
-        let ext_keychain = Keychain::External {
-            account: 0,
-            name: "ext test".to_string(),
-        };
-        let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
-        let ext_desc_id = ext_desc.descriptor_id();
-        let int_keychain = Keychain::Internal {
-            account: 0,
-            name: "int test".to_string(),
-        };
-        let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
-        let int_desc_id = int_desc.descriptor_id();
-
-        let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
-        let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
-        let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
-        let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
-        let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
-        let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
-
-        let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
-        let txout0_0 = tx0.output.first().unwrap().clone();
-        let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
-        let txout1_0 = tx1.output.first().unwrap().clone();
-
-        let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
-        let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
-
-        let tx_graph_changeset = tx_graph::ChangeSet::<A> {
-            txs: [tx0.clone(), tx1.clone()].into(),
-            txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
-            anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
-            last_seen: [
-                (tx0.compute_txid(), 1598918400),
-                (tx1.compute_txid(), 1598919121),
-                (tx2.compute_txid(), 1608919121),
-            ]
-            .into(),
-        };
-
-        let keychain_changeset = keychain_txout::ChangeSet {
-            keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
-            last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
-        };
-
-        let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
-            indexed_tx_graph::ChangeSet {
-                graph: tx_graph_changeset,
-                indexer: keychain_changeset,
-            };
-
-        // test changesets to write to db
-        let mut changesets = Vec::new();
-
-        changesets.push(CombinedChangeSet {
-            chain: block_changeset,
-            indexed_tx_graph: graph_changeset,
-            network: network_changeset,
-        });
-
-        // create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
-        let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
-            txs: [tx2.clone()].into(),
-            txouts: BTreeMap::default(),
-            anchors: BTreeSet::default(),
-            last_seen: [(tx2.compute_txid(), 1708919121)].into(),
-        };
-
-        let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
-            indexed_tx_graph::ChangeSet {
-                graph: tx_graph_changeset2,
-                indexer: keychain_txout::ChangeSet::default(),
-            };
-
-        changesets.push(CombinedChangeSet {
-            chain: local_chain::ChangeSet::default(),
-            indexed_tx_graph: graph_changeset2,
-            network: None,
-        });
-
-        // create changeset that adds a new anchor2 for tx0 and tx1
-        let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
-            txs: BTreeSet::default(),
-            txouts: BTreeMap::default(),
-            anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
-            last_seen: BTreeMap::default(),
-        };
-
-        let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
-            indexed_tx_graph::ChangeSet {
-                graph: tx_graph_changeset3,
-                indexer: keychain_txout::ChangeSet::default(),
-            };
-
-        changesets.push(CombinedChangeSet {
-            chain: local_chain::ChangeSet::default(),
-            indexed_tx_graph: graph_changeset3,
-            network: None,
-        });
-
-        // aggregated test changesets
-        let agg_test_changesets =
-            changesets
-                .iter()
-                .fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
-                    i.merge(cs.clone());
-                    i
-                });
-
-        (changesets, agg_test_changesets)
-    }
-}
index 9c141336ddac70b32dafa2a966a163a041ec92d2..6f69f48d4888813653af05ad74c4c394aae7df54 100644 (file)
@@ -19,22 +19,26 @@ bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features
 serde = { version = "^1.0", features = ["derive"] }
 serde_json = { version = "^1.0" }
 bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
+bdk_file_store = { path = "../file_store", version = "0.13.0", optional = true }
 
 # Optional dependencies
 bip39 = { version = "2.0", optional = true }
 
 [features]
-default = ["std"]
+default = ["std", "file_store"]
 std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
 compiler = ["miniscript/compiler"]
 all-keys = ["keys-bip39"]
 keys-bip39 = ["bip39"]
+sqlite = ["bdk_chain/sqlite"]
+file_store = ["bdk_file_store"]
 
 [dev-dependencies]
 lazy_static = "1.4"
 assert_matches = "1.5.0"
 tempfile = "3"
-bdk_sqlite = { path = "../sqlite" }
+bdk_chain = { path = "../chain", features = ["sqlite"] }
+bdk_wallet = { path = ".", features = ["sqlite", "file_store"] }
 bdk_file_store = { path = "../file_store" }
 anyhow = "1"
 rand = "^0.8"
index be780b6c31d2d40c8986eaabbae52e55f8f93269..2e5b2cc875fba4f65e5b51bc7ec159ba5b1926f0 100644 (file)
@@ -57,18 +57,17 @@ that the `Wallet` can use to update its view of the chain.
 
 ## Persistence
 
-To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
+To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::WalletChangeSet`].
 
 **Implementations**
 
 * [`bdk_file_store`]: Stores wallet changes in a simple flat file.
-* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
 
 **Example**
 
 <!-- compile_fail because outpoint and txout are fake variables -->
 ```rust,no_run
-use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
+use bdk_wallet::{bitcoin::Network, CreateParams, LoadParams, KeychainKind, ChangeSet};
 
 // Open or create a new file store for wallet data.
 let mut db =
@@ -76,21 +75,22 @@ let mut db =
         .expect("create store");
 
 // Create a wallet with initial wallet data read from the file store.
+let network = Network::Testnet;
 let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
 let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
-let 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");
+let load_params = LoadParams::with_descriptors(descriptor, change_descriptor, network)
+    .expect("must parse descriptors");
+let create_params = CreateParams::new(descriptor, change_descriptor, network)
+    .expect("must parse descriptors");
+let mut wallet = match load_params.load_wallet(&mut db).expect("wallet") {
+    Some(wallet) => wallet,
+    None => create_params.create_wallet(&mut db).expect("wallet"),
+};
 
 // Get a new address to receive bitcoin.
 let receive_address = wallet.reveal_next_address(KeychainKind::External);
 // Persist staged wallet data changes to the file store.
-let staged_changeset = wallet.take_staged();
-if let Some(changeset) = staged_changeset {
-    db.append_changeset(&changeset)
-        .expect("must commit changes to database");
-}
+wallet.persist(&mut db).expect("persist");
 println!("Your new receive address is: {}", receive_address.address);
 ```
 
@@ -233,7 +233,6 @@ conditions.
 [`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
 [`bdk_chain`]: https://docs.rs/bdk_chain/latest
 [`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
-[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
 [`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
 [`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
 [`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
index 13b905ad9c388f66466e9d57eb8fc4e0eb96d797..23102d8b913793549fb93b05198eceaa0c6ca136 100644 (file)
@@ -21,7 +21,7 @@ use bitcoin::Network;
 use miniscript::policy::Concrete;
 use miniscript::Descriptor;
 
-use bdk_wallet::{KeychainKind, Wallet};
+use bdk_wallet::{CreateParams, KeychainKind};
 
 /// Miniscript policy is a high level abstraction of spending conditions. Defined in the
 /// rust-miniscript library here  https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
@@ -77,7 +77,8 @@ fn main() -> Result<(), Box<dyn Error>> {
     );
 
     // Create a new wallet from descriptors
-    let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
+    let mut wallet = CreateParams::new(&descriptor, &internal_descriptor, Network::Regtest)?
+        .create_wallet_no_persist()?;
 
     println!(
         "First derived address from the descriptor: \n{}",
index 0d39481042194f7900add1beb58f810e8f975153..e196c2f86d566ca1fe8b15d52f4cf3c5edfc62c1 100644 (file)
@@ -281,15 +281,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
     }
 }
 
-/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
-/// descriptor
-pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
-    inner: T,
-    secp: &SecpCtx,
-    network: Network,
-) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
-    let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
-
+/// Extra checks for [`ExtendedDescriptor`].
+pub(crate) fn check_wallet_descriptor(
+    descriptor: &Descriptor<DescriptorPublicKey>,
+) -> Result<(), DescriptorError> {
     // Ensure the keys don't contain any hardened derivation steps or hardened wildcards
     let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
         if let DescriptorPublicKey::XPub(DescriptorXKey {
@@ -316,7 +311,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
     // issues
     descriptor.sanity_check()?;
 
-    Ok((descriptor, keymap))
+    Ok(())
 }
 
 #[doc(hidden)]
@@ -855,22 +850,31 @@ mod test {
     }
 
     #[test]
-    fn test_into_wallet_descriptor_checked() {
+    fn test_check_wallet_descriptor() {
         let secp = Secp256k1::new();
 
         let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
-        let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+        let (descriptor, _) = descriptor
+            .into_wallet_descriptor(&secp, Network::Testnet)
+            .expect("must parse");
+        let result = check_wallet_descriptor(&descriptor);
 
         assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
 
         let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
-        let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+        let (descriptor, _) = descriptor
+            .into_wallet_descriptor(&secp, Network::Testnet)
+            .expect("must parse");
+        let result = check_wallet_descriptor(&descriptor);
 
         assert_matches!(result, Err(DescriptorError::MultiPath));
 
         // repeated pubkeys
         let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
-        let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
+        let (descriptor, _) = descriptor
+            .into_wallet_descriptor(&secp, Network::Testnet)
+            .expect("must parse");
+        let result = check_wallet_descriptor(&descriptor);
 
         assert!(result.is_err());
     }
@@ -882,8 +886,10 @@ mod test {
         let secp = Secp256k1::new();
 
         let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
-        let (descriptor, _) =
-            into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
+        let (descriptor, _) = descriptor
+            .into_wallet_descriptor(&secp, Network::Testnet)
+            .unwrap();
+        check_wallet_descriptor(&descriptor).expect("descriptor");
 
         let descriptor = descriptor.at_derivation_index(0).unwrap();
 
index 3ee346d31d0be00e9900e3d7809a5993008fc45a..a7f726685c885b0c1e3e7dd097090df88510259c 100644 (file)
@@ -73,7 +73,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
 ///
 /// ```
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
 /// # use bdk_wallet::KeychainKind;
 /// use bdk_wallet::template::P2Pkh;
 ///
@@ -81,7 +81,8 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
 ///     bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
 /// let key_internal =
 ///     bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
-/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
+/// let mut wallet = CreateParams::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?
+///     .create_wallet_no_persist()?;
 ///
 /// assert_eq!(
 ///     wallet
@@ -105,7 +106,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
 ///
 /// ```
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
 /// # use bdk_wallet::KeychainKind;
 /// use bdk_wallet::template::P2Wpkh_P2Sh;
 ///
@@ -113,11 +114,12 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
 ///     bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
 /// let key_internal =
 ///     bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     P2Wpkh_P2Sh(key_external),
 ///     P2Wpkh_P2Sh(key_internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(
 ///     wallet
@@ -142,7 +144,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
 ///
 /// ```
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet};
+/// # use bdk_wallet::CreateParams;
 /// # use bdk_wallet::KeychainKind;
 /// use bdk_wallet::template::P2Wpkh;
 ///
@@ -150,7 +152,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
 ///     bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
 /// let key_internal =
 ///     bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
-/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
+/// let mut wallet =
+///     CreateParams::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?
+///         .create_wallet_no_persist()?;
 ///
 /// assert_eq!(
 ///     wallet
@@ -174,7 +178,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
 ///
 /// ```
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
 /// # use bdk_wallet::KeychainKind;
 /// use bdk_wallet::template::P2TR;
 ///
@@ -182,7 +186,8 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
 ///     bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
 /// let key_internal =
 ///     bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
-/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
+/// let mut wallet = CreateParams::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?
+///     .create_wallet_no_persist()?;
 ///
 /// assert_eq!(
 ///     wallet
@@ -211,15 +216,16 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip44;
 ///
 /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip44(key.clone(), KeychainKind::External),
 ///     Bip44(key, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
@@ -247,16 +253,17 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip44Public;
 ///
 /// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
 /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip44Public(key.clone(), fingerprint, KeychainKind::External),
 ///     Bip44Public(key, fingerprint, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
@@ -284,15 +291,16 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip49;
 ///
 /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip49(key.clone(), KeychainKind::External),
 ///     Bip49(key, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
@@ -320,16 +328,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip49Public;
 ///
 /// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
 /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip49Public(key.clone(), fingerprint, KeychainKind::External),
 ///     Bip49Public(key, fingerprint, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
@@ -357,15 +366,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip84;
 ///
 /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip84(key.clone(), KeychainKind::External),
 ///     Bip84(key, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
@@ -393,16 +403,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip84Public;
 ///
 /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
 /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip84Public(key.clone(), fingerprint, KeychainKind::External),
 ///     Bip84Public(key, fingerprint, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?.create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
@@ -430,15 +440,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip86;
 ///
 /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip86(key.clone(), KeychainKind::External),
 ///     Bip86(key, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
@@ -466,16 +477,17 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
 /// ```
 /// # use std::str::FromStr;
 /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet,  KeychainKind};
+/// # use bdk_wallet::{CreateParams, KeychainKind};
 /// use bdk_wallet::template::Bip86Public;
 ///
 /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
 /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
-/// let mut wallet = Wallet::new(
+/// let mut wallet = CreateParams::new(
 ///     Bip86Public(key.clone(), fingerprint, KeychainKind::External),
 ///     Bip86Public(key, fingerprint, KeychainKind::Internal),
 ///     Network::Testnet,
-/// )?;
+/// )?
+/// .create_wallet_no_persist()?;
 ///
 /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
 /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
index f7c6f3549dc9ec6dc80c4df60aaf1a3d35967e6c..da304a26cde31cc23e26e2b8092ef2870dfd12a4 100644 (file)
@@ -36,12 +36,22 @@ pub use types::*;
 pub use wallet::signer;
 pub use wallet::signer::SignOptions;
 pub use wallet::tx_builder::TxBuilder;
+pub use wallet::ChangeSet;
+pub use wallet::CreateParams;
+pub use wallet::LoadParams;
+pub use wallet::PersistedWallet;
 pub use wallet::Wallet;
 
-/// Get the version of BDK at runtime
+/// Get the version of [`bdk_wallet`](crate) at runtime.
 pub fn version() -> &'static str {
     env!("CARGO_PKG_VERSION", "unknown")
 }
 
 pub use bdk_chain as chain;
 pub(crate) use bdk_chain::collections;
+#[cfg(feature = "sqlite")]
+pub use bdk_chain::rusqlite;
+#[cfg(feature = "sqlite")]
+pub use bdk_chain::sqlite;
+
+pub use chain::WalletChangeSet;
index 4b49db144284e6835502ac1b151fd478ca85cad1..fbb72b7ea858a3db12ebd3a4cae11ccfdd06804f 100644 (file)
 //! }"#;
 //!
 //! let import = FullyNodedExport::from_str(import)?;
-//! let wallet = Wallet::new(
+//! let wallet = CreateParams::new(
 //!     &import.descriptor(),
 //!     &import.change_descriptor().expect("change descriptor"),
 //!     Network::Testnet,
-//! )?;
+//! )?
+//! .create_wallet_no_persist()?;
 //! # Ok::<_, Box<dyn std::error::Error>>(())
 //! ```
 //!
 //! # use bitcoin::*;
 //! # use bdk_wallet::wallet::export::*;
 //! # use bdk_wallet::*;
-//! let wallet = Wallet::new(
+//! let wallet = CreateParams::new(
 //!     "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
 //!     "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
 //!     Network::Testnet,
-//! )?;
+//! )?
+//! .create_wallet_no_persist()?;
 //! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
 //!
 //! println!("Exported: {}", export.to_string());
@@ -219,12 +221,15 @@ mod test {
     use bitcoin::{transaction, BlockHash, Network, Transaction};
 
     use super::*;
-    use crate::wallet::Wallet;
+    use crate::wallet::{CreateParams, Wallet};
 
     fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
         use crate::wallet::Update;
         use bdk_chain::TxGraph;
-        let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
+        let mut wallet = CreateParams::new(descriptor, change_descriptor, network)
+            .expect("must parse descriptors")
+            .create_wallet_no_persist()
+            .expect("must create wallet");
         let transaction = Transaction {
             input: vec![],
             output: vec![],
index b79cd5cf66e7aea1d8c3503245cc813543300251..d72733945550ab1098ea2cb286d81f0f8b1a99e4 100644 (file)
@@ -18,7 +18,7 @@
 //! # use bdk_wallet::signer::SignerOrdering;
 //! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
 //! # use bdk_wallet::wallet::AddressIndex::New;
-//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
 //! # use hwi::HWIClient;
 //! # use std::sync::Arc;
 //! #
 //! let first_device = devices.remove(0)?;
 //! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
 //!
-//! # let mut wallet = Wallet::new(
-//! #     "",
-//! #     None,
-//! #     Network::Testnet,
-//! # )?;
+//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
 //! #
 //! // Adding the hardware signer to the BDK wallet
 //! wallet.add_signer(
index 9db21ac71d1adaa663092ac7cf9a7fb045b457c1..5e8ad9ee31ff9363e9e21ccdf9d475f88cf6046d 100644 (file)
 //! Wallet
 //!
 //! This module defines the [`Wallet`].
-use crate::collections::{BTreeMap, HashMap};
+use crate::{
+    collections::{BTreeMap, HashMap},
+    descriptor::check_wallet_descriptor,
+};
 use alloc::{
     boxed::Box,
     string::{String, ToString},
@@ -28,8 +31,8 @@ use bdk_chain::{
     },
     spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
     tx_graph::{CanonicalTx, TxGraph, TxNode},
-    BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed,
-    IndexedTxGraph, Merge,
+    BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut,
+    Indexed, IndexedTxGraph, Merge,
 };
 use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
 use bitcoin::{
@@ -38,24 +41,28 @@ use bitcoin::{
 };
 use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
 use bitcoin::{constants::genesis_block, Amount};
-use bitcoin::{
-    secp256k1::{All, Secp256k1},
-    Weight,
-};
+use bitcoin::{secp256k1::Secp256k1, Weight};
 use core::fmt;
 use core::mem;
 use core::ops::Deref;
 use rand_core::RngCore;
 
 use descriptor::error::Error as DescriptorError;
-use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
+use miniscript::{
+    descriptor::KeyMap,
+    psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier},
+};
 
 use bdk_chain::tx_graph::CalculateFeeError;
 
 pub mod coin_selection;
 pub mod export;
+mod params;
 pub mod signer;
 pub mod tx_builder;
+pub use params::*;
+mod persisted;
+pub use persisted::*;
 pub(crate) mod utils;
 
 pub mod error;
@@ -69,8 +76,8 @@ use utils::{check_nsequence_rbf, After, Older, SecpCtx};
 
 use crate::descriptor::policy::BuildSatisfaction;
 use crate::descriptor::{
-    self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
-    ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
+    self, calc_checksum, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
+    IntoWalletDescriptor, Policy, XKeyUtils,
 };
 use crate::psbt::PsbtUtils;
 use crate::signer::SignerError;
@@ -149,7 +156,7 @@ impl From<SyncResult> for Update {
 }
 
 /// The changes made to a wallet by applying an [`Update`].
-pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationBlockTime>;
+pub type ChangeSet = bdk_chain::WalletChangeSet;
 
 /// A derived address and the index it was found at.
 /// For convenience this automatically derefs to `Address`
@@ -177,34 +184,7 @@ impl fmt::Display for AddressInfo {
     }
 }
 
-/// The error type when constructing a fresh [`Wallet`].
-///
-/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
-///
-/// [`new`]: Wallet::new
-/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
-#[derive(Debug)]
-pub enum NewError {
-    /// There was problem with the passed-in descriptor(s).
-    Descriptor(crate::descriptor::DescriptorError),
-}
-
-impl fmt::Display for NewError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            NewError::Descriptor(e) => e.fmt(f),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NewError {}
-
 /// The error type when loading a [`Wallet`] from a [`ChangeSet`].
-///
-/// Method [`load_from_changeset`] may return this error.
-///
-/// [`load_from_changeset`]: Wallet::load_from_changeset
 #[derive(Debug)]
 pub enum LoadError {
     /// There was a problem with the passed-in descriptor(s).
@@ -215,6 +195,8 @@ pub enum LoadError {
     MissingGenesis,
     /// Data loaded from persistence is missing descriptor.
     MissingDescriptor(KeychainKind),
+    /// Data loaded is unexpected.
+    Mismatch(LoadMismatch),
 }
 
 impl fmt::Display for LoadError {
@@ -226,6 +208,7 @@ impl fmt::Display for LoadError {
             LoadError::MissingDescriptor(k) => {
                 write!(f, "loaded data is missing descriptor for keychain {k:?}")
             }
+            LoadError::Mismatch(mismatch) => write!(f, "data mismatch: {mismatch:?}"),
         }
     }
 }
@@ -233,63 +216,34 @@ impl fmt::Display for LoadError {
 #[cfg(feature = "std")]
 impl std::error::Error for LoadError {}
 
-/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
-///
-/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
-///
-/// [`new_or_load`]: Wallet::new_or_load
-/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
+/// Represents a mismatch with what is loaded and what is expected from [`LoadParams`].
 #[derive(Debug)]
-pub enum NewOrLoadError {
-    /// There is a problem with the passed-in descriptor.
-    Descriptor(crate::descriptor::DescriptorError),
-    /// The loaded genesis hash does not match what was provided.
-    LoadedGenesisDoesNotMatch {
-        /// The expected genesis block hash.
-        expected: BlockHash,
-        /// The block hash loaded from persistence.
-        got: Option<BlockHash>,
-    },
-    /// The loaded network type does not match what was provided.
-    LoadedNetworkDoesNotMatch {
-        /// The expected network type.
+pub enum LoadMismatch {
+    /// Network does not match.
+    Network {
+        /// The network that is loaded.
+        loaded: Network,
+        /// The expected network.
         expected: Network,
-        /// The network type loaded from persistence.
-        got: Option<Network>,
     },
-    /// The loaded desccriptor does not match what was provided.
-    LoadedDescriptorDoesNotMatch {
-        /// The descriptor loaded from persistence.
-        got: Option<ExtendedDescriptor>,
-        /// The keychain of the descriptor not matching
+    /// Genesis hash does not match.
+    Genesis {
+        /// The genesis hash that is loaded.
+        loaded: BlockHash,
+        /// The expected genesis hash.
+        expected: BlockHash,
+    },
+    /// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match.
+    Descriptor {
+        /// Keychain identifying the descriptor.
         keychain: KeychainKind,
+        /// The loaded descriptor.
+        loaded: ExtendedDescriptor,
+        /// The expected descriptor.
+        expected: ExtendedDescriptor,
     },
 }
 
-impl fmt::Display for NewOrLoadError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            NewOrLoadError::Descriptor(e) => e.fmt(f),
-            NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
-                write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
-            }
-            NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
-                write!(f, "loaded network type is not {}, got {:?}", expected, got)
-            }
-            NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
-                write!(
-                    f,
-                    "loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
-                    got, keychain
-                )
-            }
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NewOrLoadError {}
-
 /// An error that may occur when applying a block to [`Wallet`].
 #[derive(Debug)]
 pub enum ApplyBlockError {
@@ -324,39 +278,81 @@ impl fmt::Display for ApplyBlockError {
 impl std::error::Error for ApplyBlockError {}
 
 impl Wallet {
-    /// Initialize an empty [`Wallet`].
-    pub fn new<E: IntoWalletDescriptor>(
+    /// Build a new [`Wallet`].
+    ///
+    /// If you have previously created a wallet, use [`load`](Self::load) instead.
+    ///
+    /// # Synopsis
+    ///
+    /// ```rust
+    /// # use bdk_wallet::Wallet;
+    /// # use bitcoin::Network;
+    /// # fn main() -> anyhow::Result<()> {
+    /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+    /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+    /// // Create a non-persisted wallet.
+    /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+    ///     .create_wallet_no_persist()?;
+    ///
+    /// // Create a wallet that is persisted to SQLite database.
+    /// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
+    /// # let file_path = temp_dir.path().join("store.db");
+    /// use bdk_wallet::rusqlite::Connection;
+    /// let mut conn = Connection::open(file_path)?;
+    /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+    ///     .create_wallet(&mut conn)?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn create<E: IntoWalletDescriptor>(
         descriptor: E,
         change_descriptor: E,
         network: Network,
-    ) -> Result<Self, NewError> {
-        let genesis_hash = genesis_block(network).block_hash();
-        Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
+    ) -> Result<CreateParams, DescriptorError> {
+        CreateParams::new(descriptor, change_descriptor, network)
     }
 
-    /// Initialize an empty [`Wallet`] with a custom genesis hash.
+    /// Create a new [`Wallet`] with given `params`.
     ///
-    /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
-    /// for syncing from alternative networks.
-    pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
-        descriptor: E,
-        change_descriptor: E,
-        network: Network,
-        genesis_hash: BlockHash,
-    ) -> Result<Self, NewError> {
-        let secp = Secp256k1::new();
+    /// If you have previously created a wallet, use [`load`](Self::load) instead.
+    pub fn create_with_params(params: CreateParams) -> Result<Self, DescriptorError> {
+        let secp = params.secp;
+        let network = params.network;
+        let genesis_hash = params
+            .genesis_hash
+            .unwrap_or(genesis_block(network).block_hash());
+
         let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
-        let mut index = KeychainTxOutIndex::<KeychainKind>::default();
 
-        let (signers, change_signers) =
-            create_signers(&mut index, &secp, descriptor, change_descriptor, network)
-                .map_err(NewError::Descriptor)?;
+        check_wallet_descriptor(&params.descriptor)?;
+        check_wallet_descriptor(&params.change_descriptor)?;
+        let signers = Arc::new(SignersContainer::build(
+            params.descriptor_keymap,
+            &params.descriptor,
+            &secp,
+        ));
+        let change_signers = Arc::new(SignersContainer::build(
+            params.change_descriptor_keymap,
+            &params.change_descriptor,
+            &secp,
+        ));
+        let index = create_indexer(
+            params.descriptor,
+            params.change_descriptor,
+            params.lookahead,
+        )?;
 
+        let descriptor = index.get_descriptor(&KeychainKind::External).cloned();
+        let change_descriptor = index.get_descriptor(&KeychainKind::Internal).cloned();
         let indexed_graph = IndexedTxGraph::new(index);
+        let indexed_graph_changeset = indexed_graph.initial_changeset();
 
-        let staged = ChangeSet {
-            chain: chain_changeset,
-            indexed_tx_graph: indexed_graph.initial_changeset(),
+        let stage = ChangeSet {
+            descriptor,
+            change_descriptor,
+            local_chain: chain_changeset,
+            tx_graph: indexed_graph_changeset.tx_graph,
+            indexer: indexed_graph_changeset.indexer,
             network: Some(network),
         };
 
@@ -366,11 +362,79 @@ impl Wallet {
             network,
             chain,
             indexed_graph,
-            stage: staged,
+            stage,
             secp,
         })
     }
 
+    /// Build [`Wallet`] by loading from persistence or [`ChangeSet`].
+    ///
+    /// Note that the descriptor secret keys are not persisted to the db. You can either add
+    /// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
+    /// construct wallet using [`Wallet::load_with_descriptors`].
+    ///
+    /// # Synopsis
+    ///
+    /// ```rust,no_run
+    /// # use bdk_wallet::{Wallet, ChangeSet, KeychainKind};
+    /// # use bitcoin::{BlockHash, Network, hashes::Hash};
+    /// # fn main() -> anyhow::Result<()> {
+    /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+    /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+    /// # let changeset = ChangeSet::default();
+    /// // Load a wallet from changeset (no persistence).
+    /// let wallet = Wallet::load()
+    ///     .load_wallet_no_persist(changeset)?
+    ///     .expect("must have data to load wallet");
+    ///
+    /// // Load a wallet that is persisted to SQLite database.
+    /// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
+    /// # let file_path = temp_dir.path().join("store.db");
+    /// # let external_keymap = Default::default();
+    /// # let internal_keymap = Default::default();
+    /// # let genesis_hash = BlockHash::all_zeros();
+    /// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?;
+    /// let mut wallet = Wallet::load()
+    ///     // manually include private keys
+    ///     // the alternative is to use `Wallet::load_with_descriptors`
+    ///     .keymap(KeychainKind::External, external_keymap)
+    ///     .keymap(KeychainKind::Internal, internal_keymap)
+    ///     // set a lookahead for our indexer
+    ///     .lookahead(101)
+    ///     // ensure loaded wallet's genesis hash matches this value
+    ///     .genesis_hash(genesis_hash)
+    ///     .load_wallet(&mut conn)?
+    ///     .expect("must have data to load wallet");
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn load() -> LoadParams {
+        LoadParams::new()
+    }
+
+    /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
+    /// wallet has a different `network`.
+    ///
+    /// Note that the descriptor secret keys are not persisted to the db. You can either add
+    /// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
+    /// construct wallet using [`Wallet::load_with_descriptors`].
+    pub fn load_with_network(network: Network) -> LoadParams {
+        LoadParams::with_network(network)
+    }
+
+    /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
+    /// wallet has a different `network`, `descriptor` or `change_descriptor`.
+    ///
+    /// If the passed-in descriptors contains secret keys, the keys will be included in the
+    /// constructed wallet (which means you can sign transactions).
+    pub fn load_with_descriptors<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: E,
+        network: Network,
+    ) -> Result<LoadParams, DescriptorError> {
+        LoadParams::with_descriptors(descriptor, change_descriptor, network)
+    }
+
     /// Load [`Wallet`] from the given previously persisted [`ChangeSet`].
     ///
     /// Note that the descriptor secret keys are not persisted to the db; this means that after
@@ -382,68 +446,102 @@ impl Wallet {
     ///
     /// ```rust,no_run
     /// # use bdk_wallet::Wallet;
-    /// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
-    /// # use bdk_wallet::descriptor::Descriptor;
-    /// # use bitcoin::key::Secp256k1;
-    /// # use bdk_wallet::KeychainKind;
-    /// use bdk_sqlite::{Store, rusqlite::Connection};
+    /// # use bitcoin::Network;
+    /// # use bdk_wallet::{LoadParams, KeychainKind, PersistedWallet};
+    /// use bdk_chain::sqlite::Connection;
     /// #
-    /// # fn main() -> Result<(), anyhow::Error> {
+    /// # fn main() -> anyhow::Result<()> {
     /// # 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 secp = Secp256k1::new();
-    ///
-    /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
-    /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
-    ///
-    /// 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.read()?.expect("there must be an existing changeset");
-    /// let mut wallet = Wallet::load_from_changeset(changeset)?;
-    ///
-    /// external_signer_container.signers().into_iter()
-    ///     .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
-    /// internal_signer_container.signers().into_iter()
-    ///     .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
+    /// const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+    /// const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+    ///
+    /// let mut conn = Connection::open(file_path)?;
+    /// let mut wallet: PersistedWallet =
+    ///     LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+    ///     .load_wallet(&mut conn)?
+    ///     .expect("db should have data to load wallet");
+    ///
     /// # Ok(())
     /// # }
     /// ```
-    ///
-    /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
-    /// passed-in descriptors to the [`Wallet`].
-    pub fn load_from_changeset(changeset: ChangeSet) -> Result<Self, LoadError> {
+    pub fn load_with_params(
+        changeset: ChangeSet,
+        params: LoadParams,
+    ) -> Result<Option<Self>, LoadError> {
+        if changeset.is_empty() {
+            return Ok(None);
+        }
         let secp = Secp256k1::new();
         let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
-        let chain =
-            LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
-        let mut index = KeychainTxOutIndex::<KeychainKind>::default();
+        let chain = LocalChain::from_changeset(changeset.local_chain)
+            .map_err(|_| LoadError::MissingGenesis)?;
+
         let descriptor = changeset
-            .indexed_tx_graph
-            .indexer
-            .keychains_added
-            .get(&KeychainKind::External)
-            .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?
-            .clone();
+            .descriptor
+            .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?;
         let change_descriptor = changeset
-            .indexed_tx_graph
-            .indexer
-            .keychains_added
-            .get(&KeychainKind::Internal)
-            .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?
-            .clone();
+            .change_descriptor
+            .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?;
+        check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?;
+        check_wallet_descriptor(&change_descriptor).map_err(LoadError::Descriptor)?;
+
+        // checks
+        if let Some(exp_network) = params.check_network {
+            if network != exp_network {
+                return Err(LoadError::Mismatch(LoadMismatch::Network {
+                    loaded: network,
+                    expected: exp_network,
+                }));
+            }
+        }
+        if let Some(exp_genesis_hash) = params.check_genesis_hash {
+            if chain.genesis_hash() != exp_genesis_hash {
+                return Err(LoadError::Mismatch(LoadMismatch::Genesis {
+                    loaded: chain.genesis_hash(),
+                    expected: exp_genesis_hash,
+                }));
+            }
+        }
+        if let Some(exp_descriptor) = params.check_descriptor {
+            if descriptor.descriptor_id() != exp_descriptor.descriptor_id() {
+                return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
+                    keychain: KeychainKind::External,
+                    loaded: descriptor,
+                    expected: exp_descriptor,
+                }));
+            }
+        }
+        if let Some(exp_change_descriptor) = params.check_change_descriptor {
+            if change_descriptor.descriptor_id() != exp_change_descriptor.descriptor_id() {
+                return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
+                    keychain: KeychainKind::External,
+                    loaded: change_descriptor,
+                    expected: exp_change_descriptor,
+                }));
+            }
+        }
 
-        let (signers, change_signers) =
-            create_signers(&mut index, &secp, descriptor, change_descriptor, network)
-                .expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
+        let signers = Arc::new(SignersContainer::build(
+            params.descriptor_keymap,
+            &descriptor,
+            &secp,
+        ));
+        let change_signers = Arc::new(SignersContainer::build(
+            params.change_descriptor_keymap,
+            &change_descriptor,
+            &secp,
+        ));
+        let index = create_indexer(descriptor, change_descriptor, params.lookahead)
+            .map_err(LoadError::Descriptor)?;
 
         let mut indexed_graph = IndexedTxGraph::new(index);
-        indexed_graph.apply_changeset(changeset.indexed_tx_graph);
+        indexed_graph.apply_changeset(changeset.indexer.into());
+        indexed_graph.apply_changeset(changeset.tx_graph.into());
 
         let stage = ChangeSet::default();
 
-        Ok(Wallet {
+        Ok(Some(Wallet {
             signers,
             change_signers,
             chain,
@@ -451,146 +549,7 @@ impl Wallet {
             stage,
             network,
             secp,
-        })
-    }
-
-    /// Either loads [`Wallet`] from the given [`ChangeSet`] or initializes it if one does not exist.
-    ///
-    /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
-    ///
-    /// ```rust,no_run
-    /// # use bdk_wallet::Wallet;
-    /// use bdk_sqlite::{Store, rusqlite::Connection};
-    /// # use bitcoin::Network::Testnet;
-    /// let conn = Connection::open_in_memory().expect("must open connection");
-    /// let mut db = Store::new(conn).expect("must create db");
-    /// let changeset = db.read()?;
-    ///
-    /// let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
-    /// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
-    ///
-    /// let mut wallet = Wallet::new_or_load(external_descriptor, internal_descriptor, changeset, Testnet)?;
-    /// # Ok::<(), anyhow::Error>(())
-    /// ```
-    pub fn new_or_load<E: IntoWalletDescriptor>(
-        descriptor: E,
-        change_descriptor: E,
-        changeset: Option<ChangeSet>,
-        network: Network,
-    ) -> Result<Self, NewOrLoadError> {
-        let genesis_hash = genesis_block(network).block_hash();
-        Self::new_or_load_with_genesis_hash(
-            descriptor,
-            change_descriptor,
-            changeset,
-            network,
-            genesis_hash,
-        )
-    }
-
-    /// Either loads [`Wallet`] from a [`ChangeSet`] or initializes it if one does not exist, using the
-    /// provided descriptor, change descriptor, network, and custom genesis hash.
-    ///
-    /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
-    /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
-    /// useful for syncing from alternative networks.
-    pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
-        descriptor: E,
-        change_descriptor: E,
-        changeset: Option<ChangeSet>,
-        network: Network,
-        genesis_hash: BlockHash,
-    ) -> Result<Self, NewOrLoadError> {
-        if let Some(changeset) = changeset {
-            let mut wallet = Self::load_from_changeset(changeset).map_err(|e| match e {
-                LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
-                LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
-                    expected: network,
-                    got: None,
-                },
-                LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
-                    expected: genesis_hash,
-                    got: None,
-                },
-                LoadError::MissingDescriptor(keychain) => {
-                    NewOrLoadError::LoadedDescriptorDoesNotMatch {
-                        got: None,
-                        keychain,
-                    }
-                }
-            })?;
-            if wallet.network != network {
-                return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
-                    expected: network,
-                    got: Some(wallet.network),
-                });
-            }
-            if wallet.chain.genesis_hash() != genesis_hash {
-                return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
-                    expected: genesis_hash,
-                    got: Some(wallet.chain.genesis_hash()),
-                });
-            }
-
-            let (expected_descriptor, expected_descriptor_keymap) = descriptor
-                .into_wallet_descriptor(&wallet.secp, network)
-                .map_err(NewOrLoadError::Descriptor)?;
-            let wallet_descriptor = wallet.public_descriptor(KeychainKind::External);
-            if wallet_descriptor != &expected_descriptor {
-                return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
-                    got: Some(wallet_descriptor.clone()),
-                    keychain: KeychainKind::External,
-                });
-            }
-            // if expected descriptor has private keys add them as new signers
-            if !expected_descriptor_keymap.is_empty() {
-                let signer_container = SignersContainer::build(
-                    expected_descriptor_keymap,
-                    &expected_descriptor,
-                    &wallet.secp,
-                );
-                signer_container.signers().into_iter().for_each(|signer| {
-                    wallet.add_signer(
-                        KeychainKind::External,
-                        SignerOrdering::default(),
-                        signer.clone(),
-                    )
-                });
-            }
-
-            let (expected_change_descriptor, expected_change_descriptor_keymap) = change_descriptor
-                .into_wallet_descriptor(&wallet.secp, network)
-                .map_err(NewOrLoadError::Descriptor)?;
-            let wallet_change_descriptor = wallet.public_descriptor(KeychainKind::Internal);
-            if wallet_change_descriptor != &expected_change_descriptor {
-                return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
-                    got: Some(wallet_change_descriptor.clone()),
-                    keychain: KeychainKind::Internal,
-                });
-            }
-            // if expected change descriptor has private keys add them as new signers
-            if !expected_change_descriptor_keymap.is_empty() {
-                let signer_container = SignersContainer::build(
-                    expected_change_descriptor_keymap,
-                    &expected_change_descriptor,
-                    &wallet.secp,
-                );
-                signer_container.signers().into_iter().for_each(|signer| {
-                    wallet.add_signer(
-                        KeychainKind::Internal,
-                        SignerOrdering::default(),
-                        signer.clone(),
-                    )
-                });
-            }
-
-            Ok(wallet)
-        } else {
-            Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
-                .map_err(|e| match e {
-                    NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
-                })
-        }
+        }))
     }
 
     /// Get the Bitcoin network the wallet is using.
@@ -642,17 +601,15 @@ impl Wallet {
     /// calls to this method before closing the wallet. For example:
     ///
     /// ```rust,no_run
-    /// # use bdk_wallet::wallet::{Wallet, ChangeSet};
-    /// # use bdk_wallet::KeychainKind;
-    /// 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");
+    /// # use bdk_wallet::{LoadParams, ChangeSet, KeychainKind};
+    /// use bdk_chain::sqlite::Connection;
+    /// let mut conn = Connection::open_in_memory().expect("must open connection");
+    /// let mut wallet = LoadParams::new()
+    ///     .load_wallet(&mut conn)
+    ///     .expect("database is okay")
+    ///     .expect("database has data");
     /// let next_address = wallet.reveal_next_address(KeychainKind::External);
-    /// if let Some(changeset) = wallet.take_staged() {
-    ///     db.write(&changeset)?;
-    /// }
+    /// wallet.persist(&mut conn).expect("write is okay");
     ///
     /// // Now it's safe to show the user their next address!
     /// println!("Next address: {}", next_address.address);
@@ -666,7 +623,7 @@ impl Wallet {
             .reveal_next_spk(&keychain)
             .expect("keychain must exist");
 
-        stage.merge(indexed_tx_graph::ChangeSet::from(index_changeset).into());
+        stage.merge(index_changeset.into());
 
         AddressInfo {
             index,
@@ -1110,16 +1067,38 @@ impl Wallet {
         signers.add_external(signer.id(&self.secp), ordering, signer);
     }
 
+    /// Set the keymap for a given keychain.
+    pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) {
+        let wallet_signers = match keychain {
+            KeychainKind::External => Arc::make_mut(&mut self.signers),
+            KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
+        };
+        let descriptor = self
+            .indexed_graph
+            .index
+            .get_descriptor(&keychain)
+            .expect("keychain must exist");
+        *wallet_signers = SignersContainer::build(keymap, descriptor, &self.secp);
+    }
+
+    /// Set the keymap for each keychain.
+    pub fn set_keymaps(&mut self, keymaps: impl IntoIterator<Item = (KeychainKind, KeyMap)>) {
+        for (keychain, keymap) in keymaps {
+            self.set_keymap(keychain, keymap);
+        }
+    }
+
     /// Get the signers
     ///
     /// ## Example
     ///
     /// ```
-    /// # use bdk_wallet::{Wallet, KeychainKind};
+    /// # use bdk_wallet::{CreateParams, KeychainKind};
     /// # use bdk_wallet::bitcoin::Network;
     /// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)";
     /// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)";
-    /// let wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
+    /// let wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)?
+    ///     .create_wallet_no_persist()?;
     /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) {
     ///     // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
     ///     println!("secret_key: {}", secret_key);
@@ -2424,25 +2403,23 @@ fn new_local_utxo(
     }
 }
 
-fn create_signers<E: IntoWalletDescriptor>(
-    index: &mut KeychainTxOutIndex<KeychainKind>,
-    secp: &Secp256k1<All>,
-    descriptor: E,
-    change_descriptor: E,
-    network: Network,
-) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), DescriptorError> {
-    let descriptor = into_wallet_descriptor_checked(descriptor, secp, network)?;
-    let change_descriptor = into_wallet_descriptor_checked(change_descriptor, secp, network)?;
-    let (descriptor, keymap) = descriptor;
-    let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
-    let _ = index
+fn create_indexer(
+    descriptor: ExtendedDescriptor,
+    change_descriptor: ExtendedDescriptor,
+    lookahead: u32,
+) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
+    let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead);
+
+    // let (descriptor, keymap) = descriptor;
+    // let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+    assert!(indexer
         .insert_descriptor(KeychainKind::External, descriptor)
-        .expect("this is the first descriptor we're inserting");
+        .expect("first descriptor introduced must succeed"));
 
-    let (descriptor, keymap) = change_descriptor;
-    let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
-    let _ = index
-        .insert_descriptor(KeychainKind::Internal, descriptor)
+    // let (descriptor, keymap) = change_descriptor;
+    // let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+    assert!(indexer
+        .insert_descriptor(KeychainKind::Internal, change_descriptor)
         .map_err(|e| {
             use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
             match e {
@@ -2453,9 +2430,9 @@ fn create_signers<E: IntoWalletDescriptor>(
                     unreachable!("this is the first time we're assigning internal")
                 }
             }
-        })?;
+        })?);
 
-    Ok((signers, change_signers))
+    Ok(indexer)
 }
 
 /// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
@@ -2476,16 +2453,18 @@ macro_rules! doctest_wallet {
     () => {{
         use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
         use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph};
-        use $crate::wallet::{Update, Wallet};
+        use $crate::wallet::{Update, CreateParams};
         use $crate::KeychainKind;
         let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
         let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
 
-        let mut wallet = Wallet::new(
+        let mut wallet = CreateParams::new(
             descriptor,
             change_descriptor,
             Network::Regtest,
         )
+        .unwrap()
+        .create_wallet_no_persist()
         .unwrap();
         let address = wallet.peek_address(KeychainKind::External, 0).address;
         let tx = Transaction {
diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs
new file mode 100644 (file)
index 0000000..f6fce55
--- /dev/null
@@ -0,0 +1,217 @@
+use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
+use bitcoin::{BlockHash, Network};
+use miniscript::descriptor::KeyMap;
+
+use crate::{
+    descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
+    KeychainKind, Wallet,
+};
+
+use super::{utils::SecpCtx, ChangeSet, LoadError, PersistedWallet};
+
+/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
+#[derive(Debug, Clone)]
+#[must_use]
+pub struct CreateParams {
+    pub(crate) descriptor: ExtendedDescriptor,
+    pub(crate) descriptor_keymap: KeyMap,
+    pub(crate) change_descriptor: ExtendedDescriptor,
+    pub(crate) change_descriptor_keymap: KeyMap,
+    pub(crate) network: Network,
+    pub(crate) genesis_hash: Option<BlockHash>,
+    pub(crate) lookahead: u32,
+    pub(crate) secp: SecpCtx,
+}
+
+impl CreateParams {
+    /// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
+    ///
+    /// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
+    pub fn new<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: E,
+        network: Network,
+    ) -> Result<Self, DescriptorError> {
+        let secp = SecpCtx::default();
+
+        let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(&secp, network)?;
+        let (change_descriptor, change_descriptor_keymap) =
+            change_descriptor.into_wallet_descriptor(&secp, network)?;
+
+        Ok(Self {
+            descriptor,
+            descriptor_keymap,
+            change_descriptor,
+            change_descriptor_keymap,
+            network,
+            genesis_hash: None,
+            lookahead: DEFAULT_LOOKAHEAD,
+            secp,
+        })
+    }
+
+    /// Extend the given `keychain`'s `keymap`.
+    pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
+        match keychain {
+            KeychainKind::External => &mut self.descriptor_keymap,
+            KeychainKind::Internal => &mut self.change_descriptor_keymap,
+        }
+        .extend(keymap);
+        self
+    }
+
+    /// Use a custom `genesis_hash`.
+    pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
+        self.genesis_hash = Some(genesis_hash);
+        self
+    }
+
+    /// Use custom lookahead value.
+    pub fn lookahead(mut self, lookahead: u32) -> Self {
+        self.lookahead = lookahead;
+        self
+    }
+
+    /// Create [`PersistedWallet`] with the given `Db`.
+    pub fn create_wallet<Db>(
+        self,
+        db: &mut Db,
+    ) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
+    where
+        Wallet: PersistWith<Db, CreateParams = Self>,
+    {
+        PersistedWallet::create(db, self)
+    }
+
+    /// Create [`PersistedWallet`] with the given async `Db`.
+    pub async fn create_wallet_async<Db>(
+        self,
+        db: &mut Db,
+    ) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
+    where
+        Wallet: PersistAsyncWith<Db, CreateParams = Self>,
+    {
+        PersistedWallet::create_async(db, self).await
+    }
+
+    /// Create [`Wallet`] without persistence.
+    pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
+        Wallet::create_with_params(self)
+    }
+}
+
+/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
+#[must_use]
+#[derive(Debug, Clone)]
+pub struct LoadParams {
+    pub(crate) descriptor_keymap: KeyMap,
+    pub(crate) change_descriptor_keymap: KeyMap,
+    pub(crate) lookahead: u32,
+    pub(crate) check_network: Option<Network>,
+    pub(crate) check_genesis_hash: Option<BlockHash>,
+    pub(crate) check_descriptor: Option<ExtendedDescriptor>,
+    pub(crate) check_change_descriptor: Option<ExtendedDescriptor>,
+    pub(crate) secp: SecpCtx,
+}
+
+impl LoadParams {
+    /// Construct parameters with default values.
+    ///
+    /// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
+    pub fn new() -> Self {
+        Self {
+            descriptor_keymap: KeyMap::default(),
+            change_descriptor_keymap: KeyMap::default(),
+            lookahead: DEFAULT_LOOKAHEAD,
+            check_network: None,
+            check_genesis_hash: None,
+            check_descriptor: None,
+            check_change_descriptor: None,
+            secp: SecpCtx::new(),
+        }
+    }
+
+    /// Construct parameters with `network` check.
+    pub fn with_network(network: Network) -> Self {
+        Self {
+            check_network: Some(network),
+            ..Default::default()
+        }
+    }
+
+    /// Construct parameters with descriptor checks.
+    pub fn with_descriptors<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: E,
+        network: Network,
+    ) -> Result<Self, DescriptorError> {
+        let mut params = Self::with_network(network);
+        let secp = &params.secp;
+
+        let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(secp, network)?;
+        params.check_descriptor = Some(descriptor);
+        params.descriptor_keymap = descriptor_keymap;
+
+        let (change_descriptor, change_descriptor_keymap) =
+            change_descriptor.into_wallet_descriptor(secp, network)?;
+        params.check_change_descriptor = Some(change_descriptor);
+        params.change_descriptor_keymap = change_descriptor_keymap;
+
+        Ok(params)
+    }
+
+    /// Extend the given `keychain`'s `keymap`.
+    pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
+        match keychain {
+            KeychainKind::External => &mut self.descriptor_keymap,
+            KeychainKind::Internal => &mut self.change_descriptor_keymap,
+        }
+        .extend(keymap);
+        self
+    }
+
+    /// Check for a `genesis_hash`.
+    pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
+        self.check_genesis_hash = Some(genesis_hash);
+        self
+    }
+
+    /// Use custom lookahead value.
+    pub fn lookahead(mut self, lookahead: u32) -> Self {
+        self.lookahead = lookahead;
+        self
+    }
+
+    /// Load [`PersistedWallet`] with the given `Db`.
+    pub fn load_wallet<Db>(
+        self,
+        db: &mut Db,
+    ) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
+    where
+        Wallet: PersistWith<Db, LoadParams = Self>,
+    {
+        PersistedWallet::load(db, self)
+    }
+
+    /// Load [`PersistedWallet`] with the given async `Db`.
+    pub async fn load_wallet_async<Db>(
+        self,
+        db: &mut Db,
+    ) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
+    where
+        Wallet: PersistAsyncWith<Db, LoadParams = Self>,
+    {
+        PersistedWallet::load_async(db, self).await
+    }
+
+    /// Load [`Wallet`] without persistence.
+    pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
+        Wallet::load_with_params(changeset, self)
+    }
+}
+
+impl Default for LoadParams {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs
new file mode 100644 (file)
index 0000000..fce8ad0
--- /dev/null
@@ -0,0 +1,180 @@
+use core::fmt;
+
+use crate::wallet::{ChangeSet, CreateParams, LoadError, LoadParams};
+use crate::{descriptor::DescriptorError, Wallet};
+use bdk_chain::{Merge, PersistWith};
+
+/// Represents a persisted wallet.
+pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
+
+#[cfg(feature = "sqlite")]
+impl<'c> PersistWith<bdk_chain::sqlite::Transaction<'c>> for Wallet {
+    type CreateParams = CreateParams;
+    type LoadParams = 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::sqlite::Transaction<'c>,
+        params: Self::CreateParams,
+    ) -> Result<Self, Self::CreateError> {
+        let mut wallet =
+            Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+        if let Some(changeset) = wallet.take_staged() {
+            changeset
+                .persist_to_sqlite(db)
+                .map_err(CreateWithPersistError::Persist)?;
+        }
+        Ok(wallet)
+    }
+
+    fn load(
+        conn: &mut bdk_chain::sqlite::Transaction<'c>,
+        params: Self::LoadParams,
+    ) -> Result<Option<Self>, Self::LoadError> {
+        let changeset = ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
+        if changeset.is_empty() {
+            return Ok(None);
+        }
+        Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+    }
+
+    fn persist(
+        &mut self,
+        conn: &mut bdk_chain::sqlite::Transaction,
+    ) -> Result<bool, Self::PersistError> {
+        if let Some(changeset) = self.take_staged() {
+            changeset.persist_to_sqlite(conn)?;
+            return Ok(true);
+        }
+        Ok(false)
+    }
+}
+
+#[cfg(feature = "sqlite")]
+impl PersistWith<bdk_chain::sqlite::Connection> for Wallet {
+    type CreateParams = CreateParams;
+    type LoadParams = 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::sqlite::Connection,
+        params: Self::CreateParams,
+    ) -> Result<Self, Self::CreateError> {
+        let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
+        let wallet = PersistWith::create(&mut db_tx, params)?;
+        db_tx.commit().map_err(CreateWithPersistError::Persist)?;
+        Ok(wallet)
+    }
+
+    fn load(
+        db: &mut bdk_chain::sqlite::Connection,
+        params: Self::LoadParams,
+    ) -> Result<Option<Self>, Self::LoadError> {
+        let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
+        let wallet_opt = PersistWith::load(&mut db_tx, params)?;
+        db_tx.commit().map_err(LoadWithPersistError::Persist)?;
+        Ok(wallet_opt)
+    }
+
+    fn persist(
+        &mut self,
+        db: &mut bdk_chain::sqlite::Connection,
+    ) -> Result<bool, Self::PersistError> {
+        let mut db_tx = db.transaction()?;
+        let has_changes = PersistWith::persist(self, &mut db_tx)?;
+        db_tx.commit()?;
+        Ok(has_changes)
+    }
+}
+
+#[cfg(feature = "file_store")]
+impl PersistWith<bdk_file_store::Store<ChangeSet>> for Wallet {
+    type CreateParams = CreateParams;
+    type LoadParams = LoadParams;
+    type CreateError = CreateWithPersistError<std::io::Error>;
+    type LoadError = LoadWithPersistError<bdk_file_store::AggregateChangesetsError<ChangeSet>>;
+    type PersistError = std::io::Error;
+
+    fn create(
+        db: &mut bdk_file_store::Store<ChangeSet>,
+        params: Self::CreateParams,
+    ) -> Result<Self, Self::CreateError> {
+        let mut wallet =
+            Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+        if let Some(changeset) = wallet.take_staged() {
+            db.append_changeset(&changeset)
+                .map_err(CreateWithPersistError::Persist)?;
+        }
+        Ok(wallet)
+    }
+
+    fn load(
+        db: &mut bdk_file_store::Store<ChangeSet>,
+        params: Self::LoadParams,
+    ) -> Result<Option<Self>, Self::LoadError> {
+        let changeset = db
+            .aggregate_changesets()
+            .map_err(LoadWithPersistError::Persist)?
+            .unwrap_or_default();
+        Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+    }
+
+    fn persist(
+        &mut self,
+        db: &mut bdk_file_store::Store<ChangeSet>,
+    ) -> Result<bool, Self::PersistError> {
+        if let Some(changeset) = self.take_staged() {
+            db.append_changeset(&changeset)?;
+            return Ok(true);
+        }
+        Ok(false)
+    }
+}
+
+/// Error type for [`PersistedWallet::load`].
+#[derive(Debug)]
+pub enum LoadWithPersistError<E> {
+    /// Error from persistence.
+    Persist(E),
+    /// Occurs when the loaded changeset cannot construct [`Wallet`].
+    InvalidChangeSet(LoadError),
+}
+
+impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Persist(err) => fmt::Display::fmt(err, f),
+            Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
+
+/// Error type for [`PersistedWallet::create`].
+#[derive(Debug)]
+pub enum CreateWithPersistError<E> {
+    /// Error from persistence.
+    Persist(E),
+    /// Occurs when the loaded changeset cannot contruct [`Wallet`].
+    Descriptor(DescriptorError),
+}
+
+impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Persist(err) => fmt::Display::fmt(err, f),
+            Self::Descriptor(err) => fmt::Display::fmt(&err, f),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}
index 4cd86ae9d877d716db627591ae6ae52f07712a53..c53eb6cd6b1e864cbe40b4404e4ed0fe2ee9dc0c 100644 (file)
@@ -69,7 +69,8 @@
 //!
 //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
 //! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
-//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
+//! let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)?
+//!     .create_wallet_no_persist()?;
 //! wallet.add_signer(
 //!     KeychainKind::External,
 //!     SignerOrdering(200),
index 9774ec985522d0eecbab539a0bb64a6606b84fde..131b0bd790dade1e869b209b98e24e8d5a58d096 100644 (file)
@@ -1,7 +1,7 @@
 #![allow(unused)]
 use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
 use bdk_wallet::{
-    wallet::{Update, Wallet},
+    wallet::{CreateParams, Update, Wallet},
     KeychainKind, LocalOutput,
 };
 use bitcoin::{
@@ -16,7 +16,11 @@ use std::str::FromStr;
 /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
 /// sats are the transaction fee.
 pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
-    let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
+    let mut wallet = CreateParams::new(descriptor, change, Network::Regtest)
+        .expect("must parse descriptors")
+        .create_wallet_no_persist()
+        .expect("descriptors must be valid");
+
     let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
     let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
         .expect("address")
index 15e73958fb50bc30ae80f8e47a73f8d7131a5413..2965d424cceb1ec8fe0823e7a2afde1a58c813c0 100644 (file)
@@ -3,18 +3,17 @@ extern crate alloc;
 use std::path::Path;
 use std::str::FromStr;
 
+use anyhow::Context;
 use assert_matches::assert_matches;
-use bdk_chain::collections::BTreeMap;
-use bdk_chain::COINBASE_MATURITY;
 use bdk_chain::{BlockId, ConfirmationTime};
-use bdk_sqlite::rusqlite::Connection;
+use bdk_chain::{PersistWith, COINBASE_MATURITY};
 use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
 use bdk_wallet::psbt::PsbtUtils;
 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, ChangeSet, NewError, Wallet};
+use bdk_wallet::wallet::{AddressInfo, Balance, CreateParams, LoadParams, Wallet};
 use bdk_wallet::KeychainKind;
 use bitcoin::hashes::Hash;
 use bitcoin::key::Secp256k1;
@@ -102,46 +101,44 @@ const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
 const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
 
 #[test]
-fn load_recovers_wallet() -> anyhow::Result<()> {
-    fn run<Db, New, Recover, Read, Write>(
+fn wallet_is_persisted() -> anyhow::Result<()> {
+    fn run<Db, CreateDb, OpenDb>(
         filename: &str,
-        create_new: New,
-        recover: Recover,
-        read: Read,
-        write: Write,
+        create_db: CreateDb,
+        open_db: OpenDb,
     ) -> anyhow::Result<()>
     where
-        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<()>,
+        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,
     {
         let temp_dir = tempfile::tempdir().expect("must create tempdir");
         let file_path = temp_dir.path().join(filename);
-        let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
+        let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc();
 
         // create new wallet
         let wallet_spk_index = {
-            let mut wallet =
-                Wallet::new(desc, change_desc, Network::Testnet).expect("must init wallet");
-
+            let mut db = create_db(&file_path)?;
+            let mut wallet = CreateParams::new(external_desc, internal_desc, Network::Testnet)?
+                .create_wallet(&mut db)?;
             wallet.reveal_next_address(KeychainKind::External);
 
             // persist new wallet changes
-            let mut db = create_new(&file_path).expect("must create db");
-            if let Some(changeset) = wallet.take_staged() {
-                write(&mut db, &changeset)?;
-            }
+            assert!(wallet.persist(&mut db)?, "must write");
             wallet.spk_index().clone()
         };
 
         // recover wallet
         {
-            // load persisted wallet changes
-            let db = &mut recover(&file_path).expect("must recover db");
-            let changeset = read(db).expect("must recover wallet").expect("changeset");
+            let mut db = open_db(&file_path).context("failed to recover db")?;
+            let wallet =
+                LoadParams::with_descriptors(external_desc, internal_desc, Network::Testnet)?
+                    .load_wallet(&mut db)?
+                    .expect("wallet must exist");
 
-            let wallet = Wallet::load_from_changeset(changeset).expect("must recover wallet");
             assert_eq!(wallet.network(), Network::Testnet);
             assert_eq!(
                 wallet.spk_index().keychains().collect::<Vec<_>>(),
@@ -154,7 +151,8 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
             let secp = Secp256k1::new();
             assert_eq!(
                 *wallet.public_descriptor(KeychainKind::External),
-                desc.into_wallet_descriptor(&secp, wallet.network())
+                external_desc
+                    .into_wallet_descriptor(&secp, wallet.network())
                     .unwrap()
                     .0
             );
@@ -167,166 +165,11 @@ 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(())
-}
-
-#[test]
-fn new_or_load() -> anyhow::Result<()> {
-    fn run<Db, NewOrRecover, Read, Write>(
-        filename: &str,
-        new_or_load: NewOrRecover,
-        read: Read,
-        write: Write,
-    ) -> anyhow::Result<()>
-    where
-        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);
-        let (desc, change_desc) = get_test_wpkh_with_change_desc();
-
-        // init wallet when non-existent
-        let wallet_keychains: BTreeMap<_, _> = {
-            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");
-            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 = read(&mut db)?;
-            let err = Wallet::new_or_load(desc, change_desc, changeset, Network::Bitcoin)
-                .expect_err("wrong network");
-            assert!(
-                matches!(
-                    err,
-                    bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
-                        got: Some(Network::Testnet),
-                        expected: Network::Bitcoin
-                    }
-                ),
-                "err: {}",
-                err,
-            );
-        }
-
-        // wrong genesis hash
-        {
-            let exp_blockhash = BlockHash::all_zeros();
-            let got_blockhash = bitcoin::constants::genesis_block(Network::Testnet).block_hash();
-
-            let db = &mut new_or_load(&file_path).expect("must open db");
-            let changeset = read(db)?;
-            let err = Wallet::new_or_load_with_genesis_hash(
-                desc,
-                change_desc,
-                changeset,
-                Network::Testnet,
-                exp_blockhash,
-            )
-            .expect_err("wrong genesis hash");
-            assert!(
-                matches!(
-                    err,
-                    bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
-                    if got == Some(got_blockhash) && expected == exp_blockhash
-                ),
-                "err: {}",
-                err,
-            );
-        }
-
-        // wrong external descriptor
-        {
-            let (exp_descriptor, exp_change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
-            let got_descriptor = desc
-                .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
-                .unwrap()
-                .0;
-
-            let db = &mut new_or_load(&file_path).expect("must open db");
-            let changeset = read(db)?;
-            let err =
-                Wallet::new_or_load(exp_descriptor, exp_change_desc, changeset, Network::Testnet)
-                    .expect_err("wrong external descriptor");
-            assert!(
-                matches!(
-                    err,
-                    bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
-                    if got == &Some(got_descriptor) && keychain == KeychainKind::External
-                ),
-                "err: {}",
-                err,
-            );
-        }
-
-        // wrong internal descriptor
-        {
-            let exp_descriptor = get_test_tr_single_sig();
-            let got_descriptor = change_desc
-                .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
-                .unwrap()
-                .0;
-
-            let db = &mut new_or_load(&file_path).expect("must open db");
-            let changeset = read(db)?;
-            let err = Wallet::new_or_load(desc, exp_descriptor, changeset, Network::Testnet)
-                .expect_err("wrong internal descriptor");
-            assert!(
-                matches!(
-                    err,
-                    bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
-                    if got == &Some(got_descriptor) && keychain == KeychainKind::Internal
-                ),
-                "err: {}",
-                err,
-            );
-        }
-
-        // all parameters match
-        {
-            let db = &mut new_or_load(&file_path).expect("must open db");
-            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);
-            assert!(wallet
-                .keychains()
-                .map(|(k, v)| (*k, v.clone()))
-                .eq(wallet_keychains));
-        }
-        Ok(())
-    }
-
-    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(
+    run::<bdk_chain::sqlite::Connection, _, _>(
         "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)?),
+        |path| Ok(bdk_chain::sqlite::Connection::open(path)?),
+        |path| Ok(bdk_chain::sqlite::Connection::open(path)?),
     )?;
 
     Ok(())
@@ -336,14 +179,11 @@ fn new_or_load() -> anyhow::Result<()> {
 fn test_error_external_and_internal_are_the_same() {
     // identical descriptors should fail to create wallet
     let desc = get_test_wpkh();
-    let err = Wallet::new(desc, desc, Network::Testnet);
+    let err = CreateParams::new(desc, desc, Network::Testnet)
+        .unwrap()
+        .create_wallet_no_persist();
     assert!(
-        matches!(
-            &err,
-            Err(NewError::Descriptor(
-                DescriptorError::ExternalAndInternalAreTheSame
-            ))
-        ),
+        matches!(&err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
         "expected same descriptors error, got {:?}",
         err,
     );
@@ -351,14 +191,11 @@ fn test_error_external_and_internal_are_the_same() {
     // public + private of same descriptor should fail to create wallet
     let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
     let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/0/*)";
-    let err = Wallet::new(desc, change_desc, Network::Testnet);
+    let err = CreateParams::new(desc, change_desc, Network::Testnet)
+        .unwrap()
+        .create_wallet_no_persist();
     assert!(
-        matches!(
-            err,
-            Err(NewError::Descriptor(
-                DescriptorError::ExternalAndInternalAreTheSame
-            ))
-        ),
+        matches!(err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
         "expected same descriptors error, got {:?}",
         err,
     );
@@ -1316,8 +1153,11 @@ fn test_create_tx_policy_path_required() {
 
 #[test]
 fn test_create_tx_policy_path_no_csv() {
-    let (desc, change_desc) = get_test_wpkh_with_change_desc();
-    let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).expect("wallet");
+    let (descriptor, change_descriptor) = get_test_wpkh_with_change_desc();
+    let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Regtest)
+        .expect("must parse")
+        .create_wallet_no_persist()
+        .expect("wallet");
 
     let tx = Transaction {
         version: transaction::Version::non_standard(0),
@@ -2927,9 +2767,12 @@ fn test_sign_nonstandard_sighash() {
 
 #[test]
 fn test_unused_address() {
-    let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
-    let change_desc = get_test_wpkh();
-    let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).expect("wallet");
+    let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
+    let change_descriptor = get_test_wpkh();
+    let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
+        .expect("must parse descriptors")
+        .create_wallet_no_persist()
+        .expect("wallet");
 
     // `list_unused_addresses` should be empty if we haven't revealed any
     assert!(wallet
@@ -2956,8 +2799,11 @@ fn test_unused_address() {
 #[test]
 fn test_next_unused_address() {
     let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
-    let change = get_test_wpkh();
-    let mut wallet = Wallet::new(descriptor, change, Network::Testnet).expect("wallet");
+    let change_descriptor = get_test_wpkh();
+    let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
+        .expect("must parse descriptors")
+        .create_wallet_no_persist()
+        .expect("wallet");
     assert_eq!(wallet.derivation_index(KeychainKind::External), None);
 
     assert_eq!(
@@ -3002,9 +2848,12 @@ fn test_next_unused_address() {
 
 #[test]
 fn test_peek_address_at_index() {
-    let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
-    let change_desc = get_test_wpkh();
-    let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).unwrap();
+    let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
+    let change_descriptor = get_test_wpkh();
+    let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
+        .expect("must parse descriptors")
+        .create_wallet_no_persist()
+        .expect("wallet");
 
     assert_eq!(
         wallet.peek_address(KeychainKind::External, 1).to_string(),
@@ -3039,8 +2888,11 @@ fn test_peek_address_at_index() {
 
 #[test]
 fn test_peek_address_at_index_not_derivable() {
-    let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
-                                 get_test_wpkh(), Network::Testnet).unwrap();
+    let wallet = CreateParams::new(
+        "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
+        get_test_wpkh(),
+        Network::Testnet,
+    ).unwrap().create_wallet_no_persist().unwrap();
 
     assert_eq!(
         wallet.peek_address(KeychainKind::External, 1).to_string(),
@@ -3060,8 +2912,11 @@ fn test_peek_address_at_index_not_derivable() {
 
 #[test]
 fn test_returns_index_and_address() {
-    let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
-                                 get_test_wpkh(), Network::Testnet).unwrap();
+    let mut wallet = CreateParams::new(
+        "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
+        get_test_wpkh(),
+        Network::Testnet,
+    ).unwrap().create_wallet_no_persist().unwrap();
 
     // new index 0
     assert_eq!(
@@ -3127,11 +2982,13 @@ fn test_sending_to_bip350_bech32m_address() {
 fn test_get_address() {
     use bdk_wallet::descriptor::template::Bip84;
     let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
-    let wallet = Wallet::new(
+    let wallet = CreateParams::new(
         Bip84(key, KeychainKind::External),
         Bip84(key, KeychainKind::Internal),
         Network::Regtest,
     )
+    .unwrap()
+    .create_wallet_no_persist()
     .unwrap();
 
     assert_eq!(
@@ -3160,7 +3017,10 @@ fn test_get_address() {
 #[test]
 fn test_reveal_addresses() {
     let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
-    let mut wallet = Wallet::new(desc, change_desc, Network::Signet).unwrap();
+    let mut wallet = CreateParams::new(desc, change_desc, Network::Signet)
+        .expect("must parse")
+        .create_wallet_no_persist()
+        .unwrap();
     let keychain = KeychainKind::External;
 
     let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap();
@@ -3181,11 +3041,13 @@ fn test_get_address_no_reuse() {
     use std::collections::HashSet;
 
     let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
-    let mut wallet = Wallet::new(
+    let mut wallet = CreateParams::new(
         Bip84(key, KeychainKind::External),
         Bip84(key, KeychainKind::Internal),
         Network::Regtest,
     )
+    .unwrap()
+    .create_wallet_no_persist()
     .unwrap();
 
     let mut used_set = HashSet::new();
@@ -3655,11 +3517,13 @@ fn test_taproot_sign_derive_index_from_psbt() {
     let mut psbt = builder.finish().unwrap();
 
     // re-create the wallet with an empty db
-    let wallet_empty = Wallet::new(
+    let wallet_empty = CreateParams::new(
         get_test_tr_single_sig_xprv(),
         get_test_tr_single_sig(),
         Network::Regtest,
     )
+    .unwrap()
+    .create_wallet_no_persist()
     .unwrap();
 
     // signing with an empty db means that we will only look at the psbt to infer the
@@ -3760,7 +3624,10 @@ fn test_taproot_sign_non_default_sighash() {
 #[test]
 fn test_spend_coinbase() {
     let (desc, change_desc) = get_test_wpkh_with_change_desc();
-    let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).unwrap();
+    let mut wallet = CreateParams::new(desc, change_desc, Network::Regtest)
+        .unwrap()
+        .create_wallet_no_persist()
+        .unwrap();
 
     let confirmation_height = 5;
     wallet
@@ -4014,6 +3881,7 @@ fn test_taproot_load_descriptor_duplicated_keys() {
 /// [#1483]: https://github.com/bitcoindevkit/bdk/issues/1483
 /// [#1486]: https://github.com/bitcoindevkit/bdk/pull/1486
 #[test]
+#[cfg(debug_assertions)]
 #[should_panic(
     expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0"
 )]
index c71b18fed844c4f4f14861654afb3e32575120ed..75c658510375da5e1eaaac79e8eb9bcca1657388 100644 (file)
@@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
 
 type ChangeSet = (
     local_chain::ChangeSet,
-    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
 );
 
 #[derive(Debug)]
index 9327f787395dc2ac2d54c1d25b1c29b7f2ec3dd2..05c8726d8404e7ec094d63da3202efef631a1622 100644 (file)
@@ -30,7 +30,7 @@ use clap::{Parser, Subcommand};
 pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
 pub type KeychainChangeSet<A> = (
     local_chain::ChangeSet,
-    indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>>,
+    indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
 );
 
 #[derive(Parser)]
@@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain {
 }
 
 pub struct CreateTxChange {
-    pub index_changeset: keychain_txout::ChangeSet<Keychain>,
+    pub index_changeset: keychain_txout::ChangeSet,
     pub change_keychain: Keychain,
     pub index: u32,
 }
index 31e8e70411825a49ff7121f368260875cf054181..5379d17a5cfef049da683408077a78149189d96d 100644 (file)
@@ -100,7 +100,7 @@ pub struct ScanOptions {
 
 type ChangeSet = (
     local_chain::ChangeSet,
-    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
 );
 
 fn main() -> anyhow::Result<()> {
index ffa2ea24e1c7552e2943683dfa043be14b525e09..af64226899eccaee1673c34ac2f8a137e00f1faf 100644 (file)
@@ -22,11 +22,11 @@ use example_cli::{
 };
 
 const DB_MAGIC: &[u8] = b"bdk_example_esplora";
-const DB_PATH: &str = ".bdk_esplora_example.db";
+const DB_PATH: &str = "bdk_example_esplora.db";
 
 type ChangeSet = (
     local_chain::ChangeSet,
-    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+    indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
 );
 
 #[derive(Subcommand, Debug, Clone)]
@@ -84,7 +84,7 @@ impl EsploraArgs {
             Network::Bitcoin => "https://blockstream.info/api",
             Network::Testnet => "https://blockstream.info/testnet/api",
             Network::Regtest => "http://localhost:3002",
-            Network::Signet => "https://mempool.space/signet/api",
+            Network::Signet => "http://signet.bitcoindevkit.net",
             _ => panic!("unsupported network"),
         });
 
@@ -96,7 +96,7 @@ impl EsploraArgs {
 #[derive(Parser, Debug, Clone, PartialEq)]
 pub struct ScanOptions {
     /// Max number of concurrent esplora server requests.
-    #[clap(long, default_value = "1")]
+    #[clap(long, default_value = "5")]
     pub parallel_requests: usize,
 }
 
index 2f562837fe0bfc3d556149389d3f3c90a7f3a9a8..24f26b0e995996806b7c66c9612fa60611e955cb 100644 (file)
@@ -4,7 +4,7 @@ version = "0.2.0"
 edition = "2021"
 
 [dependencies]
-bdk_wallet = { path = "../../crates/wallet" }
+bdk_wallet = { path = "../../crates/wallet", feature = ["file_store"] }
 bdk_electrum = { path = "../../crates/electrum" }
 bdk_file_store = { path = "../../crates/file_store" }
 anyhow = "1"
index bda0e91cd81ee431ae89a1191692101415adb4fd..6291a412438078f03d26da6a3347187b54434142 100644 (file)
@@ -1,53 +1,52 @@
-const DB_MAGIC: &str = "bdk_wallet_electrum_example";
-const SEND_AMOUNT: Amount = Amount::from_sat(5000);
-const STOP_GAP: usize = 50;
-const BATCH_SIZE: usize = 5;
-
-use anyhow::anyhow;
+use bdk_wallet::wallet::CreateParams;
+use bdk_wallet::wallet::LoadParams;
 use std::io::Write;
 use std::str::FromStr;
 
 use bdk_electrum::electrum_client;
 use bdk_electrum::BdkElectrumClient;
 use bdk_file_store::Store;
+use bdk_wallet::bitcoin::Network;
 use bdk_wallet::bitcoin::{Address, Amount};
 use bdk_wallet::chain::collections::HashSet;
-use bdk_wallet::{bitcoin::Network, Wallet};
 use bdk_wallet::{KeychainKind, SignOptions};
 
+const DB_MAGIC: &str = "bdk_wallet_electrum_example";
+const SEND_AMOUNT: Amount = Amount::from_sat(5000);
+const STOP_GAP: usize = 50;
+const BATCH_SIZE: usize = 5;
+
+const NETWORK: Network = Network::Testnet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002";
+
 fn main() -> Result<(), anyhow::Error> {
-    let db_path = std::env::temp_dir().join("bdk-electrum-example");
+    let db_path = "bdk-electrum-example.db";
+
     let mut db =
         Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
-    let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
-    let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
-    let changeset = db
-        .aggregate_changesets()
-        .map_err(|e| anyhow!("load changes error: {}", e))?;
-    let mut wallet = Wallet::new_or_load(
-        external_descriptor,
-        internal_descriptor,
-        changeset,
-        Network::Testnet,
-    )?;
+
+    let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+    let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+    let mut wallet = match load_params.load_wallet(&mut db)? {
+        Some(wallet) => wallet,
+        None => create_params.create_wallet(&mut db)?,
+    };
 
     let address = wallet.next_unused_address(KeychainKind::External);
-    if let Some(changeset) = wallet.take_staged() {
-        db.append_changeset(&changeset)?;
-    }
+    wallet.persist(&mut db)?;
     println!("Generated Address: {}", address);
 
     let balance = wallet.balance();
     println!("Wallet balance before syncing: {} sats", balance.total());
 
     print!("Syncing...");
-    let client = BdkElectrumClient::new(electrum_client::Client::new(
-        "ssl://electrum.blockstream.info:60002",
-    )?);
+    let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
 
     // Populate the electrum client's transaction cache so it doesn't redownload transaction we
     // already have.
-    client.populate_tx_cache(&wallet);
+    client.populate_tx_cache(wallet.tx_graph());
 
     let request = wallet
         .start_full_scan()
@@ -71,9 +70,7 @@ fn main() -> Result<(), anyhow::Error> {
     println!();
 
     wallet.apply_update(update)?;
-    if let Some(changeset) = wallet.take_staged() {
-        db.append_changeset(&changeset)?;
-    }
+    wallet.persist(&mut db)?;
 
     let balance = wallet.balance();
     println!("Wallet balance after syncing: {} sats", balance.total());
index 2a71622cac1f0efccdbabd787345f327c5fbc43e..31bf39aa0b530179a0a6a2481aace5e0bdffade8 100644 (file)
@@ -6,8 +6,7 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-bdk_wallet = { path = "../../crates/wallet" }
+bdk_wallet = { path = "../../crates/wallet", features = ["sqlite"] }
 bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
-bdk_sqlite = { path = "../../crates/sqlite" }
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
 anyhow = "1"
index 0fd82b98594ce3e0b9f63cacf5ac8ad6089241b0..96d1faf916bdc1152d5459c45b61ae776ecc51a5 100644 (file)
@@ -1,76 +1,55 @@
-use std::{collections::BTreeSet, io::Write, str::FromStr};
+use std::{collections::BTreeSet, io::Write};
 
+use anyhow::Ok;
 use bdk_esplora::{esplora_client, EsploraAsyncExt};
 use bdk_wallet::{
-    bitcoin::{Address, Amount, Network, Script},
-    KeychainKind, SignOptions, Wallet,
+    bitcoin::{Amount, Network},
+    rusqlite::Connection,
+    wallet::{CreateParams, LoadParams},
+    KeychainKind, SignOptions,
 };
 
-use bdk_sqlite::{rusqlite::Connection, Store};
-
 const SEND_AMOUNT: Amount = Amount::from_sat(5000);
-const STOP_GAP: usize = 50;
+const STOP_GAP: usize = 5;
 const PARALLEL_REQUESTS: usize = 5;
 
+const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
+const NETWORK: Network = Network::Signet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
+
 #[tokio::main]
 async fn main() -> Result<(), anyhow::Error> {
-    let db_path = "bdk-esplora-async-example.sqlite";
-    let conn = Connection::open(db_path)?;
-    let mut db = Store::new(conn)?;
-    let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
-    let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
-    let changeset = db.read()?;
-
-    let mut wallet = Wallet::new_or_load(
-        external_descriptor,
-        internal_descriptor,
-        changeset,
-        Network::Signet,
-    )?;
+    let mut conn = Connection::open(DB_PATH)?;
+
+    let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+    let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+    let mut wallet = match load_params.load_wallet(&mut conn)? {
+        Some(wallet) => wallet,
+        None => create_params.create_wallet(&mut conn)?,
+    };
 
     let address = wallet.next_unused_address(KeychainKind::External);
-    if let Some(changeset) = wallet.take_staged() {
-        db.write(&changeset)?;
-    }
-    println!("Generated Address: {}", address);
+    wallet.persist(&mut conn)?;
+    println!("Next unused address: ({}) {}", address.index, address);
 
     let balance = wallet.balance();
     println!("Wallet balance before syncing: {} sats", balance.total());
 
     print!("Syncing...");
-    let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
-
-    fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
-        let mut once = Some(());
-        let mut stdout = std::io::stdout();
-        move |spk_i, _| {
-            match once.take() {
-                Some(_) => print!("\nScanning keychain [{:?}]", kind),
-                None => print!(" {:<3}", spk_i),
-            };
-            stdout.flush().expect("must flush");
-        }
-    }
-    let request = wallet
-        .start_full_scan()
-        .inspect_spks_for_all_keychains({
-            let mut once = BTreeSet::<KeychainKind>::new();
-            move |keychain, spk_i, _| {
-                match once.insert(keychain) {
-                    true => print!("\nScanning keychain [{:?}]", keychain),
-                    false => print!(" {:<3}", spk_i),
-                }
-                std::io::stdout().flush().expect("must flush")
+    let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
+
+    let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
+        let mut once = BTreeSet::<KeychainKind>::new();
+        move |keychain, spk_i, _| {
+            if once.insert(keychain) {
+                print!("\nScanning keychain [{:?}] ", keychain);
             }
-        })
-        .inspect_spks_for_keychain(
-            KeychainKind::External,
-            generate_inspect(KeychainKind::External),
-        )
-        .inspect_spks_for_keychain(
-            KeychainKind::Internal,
-            generate_inspect(KeychainKind::Internal),
-        );
+            print!(" {:<3}", spk_i);
+            std::io::stdout().flush().expect("must flush")
+        }
+    });
 
     let mut update = client
         .full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
@@ -79,9 +58,7 @@ async fn main() -> Result<(), anyhow::Error> {
     let _ = update.graph_update.update_last_seen_unconfirmed(now);
 
     wallet.apply_update(update)?;
-    if let Some(changeset) = wallet.take_staged() {
-        db.write(&changeset)?;
-    }
+    wallet.persist(&mut conn)?;
     println!();
 
     let balance = wallet.balance();
@@ -95,12 +72,9 @@ async fn main() -> Result<(), anyhow::Error> {
         std::process::exit(0);
     }
 
-    let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
-        .require_network(Network::Signet)?;
-
     let mut tx_builder = wallet.build_tx();
     tx_builder
-        .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
+        .add_recipient(address.script_pubkey(), SEND_AMOUNT)
         .enable_rbf();
 
     let mut psbt = tx_builder.finish()?;
index 32211b04bf69c31fdc729004267e82d2b402ed38..2d2146ef7a44cc962e112a20f11f359044dfe0ab 100644 (file)
@@ -1,52 +1,56 @@
-const DB_MAGIC: &str = "bdk_wallet_esplora_example";
-const SEND_AMOUNT: Amount = Amount::from_sat(1000);
-const STOP_GAP: usize = 5;
-const PARALLEL_REQUESTS: usize = 1;
-
-use std::{collections::BTreeSet, io::Write, str::FromStr};
+use std::{collections::BTreeSet, io::Write};
 
 use bdk_esplora::{esplora_client, EsploraExt};
 use bdk_file_store::Store;
 use bdk_wallet::{
-    bitcoin::{Address, Amount, Network},
-    KeychainKind, SignOptions, Wallet,
+    bitcoin::{Amount, Network},
+    wallet::{CreateParams, LoadParams},
+    KeychainKind, SignOptions,
 };
 
+const DB_MAGIC: &str = "bdk_wallet_esplora_example";
+const DB_PATH: &str = "bdk-example-esplora-blocking.db";
+const SEND_AMOUNT: Amount = Amount::from_sat(5000);
+const STOP_GAP: usize = 5;
+const PARALLEL_REQUESTS: usize = 5;
+
+const NETWORK: Network = Network::Signet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
+
 fn main() -> Result<(), anyhow::Error> {
-    let db_path = std::env::temp_dir().join("bdk-esplora-example");
     let mut db =
-        Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
-    let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
-    let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
-    let changeset = db.aggregate_changesets()?;
-
-    let mut wallet = Wallet::new_or_load(
-        external_descriptor,
-        internal_descriptor,
-        changeset,
-        Network::Testnet,
-    )?;
+        Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?;
+
+    let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+    let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+
+    let mut wallet = match load_params.load_wallet(&mut db)? {
+        Some(wallet) => wallet,
+        None => create_params.create_wallet(&mut db)?,
+    };
 
     let address = wallet.next_unused_address(KeychainKind::External);
-    if let Some(changeset) = wallet.take_staged() {
-        db.append_changeset(&changeset)?;
-    }
-    println!("Generated Address: {}", address);
+    wallet.persist(&mut db)?;
+    println!(
+        "Next unused address: ({}) {}",
+        address.index, address.address
+    );
 
     let balance = wallet.balance();
     println!("Wallet balance before syncing: {} sats", balance.total());
 
     print!("Syncing...");
-    let client =
-        esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
+    let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
 
     let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
         let mut once = BTreeSet::<KeychainKind>::new();
         move |keychain, spk_i, _| {
-            match once.insert(keychain) {
-                true => print!("\nScanning keychain [{:?}]", keychain),
-                false => print!(" {:<3}", spk_i),
-            };
+            if once.insert(keychain) {
+                print!("\nScanning keychain [{:?}] ", keychain);
+            }
+            print!(" {:<3}", spk_i);
             std::io::stdout().flush().expect("must flush")
         }
     });
@@ -72,12 +76,9 @@ fn main() -> Result<(), anyhow::Error> {
         std::process::exit(0);
     }
 
-    let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
-        .require_network(Network::Testnet)?;
-
     let mut tx_builder = wallet.build_tx();
     tx_builder
-        .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
+        .add_recipient(address.script_pubkey(), SEND_AMOUNT)
         .enable_rbf();
 
     let mut psbt = tx_builder.finish()?;
index b7a9a9e4720ef2013002a972d83801bad73d630b..9e37415bf89da760ce4cb3df40af6fba637c047e 100644 (file)
@@ -6,7 +6,7 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-bdk_wallet = { path = "../../crates/wallet" }
+bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] }
 bdk_file_store = { path = "../../crates/file_store" }
 bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
 
index e09e6d7623e0ce04a0baa6710f04fe6aa0489a58..99d1f49d47ce0df8bf9c577970360817c5d045c1 100644 (file)
@@ -5,7 +5,7 @@ use bdk_bitcoind_rpc::{
 use bdk_file_store::Store;
 use bdk_wallet::{
     bitcoin::{Block, Network, Transaction},
-    wallet::Wallet,
+    wallet::{CreateParams, LoadParams},
 };
 use clap::{self, Parser};
 use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
@@ -90,14 +90,14 @@ fn main() -> anyhow::Result<()> {
         DB_MAGIC.as_bytes(),
         args.db_path,
     )?;
-    let changeset = db.aggregate_changesets()?;
 
-    let mut wallet = Wallet::new_or_load(
-        &args.descriptor,
-        &args.change_descriptor,
-        changeset,
-        args.network,
-    )?;
+    let load_params =
+        LoadParams::with_descriptors(&args.descriptor, &args.change_descriptor, args.network)?;
+    let create_params = CreateParams::new(&args.descriptor, &args.change_descriptor, args.network)?;
+    let mut wallet = match load_params.load_wallet(&mut db)? {
+        Some(wallet) => wallet,
+        None => create_params.create_wallet(&mut db)?,
+    };
     println!(
         "Loaded wallet in {}s",
         start_load_wallet.elapsed().as_secs_f32()
@@ -146,9 +146,7 @@ fn main() -> anyhow::Result<()> {
                 let connected_to = block_emission.connected_to();
                 let start_apply_block = Instant::now();
                 wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
-                if let Some(changeset) = wallet.take_staged() {
-                    db.append_changeset(&changeset)?;
-                }
+                wallet.persist(&mut db)?;
                 let elapsed = start_apply_block.elapsed().as_secs_f32();
                 println!(
                     "Applied block {} at height {} in {}s",
@@ -158,9 +156,7 @@ fn main() -> anyhow::Result<()> {
             Emission::Mempool(mempool_emission) => {
                 let start_apply_mempool = Instant::now();
                 wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
-                if let Some(changeset) = wallet.take_staged() {
-                    db.append_changeset(&changeset)?;
-                }
+                wallet.persist(&mut db)?;
                 println!(
                     "Applied unconfirmed transactions in {}s",
                     start_apply_mempool.elapsed().as_secs_f32()