From 6b43001951db7738f788bcdbb46c8c1cd3e24a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 11 Jul 2024 04:49:01 +0000 Subject: [PATCH] feat!: Rework sqlite, changesets, persistence and wallet-construction 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` which wraps a type `T` which stores staged changes to it. This adds safety when creating and or loading `T` from db. * `struct Persisted` methods, `create`, `load` and `persist`, are avaliable if `trait PersistWith` is implemented for `T`. `Db` represents the database connection and `PersistWith` should be implemented per database-type. * For async, we have `trait PersistedAsyncWith`. * `Wallet` has impls of `PersistedWith`, `PersistedWith` and `PersistedWith` 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`. --- Cargo.toml | 1 - crates/bitcoind_rpc/tests/test_emitter.rs | 25 +- crates/chain/Cargo.toml | 5 + crates/chain/src/changeset.rs | 220 ++++-- crates/chain/src/indexed_tx_graph.rs | 82 +- crates/chain/src/indexer/keychain_txout.rs | 211 ++--- crates/chain/src/indexer/spk_txout.rs | 2 +- crates/chain/src/lib.rs | 12 +- crates/chain/src/local_chain.rs | 176 ++++- crates/chain/src/persist.rs | 135 ++++ crates/chain/src/sqlite.rs | 332 ++++++++ crates/chain/src/tx_graph.rs | 182 +++++ crates/chain/tests/common/tx_template.rs | 2 +- crates/chain/tests/test_indexed_tx_graph.rs | 20 +- .../chain/tests/test_keychain_txout_index.rs | 79 +- crates/chain/tests/test_spk_txout_index.rs | 2 +- crates/electrum/tests/test_electrum.rs | 3 +- crates/hwi/src/lib.rs | 11 +- crates/sqlite/Cargo.toml | 17 - crates/sqlite/README.md | 8 - crates/sqlite/schema/schema_0.sql | 69 -- crates/sqlite/src/lib.rs | 34 - crates/sqlite/src/schema.rs | 96 --- crates/sqlite/src/store.rs | 734 ------------------ crates/wallet/Cargo.toml | 8 +- crates/wallet/README.md | 25 +- crates/wallet/examples/compiler.rs | 5 +- crates/wallet/src/descriptor/mod.rs | 38 +- crates/wallet/src/descriptor/template.rs | 78 +- crates/wallet/src/lib.rs | 12 +- crates/wallet/src/wallet/export.rs | 17 +- crates/wallet/src/wallet/hardwaresigner.rs | 8 +- crates/wallet/src/wallet/mod.rs | 641 ++++++++------- crates/wallet/src/wallet/params.rs | 217 ++++++ crates/wallet/src/wallet/persisted.rs | 180 +++++ crates/wallet/src/wallet/signer.rs | 3 +- crates/wallet/tests/common.rs | 8 +- crates/wallet/tests/wallet.rs | 304 ++------ .../example_bitcoind_rpc_polling/src/main.rs | 2 +- example-crates/example_cli/src/lib.rs | 4 +- example-crates/example_electrum/src/main.rs | 2 +- example-crates/example_esplora/src/main.rs | 8 +- example-crates/wallet_electrum/Cargo.toml | 2 +- example-crates/wallet_electrum/src/main.rs | 55 +- .../wallet_esplora_async/Cargo.toml | 3 +- .../wallet_esplora_async/src/main.rs | 98 +-- .../wallet_esplora_blocking/src/main.rs | 69 +- example-crates/wallet_rpc/Cargo.toml | 2 +- example-crates/wallet_rpc/src/main.rs | 24 +- 49 files changed, 2215 insertions(+), 2056 deletions(-) create mode 100644 crates/chain/src/persist.rs create mode 100644 crates/chain/src/sqlite.rs delete mode 100644 crates/sqlite/Cargo.toml delete mode 100644 crates/sqlite/README.md delete mode 100644 crates/sqlite/schema/schema_0.sql delete mode 100644 crates/sqlite/src/lib.rs delete mode 100644 crates/sqlite/src/schema.rs delete mode 100644 crates/sqlite/src/store.rs create mode 100644 crates/wallet/src/wallet/params.rs create mode 100644 crates/wallet/src/wallet/persisted.rs diff --git a/Cargo.toml b/Cargo.toml index 201bb21f3..f9dbbf885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/wallet", "crates/chain", "crates/file_store", - "crates/sqlite", "crates/electrum", "crates/esplora", "crates/bitcoind_rpc", diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index d7c8b60f7..3a5c67055 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -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 { + 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(()) diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index df3fe41e9..2e704b1de 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -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"] diff --git a/crates/chain/src/changeset.rs b/crates/chain/src/changeset.rs index e5777f46c..77a7ca233 100644 --- a/crates/chain/src/changeset.rs +++ b/crates/chain/src/changeset.rs @@ -1,92 +1,206 @@ +use crate::{ConfirmationBlockTime, Merge}; + +type IndexedTxGraphChangeSet = + crate::indexed_tx_graph::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 { - /// 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>, +pub struct WalletChangeSet { + /// Descriptor for recipient addresses. + pub descriptor: Option>, + /// Descriptor for change addresses. + pub change_descriptor: Option>, /// Stores the network type of the transaction data. pub network: Option, + /// 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, + /// Changes to [`KeychainTxOutIndex`](crate::keychain_txout::KeychainTxOutIndex). + pub indexer: crate::keychain_txout::ChangeSet, } -#[cfg(feature = "miniscript")] -impl core::default::Default for CombinedChangeSet { - fn default() -> Self { - Self { - chain: core::default::Default::default(), - indexed_tx_graph: core::default::Default::default(), - network: None, - } - } -} - -#[cfg(feature = "miniscript")] -impl crate::Merge for CombinedChangeSet { +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::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")?, + row.get::<_, Sql>>("change_descriptor")?, + row.get::<_, Sql>("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 From for CombinedChangeSet { +impl From for WalletChangeSet { fn from(chain: crate::local_chain::ChangeSet) -> Self { Self { - chain, + local_chain: chain, + ..Default::default() + } + } +} + +impl From 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 From>> - for CombinedChangeSet -{ - fn from( - indexed_tx_graph: crate::indexed_tx_graph::ChangeSet< - A, - crate::indexer::keychain_txout::ChangeSet, - >, - ) -> Self { +impl From> for WalletChangeSet { + fn from(tx_graph: crate::tx_graph::ChangeSet) -> Self { Self { - indexed_tx_graph, + tx_graph, ..Default::default() } } } -#[cfg(feature = "miniscript")] -impl From> for CombinedChangeSet { - fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { +impl From 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() } } diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 7181768a1..a8048d845 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -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 IndexedTxGraph { pub fn apply_changeset(&mut self, changeset: 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 { 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) -> 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 { 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 { 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 { 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 AsRef> for IndexedTxGraph { + fn as_ref(&self) -> &TxGraph { + &self.graph } } @@ -285,7 +317,7 @@ where #[must_use] pub struct ChangeSet { /// [`TxGraph`] changeset. - pub graph: tx_graph::ChangeSet, + pub tx_graph: tx_graph::ChangeSet, /// [`Indexer`] changeset. pub indexer: IA, } @@ -293,7 +325,7 @@ pub struct ChangeSet { impl Default for ChangeSet { fn default() -> Self { Self { - graph: Default::default(), + tx_graph: Default::default(), indexer: Default::default(), } } @@ -301,38 +333,30 @@ impl Default for ChangeSet { impl Merge for ChangeSet { 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 From> for ChangeSet { fn from(graph: tx_graph::ChangeSet) -> Self { Self { - graph, + tx_graph: graph, ..Default::default() } } } #[cfg(feature = "miniscript")] -impl From> - for ChangeSet> -{ - fn from(indexer: crate::indexer::keychain_txout::ChangeSet) -> Self { +impl From for ChangeSet { + fn from(indexer: crate::keychain_txout::ChangeSet) -> Self { Self { - graph: Default::default(), + tx_graph: Default::default(), indexer, } } } - -impl AsRef> for IndexedTxGraph { - fn as_ref(&self) -> &TxGraph { - &self.graph - } -} diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 4bbf026af..4b81304f7 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -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 Default for KeychainTxOutIndex { } impl Indexer for KeychainTxOutIndex { - type ChangeSet = ChangeSet; + type ChangeSet = ChangeSet; fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet { let mut changeset = ChangeSet::default(); @@ -154,7 +155,7 @@ impl Indexer for KeychainTxOutIndex { } fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet { - let mut changeset = ChangeSet::::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 Indexer for KeychainTxOutIndex { 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 KeychainTxOutIndex { /// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so /// will return a [`InsertDescriptorError`]. /// - /// `[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 KeychainTxOutIndex { &mut self, keychain: K, descriptor: Descriptor, - ) -> Result, InsertDescriptorError> { - let mut changeset = ChangeSet::::default(); + ) -> Result> { 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 KeychainTxOutIndex { 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 KeychainTxOutIndex { } /// Convenience method to call [`Self::reveal_to_target`] on multiple keychains. - pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap) -> ChangeSet { + pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap) -> ChangeSet { let mut changeset = ChangeSet::default(); for (keychain, &index) in keychains { @@ -656,7 +650,7 @@ impl KeychainTxOutIndex { &mut self, keychain: &K, target_index: u32, - ) -> Option<(Vec>, ChangeSet)> { + ) -> Option<(Vec>, ChangeSet)> { let mut changeset = ChangeSet::default(); let mut spks: Vec> = vec![]; while let Some((i, new)) = self.next_index(keychain) { @@ -687,7 +681,7 @@ impl KeychainTxOutIndex { /// 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, ChangeSet)> { + pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { let (next_index, new) = self.next_index(keychain)?; let mut changeset = ChangeSet::default(); @@ -717,7 +711,7 @@ impl KeychainTxOutIndex { /// 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, ChangeSet)> { + pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed, ChangeSet)> { let next_unused = self .unused_keychain_spks(keychain) .next() @@ -780,27 +774,80 @@ impl KeychainTxOutIndex { } /// Applies the `ChangeSet` to the [`KeychainTxOutIndex`] - /// - /// Keychains added by the `keychains_added` field of `ChangeSet` 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) { - 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::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>("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 std::error::Error for InsertDescriptorError {} /// `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 { - /// Contains the keychains that have been added and their respective descriptor - pub keychains_added: BTreeMap>, +pub struct ChangeSet { /// Contains for each descriptor_id the last revealed index of derivation pub last_revealed: BTreeMap, } -impl Merge for ChangeSet { - /// Merge another [`ChangeSet`] 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 Merge for ChangeSet { /// Returns whether the changeset are empty. fn is_empty(&self) -> bool { - self.last_revealed.is_empty() && self.keychains_added.is_empty() - } -} - -impl Default for ChangeSet { - 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); - -impl core::fmt::Display for NoSuchKeychain { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "no such keychain {:?} exists", &self.0) + self.last_revealed.is_empty() } } diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs index ead446a76..b3cd923ee 100644 --- a/crates/chain/src/indexer/spk_txout.rs +++ b/crates/chain/src/indexer/spk_txout.rs @@ -208,7 +208,7 @@ impl SpkTxOutIndex { /// # 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(); diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index ec0c61a35..b95a1594e 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -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)] diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 2c396cb33..32394bc58 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -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>; - /// 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 { - 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 { - 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>, +} + +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)>> From for ChangeSet { + fn from(blocks: B) -> Self { + Self { + blocks: blocks.into_iter().collect(), + } + } +} + +impl FromIterator<(u32, Option)> for ChangeSet { + fn from_iter)>>(iter: T) -> Self { + Self { + blocks: iter.into_iter().collect(), + } + } +} + +impl FromIterator<(u32, BlockHash)> for ChangeSet { + fn from_iter>(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::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>("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 index 000000000..cdaf6d5e6 --- /dev/null +++ b/crates/chain/src/persist.rs @@ -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: 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; + + /// Load the type from the `Db`. + fn load(db: &mut Db, params: Self::LoadParams) -> Result, Self::LoadError>; + + /// Persist staged changes into `Db`. + fn persist(&mut self, db: &mut Db) -> Result; +} + +type FutureResult<'a, T, E> = Pin> + Send + 'a>>; + +/// Trait that persists the type with an async `Db`. +pub trait PersistAsyncWith: 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; + + /// Load the type from `Db`. + fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult, 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 { + inner: T, +} + +impl Persisted { + /// Create a new persisted `T`. + pub fn create(db: &mut Db, params: T::CreateParams) -> Result + where + T: PersistWith, + { + T::create(db, params).map(|inner| Self { inner }) + } + + /// Create a new persisted `T` with async `Db`. + pub async fn create_async( + db: &mut Db, + params: T::CreateParams, + ) -> Result + where + T: PersistAsyncWith, + { + T::create(db, params).await.map(|inner| Self { inner }) + } + + /// Construct a persisted `T` from `Db`. + pub fn load(db: &mut Db, params: T::LoadParams) -> Result, T::LoadError> + where + T: PersistWith, + { + Ok(T::load(db, params)?.map(|inner| Self { inner })) + } + + /// Contruct a persisted `T` from an async `Db`. + pub async fn load_async( + db: &mut Db, + params: T::LoadParams, + ) -> Result, T::LoadError> + where + T: PersistAsyncWith, + { + Ok(T::load(db, params).await?.map(|inner| Self { inner })) + } + + /// Persist staged changes of `T` into `Db`. + pub fn persist(&mut self, db: &mut Db) -> Result + where + T: PersistWith, + { + 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 + where + T: PersistAsyncWith, + { + self.inner.persist(db).await + } +} + +impl Deref for Persisted { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Persisted { + 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 index 000000000..e8b1cb52c --- /dev/null +++ b/crates/chain/src/sqlite.rs @@ -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>; + + /// Write data into table(s). + fn write_changeset( + &self, + db_tx: &Transaction, + changeset: &Self::ChangeSet, + ) -> rusqlite::Result<()>; +} + +// TODO: Use macros +impl 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> { + 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

{ + conn: rusqlite::Connection, + params: P, +} + +impl Persister

{ + /// 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( + self, + params: P, + ) -> rusqlite::Result<(Persister

, Option)>; +} + +impl ConnectionExt for rusqlite::Connection { + fn into_persister( + mut self, + params: P, + ) -> rusqlite::Result<(Persister

, Option)> { + 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> { + 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(pub T); + +impl From for Sql { + fn from(value: T) -> Self { + Self(value) + } +} + +impl Deref for Sql { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + bitcoin::Txid::from_str(value.as_str()?) + .map(Self) + .map_err(from_sql_error) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.to_string().into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + bitcoin::BlockHash::from_str(value.as_str()?) + .map(Self) + .map_err(from_sql_error) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.to_string().into()) + } +} + +#[cfg(feature = "miniscript")] +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + crate::DescriptorId::from_str(value.as_str()?) + .map(Self) + .map_err(from_sql_error) + } +} + +#[cfg(feature = "miniscript")] +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.to_string().into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?) + .map(Self) + .map_err(from_sql_error) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + let mut bytes = Vec::::new(); + self.consensus_encode(&mut bytes).map_err(to_sql_error)?; + Ok(bytes.into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + Ok(bitcoin::Script::from_bytes(value.as_bytes()?) + .to_owned() + .into()) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.as_bytes().into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into()) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?; + Ok(amount.into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + serde_json::from_str(value.as_str()?) + .map(Sql) + .map_err(from_sql_error) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + serde_json::to_string(&self.0) + .map(Into::into) + .map_err(to_sql_error) + } +} + +#[cfg(feature = "miniscript")] +impl FromSql for Sql> { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + miniscript::Descriptor::from_str(value.as_str()?) + .map(Self) + .map_err(from_sql_error) + } +} + +#[cfg(feature = "miniscript")] +impl ToSql for Sql> { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.to_string().into()) + } +} + +impl FromSql for Sql { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + bitcoin::Network::from_str(value.as_str()?) + .map(Self) + .map_err(from_sql_error) + } +} + +impl ToSql for Sql { + fn to_sql(&self) -> rusqlite::Result> { + Ok(self.to_string().into()) + } +} + +fn from_sql_error(err: E) -> FromSqlError { + FromSqlError::Other(Box::new(err)) +} + +fn to_sql_error(err: E) -> rusqlite::Error { + rusqlite::Error::ToSqlConversionFailure(Box::new(err)) +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 8c11e737a..67f6307af 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1293,6 +1293,188 @@ impl ChangeSet { } } +#[cfg(feature = "sqlite")] +impl ChangeSet +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::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")?, + row.get::<_, Option>>("raw_tx")?, + row.get::<_, Option>("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")?, + row.get::<_, u32>("vout")?, + row.get::<_, Sql>("value")?, + row.get::<_, Sql>("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>("json(anchor)")?, + row.get::<_, Sql>("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 Merge for ChangeSet { fn merge(&mut self, other: Self) { // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 3337fb436..6445fb63c 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -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, diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 01d25c061..e5e66a74b 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -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 { name: &'static str, diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 517698c90..06e2e767c 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -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![ - (&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![ - (&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![ - (&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::::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] = &[ + 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::::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::::new(0); + indexer_b + .insert_descriptor(TestKeychain::External, desc.clone()) + .expect("must insert keychain"); let aggregate_changesets = changesets .iter() .cloned() diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index ccad2af04..3d3b82e89 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -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, }; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 825454331..f0ff460b2 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -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}; diff --git a/crates/hwi/src/lib.rs b/crates/hwi/src/lib.rs index 73a8fc539..e06722755 100644 --- a/crates/hwi/src/lib.rs +++ b/crates/hwi/src/lib.rs @@ -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> { //! let mut devices = HWIClient::enumerate()?; @@ -18,11 +20,8 @@ //! 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 index 8bd161aa3..000000000 --- a/crates/sqlite/Cargo.toml +++ /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 index ba612bded..000000000 --- a/crates/sqlite/README.md +++ /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 index 9b6d18040..000000000 --- a/crates/sqlite/schema/schema_0.sql +++ /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 index ef81a4f0e..000000000 --- a/crates/sqlite/src/lib.rs +++ /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 index ef7c4e044..000000000 --- a/crates/sqlite/src/schema.rs +++ /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 Store { - /// 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::>() - .join(" "); - // split into statements - s.split(';') - // remove extra spaces - .map(|s| { - s.trim() - .split(' ') - .filter(|s| !s.is_empty()) - .collect::>() - .join(" ") - }) - .collect::>() - }) - // remove empty statements - .filter(|s| !s.is_empty()) - .collect::>(); - - 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 { - 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 { - 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 index 5b7992518..000000000 --- a/crates/sqlite/src/store.rs +++ /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 { - // A rusqlite connection to the SQLite database. Uses a Mutex for thread safety. - conn: Mutex, - keychain_marker: PhantomData, - anchor_marker: PhantomData, -} - -impl Debug for Store { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.conn, f) - } -} - -impl Store -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::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 { - let connection = self.conn.get_mut().expect("unlocked connection mutex"); - connection.transaction().map_err(Error::Sqlite) - } -} - -/// Network table related functions. -impl Store { - /// Insert [`Network`] for which all other tables data is valid. - /// - /// Error if trying to insert different network value. - fn insert_network( - current_network: &Option, - db_transaction: &rusqlite::Transaction, - network_changeset: &Option, - ) -> 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, 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::(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 Store { - /// 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>, 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::(0); - let hash = row.get_unwrap::(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 Store -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>, - ) -> 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>, - ) -> 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>, 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::(0); - let keychain = serde_json::from_str::(keychain.as_str()).expect("keychain"); - let descriptor = row.get_unwrap::(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, 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::(0); - let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor"); - let descriptor_id = descriptor.descriptor_id(); - let last_revealed = row.get_unwrap::(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 Store { - /// Insert transactions. - /// - /// Error if trying to insert existing txid. - fn insert_txs( - db_transaction: &rusqlite::Transaction, - tx_graph_changeset: &indexed_tx_graph::ChangeSet>, - ) -> 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>, 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::>(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, 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::(0); - let txid = Txid::from_str(&txid).expect("txid"); - let last_seen = row.get_unwrap::(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>, - ) -> 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, 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::(0); - let txid = Txid::from_str(&txid).expect("txid"); - let vout = row.get_unwrap::(1); - let outpoint = OutPoint::new(txid, vout); - let value = row.get_unwrap::(2); - let script_pubkey = row.get_unwrap::>(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>, - ) -> 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 Store -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>, - ) -> 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, 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::(0); - let hash = BlockHash::from_str(hash.as_str()).expect("block hash"); - let anchor = row.get_unwrap::(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::(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 Store -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) -> 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(¤t_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>, 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 = 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> = - 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::::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::::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( - anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A, - ) -> ( - Vec>, - CombinedChangeSet, - ) { - 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::::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap(); - let tx0: Arc = Arc::new(deserialize(tx0_hex.as_slice()).unwrap()); - let tx1_hex = Vec::::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(); - let tx1: Arc = Arc::new(deserialize(tx1_hex.as_slice()).unwrap()); - let tx2_hex = Vec::::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap(); - let tx2: Arc = 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:: { - 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> = - 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:: { - txs: [tx2.clone()].into(), - txouts: BTreeMap::default(), - anchors: BTreeSet::default(), - last_seen: [(tx2.compute_txid(), 1708919121)].into(), - }; - - let graph_changeset2: indexed_tx_graph::ChangeSet> = - 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:: { - 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> = - 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::::default(), |mut i, cs| { - i.merge(cs.clone()); - i - }); - - (changesets, agg_test_changesets) - } -} diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 9c141336d..6f69f48d4 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -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" diff --git a/crates/wallet/README.md b/crates/wallet/README.md index be780b6c3..2e5b2cc87 100644 --- a/crates/wallet/README.md +++ b/crates/wallet/README.md @@ -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** ```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 diff --git a/crates/wallet/examples/compiler.rs b/crates/wallet/examples/compiler.rs index 13b905ad9..23102d8b9 100644 --- a/crates/wallet/examples/compiler.rs +++ b/crates/wallet/examples/compiler.rs @@ -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> { ); // 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{}", diff --git a/crates/wallet/src/descriptor/mod.rs b/crates/wallet/src/descriptor/mod.rs index 0d3948104..e196c2f86 100644 --- a/crates/wallet/src/descriptor/mod.rs +++ b/crates/wallet/src/descriptor/mod.rs @@ -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( - 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, +) -> 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( // 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(); diff --git a/crates/wallet/src/descriptor/template.rs b/crates/wallet/src/descriptor/template.rs index 3ee346d31..a7f726685 100644 --- a/crates/wallet/src/descriptor/template.rs +++ b/crates/wallet/src/descriptor/template.rs @@ -73,7 +73,7 @@ impl 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 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> DescriptorTemplate for P2Pkh { /// /// ``` /// # 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> DescriptorTemplate for P2Pkh { /// 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> DescriptorTemplate for P2Wpkh_P2Sh { /// /// ``` /// # 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> DescriptorTemplate for P2Wpkh_P2Sh { /// 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> DescriptorTemplate for P2Wpkh { /// /// ``` /// # 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> DescriptorTemplate for P2Wpkh { /// 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> DescriptorTemplate for P2TR { /// ``` /// # 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> DescriptorTemplate for Bip44 { /// ``` /// # 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> DescriptorTemplate for Bip44Public { /// ``` /// # 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> DescriptorTemplate for Bip49 { /// ``` /// # 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> DescriptorTemplate for Bip49Public { /// ``` /// # 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> DescriptorTemplate for Bip84 { /// ``` /// # 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> DescriptorTemplate for Bip84Public { /// ``` /// # 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> DescriptorTemplate for Bip86 { /// ``` /// # 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"); diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index f7c6f3549..da304a26c 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -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; diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 4b49db144..fbb72b7ea 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -29,11 +29,12 @@ //! }"#; //! //! 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>(()) //! ``` //! @@ -42,11 +43,12 @@ //! # 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![], diff --git a/crates/wallet/src/wallet/hardwaresigner.rs b/crates/wallet/src/wallet/hardwaresigner.rs index b79cd5cf6..d72733945 100644 --- a/crates/wallet/src/wallet/hardwaresigner.rs +++ b/crates/wallet/src/wallet/hardwaresigner.rs @@ -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; //! # @@ -30,11 +30,7 @@ //! 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( diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9db21ac71..5e8ad9ee3 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -12,7 +12,10 @@ //! 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 for Update { } /// The changes made to a wallet by applying an [`Update`]. -pub type ChangeSet = bdk_chain::CombinedChangeSet; +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, - }, - /// 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, }, - /// The loaded desccriptor does not match what was provided. - LoadedDescriptorDoesNotMatch { - /// The descriptor loaded from persistence. - got: Option, - /// 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( + /// 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( descriptor: E, change_descriptor: E, network: Network, - ) -> Result { - let genesis_hash = genesis_block(network).block_hash(); - Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash) + ) -> Result { + 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( - descriptor: E, - change_descriptor: E, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - let secp = Secp256k1::new(); + /// If you have previously created a wallet, use [`load`](Self::load) instead. + pub fn create_with_params(params: CreateParams) -> Result { + 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::::default(); - let (signers, change_signers) = - create_signers(&mut index, &secp, descriptor, change_descriptor, network) - .map_err(NewError::Descriptor)?; + check_wallet_descriptor(¶ms.descriptor)?; + check_wallet_descriptor(¶ms.change_descriptor)?; + let signers = Arc::new(SignersContainer::build( + params.descriptor_keymap, + ¶ms.descriptor, + &secp, + )); + let change_signers = Arc::new(SignersContainer::build( + params.change_descriptor_keymap, + ¶ms.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( + descriptor: E, + change_descriptor: E, + network: Network, + ) -> Result { + 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 { + pub fn load_with_params( + changeset: ChangeSet, + params: LoadParams, + ) -> Result, 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::::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( - descriptor: E, - change_descriptor: E, - changeset: Option, - network: Network, - ) -> Result { - 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( - descriptor: E, - change_descriptor: E, - changeset: Option, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - 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) { + 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( - index: &mut KeychainTxOutIndex, - secp: &Secp256k1, - descriptor: E, - change_descriptor: E, - network: Network, -) -> Result<(Arc, Arc), 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, DescriptorError> { + let mut indexer = KeychainTxOutIndex::::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( 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 index 000000000..f6fce556f --- /dev/null +++ b/crates/wallet/src/wallet/params.rs @@ -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, + 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( + descriptor: E, + change_descriptor: E, + network: Network, + ) -> Result { + 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( + self, + db: &mut Db, + ) -> Result>::CreateError> + where + Wallet: PersistWith, + { + PersistedWallet::create(db, self) + } + + /// Create [`PersistedWallet`] with the given async `Db`. + pub async fn create_wallet_async( + self, + db: &mut Db, + ) -> Result>::CreateError> + where + Wallet: PersistAsyncWith, + { + PersistedWallet::create_async(db, self).await + } + + /// Create [`Wallet`] without persistence. + pub fn create_wallet_no_persist(self) -> Result { + 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, + pub(crate) check_genesis_hash: Option, + pub(crate) check_descriptor: Option, + pub(crate) check_change_descriptor: Option, + 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( + descriptor: E, + change_descriptor: E, + network: Network, + ) -> Result { + let mut params = Self::with_network(network); + let secp = ¶ms.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( + self, + db: &mut Db, + ) -> Result, >::LoadError> + where + Wallet: PersistWith, + { + PersistedWallet::load(db, self) + } + + /// Load [`PersistedWallet`] with the given async `Db`. + pub async fn load_wallet_async( + self, + db: &mut Db, + ) -> Result, >::LoadError> + where + Wallet: PersistAsyncWith, + { + PersistedWallet::load_async(db, self).await + } + + /// Load [`Wallet`] without persistence. + pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result, 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 index 000000000..fce8ad058 --- /dev/null +++ b/crates/wallet/src/wallet/persisted.rs @@ -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; + +#[cfg(feature = "sqlite")] +impl<'c> PersistWith> for Wallet { + type CreateParams = CreateParams; + type LoadParams = LoadParams; + + type CreateError = CreateWithPersistError; + type LoadError = LoadWithPersistError; + type PersistError = bdk_chain::rusqlite::Error; + + fn create( + db: &mut bdk_chain::sqlite::Transaction<'c>, + params: Self::CreateParams, + ) -> Result { + let mut wallet = + Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + if let Some(changeset) = wallet.take_staged() { + changeset + .persist_to_sqlite(db) + .map_err(CreateWithPersistError::Persist)?; + } + Ok(wallet) + } + + fn load( + conn: &mut bdk_chain::sqlite::Transaction<'c>, + params: Self::LoadParams, + ) -> Result, 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 { + if let Some(changeset) = self.take_staged() { + changeset.persist_to_sqlite(conn)?; + return Ok(true); + } + Ok(false) + } +} + +#[cfg(feature = "sqlite")] +impl PersistWith for Wallet { + type CreateParams = CreateParams; + type LoadParams = LoadParams; + + type CreateError = CreateWithPersistError; + type LoadError = LoadWithPersistError; + type PersistError = bdk_chain::rusqlite::Error; + + fn create( + db: &mut bdk_chain::sqlite::Connection, + params: Self::CreateParams, + ) -> Result { + 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, 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 { + 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> for Wallet { + type CreateParams = CreateParams; + type LoadParams = LoadParams; + type CreateError = CreateWithPersistError; + type LoadError = LoadWithPersistError>; + type PersistError = std::io::Error; + + fn create( + db: &mut bdk_file_store::Store, + params: Self::CreateParams, + ) -> Result { + let mut wallet = + Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + if let Some(changeset) = wallet.take_staged() { + db.append_changeset(&changeset) + .map_err(CreateWithPersistError::Persist)?; + } + Ok(wallet) + } + + fn load( + db: &mut bdk_file_store::Store, + params: Self::LoadParams, + ) -> Result, 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, + ) -> Result { + 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 { + /// Error from persistence. + Persist(E), + /// Occurs when the loaded changeset cannot construct [`Wallet`]. + InvalidChangeSet(LoadError), +} + +impl fmt::Display for LoadWithPersistError { + 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 std::error::Error for LoadWithPersistError {} + +/// Error type for [`PersistedWallet::create`]. +#[derive(Debug)] +pub enum CreateWithPersistError { + /// Error from persistence. + Persist(E), + /// Occurs when the loaded changeset cannot contruct [`Wallet`]. + Descriptor(DescriptorError), +} + +impl fmt::Display for CreateWithPersistError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Persist(err) => fmt::Display::fmt(err, f), + Self::Descriptor(err) => fmt::Display::fmt(&err, f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CreateWithPersistError {} diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index 4cd86ae9d..c53eb6cd6 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -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), diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 9774ec985..131b0bd79 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -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") diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 15e73958f..2965d424c 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -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( +fn wallet_is_persisted() -> anyhow::Result<()> { + fn run( 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, - Recover: Fn(&Path) -> anyhow::Result, - Read: Fn(&mut Db) -> anyhow::Result>, - Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>, + CreateDb: Fn(&Path) -> anyhow::Result, + OpenDb: Fn(&Path) -> anyhow::Result, + Wallet: PersistWith, + >::CreateError: std::error::Error + Send + Sync + 'static, + >::LoadError: std::error::Error + Send + Sync + 'static, + >::PersistError: std::error::Error + Send + Sync + 'static, { 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::>(), @@ -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( - filename: &str, - new_or_load: NewOrRecover, - read: Read, - write: Write, - ) -> anyhow::Result<()> - where - NewOrRecover: Fn(&Path) -> anyhow::Result, - Read: Fn(&mut Db) -> anyhow::Result>, - 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::( "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" )] diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index c71b18fed..75c658510 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet, ); #[derive(Debug)] diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 9327f7873..05c8726d8 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -30,7 +30,7 @@ use clap::{Parser, Subcommand}; pub type KeychainTxGraph = IndexedTxGraph>; pub type KeychainChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet, ); #[derive(Parser)] @@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain { } pub struct CreateTxChange { - pub index_changeset: keychain_txout::ChangeSet, + pub index_changeset: keychain_txout::ChangeSet, pub change_keychain: Keychain, pub index: u32, } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 31e8e7041..5379d17a5 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -100,7 +100,7 @@ pub struct ScanOptions { type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet, ); fn main() -> anyhow::Result<()> { diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index ffa2ea24e..af6422689 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -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>, + indexed_tx_graph::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, } diff --git a/example-crates/wallet_electrum/Cargo.toml b/example-crates/wallet_electrum/Cargo.toml index 2f562837f..24f26b0e9 100644 --- a/example-crates/wallet_electrum/Cargo.toml +++ b/example-crates/wallet_electrum/Cargo.toml @@ -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" diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index bda0e91cd..6291a4124 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -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::::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()); diff --git a/example-crates/wallet_esplora_async/Cargo.toml b/example-crates/wallet_esplora_async/Cargo.toml index 2a71622ca..31bf39aa0 100644 --- a/example-crates/wallet_esplora_async/Cargo.toml +++ b/example-crates/wallet_esplora_async/Cargo.toml @@ -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" diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 0fd82b985..96d1faf91 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -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::::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::::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()?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 32211b04b..2d2146ef7 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -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::::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::::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::::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()?; diff --git a/example-crates/wallet_rpc/Cargo.toml b/example-crates/wallet_rpc/Cargo.toml index b7a9a9e47..9e37415bf 100644 --- a/example-crates/wallet_rpc/Cargo.toml +++ b/example-crates/wallet_rpc/Cargo.toml @@ -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" } diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs index e09e6d762..99d1f49d4 100644 --- a/example-crates/wallet_rpc/src/main.rs +++ b/example-crates/wallet_rpc/src/main.rs @@ -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()