"crates/wallet",
"crates/chain",
"crates/file_store",
- "crates/sqlite",
"crates/electrum",
"crates/esplora",
"crates/bitcoind_rpc",
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};
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
- BTreeMap::from([(height, Some(hash))]),
+ [(height, Some(hash))].into(),
"chain update changeset is unexpected",
);
}
assert_eq!(
local_chain.apply_update(emission.checkpoint,)?,
if exp_height == exp_hashes.len() - reorged_blocks.len() {
- core::iter::once((height, Some(hash)))
- .chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
- .collect::<bdk_chain::local_chain::ChangeSet>()
+ bdk_chain::local_chain::ChangeSet {
+ blocks: core::iter::once((height, Some(hash)))
+ .chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
+ .collect(),
+ }
} else {
- BTreeMap::from([(height, Some(hash))])
+ [(height, Some(hash))].into()
},
"chain update changeset is unexpected",
);
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())
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
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(())
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"
default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
+sqlite = ["std", "rusqlite", "serde", "serde_json"]
+use crate::{ConfirmationBlockTime, Merge};
+
+type IndexedTxGraphChangeSet =
+ crate::indexed_tx_graph::ChangeSet<ConfirmationBlockTime, crate::keychain_txout::ChangeSet>;
+
/// A changeset containing [`crate`] structures typically persisted together.
-#[cfg(feature = "miniscript")]
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(crate::serde::Deserialize, crate::serde::Serialize),
- serde(
- crate = "crate::serde",
- bound(
- deserialize = "A: Ord + crate::serde::Deserialize<'de>, K: Ord + crate::serde::Deserialize<'de>",
- serialize = "A: Ord + crate::serde::Serialize, K: Ord + crate::serde::Serialize",
- ),
- )
+ serde(crate = "crate::serde")
)]
-pub struct CombinedChangeSet<K, A> {
- /// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
- pub chain: crate::local_chain::ChangeSet,
- /// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph).
- pub indexed_tx_graph:
- crate::indexed_tx_graph::ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>,
+pub struct WalletChangeSet {
+ /// Descriptor for recipient addresses.
+ pub descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
+ /// Descriptor for change addresses.
+ pub change_descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
+ /// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
+ pub local_chain: crate::local_chain::ChangeSet,
+ /// Changes to [`TxGraph`](crate::tx_graph::TxGraph).
+ pub tx_graph: crate::tx_graph::ChangeSet<crate::ConfirmationBlockTime>,
+ /// Changes to [`KeychainTxOutIndex`](crate::keychain_txout::KeychainTxOutIndex).
+ pub indexer: crate::keychain_txout::ChangeSet,
}
-#[cfg(feature = "miniscript")]
-impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
- fn default() -> Self {
- Self {
- chain: core::default::Default::default(),
- indexed_tx_graph: core::default::Default::default(),
- network: None,
- }
- }
-}
-
-#[cfg(feature = "miniscript")]
-impl<K: Ord, A: crate::Anchor> crate::Merge for CombinedChangeSet<K, A> {
+impl Merge for WalletChangeSet {
+ /// Merge another [`WalletChangeSet`] into itself.
+ ///
+ /// The `keychains_added` field respects the invariants of... TODO: FINISH THIS!
fn merge(&mut self, other: Self) {
- crate::Merge::merge(&mut self.chain, other.chain);
- crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph);
+ if other.descriptor.is_some() {
+ debug_assert!(
+ self.descriptor.is_none() || self.descriptor == other.descriptor,
+ "descriptor must never change"
+ );
+ self.descriptor = other.descriptor;
+ }
+ if other.change_descriptor.is_some() {
+ debug_assert!(
+ self.change_descriptor.is_none()
+ || self.change_descriptor == other.change_descriptor,
+ "change descriptor must never change"
+ );
+ }
if other.network.is_some() {
debug_assert!(
self.network.is_none() || self.network == other.network,
- "network type must either be just introduced or remain the same"
+ "network must never change"
);
self.network = other.network;
}
+
+ crate::Merge::merge(&mut self.local_chain, other.local_chain);
+ crate::Merge::merge(&mut self.tx_graph, other.tx_graph);
+ crate::Merge::merge(&mut self.indexer, other.indexer);
}
fn is_empty(&self) -> bool {
- self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
+ self.descriptor.is_none()
+ && self.change_descriptor.is_none()
+ && self.network.is_none()
+ && self.local_chain.is_empty()
+ && self.tx_graph.is_empty()
+ && self.indexer.is_empty()
+ }
+}
+
+#[cfg(feature = "sqlite")]
+impl WalletChangeSet {
+ /// Schema name for wallet.
+ pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
+ /// Name of table to store wallet descriptors and network.
+ pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
+
+ /// Initialize sqlite tables for wallet schema & table.
+ fn init_wallet_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ let schema_v0: &[&str] = &[&format!(
+ "CREATE TABLE {} ( \
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \
+ descriptor TEXT, \
+ change_descriptor TEXT, \
+ network TEXT \
+ ) STRICT;",
+ Self::WALLET_TABLE_NAME,
+ )];
+ crate::sqlite::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])
+ }
+
+ /// Recover a [`WalletChangeSet`] from sqlite database.
+ pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+ Self::init_wallet_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+ use miniscript::{Descriptor, DescriptorPublicKey};
+ use rusqlite::OptionalExtension;
+
+ let mut changeset = Self::default();
+
+ let mut wallet_statement = db_tx.prepare(&format!(
+ "SELECT descriptor, change_descriptor, network FROM {}",
+ Self::WALLET_TABLE_NAME,
+ ))?;
+ let row = wallet_statement
+ .query_row([], |row| {
+ Ok((
+ row.get::<_, Sql<Descriptor<DescriptorPublicKey>>>("descriptor")?,
+ row.get::<_, Sql<Descriptor<DescriptorPublicKey>>>("change_descriptor")?,
+ row.get::<_, Sql<bitcoin::Network>>("network")?,
+ ))
+ })
+ .optional()?;
+ if let Some((Sql(desc), Sql(change_desc), Sql(network))) = row {
+ changeset.descriptor = Some(desc);
+ changeset.change_descriptor = Some(change_desc);
+ changeset.network = Some(network);
+ }
+
+ changeset.local_chain = crate::local_chain::ChangeSet::from_sqlite(db_tx)?;
+ changeset.tx_graph = crate::tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
+ changeset.indexer = crate::indexer::keychain_txout::ChangeSet::from_sqlite(db_tx)?;
+
+ Ok(changeset)
+ }
+
+ /// Persist [`WalletChangeSet`] to sqlite database.
+ pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ Self::init_wallet_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+ use rusqlite::named_params;
+
+ let mut descriptor_statement = db_tx.prepare_cached(&format!(
+ "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor",
+ Self::WALLET_TABLE_NAME,
+ ))?;
+ if let Some(descriptor) = &self.descriptor {
+ descriptor_statement.execute(named_params! {
+ ":id": 0,
+ ":descriptor": Sql(descriptor.clone()),
+ })?;
+ }
+
+ let mut change_descriptor_statement = db_tx.prepare_cached(&format!(
+ "INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor",
+ Self::WALLET_TABLE_NAME,
+ ))?;
+ if let Some(change_descriptor) = &self.change_descriptor {
+ change_descriptor_statement.execute(named_params! {
+ ":id": 0,
+ ":change_descriptor": Sql(change_descriptor.clone()),
+ })?;
+ }
+
+ let mut network_statement = db_tx.prepare_cached(&format!(
+ "INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network",
+ Self::WALLET_TABLE_NAME,
+ ))?;
+ if let Some(network) = self.network {
+ network_statement.execute(named_params! {
+ ":id": 0,
+ ":network": Sql(network),
+ })?;
+ }
+
+ self.local_chain.persist_to_sqlite(db_tx)?;
+ self.tx_graph.persist_to_sqlite(db_tx)?;
+ self.indexer.persist_to_sqlite(db_tx)?;
+ Ok(())
}
}
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
+impl From<crate::local_chain::ChangeSet> for WalletChangeSet {
fn from(chain: crate::local_chain::ChangeSet) -> Self {
Self {
- chain,
+ local_chain: chain,
+ ..Default::default()
+ }
+ }
+}
+
+impl From<IndexedTxGraphChangeSet> for WalletChangeSet {
+ fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self {
+ Self {
+ tx_graph: indexed_tx_graph.tx_graph,
+ indexer: indexed_tx_graph.indexer,
..Default::default()
}
}
}
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>>
- for CombinedChangeSet<K, A>
-{
- fn from(
- indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<
- A,
- crate::indexer::keychain_txout::ChangeSet<K>,
- >,
- ) -> Self {
+impl From<crate::tx_graph::ChangeSet<ConfirmationBlockTime>> for WalletChangeSet {
+ fn from(tx_graph: crate::tx_graph::ChangeSet<ConfirmationBlockTime>) -> Self {
Self {
- indexed_tx_graph,
+ tx_graph,
..Default::default()
}
}
}
-#[cfg(feature = "miniscript")]
-impl<K, A> From<crate::indexer::keychain_txout::ChangeSet<K>> for CombinedChangeSet<K, A> {
- fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self {
+impl From<crate::keychain_txout::ChangeSet> for WalletChangeSet {
+ fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
Self {
- indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
- indexer,
- ..Default::default()
- },
+ indexer,
..Default::default()
}
}
//! 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};
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
self.index.apply_changeset(changeset.indexer);
- for tx in &changeset.graph.txs {
+ for tx in &changeset.tx_graph.txs {
self.index.index_tx(tx);
}
- for (&outpoint, txout) in &changeset.graph.txouts {
+ for (&outpoint, txout) in &changeset.tx_graph.txouts {
self.index.index_txout(outpoint, txout);
}
- self.graph.apply_changeset(changeset.graph);
+ self.graph.apply_changeset(changeset.tx_graph);
}
/// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.initial_changeset();
let indexer = self.index.initial_changeset();
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
}
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.apply_update(update);
let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
/// Insert a floating `txout` of given `outpoint`.
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_txout(outpoint, txout);
let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
/// Insert and index a transaction into the graph.
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_tx(tx);
let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
/// Insert an `anchor` for a given transaction.
}
}
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
.map(|(tx, seen_at)| (tx.clone(), seen_at)),
);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
/// Batch insert unconfirmed transactions.
) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.batch_insert_unconfirmed(txs);
let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
}
}
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));
}
}
graph.merge(self.graph.insert_tx(tx.clone()));
}
let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet { graph, indexer }
+ ChangeSet {
+ tx_graph: graph,
+ indexer,
+ }
+ }
+}
+
+impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
+ fn as_ref(&self) -> &TxGraph<A> {
+ &self.graph
}
}
#[must_use]
pub struct ChangeSet<A, IA> {
/// [`TxGraph`] changeset.
- pub graph: tx_graph::ChangeSet<A>,
+ pub tx_graph: tx_graph::ChangeSet<A>,
/// [`Indexer`] changeset.
pub indexer: IA,
}
impl<A, IA: Default> Default for ChangeSet<A, IA> {
fn default() -> Self {
Self {
- graph: Default::default(),
+ tx_graph: Default::default(),
indexer: Default::default(),
}
}
impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> {
fn merge(&mut self, other: Self) {
- self.graph.merge(other.graph);
+ self.tx_graph.merge(other.tx_graph);
self.indexer.merge(other.indexer);
}
fn is_empty(&self) -> bool {
- self.graph.is_empty() && self.indexer.is_empty()
+ self.tx_graph.is_empty() && self.indexer.is_empty()
}
}
impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
fn from(graph: tx_graph::ChangeSet<A>) -> Self {
Self {
- graph,
+ tx_graph: graph,
..Default::default()
}
}
}
#[cfg(feature = "miniscript")]
-impl<A, K> From<crate::indexer::keychain_txout::ChangeSet<K>>
- for ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>
-{
- fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self {
+impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
+ fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
Self {
- graph: Default::default(),
+ tx_graph: Default::default(),
indexer,
}
}
}
-
-impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
- fn as_ref(&self) -> &TxGraph<A> {
- &self.graph
- }
-}
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};
}
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
- type ChangeSet = ChangeSet<K>;
+ type ChangeSet = ChangeSet;
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
let mut changeset = ChangeSet::default();
}
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet {
- let mut changeset = ChangeSet::<K>::default();
+ let mut changeset = ChangeSet::default();
let txid = tx.compute_txid();
for (op, txout) in tx.output.iter().enumerate() {
changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout));
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(),
}
}
/// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so
/// will return a [`InsertDescriptorError<K>`].
///
- /// `[KeychainTxOutIndex]` will prevent you from inserting two descriptors which derive the same
+ /// [`KeychainTxOutIndex`] will prevent you from inserting two descriptors which derive the same
/// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at
/// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level
/// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have
&mut self,
keychain: K,
descriptor: Descriptor<DescriptorPublicKey>,
- ) -> Result<ChangeSet<K>, InsertDescriptorError<K>> {
- let mut changeset = ChangeSet::<K>::default();
+ ) -> Result<bool, InsertDescriptorError<K>> {
let did = descriptor.descriptor_id();
if !self.keychain_to_descriptor_id.contains_key(&keychain)
&& !self.descriptor_id_to_keychain.contains_key(&did)
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
}
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
- pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet<K> {
+ pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet {
let mut changeset = ChangeSet::default();
for (keychain, &index) in keychains {
&mut self,
keychain: &K,
target_index: u32,
- ) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet<K>)> {
+ ) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet)> {
let mut changeset = ChangeSet::default();
let mut spks: Vec<Indexed<ScriptBuf>> = vec![];
while let Some((i, new)) = self.next_index(keychain) {
/// 1. The descriptor has no wildcard and already has one script revealed.
/// 2. The descriptor has already revealed scripts up to the numeric bound.
/// 3. There is no descriptor associated with the given keychain.
- pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
+ pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
let (next_index, new) = self.next_index(keychain)?;
let mut changeset = ChangeSet::default();
/// could be revealed (see [`reveal_next_spk`] for when this happens).
///
/// [`reveal_next_spk`]: Self::reveal_next_spk
- pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
+ pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
let next_unused = self
.unused_keychain_spks(keychain)
.next()
}
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
- ///
- /// Keychains added by the `keychains_added` field of `ChangeSet<K>` respect the one-to-one
- /// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will
- /// panic if `debug_assertions` are enabled).
- pub fn apply_changeset(&mut self, changeset: ChangeSet<K>) {
- let ChangeSet {
- keychains_added,
- last_revealed,
- } = changeset;
- for (keychain, descriptor) in keychains_added {
- let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor);
- }
-
- for (&desc_id, &index) in &last_revealed {
+ pub fn apply_changeset(&mut self, changeset: ChangeSet) {
+ for (&desc_id, &index) in &changeset.last_revealed {
let v = self.last_revealed.entry(desc_id).or_default();
*v = index.max(*v);
+ self.replenish_inner_index_did(desc_id, self.lookahead);
}
+ }
+}
- for did in last_revealed.keys() {
- self.replenish_inner_index_did(*did, self.lookahead);
+#[cfg(feature = "sqlite")]
+impl ChangeSet {
+ /// Schema name for the changeset.
+ pub const SCHEMA_NAME: &'static str = "bdk_keychaintxout";
+ /// Name for table that stores last revealed indices per descriptor id.
+ pub const LAST_REVEALED_TABLE_NAME: &'static str = "bdk_descriptor_last_revealed";
+
+ /// Initialize sqlite tables for persisting [`KeychainTxOutIndex`].
+ fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ let schema_v0: &[&str] = &[
+ // last revealed
+ &format!(
+ "CREATE TABLE {} ( \
+ descriptor_id TEXT PRIMARY KEY NOT NULL, \
+ last_revealed INTEGER NOT NULL \
+ ) STRICT",
+ Self::LAST_REVEALED_TABLE_NAME,
+ ),
+ ];
+ crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+ }
+
+ /// Construct [`KeychainTxOutIndex`] from sqlite database and given parameters.
+ pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+
+ let mut changeset = Self::default();
+
+ let mut statement = db_tx.prepare(&format!(
+ "SELECT descriptor_id, last_revealed FROM {}",
+ Self::LAST_REVEALED_TABLE_NAME,
+ ))?;
+ let row_iter = statement.query_map([], |row| {
+ Ok((
+ row.get::<_, Sql<DescriptorId>>("descriptor_id")?,
+ row.get::<_, u32>("last_revealed")?,
+ ))
+ })?;
+ for row in row_iter {
+ let (Sql(descriptor_id), last_revealed) = row?;
+ changeset.last_revealed.insert(descriptor_id, last_revealed);
}
+
+ Ok(changeset)
+ }
+
+ /// Persist `changeset` to the sqlite database.
+ pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::rusqlite::named_params;
+ use crate::sqlite::Sql;
+
+ let mut statement = db_tx.prepare_cached(&format!(
+ "REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)",
+ Self::LAST_REVEALED_TABLE_NAME,
+ ))?;
+ for (&descriptor_id, &last_revealed) in &self.last_revealed {
+ statement.execute(named_params! {
+ ":descriptor_id": Sql(descriptor_id),
+ ":last_revealed": last_revealed,
+ })?;
+ }
+
+ Ok(())
}
}
/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the
/// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself.
///
-/// [`apply_changeset`]: KeychainTxOutIndex::apply_changeset
-/// [`Merge`]: Self::merge
-#[derive(Clone, Debug, PartialEq)]
+/// [`KeychainTxOutIndex`]: crate::keychain_txout::KeychainTxOutIndex
+/// [`apply_changeset`]: crate::keychain_txout::KeychainTxOutIndex::apply_changeset
+/// [`merge`]: Self::merge
+#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
- serde(
- crate = "serde_crate",
- bound(
- deserialize = "K: Ord + serde::Deserialize<'de>",
- serialize = "K: Ord + serde::Serialize"
- )
- )
+ serde(crate = "serde_crate")
)]
#[must_use]
-pub struct ChangeSet<K> {
- /// Contains the keychains that have been added and their respective descriptor
- pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
+pub struct ChangeSet {
/// Contains for each descriptor_id the last revealed index of derivation
pub last_revealed: BTreeMap<DescriptorId, u32>,
}
-impl<K: Ord> Merge for ChangeSet<K> {
- /// Merge another [`ChangeSet<K>`] into self.
- ///
- /// For the `keychains_added` field this method respects the invariants of
- /// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two.
- ///
- /// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor
+impl Merge for ChangeSet {
+ /// Merge another [`ChangeSet`] into self.
fn merge(&mut self, other: Self) {
- for (new_keychain, new_descriptor) in other.keychains_added {
- // enforce 1-to-1 invariance
- if !self.keychains_added.contains_key(&new_keychain)
- // FIXME: very inefficient
- && self
- .keychains_added
- .values()
- .all(|descriptor| descriptor != &new_descriptor)
- {
- self.keychains_added.insert(new_keychain, new_descriptor);
- }
- }
-
// for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than
// what was originally in `self`.
for (desc_id, index) in other.last_revealed {
/// Returns whether the changeset are empty.
fn is_empty(&self) -> bool {
- self.last_revealed.is_empty() && self.keychains_added.is_empty()
- }
-}
-
-impl<K> Default for ChangeSet<K> {
- fn default() -> Self {
- Self {
- last_revealed: BTreeMap::default(),
- keychains_added: BTreeMap::default(),
- }
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`].
-pub struct NoSuchKeychain<K>(K);
-
-impl<K: Debug> core::fmt::Display for NoSuchKeychain<K> {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- write!(f, "no such keychain {:?} exists", &self.0)
+ self.last_revealed.is_empty()
}
}
/// # 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();
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;
pub use tx_graph::TxGraph;
mod chain_oracle;
pub use chain_oracle::*;
+mod persist;
+pub use persist::*;
#[doc(hidden)]
pub mod example_utils;
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)]
use core::ops::RangeBounds;
use crate::collections::BTreeMap;
-use crate::{BlockId, ChainOracle};
+use crate::{BlockId, ChainOracle, Merge};
use alloc::sync::Arc;
use bitcoin::block::Header;
use bitcoin::BlockHash;
-/// The [`ChangeSet`] represents changes to [`LocalChain`].
-///
-/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
-/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
-pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
-
/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
/// transaction anchor.
///
/// Apply `changeset` to the checkpoint.
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
- if let Some(start_height) = changeset.keys().next().cloned() {
+ if let Some(start_height) = changeset.blocks.keys().next().cloned() {
// changes after point of agreement
let mut extension = BTreeMap::default();
// point of agreement
}
}
- for (&height, &hash) in changeset {
+ for (&height, &hash) in &changeset.blocks {
match hash {
Some(hash) => {
extension.insert(height, hash);
/// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
- let genesis_entry = changeset.get(&0).copied().flatten();
+ let genesis_entry = changeset.blocks.get(&0).copied().flatten();
let genesis_hash = match genesis_entry {
Some(hash) => hash,
None => return Err(MissingGenesisError),
}
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)
}
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);
}
/// 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.
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 {
}
}
+/// The [`ChangeSet`] represents changes to [`LocalChain`].
+#[derive(Debug, Default, Clone, PartialEq)]
+#[cfg_attr(
+ feature = "serde",
+ derive(serde::Deserialize, serde::Serialize),
+ serde(crate = "serde_crate")
+)]
+pub struct ChangeSet {
+ /// Changes to the [`LocalChain`] blocks.
+ ///
+ /// The key represents the block height, and the value either represents added a new [`CheckPoint`]
+ /// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
+ pub blocks: BTreeMap<u32, Option<BlockHash>>,
+}
+
+impl Merge for ChangeSet {
+ fn merge(&mut self, other: Self) {
+ Merge::merge(&mut self.blocks, other.blocks)
+ }
+
+ fn is_empty(&self) -> bool {
+ self.blocks.is_empty()
+ }
+}
+
+impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
+ fn from(blocks: B) -> Self {
+ Self {
+ blocks: blocks.into_iter().collect(),
+ }
+ }
+}
+
+impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
+ fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
+ Self {
+ blocks: iter.into_iter().collect(),
+ }
+ }
+}
+
+impl FromIterator<(u32, BlockHash)> for ChangeSet {
+ fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
+ Self {
+ blocks: iter
+ .into_iter()
+ .map(|(height, hash)| (height, Some(hash)))
+ .collect(),
+ }
+ }
+}
+
+#[cfg(feature = "sqlite")]
+impl ChangeSet {
+ /// Schema name for the changeset.
+ pub const SCHEMA_NAME: &'static str = "bdk_localchain";
+ /// Name of sqlite table that stores blocks of [`LocalChain`].
+ pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks";
+
+ /// Initialize sqlite tables for persisting [`LocalChain`].
+ fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ let schema_v0: &[&str] = &[
+ // blocks
+ &format!(
+ "CREATE TABLE {} ( \
+ block_height INTEGER PRIMARY KEY NOT NULL, \
+ block_hash TEXT NOT NULL \
+ ) STRICT",
+ Self::BLOCKS_TABLE_NAME,
+ ),
+ ];
+ crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+ }
+
+ /// Construct a [`LocalChain`] from sqlite database.
+ pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+
+ let mut changeset = Self::default();
+
+ let mut statement = db_tx.prepare(&format!(
+ "SELECT block_height, block_hash FROM {}",
+ Self::BLOCKS_TABLE_NAME,
+ ))?;
+ let row_iter = statement.query_map([], |row| {
+ Ok((
+ row.get::<_, u32>("block_height")?,
+ row.get::<_, Sql<BlockHash>>("block_hash")?,
+ ))
+ })?;
+ for row in row_iter {
+ let (height, Sql(hash)) = row?;
+ changeset.blocks.insert(height, Some(hash));
+ }
+
+ Ok(changeset)
+ }
+
+ /// Persist `changeset` to the sqlite database.
+ pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+ use rusqlite::named_params;
+
+ let mut replace_statement = db_tx.prepare_cached(&format!(
+ "REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)",
+ Self::BLOCKS_TABLE_NAME,
+ ))?;
+ let mut delete_statement = db_tx.prepare_cached(&format!(
+ "DELETE FROM {} WHERE block_height=:block_height",
+ Self::BLOCKS_TABLE_NAME,
+ ))?;
+ for (&height, &hash) in &self.blocks {
+ match hash {
+ Some(hash) => replace_statement.execute(named_params! {
+ ":block_height": height,
+ ":block_hash": Sql(hash),
+ })?,
+ None => delete_statement.execute(named_params! {
+ ":block_height": height,
+ })?,
+ };
+ }
+
+ Ok(())
+ }
+}
+
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
#[derive(Clone, Debug, PartialEq)]
pub struct MissingGenesisError;
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
} 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;
}
--- /dev/null
+use core::{
+ future::Future,
+ ops::{Deref, DerefMut},
+ pin::Pin,
+};
+
+use alloc::boxed::Box;
+
+/// Trait that persists the type with `Db`.
+///
+/// Methods of this trait should not be called directly.
+pub trait PersistWith<Db>: Sized {
+ /// Parameters for [`PersistWith::create`].
+ type CreateParams;
+ /// Parameters for [`PersistWith::load`].
+ type LoadParams;
+ /// Error type of [`PersistWith::create`].
+ type CreateError;
+ /// Error type of [`PersistWith::load`].
+ type LoadError;
+ /// Error type of [`PersistWith::persist`].
+ type PersistError;
+
+ /// Create the type and initialize the `Db`.
+ fn create(db: &mut Db, params: Self::CreateParams) -> Result<Self, Self::CreateError>;
+
+ /// Load the type from the `Db`.
+ fn load(db: &mut Db, params: Self::LoadParams) -> Result<Option<Self>, Self::LoadError>;
+
+ /// Persist staged changes into `Db`.
+ fn persist(&mut self, db: &mut Db) -> Result<bool, Self::PersistError>;
+}
+
+type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
+
+/// Trait that persists the type with an async `Db`.
+pub trait PersistAsyncWith<Db>: Sized {
+ /// Parameters for [`PersistAsyncWith::create`].
+ type CreateParams;
+ /// Parameters for [`PersistAsyncWith::load`].
+ type LoadParams;
+ /// Error type of [`PersistAsyncWith::create`].
+ type CreateError;
+ /// Error type of [`PersistAsyncWith::load`].
+ type LoadError;
+ /// Error type of [`PersistAsyncWith::persist`].
+ type PersistError;
+
+ /// Create the type and initialize the `Db`.
+ fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult<Self, Self::CreateError>;
+
+ /// Load the type from `Db`.
+ fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult<Option<Self>, Self::LoadError>;
+
+ /// Persist staged changes into `Db`.
+ fn persist<'a>(&'a mut self, db: &'a mut Db) -> FutureResult<'a, bool, Self::PersistError>;
+}
+
+/// Represents a persisted `T`.
+pub struct Persisted<T> {
+ inner: T,
+}
+
+impl<T> Persisted<T> {
+ /// Create a new persisted `T`.
+ pub fn create<Db>(db: &mut Db, params: T::CreateParams) -> Result<Self, T::CreateError>
+ where
+ T: PersistWith<Db>,
+ {
+ T::create(db, params).map(|inner| Self { inner })
+ }
+
+ /// Create a new persisted `T` with async `Db`.
+ pub async fn create_async<Db>(
+ db: &mut Db,
+ params: T::CreateParams,
+ ) -> Result<Self, T::CreateError>
+ where
+ T: PersistAsyncWith<Db>,
+ {
+ T::create(db, params).await.map(|inner| Self { inner })
+ }
+
+ /// Construct a persisted `T` from `Db`.
+ pub fn load<Db>(db: &mut Db, params: T::LoadParams) -> Result<Option<Self>, T::LoadError>
+ where
+ T: PersistWith<Db>,
+ {
+ Ok(T::load(db, params)?.map(|inner| Self { inner }))
+ }
+
+ /// Contruct a persisted `T` from an async `Db`.
+ pub async fn load_async<Db>(
+ db: &mut Db,
+ params: T::LoadParams,
+ ) -> Result<Option<Self>, T::LoadError>
+ where
+ T: PersistAsyncWith<Db>,
+ {
+ Ok(T::load(db, params).await?.map(|inner| Self { inner }))
+ }
+
+ /// Persist staged changes of `T` into `Db`.
+ pub fn persist<Db>(&mut self, db: &mut Db) -> Result<bool, T::PersistError>
+ where
+ T: PersistWith<Db>,
+ {
+ self.inner.persist(db)
+ }
+
+ /// Persist staged changes of `T` into an async `Db`.
+ pub async fn persist_async<'a, Db>(
+ &'a mut self,
+ db: &'a mut Db,
+ ) -> Result<bool, T::PersistError>
+ where
+ T: PersistAsyncWith<Db>,
+ {
+ self.inner.persist(db).await
+ }
+}
+
+impl<T> Deref for Persisted<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl<T> DerefMut for Persisted<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
--- /dev/null
+//! Module for stuff
+
+use core::{fmt::Debug, ops::Deref, str::FromStr};
+
+use alloc::{borrow::ToOwned, boxed::Box, string::ToString, vec::Vec};
+use bitcoin::consensus::{Decodable, Encodable};
+pub use rusqlite;
+pub use rusqlite::Connection;
+use rusqlite::OptionalExtension;
+pub use rusqlite::Transaction;
+use rusqlite::{
+ named_params,
+ types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
+ ToSql,
+};
+
+use crate::{Anchor, Merge};
+
+/// Parameters for [`Persister`].
+pub trait PersistParams {
+ /// Data type that is loaded and written to the database.
+ type ChangeSet: Default + Merge;
+
+ /// Initialize SQL tables.
+ fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()>;
+
+ /// Load all data from tables.
+ fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>>;
+
+ /// Write data into table(s).
+ fn write_changeset(
+ &self,
+ db_tx: &Transaction,
+ changeset: &Self::ChangeSet,
+ ) -> rusqlite::Result<()>;
+}
+
+// TODO: Use macros
+impl<A: PersistParams, B: PersistParams> PersistParams for (A, B) {
+ type ChangeSet = (A::ChangeSet, B::ChangeSet);
+
+ fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()> {
+ self.0.initialize_tables(db_tx)?;
+ self.1.initialize_tables(db_tx)?;
+ Ok(())
+ }
+
+ fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>> {
+ let changeset = (
+ self.0.load_changeset(db_tx)?.unwrap_or_default(),
+ self.1.load_changeset(db_tx)?.unwrap_or_default(),
+ );
+ if changeset.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(changeset))
+ }
+ }
+
+ fn write_changeset(
+ &self,
+ db_tx: &Transaction,
+ changeset: &Self::ChangeSet,
+ ) -> rusqlite::Result<()> {
+ self.0.write_changeset(db_tx, &changeset.0)?;
+ self.1.write_changeset(db_tx, &changeset.1)?;
+ Ok(())
+ }
+}
+
+/// Persists data in to a relational schema based [SQLite] database file.
+///
+/// The changesets loaded or stored represent changes to keychain and blockchain data.
+///
+/// [SQLite]: https://www.sqlite.org/index.html
+#[derive(Debug)]
+pub struct Persister<P> {
+ conn: rusqlite::Connection,
+ params: P,
+}
+
+impl<P: PersistParams> Persister<P> {
+ /// Persist changeset to the database connection.
+ pub fn persist(&mut self, changeset: &P::ChangeSet) -> rusqlite::Result<()> {
+ if !changeset.is_empty() {
+ let db_tx = self.conn.transaction()?;
+ self.params.write_changeset(&db_tx, changeset)?;
+ db_tx.commit()?;
+ }
+ Ok(())
+ }
+}
+
+/// Extends [`rusqlite::Connection`] to transform into a [`Persister`].
+pub trait ConnectionExt: Sized {
+ /// Transform into a [`Persister`].
+ fn into_persister<P: PersistParams>(
+ self,
+ params: P,
+ ) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)>;
+}
+
+impl ConnectionExt for rusqlite::Connection {
+ fn into_persister<P: PersistParams>(
+ mut self,
+ params: P,
+ ) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)> {
+ let db_tx = self.transaction()?;
+ params.initialize_tables(&db_tx)?;
+ let changeset = params.load_changeset(&db_tx)?;
+ db_tx.commit()?;
+ let persister = Persister { conn: self, params };
+ Ok((persister, changeset))
+ }
+}
+
+/// Table name for schemas.
+pub const SCHEMAS_TABLE_NAME: &str = "bdk_schemas";
+
+/// Initialize the schema table.
+fn init_schemas_table(db_tx: &Transaction) -> rusqlite::Result<()> {
+ let sql = format!("CREATE TABLE IF NOT EXISTS {}( name TEXT PRIMARY KEY NOT NULL, version INTEGER NOT NULL ) STRICT", SCHEMAS_TABLE_NAME);
+ db_tx.execute(&sql, ())?;
+ Ok(())
+}
+
+/// Get schema version of `schema_name`.
+fn schema_version(db_tx: &Transaction, schema_name: &str) -> rusqlite::Result<Option<u32>> {
+ let sql = format!(
+ "SELECT version FROM {} WHERE name=:name",
+ SCHEMAS_TABLE_NAME
+ );
+ db_tx
+ .query_row(&sql, named_params! { ":name": schema_name }, |row| {
+ row.get::<_, u32>("version")
+ })
+ .optional()
+}
+
+/// Set the `schema_version` of `schema_name`.
+fn set_schema_version(
+ db_tx: &Transaction,
+ schema_name: &str,
+ schema_version: u32,
+) -> rusqlite::Result<()> {
+ let sql = format!(
+ "REPLACE INTO {}(name, version) VALUES(:name, :version)",
+ SCHEMAS_TABLE_NAME,
+ );
+ db_tx.execute(
+ &sql,
+ named_params! { ":name": schema_name, ":version": schema_version },
+ )?;
+ Ok(())
+}
+
+/// Runs logic that initializes/migrates the table schemas.
+pub fn migrate_schema(
+ db_tx: &Transaction,
+ schema_name: &str,
+ versioned_scripts: &[&[&str]],
+) -> rusqlite::Result<()> {
+ init_schemas_table(db_tx)?;
+ let current_version = schema_version(db_tx, schema_name)?;
+ let exec_from = current_version.map_or(0_usize, |v| v as usize + 1);
+ let scripts_to_exec = versioned_scripts.iter().enumerate().skip(exec_from);
+ for (version, &script) in scripts_to_exec {
+ set_schema_version(db_tx, schema_name, version as u32)?;
+ for statement in script {
+ db_tx.execute(statement, ())?;
+ }
+ }
+ Ok(())
+}
+
+/// A wrapper so that we can impl [FromSql] and [ToSql] for multiple types.
+pub struct Sql<T>(pub T);
+
+impl<T> From<T> for Sql<T> {
+ fn from(value: T) -> Self {
+ Self(value)
+ }
+}
+
+impl<T> Deref for Sql<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl FromSql for Sql<bitcoin::Txid> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ bitcoin::Txid::from_str(value.as_str()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+impl ToSql for Sql<bitcoin::Txid> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.to_string().into())
+ }
+}
+
+impl FromSql for Sql<bitcoin::BlockHash> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ bitcoin::BlockHash::from_str(value.as_str()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+impl ToSql for Sql<bitcoin::BlockHash> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.to_string().into())
+ }
+}
+
+#[cfg(feature = "miniscript")]
+impl FromSql for Sql<crate::DescriptorId> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ crate::DescriptorId::from_str(value.as_str()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+#[cfg(feature = "miniscript")]
+impl ToSql for Sql<crate::DescriptorId> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.to_string().into())
+ }
+}
+
+impl FromSql for Sql<bitcoin::Transaction> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+impl ToSql for Sql<bitcoin::Transaction> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ let mut bytes = Vec::<u8>::new();
+ self.consensus_encode(&mut bytes).map_err(to_sql_error)?;
+ Ok(bytes.into())
+ }
+}
+
+impl FromSql for Sql<bitcoin::ScriptBuf> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ Ok(bitcoin::Script::from_bytes(value.as_bytes()?)
+ .to_owned()
+ .into())
+ }
+}
+
+impl ToSql for Sql<bitcoin::ScriptBuf> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.as_bytes().into())
+ }
+}
+
+impl FromSql for Sql<bitcoin::Amount> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into())
+ }
+}
+
+impl ToSql for Sql<bitcoin::Amount> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?;
+ Ok(amount.into())
+ }
+}
+
+impl<A: Anchor + serde_crate::de::DeserializeOwned> FromSql for Sql<A> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ serde_json::from_str(value.as_str()?)
+ .map(Sql)
+ .map_err(from_sql_error)
+ }
+}
+
+impl<A: Anchor + serde_crate::Serialize> ToSql for Sql<A> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ serde_json::to_string(&self.0)
+ .map(Into::into)
+ .map_err(to_sql_error)
+ }
+}
+
+#[cfg(feature = "miniscript")]
+impl FromSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ miniscript::Descriptor::from_str(value.as_str()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+#[cfg(feature = "miniscript")]
+impl ToSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.to_string().into())
+ }
+}
+
+impl FromSql for Sql<bitcoin::Network> {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ bitcoin::Network::from_str(value.as_str()?)
+ .map(Self)
+ .map_err(from_sql_error)
+ }
+}
+
+impl ToSql for Sql<bitcoin::Network> {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
+ Ok(self.to_string().into())
+ }
+}
+
+fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> FromSqlError {
+ FromSqlError::Other(Box::new(err))
+}
+
+fn to_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> rusqlite::Error {
+ rusqlite::Error::ToSqlConversionFailure(Box::new(err))
+}
}
}
+#[cfg(feature = "sqlite")]
+impl<A> ChangeSet<A>
+where
+ A: Anchor + Clone + Ord + serde::Serialize + serde::de::DeserializeOwned,
+{
+ /// Schema name for the [`ChangeSet`].
+ pub const SCHEMA_NAME: &'static str = "bdk_txgraph";
+ /// Name of table that stores full transactions and `last_seen` timestamps.
+ pub const TXS_TABLE_NAME: &'static str = "bdk_txs";
+ /// Name of table that stores floating txouts.
+ pub const TXOUTS_TABLE_NAME: &'static str = "bdk_txouts";
+ /// Name of table that stores [`Anchor`]s.
+ pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors";
+
+ /// Initialize sqlite tables.
+ fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ let schema_v0: &[&str] = &[
+ // full transactions
+ &format!(
+ "CREATE TABLE {} ( \
+ txid TEXT PRIMARY KEY NOT NULL, \
+ raw_tx BLOB, \
+ last_seen INTEGER \
+ ) STRICT",
+ Self::TXS_TABLE_NAME,
+ ),
+ // floating txouts
+ &format!(
+ "CREATE TABLE {} ( \
+ txid TEXT NOT NULL, \
+ vout INTEGER NOT NULL, \
+ value INTEGER NOT NULL, \
+ script BLOB NOT NULL, \
+ PRIMARY KEY (txid, vout) \
+ ) STRICT",
+ Self::TXOUTS_TABLE_NAME,
+ ),
+ // anchors
+ &format!(
+ "CREATE TABLE {} ( \
+ txid TEXT NOT NULL REFERENCES {} (txid), \
+ block_height INTEGER NOT NULL, \
+ block_hash TEXT NOT NULL, \
+ anchor BLOB NOT NULL, \
+ PRIMARY KEY (txid, block_height, block_hash) \
+ ) STRICT",
+ Self::ANCHORS_TABLE_NAME,
+ Self::TXS_TABLE_NAME,
+ ),
+ ];
+ crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
+ }
+
+ /// Construct a [`TxGraph`] from an sqlite database.
+ pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::sqlite::Sql;
+
+ let mut changeset = Self::default();
+
+ let mut statement = db_tx.prepare(&format!(
+ "SELECT txid, raw_tx, last_seen FROM {}",
+ Self::TXS_TABLE_NAME,
+ ))?;
+ let row_iter = statement.query_map([], |row| {
+ Ok((
+ row.get::<_, Sql<Txid>>("txid")?,
+ row.get::<_, Option<Sql<Transaction>>>("raw_tx")?,
+ row.get::<_, Option<u64>>("last_seen")?,
+ ))
+ })?;
+ for row in row_iter {
+ let (Sql(txid), tx, last_seen) = row?;
+ if let Some(Sql(tx)) = tx {
+ changeset.txs.insert(Arc::new(tx));
+ }
+ if let Some(last_seen) = last_seen {
+ changeset.last_seen.insert(txid, last_seen);
+ }
+ }
+
+ let mut statement = db_tx.prepare(&format!(
+ "SELECT txid, vout, value, script FROM {}",
+ Self::TXOUTS_TABLE_NAME,
+ ))?;
+ let row_iter = statement.query_map([], |row| {
+ Ok((
+ row.get::<_, Sql<Txid>>("txid")?,
+ row.get::<_, u32>("vout")?,
+ row.get::<_, Sql<Amount>>("value")?,
+ row.get::<_, Sql<bitcoin::ScriptBuf>>("script")?,
+ ))
+ })?;
+ for row in row_iter {
+ let (Sql(txid), vout, Sql(value), Sql(script_pubkey)) = row?;
+ changeset.txouts.insert(
+ OutPoint { txid, vout },
+ TxOut {
+ value,
+ script_pubkey,
+ },
+ );
+ }
+
+ let mut statement = db_tx.prepare(&format!(
+ "SELECT json(anchor), txid FROM {}",
+ Self::ANCHORS_TABLE_NAME,
+ ))?;
+ let row_iter = statement.query_map([], |row| {
+ Ok((
+ row.get::<_, Sql<A>>("json(anchor)")?,
+ row.get::<_, Sql<Txid>>("txid")?,
+ ))
+ })?;
+ for row in row_iter {
+ let (Sql(anchor), Sql(txid)) = row?;
+ changeset.anchors.insert((anchor, txid));
+ }
+
+ Ok(changeset)
+ }
+
+ /// Persist `changeset` to the sqlite database.
+ pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
+ Self::init_sqlite_tables(db_tx)?;
+ use crate::rusqlite::named_params;
+ use crate::sqlite::Sql;
+
+ let mut statement = db_tx.prepare_cached(&format!(
+ "INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx",
+ Self::TXS_TABLE_NAME,
+ ))?;
+ for tx in &self.txs {
+ statement.execute(named_params! {
+ ":txid": Sql(tx.compute_txid()),
+ ":raw_tx": Sql(tx.as_ref().clone()),
+ })?;
+ }
+
+ let mut statement = db_tx
+ .prepare_cached(&format!(
+ "INSERT INTO {}(txid, last_seen) VALUES(:txid, :last_seen) ON CONFLICT(txid) DO UPDATE SET last_seen=:last_seen",
+ Self::TXS_TABLE_NAME,
+ ))?;
+ for (&txid, &last_seen) in &self.last_seen {
+ statement.execute(named_params! {
+ ":txid": Sql(txid),
+ ":last_seen": Some(last_seen),
+ })?;
+ }
+
+ let mut statement = db_tx.prepare_cached(&format!(
+ "REPLACE INTO {}(txid, vout, value, script) VALUES(:txid, :vout, :value, :script)",
+ Self::TXOUTS_TABLE_NAME,
+ ))?;
+ for (op, txo) in &self.txouts {
+ statement.execute(named_params! {
+ ":txid": Sql(op.txid),
+ ":vout": op.vout,
+ ":value": Sql(txo.value),
+ ":script": Sql(txo.script_pubkey.clone()),
+ })?;
+ }
+
+ let mut statement = db_tx.prepare_cached(&format!(
+ "REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))",
+ Self::ANCHORS_TABLE_NAME,
+ ))?;
+ for (anchor, txid) in &self.anchors {
+ let anchor_block = anchor.anchor_block();
+ statement.execute(named_params! {
+ ":txid": Sql(*txid),
+ ":block_height": anchor_block.height,
+ ":block_hash": Sql(anchor_block.hash),
+ ":anchor": Sql(anchor.clone()),
+ })?;
+ }
+
+ Ok(())
+ }
+}
+
impl<A: Ord> Merge for ChangeSet<A> {
fn merge(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
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,
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,
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(),
},
};
// 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(),
},
};
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
#[test]
fn test_get_chain_position() {
use bdk_chain::local_chain::CheckPoint;
+ use bdk_chain::spk_txout::SpkTxOutIndex;
use bdk_chain::BlockId;
- use bdk_chain::SpkTxOutIndex;
struct TestCase<A> {
name: &'static str,
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);
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
}
-#[test]
-fn when_apply_contradictory_changesets_they_are_ignored() {
- let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
- let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
- let mut txout_index =
- init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
- assert_eq!(
- txout_index.keychains().collect::<Vec<_>>(),
- vec![
- (&TestKeychain::External, &external_descriptor),
- (&TestKeychain::Internal, &internal_descriptor)
- ]
- );
-
- let changeset = ChangeSet {
- keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
- last_revealed: [].into(),
- };
- txout_index.apply_changeset(changeset);
-
- assert_eq!(
- txout_index.keychains().collect::<Vec<_>>(),
- vec![
- (&TestKeychain::External, &external_descriptor),
- (&TestKeychain::Internal, &internal_descriptor)
- ]
- );
-
- let changeset = ChangeSet {
- keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
- last_revealed: [].into(),
- };
- txout_index.apply_changeset(changeset);
-
- assert_eq!(
- txout_index.keychains().collect::<Vec<_>>(),
- vec![
- (&TestKeychain::External, &external_descriptor),
- (&TestKeychain::Internal, &internal_descriptor)
- ]
- );
-}
-
#[test]
fn test_set_all_derivation_indices() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to),
ChangeSet {
- keychains_added: BTreeMap::new(),
last_revealed: last_revealed.clone()
}
);
}
#[test]
-fn insert_descriptor_no_change() {
- let secp = Secp256k1::signing_only();
- let (desc, _) =
- Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
- let mut txout_index = KeychainTxOutIndex::<()>::default();
- assert_eq!(
- txout_index.insert_descriptor((), desc.clone()),
- Ok(ChangeSet {
- keychains_added: [((), desc.clone())].into(),
- last_revealed: Default::default()
- }),
- );
- assert_eq!(
- txout_index.insert_descriptor((), desc.clone()),
- Ok(ChangeSet::default()),
- "inserting the same descriptor for keychain should return an empty changeset",
- );
-}
-
-#[test]
-#[cfg(not(debug_assertions))]
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
let desc = parse_descriptor(DESCRIPTORS[0]);
- let changesets: &[ChangeSet<TestKeychain>] = &[
+ let changesets: &[ChangeSet] = &[
ChangeSet {
- keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
- last_revealed: [].into(),
+ last_revealed: [(desc.descriptor_id(), 10)].into(),
},
ChangeSet {
- keychains_added: [(TestKeychain::External, desc.clone())].into(),
last_revealed: [(desc.descriptor_id(), 12)].into(),
},
];
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
+ indexer_a
+ .insert_descriptor(TestKeychain::External, desc.clone())
+ .expect("must insert keychain");
for changeset in changesets {
indexer_a.apply_changeset(changeset.clone());
}
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
+ indexer_b
+ .insert_descriptor(TestKeychain::External, desc.clone())
+ .expect("must insert keychain");
let aggregate_changesets = changesets
.iter()
.cloned()
-use bdk_chain::{Indexer, SpkTxOutIndex};
+use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
};
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};
//! used with hardware wallets.
//! ```no_run
//! # use bdk_wallet::bitcoin::Network;
+//! # use bdk_wallet::descriptor::Descriptor;
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_hwi::HWISigner;
-//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+//! # use bdk_wallet::{KeychainKind, SignOptions};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
+//! # use std::str::FromStr;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut devices = HWIClient::enumerate()?;
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
-//! # let mut wallet = Wallet::new(
-//! # "",
-//! # "",
-//! # Network::Testnet,
-//! # )?;
+//! # let mut wallet = bdk_wallet::CreateParams::new("", "", Network::Testnet)?
+//! # .create_wallet_no_persist()?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
+++ /dev/null
-[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
+++ /dev/null
-# 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
+++ /dev/null
--- 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
+++ /dev/null
-#![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 {}
+++ /dev/null
-use crate::Store;
-use rusqlite::{named_params, Connection, Error};
-
-const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
-const MIGRATIONS: &[&str] = &[SCHEMA_0];
-
-/// Schema migration related functions.
-impl<K, A> Store<K, A> {
- /// Migrate sqlite db schema to latest version.
- pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
- let stmts = &MIGRATIONS
- .iter()
- .flat_map(|stmt| {
- // remove comment lines
- let s = stmt
- .split('\n')
- .filter(|l| !l.starts_with("--") && !l.is_empty())
- .collect::<Vec<_>>()
- .join(" ");
- // split into statements
- s.split(';')
- // remove extra spaces
- .map(|s| {
- s.trim()
- .split(' ')
- .filter(|s| !s.is_empty())
- .collect::<Vec<_>>()
- .join(" ")
- })
- .collect::<Vec<_>>()
- })
- // remove empty statements
- .filter(|s| !s.is_empty())
- .collect::<Vec<String>>();
-
- let version = Self::get_schema_version(conn)?;
- let stmts = &stmts[(version as usize)..];
-
- // begin transaction, all migration statements and new schema version commit or rollback
- let tx = conn.transaction()?;
-
- // execute every statement and return `Some` new schema version
- // if execution fails, return `Error::Rusqlite`
- // if no statements executed returns `None`
- let new_version = stmts
- .iter()
- .enumerate()
- .map(|version_stmt| {
- tx.execute(version_stmt.1.as_str(), [])
- // map result value to next migration version
- .map(|_| version_stmt.0 as i32 + version + 1)
- })
- .last()
- .transpose()?;
-
- // if `Some` new statement version, set new schema version
- if let Some(version) = new_version {
- Self::set_schema_version(&tx, version)?;
- }
-
- // commit transaction
- tx.commit()?;
- Ok(())
- }
-
- fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
- let statement = conn.prepare_cached("SELECT version FROM version");
- match statement {
- Err(Error::SqliteFailure(e, Some(msg))) => {
- if msg == "no such table: version" {
- Ok(0)
- } else {
- Err(Error::SqliteFailure(e, Some(msg)))
- }
- }
- Ok(mut stmt) => {
- let mut rows = stmt.query([])?;
- match rows.next()? {
- Some(row) => {
- let version: i32 = row.get(0)?;
- Ok(version)
- }
- None => Ok(0),
- }
- }
- _ => Ok(0),
- }
- }
-
- fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
- conn.execute(
- "UPDATE version SET version=:version",
- named_params! {":version": version},
- )
- }
-}
+++ /dev/null
-use bdk_chain::bitcoin::consensus::{deserialize, serialize};
-use bdk_chain::bitcoin::hashes::Hash;
-use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
-use bdk_chain::bitcoin::{BlockHash, Txid};
-use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
-use rusqlite::{named_params, Connection};
-use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Debug;
-use std::marker::PhantomData;
-use std::str::FromStr;
-use std::sync::{Arc, Mutex};
-
-use crate::Error;
-use bdk_chain::CombinedChangeSet;
-use bdk_chain::{
- indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt,
- DescriptorId, Merge,
-};
-
-/// Persists data in to a relational schema based [SQLite] database file.
-///
-/// The changesets loaded or stored represent changes to keychain and blockchain data.
-///
-/// [SQLite]: https://www.sqlite.org/index.html
-pub struct Store<K, A> {
- // A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
- conn: Mutex<Connection>,
- keychain_marker: PhantomData<K>,
- anchor_marker: PhantomData<A>,
-}
-
-impl<K, A> Debug for Store<K, A> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- Debug::fmt(&self.conn, f)
- }
-}
-
-impl<K, A> Store<K, A>
-where
- K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
- A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
- /// Creates a new store from a [`Connection`].
- pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
- Self::migrate(&mut conn)?;
-
- Ok(Self {
- conn: Mutex::new(conn),
- keychain_marker: Default::default(),
- anchor_marker: Default::default(),
- })
- }
-
- pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
- let connection = self.conn.get_mut().expect("unlocked connection mutex");
- connection.transaction().map_err(Error::Sqlite)
- }
-}
-
-/// Network table related functions.
-impl<K, A> Store<K, A> {
- /// Insert [`Network`] for which all other tables data is valid.
- ///
- /// Error if trying to insert different network value.
- fn insert_network(
- current_network: &Option<Network>,
- db_transaction: &rusqlite::Transaction,
- network_changeset: &Option<Network>,
- ) -> Result<(), Error> {
- if let Some(network) = network_changeset {
- match current_network {
- // if no network change do nothing
- Some(current_network) if current_network == network => Ok(()),
- // if new network not the same as current, error
- Some(current_network) => Err(Error::Network {
- expected: *current_network,
- given: *network,
- }),
- // insert network if none exists
- None => {
- let insert_network_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO network (name) VALUES (:name)")
- .expect("insert network statement");
- let name = network.to_string();
- insert_network_stmt
- .execute(named_params! {":name": name })
- .map_err(Error::Sqlite)?;
- Ok(())
- }
- }
- } else {
- Ok(())
- }
- }
-
- /// Select the valid [`Network`] for this database, or `None` if not set.
- fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
- let mut select_network_stmt = db_transaction
- .prepare_cached("SELECT name FROM network WHERE rowid = 1")
- .expect("select network statement");
-
- let network = select_network_stmt
- .query_row([], |row| {
- let network = row.get_unwrap::<usize, String>(0);
- let network = Network::from_str(network.as_str()).expect("valid network");
- Ok(network)
- })
- .map_err(Error::Sqlite);
- match network {
- Ok(network) => Ok(Some(network)),
- Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
- Err(e) => Err(e),
- }
- }
-}
-
-/// Block table related functions.
-impl<K, A> Store<K, A> {
- /// Insert or delete local chain blocks.
- ///
- /// Error if trying to insert existing block hash.
- fn insert_or_delete_blocks(
- db_transaction: &rusqlite::Transaction,
- chain_changeset: &local_chain::ChangeSet,
- ) -> Result<(), Error> {
- for (height, hash) in chain_changeset.iter() {
- match hash {
- // add new hash at height
- Some(hash) => {
- let insert_block_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
- .expect("insert block statement");
- let hash = hash.to_string();
- insert_block_stmt
- .execute(named_params! {":hash": hash, ":height": height })
- .map_err(Error::Sqlite)?;
- }
- // delete block at height
- None => {
- let delete_block_stmt = &mut db_transaction
- .prepare_cached("DELETE FROM block WHERE height IS :height")
- .expect("delete block statement");
- delete_block_stmt
- .execute(named_params! {":height": height })
- .map_err(Error::Sqlite)?;
- }
- }
- }
-
- Ok(())
- }
-
- /// Select all blocks.
- fn select_blocks(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
- let mut select_blocks_stmt = db_transaction
- .prepare_cached("SELECT height, hash FROM block")
- .expect("select blocks statement");
-
- let blocks = select_blocks_stmt
- .query_map([], |row| {
- let height = row.get_unwrap::<usize, u32>(0);
- let hash = row.get_unwrap::<usize, String>(1);
- let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
- Ok((height, hash))
- })
- .map_err(Error::Sqlite)?;
- blocks
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-}
-
-/// Keychain table related functions.
-///
-/// The keychain objects are stored as [`JSONB`] data.
-/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
-impl<K, A> Store<K, A>
-where
- K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
- A: Anchor + Send,
-{
- /// Insert keychain with descriptor and last active index.
- ///
- /// If keychain exists only update last active index.
- fn insert_keychains(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- let keychain_changeset = &tx_graph_changeset.indexer;
- for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
- let insert_keychain_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
- .expect("insert keychain statement");
- let keychain_json = serde_json::to_string(keychain).expect("keychain json");
- let descriptor_id = descriptor.descriptor_id().to_byte_array();
- let descriptor = descriptor.to_string();
- insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-
- /// Update descriptor last revealed index.
- fn update_last_revealed(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- let keychain_changeset = &tx_graph_changeset.indexer;
- for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
- let update_last_revealed_stmt = &mut db_transaction
- .prepare_cached(
- "UPDATE keychain SET last_revealed = :last_revealed
- WHERE descriptor_id = :descriptor_id",
- )
- .expect("update last revealed statement");
- let descriptor_id = descriptor_id.to_byte_array();
- update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-
- /// Select keychains added.
- fn select_keychains(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
- let mut select_keychains_added_stmt = db_transaction
- .prepare_cached("SELECT json(keychain), descriptor FROM keychain")
- .expect("select keychains statement");
-
- let keychains = select_keychains_added_stmt
- .query_map([], |row| {
- let keychain = row.get_unwrap::<usize, String>(0);
- let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
- let descriptor = row.get_unwrap::<usize, String>(1);
- let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
- Ok((keychain, descriptor))
- })
- .map_err(Error::Sqlite)?;
- keychains
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-
- /// Select descriptor last revealed indexes.
- fn select_last_revealed(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeMap<DescriptorId, u32>, Error> {
- let mut select_last_revealed_stmt = db_transaction
- .prepare_cached(
- "SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
- )
- .expect("select last revealed statement");
-
- let last_revealed = select_last_revealed_stmt
- .query_map([], |row| {
- let descriptor = row.get_unwrap::<usize, String>(0);
- let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
- let descriptor_id = descriptor.descriptor_id();
- let last_revealed = row.get_unwrap::<usize, u32>(1);
- Ok((descriptor_id, last_revealed))
- })
- .map_err(Error::Sqlite)?;
- last_revealed
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-}
-
-/// Tx (transaction) and txout (transaction output) table related functions.
-impl<K, A> Store<K, A> {
- /// Insert transactions.
- ///
- /// Error if trying to insert existing txid.
- fn insert_txs(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- for tx in tx_graph_changeset.graph.txs.iter() {
- let insert_tx_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
- .expect("insert or update tx whole_tx statement");
- let txid = tx.compute_txid().to_string();
- let whole_tx = serialize(&tx);
- insert_tx_stmt
- .execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-
- /// Select all transactions.
- fn select_txs(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeSet<Arc<Transaction>>, Error> {
- let mut select_tx_stmt = db_transaction
- .prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
- .expect("select tx statement");
-
- let txs = select_tx_stmt
- .query_map([], |row| {
- let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
- let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
- Ok(Arc::new(whole_tx))
- })
- .map_err(Error::Sqlite)?;
-
- txs.into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-
- /// Select all transactions with last_seen values.
- fn select_last_seen(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeMap<Txid, u64>, Error> {
- // load tx last_seen
- let mut select_last_seen_stmt = db_transaction
- .prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
- .expect("select tx last seen statement");
-
- let last_seen = select_last_seen_stmt
- .query_map([], |row| {
- let txid = row.get_unwrap::<usize, String>(0);
- let txid = Txid::from_str(&txid).expect("txid");
- let last_seen = row.get_unwrap::<usize, u64>(1);
- Ok((txid, last_seen))
- })
- .map_err(Error::Sqlite)?;
- last_seen
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-
- /// Insert txouts.
- ///
- /// Error if trying to insert existing outpoint.
- fn insert_txouts(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- for txout in tx_graph_changeset.graph.txouts.iter() {
- let insert_txout_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
- .expect("insert txout statement");
- let txid = txout.0.txid.to_string();
- let vout = txout.0.vout;
- let value = txout.1.value.to_sat();
- let script = txout.1.script_pubkey.as_bytes();
- insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-
- /// Select all transaction outputs.
- fn select_txouts(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
- // load tx outs
- let mut select_txout_stmt = db_transaction
- .prepare_cached("SELECT txid, vout, value, script FROM txout")
- .expect("select txout statement");
-
- let txouts = select_txout_stmt
- .query_map([], |row| {
- let txid = row.get_unwrap::<usize, String>(0);
- let txid = Txid::from_str(&txid).expect("txid");
- let vout = row.get_unwrap::<usize, u32>(1);
- let outpoint = OutPoint::new(txid, vout);
- let value = row.get_unwrap::<usize, u64>(2);
- let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
- let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
- let txout = TxOut {
- value: Amount::from_sat(value),
- script_pubkey,
- };
- Ok((outpoint, txout))
- })
- .map_err(Error::Sqlite)?;
- txouts
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-
- /// Update transaction last seen times.
- fn update_last_seen(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
- let insert_or_update_tx_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
- .expect("insert or update tx last_seen statement");
- let txid = tx_last_seen.0.to_string();
- let last_seen = *tx_last_seen.1;
- insert_or_update_tx_stmt
- .execute(named_params! {":txid": txid, ":last_seen": last_seen })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-}
-
-/// Anchor table related functions.
-impl<K, A> Store<K, A>
-where
- K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
- A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
- /// Insert anchors.
- fn insert_anchors(
- db_transaction: &rusqlite::Transaction,
- tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
- ) -> Result<(), Error> {
- // serde_json::to_string
- for anchor in tx_graph_changeset.graph.anchors.iter() {
- let insert_anchor_stmt = &mut db_transaction
- .prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
- .expect("insert anchor statement");
- let block_hash = anchor.0.anchor_block().hash.to_string();
- let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
- let txid = anchor.1.to_string();
- insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
- .map_err(Error::Sqlite)?;
- }
- Ok(())
- }
-
- /// Select all anchors.
- fn select_anchors(
- db_transaction: &rusqlite::Transaction,
- ) -> Result<BTreeSet<(A, Txid)>, Error> {
- // serde_json::from_str
- let mut select_anchor_stmt = db_transaction
- .prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
- .expect("select anchor statement");
- let anchors = select_anchor_stmt
- .query_map([], |row| {
- let hash = row.get_unwrap::<usize, String>(0);
- let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
- let anchor = row.get_unwrap::<usize, String>(1);
- let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
- // double check anchor blob block hash matches
- assert_eq!(hash, anchor.anchor_block().hash);
- let txid = row.get_unwrap::<usize, String>(2);
- let txid = Txid::from_str(&txid).expect("txid");
- Ok((anchor, txid))
- })
- .map_err(Error::Sqlite)?;
- anchors
- .into_iter()
- .map(|row| row.map_err(Error::Sqlite))
- .collect()
- }
-}
-
-/// Functions to read and write all [`CombinedChangeSet`] data.
-impl<K, A> Store<K, A>
-where
- K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
- A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
-{
- /// Write the given `changeset` atomically.
- pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
- // no need to write anything if changeset is empty
- if changeset.is_empty() {
- return Ok(());
- }
-
- let db_transaction = self.db_transaction()?;
-
- let network_changeset = &changeset.network;
- let current_network = Self::select_network(&db_transaction)?;
- Self::insert_network(¤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<Option<CombinedChangeSet<K, A>>, Error> {
- let db_transaction = self.db_transaction()?;
-
- let network = Self::select_network(&db_transaction)?;
- let chain = Self::select_blocks(&db_transaction)?;
- let keychains_added = Self::select_keychains(&db_transaction)?;
- let last_revealed = Self::select_last_revealed(&db_transaction)?;
- let txs = Self::select_txs(&db_transaction)?;
- let last_seen = Self::select_last_seen(&db_transaction)?;
- let txouts = Self::select_txouts(&db_transaction)?;
- let anchors = Self::select_anchors(&db_transaction)?;
-
- let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
- txs,
- txouts,
- anchors,
- last_seen,
- };
-
- let indexer = keychain_txout::ChangeSet {
- keychains_added,
- last_revealed,
- };
-
- let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>> =
- indexed_tx_graph::ChangeSet { graph, indexer };
-
- if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
- Ok(None)
- } else {
- Ok(Some(CombinedChangeSet {
- chain,
- indexed_tx_graph,
- network,
- }))
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use crate::store::Merge;
- use bdk_chain::bitcoin::consensus::encode::deserialize;
- use bdk_chain::bitcoin::constants::genesis_block;
- use bdk_chain::bitcoin::hashes::hex::FromHex;
- use bdk_chain::bitcoin::transaction::Transaction;
- use bdk_chain::bitcoin::Network::Testnet;
- use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
- use bdk_chain::miniscript::Descriptor;
- use bdk_chain::CombinedChangeSet;
- use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt};
- use std::str::FromStr;
- use std::sync::Arc;
-
- #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
- enum Keychain {
- External { account: u32, name: String },
- Internal { account: u32, name: String },
- }
-
- #[test]
- fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() {
- let (test_changesets, agg_test_changesets) =
- create_test_changesets(&|height, time, hash| ConfirmationBlockTime {
- confirmation_time: time,
- block_id: (height, hash).into(),
- });
-
- let conn = Connection::open_in_memory().expect("in memory connection");
- let mut store = Store::<Keychain, ConfirmationBlockTime>::new(conn)
- .expect("create new memory db store");
-
- test_changesets.iter().for_each(|changeset| {
- store.write(changeset).expect("write changeset");
- });
-
- let agg_changeset = store.read().expect("aggregated changeset");
-
- assert_eq!(agg_changeset, Some(agg_test_changesets));
- }
-
- #[test]
- fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
- let (test_changesets, agg_test_changesets) =
- create_test_changesets(&|height, _time, hash| BlockId { height, hash });
-
- let conn = Connection::open_in_memory().expect("in memory connection");
- let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
-
- test_changesets.iter().for_each(|changeset| {
- store.write(changeset).expect("write changeset");
- });
-
- let agg_changeset = store.read().expect("aggregated changeset");
-
- assert_eq!(agg_changeset, Some(agg_test_changesets));
- }
-
- fn create_test_changesets<A: Anchor + Copy>(
- anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
- ) -> (
- Vec<CombinedChangeSet<Keychain, A>>,
- CombinedChangeSet<Keychain, A>,
- ) {
- let secp = &secp256k1::Secp256k1::signing_only();
-
- let network_changeset = Some(Testnet);
-
- let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
- let block_hash_1 =
- BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
- .unwrap();
- let block_hash_2 =
- BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
- .unwrap();
-
- let block_changeset = [
- (0, Some(block_hash_0)),
- (1, Some(block_hash_1)),
- (2, Some(block_hash_2)),
- ]
- .into();
-
- let ext_keychain = Keychain::External {
- account: 0,
- name: "ext test".to_string(),
- };
- let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
- let ext_desc_id = ext_desc.descriptor_id();
- let int_keychain = Keychain::Internal {
- account: 0,
- name: "int test".to_string(),
- };
- let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
- let int_desc_id = int_desc.descriptor_id();
-
- let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
- let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
- let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
- let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
- let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
- let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
-
- let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
- let txout0_0 = tx0.output.first().unwrap().clone();
- let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
- let txout1_0 = tx1.output.first().unwrap().clone();
-
- let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
- let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
-
- let tx_graph_changeset = tx_graph::ChangeSet::<A> {
- txs: [tx0.clone(), tx1.clone()].into(),
- txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
- anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
- last_seen: [
- (tx0.compute_txid(), 1598918400),
- (tx1.compute_txid(), 1598919121),
- (tx2.compute_txid(), 1608919121),
- ]
- .into(),
- };
-
- let keychain_changeset = keychain_txout::ChangeSet {
- keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
- last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
- };
-
- let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
- indexed_tx_graph::ChangeSet {
- graph: tx_graph_changeset,
- indexer: keychain_changeset,
- };
-
- // test changesets to write to db
- let mut changesets = Vec::new();
-
- changesets.push(CombinedChangeSet {
- chain: block_changeset,
- indexed_tx_graph: graph_changeset,
- network: network_changeset,
- });
-
- // create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
- let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
- txs: [tx2.clone()].into(),
- txouts: BTreeMap::default(),
- anchors: BTreeSet::default(),
- last_seen: [(tx2.compute_txid(), 1708919121)].into(),
- };
-
- let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
- indexed_tx_graph::ChangeSet {
- graph: tx_graph_changeset2,
- indexer: keychain_txout::ChangeSet::default(),
- };
-
- changesets.push(CombinedChangeSet {
- chain: local_chain::ChangeSet::default(),
- indexed_tx_graph: graph_changeset2,
- network: None,
- });
-
- // create changeset that adds a new anchor2 for tx0 and tx1
- let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
- txs: BTreeSet::default(),
- txouts: BTreeMap::default(),
- anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
- last_seen: BTreeMap::default(),
- };
-
- let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
- indexed_tx_graph::ChangeSet {
- graph: tx_graph_changeset3,
- indexer: keychain_txout::ChangeSet::default(),
- };
-
- changesets.push(CombinedChangeSet {
- chain: local_chain::ChangeSet::default(),
- indexed_tx_graph: graph_changeset3,
- network: None,
- });
-
- // aggregated test changesets
- let agg_test_changesets =
- changesets
- .iter()
- .fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
- i.merge(cs.clone());
- i
- });
-
- (changesets, agg_test_changesets)
- }
-}
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"
## Persistence
-To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
+To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::WalletChangeSet`].
**Implementations**
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
-* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
**Example**
<!-- compile_fail because outpoint and txout are fake variables -->
```rust,no_run
-use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
+use bdk_wallet::{bitcoin::Network, CreateParams, LoadParams, KeychainKind, ChangeSet};
// Open or create a new file store for wallet data.
let mut db =
.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);
```
[`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
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
);
// 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{}",
}
}
-/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
-/// descriptor
-pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
- inner: T,
- secp: &SecpCtx,
- network: Network,
-) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
- let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
-
+/// Extra checks for [`ExtendedDescriptor`].
+pub(crate) fn check_wallet_descriptor(
+ descriptor: &Descriptor<DescriptorPublicKey>,
+) -> Result<(), DescriptorError> {
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(DescriptorXKey {
// issues
descriptor.sanity_check()?;
- Ok((descriptor, keymap))
+ Ok(())
}
#[doc(hidden)]
}
#[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());
}
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();
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Pkh;
///
/// 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
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh_P2Sh;
///
/// 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
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::{Wallet};
+/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh;
///
/// 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
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
-/// # use bdk_wallet::Wallet;
+/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2TR;
///
/// 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
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
/// ```
/// # 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");
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;
//! }"#;
//!
//! let import = FullyNodedExport::from_str(import)?;
-//! let wallet = Wallet::new(
+//! let wallet = CreateParams::new(
//! &import.descriptor(),
//! &import.change_descriptor().expect("change descriptor"),
//! Network::Testnet,
-//! )?;
+//! )?
+//! .create_wallet_no_persist()?;
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
//!
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
-//! let wallet = Wallet::new(
+//! let wallet = CreateParams::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
//! Network::Testnet,
-//! )?;
+//! )?
+//! .create_wallet_no_persist()?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
//!
//! println!("Exported: {}", export.to_string());
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![],
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::wallet::AddressIndex::New;
-//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
+//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
-//! # let mut wallet = Wallet::new(
-//! # "",
-//! # None,
-//! # Network::Testnet,
-//! # )?;
+//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
//! 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},
},
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::{
};
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;
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;
}
/// The changes made to a wallet by applying an [`Update`].
-pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationBlockTime>;
+pub type ChangeSet = bdk_chain::WalletChangeSet;
/// A derived address and the index it was found at.
/// For convenience this automatically derefs to `Address`
}
}
-/// 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).
MissingGenesis,
/// Data loaded from persistence is missing descriptor.
MissingDescriptor(KeychainKind),
+ /// Data loaded is unexpected.
+ Mismatch(LoadMismatch),
}
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:?}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LoadError {}
-/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
-///
-/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
-///
-/// [`new_or_load`]: Wallet::new_or_load
-/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
+/// Represents a mismatch with what is loaded and what is expected from [`LoadParams`].
#[derive(Debug)]
-pub enum NewOrLoadError {
- /// There is a problem with the passed-in descriptor.
- Descriptor(crate::descriptor::DescriptorError),
- /// The loaded genesis hash does not match what was provided.
- LoadedGenesisDoesNotMatch {
- /// The expected genesis block hash.
- expected: BlockHash,
- /// The block hash loaded from persistence.
- got: Option<BlockHash>,
- },
- /// The loaded network type does not match what was provided.
- LoadedNetworkDoesNotMatch {
- /// The expected network type.
+pub enum LoadMismatch {
+ /// Network does not match.
+ Network {
+ /// The network that is loaded.
+ loaded: Network,
+ /// The expected network.
expected: Network,
- /// The network type loaded from persistence.
- got: Option<Network>,
},
- /// The loaded desccriptor does not match what was provided.
- LoadedDescriptorDoesNotMatch {
- /// The descriptor loaded from persistence.
- got: Option<ExtendedDescriptor>,
- /// The keychain of the descriptor not matching
+ /// Genesis hash does not match.
+ Genesis {
+ /// The genesis hash that is loaded.
+ loaded: BlockHash,
+ /// The expected genesis hash.
+ expected: BlockHash,
+ },
+ /// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match.
+ Descriptor {
+ /// Keychain identifying the descriptor.
keychain: KeychainKind,
+ /// The loaded descriptor.
+ loaded: ExtendedDescriptor,
+ /// The expected descriptor.
+ expected: ExtendedDescriptor,
},
}
-impl fmt::Display for NewOrLoadError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- NewOrLoadError::Descriptor(e) => e.fmt(f),
- NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
- write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
- }
- NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
- write!(f, "loaded network type is not {}, got {:?}", expected, got)
- }
- NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
- write!(
- f,
- "loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
- got, keychain
- )
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NewOrLoadError {}
-
/// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)]
pub enum ApplyBlockError {
impl std::error::Error for ApplyBlockError {}
impl Wallet {
- /// Initialize an empty [`Wallet`].
- pub fn new<E: IntoWalletDescriptor>(
+ /// Build a new [`Wallet`].
+ ///
+ /// If you have previously created a wallet, use [`load`](Self::load) instead.
+ ///
+ /// # Synopsis
+ ///
+ /// ```rust
+ /// # use bdk_wallet::Wallet;
+ /// # use bitcoin::Network;
+ /// # fn main() -> anyhow::Result<()> {
+ /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+ /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+ /// // Create a non-persisted wallet.
+ /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+ /// .create_wallet_no_persist()?;
+ ///
+ /// // Create a wallet that is persisted to SQLite database.
+ /// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
+ /// # let file_path = temp_dir.path().join("store.db");
+ /// use bdk_wallet::rusqlite::Connection;
+ /// let mut conn = Connection::open(file_path)?;
+ /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+ /// .create_wallet(&mut conn)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn create<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
- ) -> Result<Self, NewError> {
- let genesis_hash = genesis_block(network).block_hash();
- Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
+ ) -> Result<CreateParams, DescriptorError> {
+ CreateParams::new(descriptor, change_descriptor, network)
}
- /// Initialize an empty [`Wallet`] with a custom genesis hash.
+ /// Create a new [`Wallet`] with given `params`.
///
- /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
- /// for syncing from alternative networks.
- pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: E,
- network: Network,
- genesis_hash: BlockHash,
- ) -> Result<Self, NewError> {
- let secp = Secp256k1::new();
+ /// If you have previously created a wallet, use [`load`](Self::load) instead.
+ pub fn create_with_params(params: CreateParams) -> Result<Self, DescriptorError> {
+ let secp = params.secp;
+ let network = params.network;
+ let genesis_hash = params
+ .genesis_hash
+ .unwrap_or(genesis_block(network).block_hash());
+
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
- let mut index = KeychainTxOutIndex::<KeychainKind>::default();
- let (signers, change_signers) =
- create_signers(&mut index, &secp, descriptor, change_descriptor, network)
- .map_err(NewError::Descriptor)?;
+ check_wallet_descriptor(¶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),
};
network,
chain,
indexed_graph,
- stage: staged,
+ stage,
secp,
})
}
+ /// Build [`Wallet`] by loading from persistence or [`ChangeSet`].
+ ///
+ /// Note that the descriptor secret keys are not persisted to the db. You can either add
+ /// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
+ /// construct wallet using [`Wallet::load_with_descriptors`].
+ ///
+ /// # Synopsis
+ ///
+ /// ```rust,no_run
+ /// # use bdk_wallet::{Wallet, ChangeSet, KeychainKind};
+ /// # use bitcoin::{BlockHash, Network, hashes::Hash};
+ /// # fn main() -> anyhow::Result<()> {
+ /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+ /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+ /// # let changeset = ChangeSet::default();
+ /// // Load a wallet from changeset (no persistence).
+ /// let wallet = Wallet::load()
+ /// .load_wallet_no_persist(changeset)?
+ /// .expect("must have data to load wallet");
+ ///
+ /// // Load a wallet that is persisted to SQLite database.
+ /// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
+ /// # let file_path = temp_dir.path().join("store.db");
+ /// # let external_keymap = Default::default();
+ /// # let internal_keymap = Default::default();
+ /// # let genesis_hash = BlockHash::all_zeros();
+ /// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?;
+ /// let mut wallet = Wallet::load()
+ /// // manually include private keys
+ /// // the alternative is to use `Wallet::load_with_descriptors`
+ /// .keymap(KeychainKind::External, external_keymap)
+ /// .keymap(KeychainKind::Internal, internal_keymap)
+ /// // set a lookahead for our indexer
+ /// .lookahead(101)
+ /// // ensure loaded wallet's genesis hash matches this value
+ /// .genesis_hash(genesis_hash)
+ /// .load_wallet(&mut conn)?
+ /// .expect("must have data to load wallet");
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn load() -> LoadParams {
+ LoadParams::new()
+ }
+
+ /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
+ /// wallet has a different `network`.
+ ///
+ /// Note that the descriptor secret keys are not persisted to the db. You can either add
+ /// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
+ /// construct wallet using [`Wallet::load_with_descriptors`].
+ pub fn load_with_network(network: Network) -> LoadParams {
+ LoadParams::with_network(network)
+ }
+
+ /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
+ /// wallet has a different `network`, `descriptor` or `change_descriptor`.
+ ///
+ /// If the passed-in descriptors contains secret keys, the keys will be included in the
+ /// constructed wallet (which means you can sign transactions).
+ pub fn load_with_descriptors<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: E,
+ network: Network,
+ ) -> Result<LoadParams, DescriptorError> {
+ LoadParams::with_descriptors(descriptor, change_descriptor, network)
+ }
+
/// Load [`Wallet`] from the given previously persisted [`ChangeSet`].
///
/// Note that the descriptor secret keys are not persisted to the db; this means that after
///
/// ```rust,no_run
/// # use bdk_wallet::Wallet;
- /// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
- /// # use bdk_wallet::descriptor::Descriptor;
- /// # use bitcoin::key::Secp256k1;
- /// # use bdk_wallet::KeychainKind;
- /// use bdk_sqlite::{Store, rusqlite::Connection};
+ /// # use bitcoin::Network;
+ /// # use bdk_wallet::{LoadParams, KeychainKind, PersistedWallet};
+ /// use bdk_chain::sqlite::Connection;
/// #
- /// # fn main() -> Result<(), anyhow::Error> {
+ /// # fn main() -> anyhow::Result<()> {
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
- /// let conn = Connection::open(file_path).expect("must open connection");
- /// let mut db = Store::new(conn).expect("must create db");
- /// let secp = Secp256k1::new();
- ///
- /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
- /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
- ///
- /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
- /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
- /// let changeset = db.read()?.expect("there must be an existing changeset");
- /// let mut wallet = Wallet::load_from_changeset(changeset)?;
- ///
- /// external_signer_container.signers().into_iter()
- /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
- /// internal_signer_container.signers().into_iter()
- /// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
+ /// const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+ /// const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+ ///
+ /// let mut conn = Connection::open(file_path)?;
+ /// let mut wallet: PersistedWallet =
+ /// LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
+ /// .load_wallet(&mut conn)?
+ /// .expect("db should have data to load wallet");
+ ///
/// # Ok(())
/// # }
/// ```
- ///
- /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
- /// passed-in descriptors to the [`Wallet`].
- pub fn load_from_changeset(changeset: ChangeSet) -> Result<Self, LoadError> {
+ pub fn load_with_params(
+ changeset: ChangeSet,
+ params: LoadParams,
+ ) -> Result<Option<Self>, LoadError> {
+ if changeset.is_empty() {
+ return Ok(None);
+ }
let secp = Secp256k1::new();
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
- let chain =
- LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
- let mut index = KeychainTxOutIndex::<KeychainKind>::default();
+ let chain = LocalChain::from_changeset(changeset.local_chain)
+ .map_err(|_| LoadError::MissingGenesis)?;
+
let descriptor = changeset
- .indexed_tx_graph
- .indexer
- .keychains_added
- .get(&KeychainKind::External)
- .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?
- .clone();
+ .descriptor
+ .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?;
let change_descriptor = changeset
- .indexed_tx_graph
- .indexer
- .keychains_added
- .get(&KeychainKind::Internal)
- .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?
- .clone();
+ .change_descriptor
+ .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?;
+ check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?;
+ check_wallet_descriptor(&change_descriptor).map_err(LoadError::Descriptor)?;
+
+ // checks
+ if let Some(exp_network) = params.check_network {
+ if network != exp_network {
+ return Err(LoadError::Mismatch(LoadMismatch::Network {
+ loaded: network,
+ expected: exp_network,
+ }));
+ }
+ }
+ if let Some(exp_genesis_hash) = params.check_genesis_hash {
+ if chain.genesis_hash() != exp_genesis_hash {
+ return Err(LoadError::Mismatch(LoadMismatch::Genesis {
+ loaded: chain.genesis_hash(),
+ expected: exp_genesis_hash,
+ }));
+ }
+ }
+ if let Some(exp_descriptor) = params.check_descriptor {
+ if descriptor.descriptor_id() != exp_descriptor.descriptor_id() {
+ return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
+ keychain: KeychainKind::External,
+ loaded: descriptor,
+ expected: exp_descriptor,
+ }));
+ }
+ }
+ if let Some(exp_change_descriptor) = params.check_change_descriptor {
+ if change_descriptor.descriptor_id() != exp_change_descriptor.descriptor_id() {
+ return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
+ keychain: KeychainKind::External,
+ loaded: change_descriptor,
+ expected: exp_change_descriptor,
+ }));
+ }
+ }
- let (signers, change_signers) =
- create_signers(&mut index, &secp, descriptor, change_descriptor, network)
- .expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
+ let signers = Arc::new(SignersContainer::build(
+ params.descriptor_keymap,
+ &descriptor,
+ &secp,
+ ));
+ let change_signers = Arc::new(SignersContainer::build(
+ params.change_descriptor_keymap,
+ &change_descriptor,
+ &secp,
+ ));
+ let index = create_indexer(descriptor, change_descriptor, params.lookahead)
+ .map_err(LoadError::Descriptor)?;
let mut indexed_graph = IndexedTxGraph::new(index);
- indexed_graph.apply_changeset(changeset.indexed_tx_graph);
+ indexed_graph.apply_changeset(changeset.indexer.into());
+ indexed_graph.apply_changeset(changeset.tx_graph.into());
let stage = ChangeSet::default();
- Ok(Wallet {
+ Ok(Some(Wallet {
signers,
change_signers,
chain,
stage,
network,
secp,
- })
- }
-
- /// Either loads [`Wallet`] from the given [`ChangeSet`] or initializes it if one does not exist.
- ///
- /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
- ///
- /// ```rust,no_run
- /// # use bdk_wallet::Wallet;
- /// use bdk_sqlite::{Store, rusqlite::Connection};
- /// # use bitcoin::Network::Testnet;
- /// let conn = Connection::open_in_memory().expect("must open connection");
- /// let mut db = Store::new(conn).expect("must create db");
- /// let changeset = db.read()?;
- ///
- /// let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
- /// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
- ///
- /// let mut wallet = Wallet::new_or_load(external_descriptor, internal_descriptor, changeset, Testnet)?;
- /// # Ok::<(), anyhow::Error>(())
- /// ```
- pub fn new_or_load<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: E,
- changeset: Option<ChangeSet>,
- network: Network,
- ) -> Result<Self, NewOrLoadError> {
- let genesis_hash = genesis_block(network).block_hash();
- Self::new_or_load_with_genesis_hash(
- descriptor,
- change_descriptor,
- changeset,
- network,
- genesis_hash,
- )
- }
-
- /// Either loads [`Wallet`] from a [`ChangeSet`] or initializes it if one does not exist, using the
- /// provided descriptor, change descriptor, network, and custom genesis hash.
- ///
- /// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
- /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
- /// useful for syncing from alternative networks.
- pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
- descriptor: E,
- change_descriptor: E,
- changeset: Option<ChangeSet>,
- network: Network,
- genesis_hash: BlockHash,
- ) -> Result<Self, NewOrLoadError> {
- if let Some(changeset) = changeset {
- let mut wallet = Self::load_from_changeset(changeset).map_err(|e| match e {
- LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
- LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
- expected: network,
- got: None,
- },
- LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
- expected: genesis_hash,
- got: None,
- },
- LoadError::MissingDescriptor(keychain) => {
- NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: None,
- keychain,
- }
- }
- })?;
- if wallet.network != network {
- return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
- expected: network,
- got: Some(wallet.network),
- });
- }
- if wallet.chain.genesis_hash() != genesis_hash {
- return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
- expected: genesis_hash,
- got: Some(wallet.chain.genesis_hash()),
- });
- }
-
- let (expected_descriptor, expected_descriptor_keymap) = descriptor
- .into_wallet_descriptor(&wallet.secp, network)
- .map_err(NewOrLoadError::Descriptor)?;
- let wallet_descriptor = wallet.public_descriptor(KeychainKind::External);
- if wallet_descriptor != &expected_descriptor {
- return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: Some(wallet_descriptor.clone()),
- keychain: KeychainKind::External,
- });
- }
- // if expected descriptor has private keys add them as new signers
- if !expected_descriptor_keymap.is_empty() {
- let signer_container = SignersContainer::build(
- expected_descriptor_keymap,
- &expected_descriptor,
- &wallet.secp,
- );
- signer_container.signers().into_iter().for_each(|signer| {
- wallet.add_signer(
- KeychainKind::External,
- SignerOrdering::default(),
- signer.clone(),
- )
- });
- }
-
- let (expected_change_descriptor, expected_change_descriptor_keymap) = change_descriptor
- .into_wallet_descriptor(&wallet.secp, network)
- .map_err(NewOrLoadError::Descriptor)?;
- let wallet_change_descriptor = wallet.public_descriptor(KeychainKind::Internal);
- if wallet_change_descriptor != &expected_change_descriptor {
- return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
- got: Some(wallet_change_descriptor.clone()),
- keychain: KeychainKind::Internal,
- });
- }
- // if expected change descriptor has private keys add them as new signers
- if !expected_change_descriptor_keymap.is_empty() {
- let signer_container = SignersContainer::build(
- expected_change_descriptor_keymap,
- &expected_change_descriptor,
- &wallet.secp,
- );
- signer_container.signers().into_iter().for_each(|signer| {
- wallet.add_signer(
- KeychainKind::Internal,
- SignerOrdering::default(),
- signer.clone(),
- )
- });
- }
-
- Ok(wallet)
- } else {
- Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
- .map_err(|e| match e {
- NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
- })
- }
+ }))
}
/// Get the Bitcoin network the wallet is using.
/// 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);
.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,
signers.add_external(signer.id(&self.secp), ordering, signer);
}
+ /// Set the keymap for a given keychain.
+ pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) {
+ let wallet_signers = match keychain {
+ KeychainKind::External => Arc::make_mut(&mut self.signers),
+ KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
+ };
+ let descriptor = self
+ .indexed_graph
+ .index
+ .get_descriptor(&keychain)
+ .expect("keychain must exist");
+ *wallet_signers = SignersContainer::build(keymap, descriptor, &self.secp);
+ }
+
+ /// Set the keymap for each keychain.
+ pub fn set_keymaps(&mut self, keymaps: impl IntoIterator<Item = (KeychainKind, KeyMap)>) {
+ for (keychain, keymap) in keymaps {
+ self.set_keymap(keychain, keymap);
+ }
+ }
+
/// Get the signers
///
/// ## Example
///
/// ```
- /// # use bdk_wallet::{Wallet, KeychainKind};
+ /// # use bdk_wallet::{CreateParams, KeychainKind};
/// # use bdk_wallet::bitcoin::Network;
/// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)";
/// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)";
- /// let wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
+ /// let wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)?
+ /// .create_wallet_no_persist()?;
/// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) {
/// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
/// println!("secret_key: {}", secret_key);
}
}
-fn create_signers<E: IntoWalletDescriptor>(
- index: &mut KeychainTxOutIndex<KeychainKind>,
- secp: &Secp256k1<All>,
- descriptor: E,
- change_descriptor: E,
- network: Network,
-) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), DescriptorError> {
- let descriptor = into_wallet_descriptor_checked(descriptor, secp, network)?;
- let change_descriptor = into_wallet_descriptor_checked(change_descriptor, secp, network)?;
- let (descriptor, keymap) = descriptor;
- let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
- let _ = index
+fn create_indexer(
+ descriptor: ExtendedDescriptor,
+ change_descriptor: ExtendedDescriptor,
+ lookahead: u32,
+) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
+ let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead);
+
+ // let (descriptor, keymap) = descriptor;
+ // let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+ assert!(indexer
.insert_descriptor(KeychainKind::External, descriptor)
- .expect("this is the first descriptor we're inserting");
+ .expect("first descriptor introduced must succeed"));
- let (descriptor, keymap) = change_descriptor;
- let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
- let _ = index
- .insert_descriptor(KeychainKind::Internal, descriptor)
+ // let (descriptor, keymap) = change_descriptor;
+ // let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
+ assert!(indexer
+ .insert_descriptor(KeychainKind::Internal, change_descriptor)
.map_err(|e| {
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
match e {
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.
() => {{
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 {
--- /dev/null
+use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
+use bitcoin::{BlockHash, Network};
+use miniscript::descriptor::KeyMap;
+
+use crate::{
+ descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
+ KeychainKind, Wallet,
+};
+
+use super::{utils::SecpCtx, ChangeSet, LoadError, PersistedWallet};
+
+/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
+#[derive(Debug, Clone)]
+#[must_use]
+pub struct CreateParams {
+ pub(crate) descriptor: ExtendedDescriptor,
+ pub(crate) descriptor_keymap: KeyMap,
+ pub(crate) change_descriptor: ExtendedDescriptor,
+ pub(crate) change_descriptor_keymap: KeyMap,
+ pub(crate) network: Network,
+ pub(crate) genesis_hash: Option<BlockHash>,
+ pub(crate) lookahead: u32,
+ pub(crate) secp: SecpCtx,
+}
+
+impl CreateParams {
+ /// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
+ ///
+ /// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
+ pub fn new<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: E,
+ network: Network,
+ ) -> Result<Self, DescriptorError> {
+ let secp = SecpCtx::default();
+
+ let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(&secp, network)?;
+ let (change_descriptor, change_descriptor_keymap) =
+ change_descriptor.into_wallet_descriptor(&secp, network)?;
+
+ Ok(Self {
+ descriptor,
+ descriptor_keymap,
+ change_descriptor,
+ change_descriptor_keymap,
+ network,
+ genesis_hash: None,
+ lookahead: DEFAULT_LOOKAHEAD,
+ secp,
+ })
+ }
+
+ /// Extend the given `keychain`'s `keymap`.
+ pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
+ match keychain {
+ KeychainKind::External => &mut self.descriptor_keymap,
+ KeychainKind::Internal => &mut self.change_descriptor_keymap,
+ }
+ .extend(keymap);
+ self
+ }
+
+ /// Use a custom `genesis_hash`.
+ pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
+ self.genesis_hash = Some(genesis_hash);
+ self
+ }
+
+ /// Use custom lookahead value.
+ pub fn lookahead(mut self, lookahead: u32) -> Self {
+ self.lookahead = lookahead;
+ self
+ }
+
+ /// Create [`PersistedWallet`] with the given `Db`.
+ pub fn create_wallet<Db>(
+ self,
+ db: &mut Db,
+ ) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
+ where
+ Wallet: PersistWith<Db, CreateParams = Self>,
+ {
+ PersistedWallet::create(db, self)
+ }
+
+ /// Create [`PersistedWallet`] with the given async `Db`.
+ pub async fn create_wallet_async<Db>(
+ self,
+ db: &mut Db,
+ ) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
+ where
+ Wallet: PersistAsyncWith<Db, CreateParams = Self>,
+ {
+ PersistedWallet::create_async(db, self).await
+ }
+
+ /// Create [`Wallet`] without persistence.
+ pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
+ Wallet::create_with_params(self)
+ }
+}
+
+/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
+#[must_use]
+#[derive(Debug, Clone)]
+pub struct LoadParams {
+ pub(crate) descriptor_keymap: KeyMap,
+ pub(crate) change_descriptor_keymap: KeyMap,
+ pub(crate) lookahead: u32,
+ pub(crate) check_network: Option<Network>,
+ pub(crate) check_genesis_hash: Option<BlockHash>,
+ pub(crate) check_descriptor: Option<ExtendedDescriptor>,
+ pub(crate) check_change_descriptor: Option<ExtendedDescriptor>,
+ pub(crate) secp: SecpCtx,
+}
+
+impl LoadParams {
+ /// Construct parameters with default values.
+ ///
+ /// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
+ pub fn new() -> Self {
+ Self {
+ descriptor_keymap: KeyMap::default(),
+ change_descriptor_keymap: KeyMap::default(),
+ lookahead: DEFAULT_LOOKAHEAD,
+ check_network: None,
+ check_genesis_hash: None,
+ check_descriptor: None,
+ check_change_descriptor: None,
+ secp: SecpCtx::new(),
+ }
+ }
+
+ /// Construct parameters with `network` check.
+ pub fn with_network(network: Network) -> Self {
+ Self {
+ check_network: Some(network),
+ ..Default::default()
+ }
+ }
+
+ /// Construct parameters with descriptor checks.
+ pub fn with_descriptors<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: E,
+ network: Network,
+ ) -> Result<Self, DescriptorError> {
+ let mut params = Self::with_network(network);
+ let secp = ¶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<Db>(
+ self,
+ db: &mut Db,
+ ) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
+ where
+ Wallet: PersistWith<Db, LoadParams = Self>,
+ {
+ PersistedWallet::load(db, self)
+ }
+
+ /// Load [`PersistedWallet`] with the given async `Db`.
+ pub async fn load_wallet_async<Db>(
+ self,
+ db: &mut Db,
+ ) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
+ where
+ Wallet: PersistAsyncWith<Db, LoadParams = Self>,
+ {
+ PersistedWallet::load_async(db, self).await
+ }
+
+ /// Load [`Wallet`] without persistence.
+ pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
+ Wallet::load_with_params(changeset, self)
+ }
+}
+
+impl Default for LoadParams {
+ fn default() -> Self {
+ Self::new()
+ }
+}
--- /dev/null
+use core::fmt;
+
+use crate::wallet::{ChangeSet, CreateParams, LoadError, LoadParams};
+use crate::{descriptor::DescriptorError, Wallet};
+use bdk_chain::{Merge, PersistWith};
+
+/// Represents a persisted wallet.
+pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
+
+#[cfg(feature = "sqlite")]
+impl<'c> PersistWith<bdk_chain::sqlite::Transaction<'c>> for Wallet {
+ type CreateParams = CreateParams;
+ type LoadParams = LoadParams;
+
+ type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
+ type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
+ type PersistError = bdk_chain::rusqlite::Error;
+
+ fn create(
+ db: &mut bdk_chain::sqlite::Transaction<'c>,
+ params: Self::CreateParams,
+ ) -> Result<Self, Self::CreateError> {
+ let mut wallet =
+ Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+ if let Some(changeset) = wallet.take_staged() {
+ changeset
+ .persist_to_sqlite(db)
+ .map_err(CreateWithPersistError::Persist)?;
+ }
+ Ok(wallet)
+ }
+
+ fn load(
+ conn: &mut bdk_chain::sqlite::Transaction<'c>,
+ params: Self::LoadParams,
+ ) -> Result<Option<Self>, Self::LoadError> {
+ let changeset = ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
+ if changeset.is_empty() {
+ return Ok(None);
+ }
+ Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+ }
+
+ fn persist(
+ &mut self,
+ conn: &mut bdk_chain::sqlite::Transaction,
+ ) -> Result<bool, Self::PersistError> {
+ if let Some(changeset) = self.take_staged() {
+ changeset.persist_to_sqlite(conn)?;
+ return Ok(true);
+ }
+ Ok(false)
+ }
+}
+
+#[cfg(feature = "sqlite")]
+impl PersistWith<bdk_chain::sqlite::Connection> for Wallet {
+ type CreateParams = CreateParams;
+ type LoadParams = LoadParams;
+
+ type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
+ type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
+ type PersistError = bdk_chain::rusqlite::Error;
+
+ fn create(
+ db: &mut bdk_chain::sqlite::Connection,
+ params: Self::CreateParams,
+ ) -> Result<Self, Self::CreateError> {
+ let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
+ let wallet = PersistWith::create(&mut db_tx, params)?;
+ db_tx.commit().map_err(CreateWithPersistError::Persist)?;
+ Ok(wallet)
+ }
+
+ fn load(
+ db: &mut bdk_chain::sqlite::Connection,
+ params: Self::LoadParams,
+ ) -> Result<Option<Self>, Self::LoadError> {
+ let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
+ let wallet_opt = PersistWith::load(&mut db_tx, params)?;
+ db_tx.commit().map_err(LoadWithPersistError::Persist)?;
+ Ok(wallet_opt)
+ }
+
+ fn persist(
+ &mut self,
+ db: &mut bdk_chain::sqlite::Connection,
+ ) -> Result<bool, Self::PersistError> {
+ let mut db_tx = db.transaction()?;
+ let has_changes = PersistWith::persist(self, &mut db_tx)?;
+ db_tx.commit()?;
+ Ok(has_changes)
+ }
+}
+
+#[cfg(feature = "file_store")]
+impl PersistWith<bdk_file_store::Store<ChangeSet>> for Wallet {
+ type CreateParams = CreateParams;
+ type LoadParams = LoadParams;
+ type CreateError = CreateWithPersistError<std::io::Error>;
+ type LoadError = LoadWithPersistError<bdk_file_store::AggregateChangesetsError<ChangeSet>>;
+ type PersistError = std::io::Error;
+
+ fn create(
+ db: &mut bdk_file_store::Store<ChangeSet>,
+ params: Self::CreateParams,
+ ) -> Result<Self, Self::CreateError> {
+ let mut wallet =
+ Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
+ if let Some(changeset) = wallet.take_staged() {
+ db.append_changeset(&changeset)
+ .map_err(CreateWithPersistError::Persist)?;
+ }
+ Ok(wallet)
+ }
+
+ fn load(
+ db: &mut bdk_file_store::Store<ChangeSet>,
+ params: Self::LoadParams,
+ ) -> Result<Option<Self>, Self::LoadError> {
+ let changeset = db
+ .aggregate_changesets()
+ .map_err(LoadWithPersistError::Persist)?
+ .unwrap_or_default();
+ Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
+ }
+
+ fn persist(
+ &mut self,
+ db: &mut bdk_file_store::Store<ChangeSet>,
+ ) -> Result<bool, Self::PersistError> {
+ if let Some(changeset) = self.take_staged() {
+ db.append_changeset(&changeset)?;
+ return Ok(true);
+ }
+ Ok(false)
+ }
+}
+
+/// Error type for [`PersistedWallet::load`].
+#[derive(Debug)]
+pub enum LoadWithPersistError<E> {
+ /// Error from persistence.
+ Persist(E),
+ /// Occurs when the loaded changeset cannot construct [`Wallet`].
+ InvalidChangeSet(LoadError),
+}
+
+impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Persist(err) => fmt::Display::fmt(err, f),
+ Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
+
+/// Error type for [`PersistedWallet::create`].
+#[derive(Debug)]
+pub enum CreateWithPersistError<E> {
+ /// Error from persistence.
+ Persist(E),
+ /// Occurs when the loaded changeset cannot contruct [`Wallet`].
+ Descriptor(DescriptorError),
+}
+
+impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Persist(err) => fmt::Display::fmt(err, f),
+ Self::Descriptor(err) => fmt::Display::fmt(&err, f),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}
//!
//! 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),
#![allow(unused)]
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{
- wallet::{Update, Wallet},
+ wallet::{CreateParams, Update, Wallet},
KeychainKind, LocalOutput,
};
use bitcoin::{
/// 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")
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;
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test]
-fn load_recovers_wallet() -> anyhow::Result<()> {
- fn run<Db, New, Recover, Read, Write>(
+fn wallet_is_persisted() -> anyhow::Result<()> {
+ fn run<Db, CreateDb, OpenDb>(
filename: &str,
- create_new: New,
- recover: Recover,
- read: Read,
- write: Write,
+ create_db: CreateDb,
+ open_db: OpenDb,
) -> anyhow::Result<()>
where
- New: Fn(&Path) -> anyhow::Result<Db>,
- Recover: Fn(&Path) -> anyhow::Result<Db>,
- Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
- Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
+ CreateDb: Fn(&Path) -> anyhow::Result<Db>,
+ OpenDb: Fn(&Path) -> anyhow::Result<Db>,
+ Wallet: PersistWith<Db, CreateParams = CreateParams, LoadParams = LoadParams>,
+ <Wallet as PersistWith<Db>>::CreateError: std::error::Error + Send + Sync + 'static,
+ <Wallet as PersistWith<Db>>::LoadError: std::error::Error + Send + Sync + 'static,
+ <Wallet as PersistWith<Db>>::PersistError: std::error::Error + Send + Sync + 'static,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
- let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
+ let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc();
// create new wallet
let wallet_spk_index = {
- let mut wallet =
- Wallet::new(desc, change_desc, Network::Testnet).expect("must init wallet");
-
+ let mut db = create_db(&file_path)?;
+ let mut wallet = CreateParams::new(external_desc, internal_desc, Network::Testnet)?
+ .create_wallet(&mut db)?;
wallet.reveal_next_address(KeychainKind::External);
// persist new wallet changes
- let mut db = create_new(&file_path).expect("must create db");
- if let Some(changeset) = wallet.take_staged() {
- write(&mut db, &changeset)?;
- }
+ assert!(wallet.persist(&mut db)?, "must write");
wallet.spk_index().clone()
};
// recover wallet
{
- // load persisted wallet changes
- let db = &mut recover(&file_path).expect("must recover db");
- let changeset = read(db).expect("must recover wallet").expect("changeset");
+ let mut db = open_db(&file_path).context("failed to recover db")?;
+ let wallet =
+ LoadParams::with_descriptors(external_desc, internal_desc, Network::Testnet)?
+ .load_wallet(&mut db)?
+ .expect("wallet must exist");
- let wallet = Wallet::load_from_changeset(changeset).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(
wallet.spk_index().keychains().collect::<Vec<_>>(),
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
);
"store.db",
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
- |db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
- |db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
- )?;
- run(
- "store.sqlite",
- |path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
- |path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
- |db| Ok(bdk_sqlite::Store::read(db)?),
- |db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
- )?;
-
- Ok(())
-}
-
-#[test]
-fn new_or_load() -> anyhow::Result<()> {
- fn run<Db, NewOrRecover, Read, Write>(
- filename: &str,
- new_or_load: NewOrRecover,
- read: Read,
- write: Write,
- ) -> anyhow::Result<()>
- where
- NewOrRecover: Fn(&Path) -> anyhow::Result<Db>,
- Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
- Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
- {
- let temp_dir = tempfile::tempdir().expect("must create tempdir");
- let file_path = temp_dir.path().join(filename);
- let (desc, change_desc) = get_test_wpkh_with_change_desc();
-
- // init wallet when non-existent
- let wallet_keychains: BTreeMap<_, _> = {
- let wallet = &mut Wallet::new_or_load(desc, change_desc, None, Network::Testnet)
- .expect("must init wallet");
- let mut db = new_or_load(&file_path).expect("must create db");
- if let Some(changeset) = wallet.take_staged() {
- write(&mut db, &changeset)?;
- }
- wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
- };
-
- // wrong network
- {
- let mut db = new_or_load(&file_path).expect("must create db");
- let changeset = read(&mut db)?;
- let err = Wallet::new_or_load(desc, change_desc, changeset, Network::Bitcoin)
- .expect_err("wrong network");
- assert!(
- matches!(
- err,
- bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
- got: Some(Network::Testnet),
- expected: Network::Bitcoin
- }
- ),
- "err: {}",
- err,
- );
- }
-
- // wrong genesis hash
- {
- let exp_blockhash = BlockHash::all_zeros();
- let got_blockhash = bitcoin::constants::genesis_block(Network::Testnet).block_hash();
-
- let db = &mut new_or_load(&file_path).expect("must open db");
- let changeset = read(db)?;
- let err = Wallet::new_or_load_with_genesis_hash(
- desc,
- change_desc,
- changeset,
- Network::Testnet,
- exp_blockhash,
- )
- .expect_err("wrong genesis hash");
- assert!(
- matches!(
- err,
- bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
- if got == Some(got_blockhash) && expected == exp_blockhash
- ),
- "err: {}",
- err,
- );
- }
-
- // wrong external descriptor
- {
- let (exp_descriptor, exp_change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
- let got_descriptor = desc
- .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
- .unwrap()
- .0;
-
- let db = &mut new_or_load(&file_path).expect("must open db");
- let changeset = read(db)?;
- let err =
- Wallet::new_or_load(exp_descriptor, exp_change_desc, changeset, Network::Testnet)
- .expect_err("wrong external descriptor");
- assert!(
- matches!(
- err,
- bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
- if got == &Some(got_descriptor) && keychain == KeychainKind::External
- ),
- "err: {}",
- err,
- );
- }
-
- // wrong internal descriptor
- {
- let exp_descriptor = get_test_tr_single_sig();
- let got_descriptor = change_desc
- .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
- .unwrap()
- .0;
-
- let db = &mut new_or_load(&file_path).expect("must open db");
- let changeset = read(db)?;
- let err = Wallet::new_or_load(desc, exp_descriptor, changeset, Network::Testnet)
- .expect_err("wrong internal descriptor");
- assert!(
- matches!(
- err,
- bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
- if got == &Some(got_descriptor) && keychain == KeychainKind::Internal
- ),
- "err: {}",
- err,
- );
- }
-
- // all parameters match
- {
- let db = &mut new_or_load(&file_path).expect("must open db");
- let changeset = read(db)?;
- let wallet = Wallet::new_or_load(desc, change_desc, changeset, Network::Testnet)
- .expect("must recover wallet");
- assert_eq!(wallet.network(), Network::Testnet);
- assert!(wallet
- .keychains()
- .map(|(k, v)| (*k, v.clone()))
- .eq(wallet_keychains));
- }
- Ok(())
- }
-
- run(
- "store.db",
- |path| Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?),
- |db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
- |db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
)?;
- run(
+ run::<bdk_chain::sqlite::Connection, _, _>(
"store.sqlite",
- |path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
- |db| Ok(bdk_sqlite::Store::read(db)?),
- |db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
+ |path| Ok(bdk_chain::sqlite::Connection::open(path)?),
+ |path| Ok(bdk_chain::sqlite::Connection::open(path)?),
)?;
Ok(())
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,
);
// 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,
);
#[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),
#[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
#[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!(
#[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(),
#[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(),
#[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!(
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!(
#[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();
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();
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
#[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
/// [#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"
)]
type ChangeSet = (
local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+ indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
#[derive(Debug)]
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
pub type KeychainChangeSet<A> = (
local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>>,
+ indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
);
#[derive(Parser)]
}
pub struct CreateTxChange {
- pub index_changeset: keychain_txout::ChangeSet<Keychain>,
+ pub index_changeset: keychain_txout::ChangeSet,
pub change_keychain: Keychain,
pub index: u32,
}
type ChangeSet = (
local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+ indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
fn main() -> anyhow::Result<()> {
};
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
-const DB_PATH: &str = ".bdk_esplora_example.db";
+const DB_PATH: &str = "bdk_example_esplora.db";
type ChangeSet = (
local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
+ indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
);
#[derive(Subcommand, Debug, Clone)]
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"),
});
#[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,
}
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"
-const DB_MAGIC: &str = "bdk_wallet_electrum_example";
-const SEND_AMOUNT: Amount = Amount::from_sat(5000);
-const STOP_GAP: usize = 50;
-const BATCH_SIZE: usize = 5;
-
-use anyhow::anyhow;
+use bdk_wallet::wallet::CreateParams;
+use bdk_wallet::wallet::LoadParams;
use std::io::Write;
use std::str::FromStr;
use bdk_electrum::electrum_client;
use bdk_electrum::BdkElectrumClient;
use bdk_file_store::Store;
+use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::{Address, Amount};
use bdk_wallet::chain::collections::HashSet;
-use bdk_wallet::{bitcoin::Network, Wallet};
use bdk_wallet::{KeychainKind, SignOptions};
+const DB_MAGIC: &str = "bdk_wallet_electrum_example";
+const SEND_AMOUNT: Amount = Amount::from_sat(5000);
+const STOP_GAP: usize = 50;
+const BATCH_SIZE: usize = 5;
+
+const NETWORK: Network = Network::Testnet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002";
+
fn main() -> Result<(), anyhow::Error> {
- let db_path = std::env::temp_dir().join("bdk-electrum-example");
+ let db_path = "bdk-electrum-example.db";
+
let mut db =
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
- let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
- let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
- let changeset = db
- .aggregate_changesets()
- .map_err(|e| anyhow!("load changes error: {}", e))?;
- let mut wallet = Wallet::new_or_load(
- external_descriptor,
- internal_descriptor,
- changeset,
- Network::Testnet,
- )?;
+
+ let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+ let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+ let mut wallet = match load_params.load_wallet(&mut db)? {
+ Some(wallet) => wallet,
+ None => create_params.create_wallet(&mut db)?,
+ };
let address = wallet.next_unused_address(KeychainKind::External);
- if let Some(changeset) = wallet.take_staged() {
- db.append_changeset(&changeset)?;
- }
+ wallet.persist(&mut db)?;
println!("Generated Address: {}", address);
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
- let client = BdkElectrumClient::new(electrum_client::Client::new(
- "ssl://electrum.blockstream.info:60002",
- )?);
+ let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
// Populate the electrum client's transaction cache so it doesn't redownload transaction we
// already have.
- client.populate_tx_cache(&wallet);
+ client.populate_tx_cache(wallet.tx_graph());
let request = wallet
.start_full_scan()
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());
# 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"
-use std::{collections::BTreeSet, io::Write, str::FromStr};
+use std::{collections::BTreeSet, io::Write};
+use anyhow::Ok;
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{
- bitcoin::{Address, Amount, Network, Script},
- KeychainKind, SignOptions, Wallet,
+ bitcoin::{Amount, Network},
+ rusqlite::Connection,
+ wallet::{CreateParams, LoadParams},
+ KeychainKind, SignOptions,
};
-use bdk_sqlite::{rusqlite::Connection, Store};
-
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
-const STOP_GAP: usize = 50;
+const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 5;
+const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
+const NETWORK: Network = Network::Signet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
+
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
- let db_path = "bdk-esplora-async-example.sqlite";
- let conn = Connection::open(db_path)?;
- let mut db = Store::new(conn)?;
- let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
- let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
- let changeset = db.read()?;
-
- let mut wallet = Wallet::new_or_load(
- external_descriptor,
- internal_descriptor,
- changeset,
- Network::Signet,
- )?;
+ let mut conn = Connection::open(DB_PATH)?;
+
+ let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+ let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+ let mut wallet = match load_params.load_wallet(&mut conn)? {
+ Some(wallet) => wallet,
+ None => create_params.create_wallet(&mut conn)?,
+ };
let address = wallet.next_unused_address(KeychainKind::External);
- if let Some(changeset) = wallet.take_staged() {
- db.write(&changeset)?;
- }
- println!("Generated Address: {}", address);
+ wallet.persist(&mut conn)?;
+ println!("Next unused address: ({}) {}", address.index, address);
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
- let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
-
- fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
- let mut once = Some(());
- let mut stdout = std::io::stdout();
- move |spk_i, _| {
- match once.take() {
- Some(_) => print!("\nScanning keychain [{:?}]", kind),
- None => print!(" {:<3}", spk_i),
- };
- stdout.flush().expect("must flush");
- }
- }
- let request = wallet
- .start_full_scan()
- .inspect_spks_for_all_keychains({
- let mut once = BTreeSet::<KeychainKind>::new();
- move |keychain, spk_i, _| {
- match once.insert(keychain) {
- true => print!("\nScanning keychain [{:?}]", keychain),
- false => print!(" {:<3}", spk_i),
- }
- std::io::stdout().flush().expect("must flush")
+ let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
+
+ let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
+ let mut once = BTreeSet::<KeychainKind>::new();
+ move |keychain, spk_i, _| {
+ if once.insert(keychain) {
+ print!("\nScanning keychain [{:?}] ", keychain);
}
- })
- .inspect_spks_for_keychain(
- KeychainKind::External,
- generate_inspect(KeychainKind::External),
- )
- .inspect_spks_for_keychain(
- KeychainKind::Internal,
- generate_inspect(KeychainKind::Internal),
- );
+ print!(" {:<3}", spk_i);
+ std::io::stdout().flush().expect("must flush")
+ }
+ });
let mut update = client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
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();
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()?;
-const DB_MAGIC: &str = "bdk_wallet_esplora_example";
-const SEND_AMOUNT: Amount = Amount::from_sat(1000);
-const STOP_GAP: usize = 5;
-const PARALLEL_REQUESTS: usize = 1;
-
-use std::{collections::BTreeSet, io::Write, str::FromStr};
+use std::{collections::BTreeSet, io::Write};
use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store;
use bdk_wallet::{
- bitcoin::{Address, Amount, Network},
- KeychainKind, SignOptions, Wallet,
+ bitcoin::{Amount, Network},
+ wallet::{CreateParams, LoadParams},
+ KeychainKind, SignOptions,
};
+const DB_MAGIC: &str = "bdk_wallet_esplora_example";
+const DB_PATH: &str = "bdk-example-esplora-blocking.db";
+const SEND_AMOUNT: Amount = Amount::from_sat(5000);
+const STOP_GAP: usize = 5;
+const PARALLEL_REQUESTS: usize = 5;
+
+const NETWORK: Network = Network::Signet;
+const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
+const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
+const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
+
fn main() -> Result<(), anyhow::Error> {
- let db_path = std::env::temp_dir().join("bdk-esplora-example");
let mut db =
- Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
- let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
- let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
- let changeset = db.aggregate_changesets()?;
-
- let mut wallet = Wallet::new_or_load(
- external_descriptor,
- internal_descriptor,
- changeset,
- Network::Testnet,
- )?;
+ Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?;
+
+ let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+ let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
+
+ let mut wallet = match load_params.load_wallet(&mut db)? {
+ Some(wallet) => wallet,
+ None => create_params.create_wallet(&mut db)?,
+ };
let address = wallet.next_unused_address(KeychainKind::External);
- if let Some(changeset) = wallet.take_staged() {
- db.append_changeset(&changeset)?;
- }
- println!("Generated Address: {}", address);
+ wallet.persist(&mut db)?;
+ println!(
+ "Next unused address: ({}) {}",
+ address.index, address.address
+ );
let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing...");
- let client =
- esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
+ let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
- match once.insert(keychain) {
- true => print!("\nScanning keychain [{:?}]", keychain),
- false => print!(" {:<3}", spk_i),
- };
+ if once.insert(keychain) {
+ print!("\nScanning keychain [{:?}] ", keychain);
+ }
+ print!(" {:<3}", spk_i);
std::io::stdout().flush().expect("must flush")
}
});
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()?;
# 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" }
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};
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()
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",
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()