feat!: Rework sqlite, changesets, persistence and wallet-construction

Rework sqlite: Instead of only supported one schema (defined in
`bdk_sqlite`), we have a schema per changeset type for more flexiblity.

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

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

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

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

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

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

* Introduce a builder pattern when constructing a `Wallet`. For loading
  from persistence or `ChangeSet`, we have `LoadParams`. For creating a
  new wallet, we have `CreateParams`.
This commit is contained in:
志宇 2024-07-11 04:49:01 +00:00
parent d99b3ef4b4
commit 6b43001951
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
49 changed files with 2217 additions and 2058 deletions

View File

@ -4,7 +4,6 @@ members = [
"crates/wallet", "crates/wallet",
"crates/chain", "crates/chain",
"crates/file_store", "crates/file_store",
"crates/sqlite",
"crates/electrum", "crates/electrum",
"crates/esplora", "crates/esplora",
"crates/bitcoind_rpc", "crates/bitcoind_rpc",

View File

@ -4,7 +4,8 @@ use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{ use bdk_chain::{
bitcoin::{Address, Amount, Txid}, bitcoin::{Address, Amount, Txid},
local_chain::{CheckPoint, LocalChain}, local_chain::{CheckPoint, LocalChain},
Balance, BlockId, IndexedTxGraph, Merge, SpkTxOutIndex, spk_txout::SpkTxOutIndex,
Balance, BlockId, IndexedTxGraph, Merge,
}; };
use bdk_testenv::{anyhow, TestEnv}; use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
@ -47,7 +48,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
assert_eq!( assert_eq!(
local_chain.apply_update(emission.checkpoint,)?, local_chain.apply_update(emission.checkpoint,)?,
BTreeMap::from([(height, Some(hash))]), [(height, Some(hash))].into(),
"chain update changeset is unexpected", "chain update changeset is unexpected",
); );
} }
@ -93,11 +94,13 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
assert_eq!( assert_eq!(
local_chain.apply_update(emission.checkpoint,)?, local_chain.apply_update(emission.checkpoint,)?,
if exp_height == exp_hashes.len() - reorged_blocks.len() { if exp_height == exp_hashes.len() - reorged_blocks.len() {
core::iter::once((height, Some(hash))) bdk_chain::local_chain::ChangeSet {
blocks: core::iter::once((height, Some(hash)))
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None))) .chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
.collect::<bdk_chain::local_chain::ChangeSet>() .collect(),
}
} else { } else {
BTreeMap::from([(height, Some(hash))]) [(height, Some(hash))].into()
}, },
"chain update changeset is unexpected", "chain update changeset is unexpected",
); );
@ -193,7 +196,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs); let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs);
assert_eq!( assert_eq!(
indexed_additions indexed_additions
.graph .tx_graph
.txs .txs
.iter() .iter()
.map(|tx| tx.compute_txid()) .map(|tx| tx.compute_txid())
@ -201,7 +204,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
exp_txids, exp_txids,
"changeset should have the 3 mempool transactions", "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 // mine a block that confirms the 3 txs
@ -224,9 +227,9 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
let height = emission.block_height(); let height = emission.block_height();
let _ = chain.apply_update(emission.checkpoint)?; let _ = chain.apply_update(emission.checkpoint)?;
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height); let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
assert!(indexed_additions.graph.txs.is_empty()); assert!(indexed_additions.tx_graph.txs.is_empty());
assert!(indexed_additions.graph.txouts.is_empty()); assert!(indexed_additions.tx_graph.txouts.is_empty());
assert_eq!(indexed_additions.graph.anchors, exp_anchors); assert_eq!(indexed_additions.tx_graph.anchors, exp_anchors);
} }
Ok(()) Ok(())

View File

@ -20,6 +20,10 @@ serde_crate = { package = "serde", version = "1", optional = true, features = ["
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] } hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
miniscript = { version = "12.0.0", optional = true, default-features = false } 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] [dev-dependencies]
rand = "0.8" rand = "0.8"
proptest = "1.2.0" proptest = "1.2.0"
@ -28,3 +32,4 @@ proptest = "1.2.0"
default = ["std", "miniscript"] default = ["std", "miniscript"]
std = ["bitcoin/std", "miniscript?/std"] std = ["bitcoin/std", "miniscript?/std"]
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"] serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
sqlite = ["std", "rusqlite", "serde", "serde_json"]

View File

@ -1,93 +1,207 @@
use crate::{ConfirmationBlockTime, Merge};
type IndexedTxGraphChangeSet =
crate::indexed_tx_graph::ChangeSet<ConfirmationBlockTime, crate::keychain_txout::ChangeSet>;
/// A changeset containing [`crate`] structures typically persisted together. /// A changeset containing [`crate`] structures typically persisted together.
#[cfg(feature = "miniscript")] #[derive(Default, Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr( #[cfg_attr(
feature = "serde", feature = "serde",
derive(crate::serde::Deserialize, crate::serde::Serialize), derive(crate::serde::Deserialize, crate::serde::Serialize),
serde( serde(crate = "crate::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",
),
)
)] )]
pub struct CombinedChangeSet<K, A> { pub struct WalletChangeSet {
/// Changes to the [`LocalChain`](crate::local_chain::LocalChain). /// Descriptor for recipient addresses.
pub chain: crate::local_chain::ChangeSet, pub descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
/// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph). /// Descriptor for change addresses.
pub indexed_tx_graph: pub change_descriptor: Option<miniscript::Descriptor<miniscript::DescriptorPublicKey>>,
crate::indexed_tx_graph::ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>,
/// Stores the network type of the transaction data. /// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>, 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 Merge for WalletChangeSet {
impl<K, A> core::default::Default for CombinedChangeSet<K, A> { /// Merge another [`WalletChangeSet`] into itself.
fn default() -> Self { ///
Self { /// The `keychains_added` field respects the invariants of... TODO: FINISH THIS!
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> {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
crate::Merge::merge(&mut self.chain, other.chain); if other.descriptor.is_some() {
crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph); 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() { if other.network.is_some() {
debug_assert!( debug_assert!(
self.network.is_none() || self.network == other.network, 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; 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 { 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 = "miniscript")] #[cfg(feature = "sqlite")]
impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> { 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(())
}
}
impl From<crate::local_chain::ChangeSet> for WalletChangeSet {
fn from(chain: crate::local_chain::ChangeSet) -> Self { fn from(chain: crate::local_chain::ChangeSet) -> Self {
Self { Self {
chain, local_chain: chain,
..Default::default() ..Default::default()
} }
} }
} }
#[cfg(feature = "miniscript")] impl From<IndexedTxGraphChangeSet> for WalletChangeSet {
impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>>> fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self {
for CombinedChangeSet<K, A>
{
fn from(
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<
A,
crate::indexer::keychain_txout::ChangeSet<K>,
>,
) -> Self {
Self { Self {
indexed_tx_graph, tx_graph: indexed_tx_graph.tx_graph,
indexer: indexed_tx_graph.indexer,
..Default::default() ..Default::default()
} }
} }
} }
#[cfg(feature = "miniscript")] impl From<crate::tx_graph::ChangeSet<ConfirmationBlockTime>> for WalletChangeSet {
impl<K, A> From<crate::indexer::keychain_txout::ChangeSet<K>> for CombinedChangeSet<K, A> { fn from(tx_graph: crate::tx_graph::ChangeSet<ConfirmationBlockTime>) -> Self {
fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self { Self {
tx_graph,
..Default::default()
}
}
}
impl From<crate::keychain_txout::ChangeSet> for WalletChangeSet {
fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
Self { Self {
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
indexer, indexer,
..Default::default() ..Default::default()
},
..Default::default()
} }
} }
} }

View File

@ -1,5 +1,7 @@
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the //! Contains the [`IndexedTxGraph`] and associated types. Refer to the
//! [`IndexedTxGraph`] documentation for more. //! [`IndexedTxGraph`] documentation for more.
use core::fmt::Debug;
use alloc::vec::Vec; use alloc::vec::Vec;
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
@ -47,21 +49,24 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) { pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
self.index.apply_changeset(changeset.indexer); self.index.apply_changeset(changeset.indexer);
for tx in &changeset.graph.txs { for tx in &changeset.tx_graph.txs {
self.index.index_tx(tx); 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.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`]. /// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> { pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.initial_changeset(); let graph = self.graph.initial_changeset();
let indexer = self.index.initial_changeset(); let indexer = self.index.initial_changeset();
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
} }
@ -89,21 +94,30 @@ where
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> { pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.apply_update(update); let graph = self.graph.apply_update(update);
let indexer = self.index_tx_graph_changeset(&graph); let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
/// Insert a floating `txout` of given `outpoint`. /// Insert a floating `txout` of given `outpoint`.
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> { pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_txout(outpoint, txout); let graph = self.graph.insert_txout(outpoint, txout);
let indexer = self.index_tx_graph_changeset(&graph); let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
/// Insert and index a transaction into the graph. /// Insert and index a transaction into the graph.
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> { pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.insert_tx(tx); let graph = self.graph.insert_tx(tx);
let indexer = self.index_tx_graph_changeset(&graph); let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
/// Insert an `anchor` for a given transaction. /// Insert an `anchor` for a given transaction.
@ -151,7 +165,10 @@ where
} }
} }
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant. /// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
@ -185,7 +202,10 @@ where
.map(|(tx, seen_at)| (tx.clone(), seen_at)), .map(|(tx, seen_at)| (tx.clone(), seen_at)),
); );
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
/// Batch insert unconfirmed transactions. /// Batch insert unconfirmed transactions.
@ -203,7 +223,10 @@ where
) -> ChangeSet<A, I::ChangeSet> { ) -> ChangeSet<A, I::ChangeSet> {
let graph = self.graph.batch_insert_unconfirmed(txs); let graph = self.graph.batch_insert_unconfirmed(txs);
let indexer = self.index_tx_graph_changeset(&graph); let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
} }
} }
@ -236,9 +259,9 @@ where
if self.index.is_tx_relevant(tx) { if self.index.is_tx_relevant(tx) {
let txid = tx.compute_txid(); let txid = tx.compute_txid();
let anchor = A::from_block_position(block, block_id, tx_pos); 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 changeset
.graph .tx_graph
.merge(self.graph.insert_anchor(txid, anchor)); .merge(self.graph.insert_anchor(txid, anchor));
} }
} }
@ -265,7 +288,16 @@ where
graph.merge(self.graph.insert_tx(tx.clone())); graph.merge(self.graph.insert_tx(tx.clone()));
} }
let indexer = self.index_tx_graph_changeset(&graph); let indexer = self.index_tx_graph_changeset(&graph);
ChangeSet { graph, indexer } ChangeSet {
tx_graph: graph,
indexer,
}
}
}
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
fn as_ref(&self) -> &TxGraph<A> {
&self.graph
} }
} }
@ -285,7 +317,7 @@ where
#[must_use] #[must_use]
pub struct ChangeSet<A, IA> { pub struct ChangeSet<A, IA> {
/// [`TxGraph`] changeset. /// [`TxGraph`] changeset.
pub graph: tx_graph::ChangeSet<A>, pub tx_graph: tx_graph::ChangeSet<A>,
/// [`Indexer`] changeset. /// [`Indexer`] changeset.
pub indexer: IA, pub indexer: IA,
} }
@ -293,7 +325,7 @@ pub struct ChangeSet<A, IA> {
impl<A, IA: Default> Default for ChangeSet<A, IA> { impl<A, IA: Default> Default for ChangeSet<A, IA> {
fn default() -> Self { fn default() -> Self {
Self { Self {
graph: Default::default(), tx_graph: Default::default(),
indexer: Default::default(), indexer: Default::default(),
} }
} }
@ -301,38 +333,30 @@ impl<A, IA: Default> Default for ChangeSet<A, IA> {
impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> { impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
self.graph.merge(other.graph); self.tx_graph.merge(other.tx_graph);
self.indexer.merge(other.indexer); self.indexer.merge(other.indexer);
} }
fn is_empty(&self) -> bool { 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> { impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
fn from(graph: tx_graph::ChangeSet<A>) -> Self { fn from(graph: tx_graph::ChangeSet<A>) -> Self {
Self { Self {
graph, tx_graph: graph,
..Default::default() ..Default::default()
} }
} }
} }
#[cfg(feature = "miniscript")] #[cfg(feature = "miniscript")]
impl<A, K> From<crate::indexer::keychain_txout::ChangeSet<K>> impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
for ChangeSet<A, crate::indexer::keychain_txout::ChangeSet<K>> fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
{
fn from(indexer: crate::indexer::keychain_txout::ChangeSet<K>) -> Self {
Self { Self {
graph: Default::default(), tx_graph: Default::default(),
indexer, indexer,
} }
} }
} }
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
fn as_ref(&self) -> &TxGraph<A> {
&self.graph
}
}

View File

@ -5,7 +5,8 @@ use crate::{
collections::*, collections::*,
miniscript::{Descriptor, DescriptorPublicKey}, miniscript::{Descriptor, DescriptorPublicKey},
spk_iter::BIP32_MAX_INDEX, 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 alloc::{borrow::ToOwned, vec::Vec};
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
@ -135,7 +136,7 @@ impl<K> Default for KeychainTxOutIndex<K> {
} }
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> { 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 { fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
let mut changeset = ChangeSet::default(); let mut changeset = ChangeSet::default();
@ -154,7 +155,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
} }
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet { 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(); let txid = tx.compute_txid();
for (op, txout) in tx.output.iter().enumerate() { for (op, txout) in tx.output.iter().enumerate() {
changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout)); changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout));
@ -164,10 +165,6 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
fn initial_changeset(&self) -> Self::ChangeSet { fn initial_changeset(&self) -> Self::ChangeSet {
ChangeSet { ChangeSet {
keychains_added: self
.keychains()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
last_revealed: self.last_revealed.clone().into_iter().collect(), last_revealed: self.last_revealed.clone().into_iter().collect(),
} }
} }
@ -354,7 +351,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so /// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so
/// will return a [`InsertDescriptorError<K>`]. /// 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 /// 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 /// 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 /// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have
@ -364,8 +361,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
&mut self, &mut self,
keychain: K, keychain: K,
descriptor: Descriptor<DescriptorPublicKey>, descriptor: Descriptor<DescriptorPublicKey>,
) -> Result<ChangeSet<K>, InsertDescriptorError<K>> { ) -> Result<bool, InsertDescriptorError<K>> {
let mut changeset = ChangeSet::<K>::default();
let did = descriptor.descriptor_id(); let did = descriptor.descriptor_id();
if !self.keychain_to_descriptor_id.contains_key(&keychain) if !self.keychain_to_descriptor_id.contains_key(&keychain)
&& !self.descriptor_id_to_keychain.contains_key(&did) && !self.descriptor_id_to_keychain.contains_key(&did)
@ -374,10 +370,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
self.keychain_to_descriptor_id.insert(keychain.clone(), did); self.keychain_to_descriptor_id.insert(keychain.clone(), did);
self.descriptor_id_to_keychain.insert(did, keychain.clone()); self.descriptor_id_to_keychain.insert(did, keychain.clone());
self.replenish_inner_index(did, &keychain, self.lookahead); self.replenish_inner_index(did, &keychain, self.lookahead);
changeset return Ok(true);
.keychains_added }
.insert(keychain.clone(), descriptor);
} else {
if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) { if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
let descriptor = self.descriptors.get(existing_desc_id).expect("invariant"); let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
if *existing_desc_id != did { if *existing_desc_id != did {
@ -398,9 +393,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
}); });
} }
} }
}
Ok(changeset) Ok(false)
} }
/// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't /// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't
@ -627,7 +621,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
} }
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains. /// 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(); let mut changeset = ChangeSet::default();
for (keychain, &index) in keychains { for (keychain, &index) in keychains {
@ -656,7 +650,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
&mut self, &mut self,
keychain: &K, keychain: &K,
target_index: u32, target_index: u32,
) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet<K>)> { ) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet)> {
let mut changeset = ChangeSet::default(); let mut changeset = ChangeSet::default();
let mut spks: Vec<Indexed<ScriptBuf>> = vec![]; let mut spks: Vec<Indexed<ScriptBuf>> = vec![];
while let Some((i, new)) = self.next_index(keychain) { while let Some((i, new)) = self.next_index(keychain) {
@ -687,7 +681,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// 1. The descriptor has no wildcard and already has one script revealed. /// 1. The descriptor has no wildcard and already has one script revealed.
/// 2. The descriptor has already revealed scripts up to the numeric bound. /// 2. The descriptor has already revealed scripts up to the numeric bound.
/// 3. There is no descriptor associated with the given keychain. /// 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 (next_index, new) = self.next_index(keychain)?;
let mut changeset = ChangeSet::default(); let mut changeset = ChangeSet::default();
@ -717,7 +711,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
/// could be revealed (see [`reveal_next_spk`] for when this happens). /// could be revealed (see [`reveal_next_spk`] for when this happens).
/// ///
/// [`reveal_next_spk`]: Self::reveal_next_spk /// [`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 let next_unused = self
.unused_keychain_spks(keychain) .unused_keychain_spks(keychain)
.next() .next()
@ -780,27 +774,80 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
} }
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`] /// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
/// pub fn apply_changeset(&mut self, changeset: ChangeSet) {
/// Keychains added by the `keychains_added` field of `ChangeSet<K>` respect the one-to-one for (&desc_id, &index) in &changeset.last_revealed {
/// 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 {
let v = self.last_revealed.entry(desc_id).or_default(); let v = self.last_revealed.entry(desc_id).or_default();
*v = index.max(*v); *v = index.max(*v);
self.replenish_inner_index_did(desc_id, 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])
} }
for did in last_revealed.keys() { /// Construct [`KeychainTxOutIndex`] from sqlite database and given parameters.
self.replenish_inner_index_did(*did, self.lookahead); pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
use crate::sqlite::Sql;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT descriptor_id, last_revealed FROM {}",
Self::LAST_REVEALED_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Sql<DescriptorId>>("descriptor_id")?,
row.get::<_, u32>("last_revealed")?,
))
})?;
for row in row_iter {
let (Sql(descriptor_id), last_revealed) = row?;
changeset.last_revealed.insert(descriptor_id, last_revealed);
} }
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
use crate::rusqlite::named_params;
use crate::sqlite::Sql;
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)",
Self::LAST_REVEALED_TABLE_NAME,
))?;
for (&descriptor_id, &last_revealed) in &self.last_revealed {
statement.execute(named_params! {
":descriptor_id": Sql(descriptor_id),
":last_revealed": last_revealed,
})?;
}
Ok(())
} }
} }
@ -860,49 +907,24 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the /// `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. /// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself.
/// ///
/// [`apply_changeset`]: KeychainTxOutIndex::apply_changeset /// [`KeychainTxOutIndex`]: crate::keychain_txout::KeychainTxOutIndex
/// [`Merge`]: Self::merge /// [`apply_changeset`]: crate::keychain_txout::KeychainTxOutIndex::apply_changeset
#[derive(Clone, Debug, PartialEq)] /// [`merge`]: Self::merge
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr( #[cfg_attr(
feature = "serde", feature = "serde",
derive(serde::Deserialize, serde::Serialize), derive(serde::Deserialize, serde::Serialize),
serde( serde(crate = "serde_crate")
crate = "serde_crate",
bound(
deserialize = "K: Ord + serde::Deserialize<'de>",
serialize = "K: Ord + serde::Serialize"
)
)
)] )]
#[must_use] #[must_use]
pub struct ChangeSet<K> { pub struct ChangeSet {
/// Contains the keychains that have been added and their respective descriptor
pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
/// Contains for each descriptor_id the last revealed index of derivation /// Contains for each descriptor_id the last revealed index of derivation
pub last_revealed: BTreeMap<DescriptorId, u32>, pub last_revealed: BTreeMap<DescriptorId, u32>,
} }
impl<K: Ord> Merge for ChangeSet<K> { impl Merge for ChangeSet {
/// Merge another [`ChangeSet<K>`] into self. /// Merge another [`ChangeSet`] into self.
///
/// For the `keychains_added` field this method respects the invariants of
/// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two.
///
/// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor
fn merge(&mut self, other: 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 // for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than
// what was originally in `self`. // what was originally in `self`.
for (desc_id, index) in other.last_revealed { for (desc_id, index) in other.last_revealed {
@ -922,25 +944,6 @@ impl<K: Ord> Merge for ChangeSet<K> {
/// Returns whether the changeset are empty. /// Returns whether the changeset are empty.
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.last_revealed.is_empty() && self.keychains_added.is_empty() self.last_revealed.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)
} }
} }

View File

@ -208,7 +208,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # use bdk_chain::SpkTxOutIndex; /// # use bdk_chain::spk_txout::SpkTxOutIndex;
/// ///
/// // imagine our spks are indexed like (keychain, derivation_index). /// // imagine our spks are indexed like (keychain, derivation_index).
/// let txout_index = SpkTxOutIndex::<(u32, u32)>::default(); /// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();

View File

@ -28,7 +28,7 @@ pub use chain_data::*;
pub mod indexed_tx_graph; pub mod indexed_tx_graph;
pub use indexed_tx_graph::IndexedTxGraph; pub use indexed_tx_graph::IndexedTxGraph;
pub mod indexer; pub mod indexer;
pub use indexer::spk_txout::*; pub use indexer::spk_txout;
pub use indexer::Indexer; pub use indexer::Indexer;
pub mod local_chain; pub mod local_chain;
mod tx_data_traits; mod tx_data_traits;
@ -37,6 +37,8 @@ pub use tx_data_traits::*;
pub use tx_graph::TxGraph; pub use tx_graph::TxGraph;
mod chain_oracle; mod chain_oracle;
pub use chain_oracle::*; pub use chain_oracle::*;
mod persist;
pub use persist::*;
#[doc(hidden)] #[doc(hidden)]
pub mod example_utils; pub mod example_utils;
@ -51,8 +53,16 @@ pub use descriptor_ext::{DescriptorExt, DescriptorId};
mod spk_iter; mod spk_iter;
#[cfg(feature = "miniscript")] #[cfg(feature = "miniscript")]
pub use spk_iter::*; pub use spk_iter::*;
#[cfg(feature = "miniscript")]
mod changeset; mod changeset;
#[cfg(feature = "miniscript")]
pub use changeset::*; 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; pub mod spk_client;
#[allow(unused_imports)] #[allow(unused_imports)]

View File

@ -4,17 +4,11 @@ use core::convert::Infallible;
use core::ops::RangeBounds; use core::ops::RangeBounds;
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
use crate::{BlockId, ChainOracle}; use crate::{BlockId, ChainOracle, Merge};
use alloc::sync::Arc; use alloc::sync::Arc;
use bitcoin::block::Header; use bitcoin::block::Header;
use bitcoin::BlockHash; 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 /// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
/// transaction anchor. /// transaction anchor.
/// ///
@ -216,7 +210,7 @@ impl CheckPoint {
/// Apply `changeset` to the checkpoint. /// Apply `changeset` to the checkpoint.
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> { 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 // changes after point of agreement
let mut extension = BTreeMap::default(); let mut extension = BTreeMap::default();
// point of agreement // point of agreement
@ -231,7 +225,7 @@ impl CheckPoint {
} }
} }
for (&height, &hash) in changeset { for (&height, &hash) in &changeset.blocks {
match hash { match hash {
Some(hash) => { Some(hash) => {
extension.insert(height, hash); extension.insert(height, hash);
@ -331,7 +325,7 @@ impl LocalChain {
/// Construct a [`LocalChain`] from an initial `changeset`. /// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> { 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 { let genesis_hash = match genesis_entry {
Some(hash) => hash, Some(hash) => hash,
None => return Err(MissingGenesisError), None => return Err(MissingGenesisError),
@ -521,12 +515,14 @@ impl LocalChain {
} }
let mut changeset = ChangeSet::default(); 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) self.apply_changeset(&changeset)
.map_err(|_| AlterCheckPointError { .map_err(|_| AlterCheckPointError {
height: 0, height: 0,
original_hash: self.genesis_hash(), original_hash: self.genesis_hash(),
update_hash: changeset.get(&0).cloned().flatten(), update_hash: changeset.blocks.get(&0).cloned().flatten(),
})?; })?;
Ok(changeset) Ok(changeset)
} }
@ -548,7 +544,7 @@ impl LocalChain {
if cp_id.height < block_id.height { if cp_id.height < block_id.height {
break; break;
} }
changeset.insert(cp_id.height, None); changeset.blocks.insert(cp_id.height, None);
if cp_id == block_id { if cp_id == block_id {
remove_from = Some(cp); remove_from = Some(cp);
} }
@ -569,13 +565,16 @@ impl LocalChain {
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
/// recover the current chain. /// recover the current chain.
pub fn initial_changeset(&self) -> ChangeSet { pub fn initial_changeset(&self) -> ChangeSet {
self.tip ChangeSet {
blocks: self
.tip
.iter() .iter()
.map(|cp| { .map(|cp| {
let block_id = cp.block_id(); let block_id = cp.block_id();
(block_id.height, Some(block_id.hash)) (block_id.height, Some(block_id.hash))
}) })
.collect() .collect(),
}
} }
/// Iterate over checkpoints in descending height order. /// Iterate over checkpoints in descending height order.
@ -587,7 +586,7 @@ impl LocalChain {
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
let mut curr_cp = self.tip.clone(); 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) { match curr_cp.get(*height) {
Some(query_cp) => { Some(query_cp) => {
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash { if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
@ -630,6 +629,135 @@ impl LocalChain {
} }
} }
/// The [`ChangeSet`] represents changes to [`LocalChain`].
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ChangeSet {
/// Changes to the [`LocalChain`] blocks.
///
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
pub blocks: BTreeMap<u32, Option<BlockHash>>,
}
impl Merge for ChangeSet {
fn merge(&mut self, other: Self) {
Merge::merge(&mut self.blocks, other.blocks)
}
fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
}
impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
fn from(blocks: B) -> Self {
Self {
blocks: blocks.into_iter().collect(),
}
}
}
impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
Self {
blocks: iter.into_iter().collect(),
}
}
}
impl FromIterator<(u32, BlockHash)> for ChangeSet {
fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
Self {
blocks: iter
.into_iter()
.map(|(height, hash)| (height, Some(hash)))
.collect(),
}
}
}
#[cfg(feature = "sqlite")]
impl ChangeSet {
/// Schema name for the changeset.
pub const SCHEMA_NAME: &'static str = "bdk_localchain";
/// Name of sqlite table that stores blocks of [`LocalChain`].
pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks";
/// Initialize sqlite tables for persisting [`LocalChain`].
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
let schema_v0: &[&str] = &[
// blocks
&format!(
"CREATE TABLE {} ( \
block_height INTEGER PRIMARY KEY NOT NULL, \
block_hash TEXT NOT NULL \
) STRICT",
Self::BLOCKS_TABLE_NAME,
),
];
crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
}
/// Construct a [`LocalChain`] from sqlite database.
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
use crate::sqlite::Sql;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT block_height, block_hash FROM {}",
Self::BLOCKS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, u32>("block_height")?,
row.get::<_, Sql<BlockHash>>("block_hash")?,
))
})?;
for row in row_iter {
let (height, Sql(hash)) = row?;
changeset.blocks.insert(height, Some(hash));
}
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
use crate::sqlite::Sql;
use rusqlite::named_params;
let mut replace_statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)",
Self::BLOCKS_TABLE_NAME,
))?;
let mut delete_statement = db_tx.prepare_cached(&format!(
"DELETE FROM {} WHERE block_height=:block_height",
Self::BLOCKS_TABLE_NAME,
))?;
for (&height, &hash) in &self.blocks {
match hash {
Some(hash) => replace_statement.execute(named_params! {
":block_height": height,
":block_hash": Sql(hash),
})?,
None => delete_statement.execute(named_params! {
":block_height": height,
})?,
};
}
Ok(())
}
}
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint. /// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct MissingGenesisError; pub struct MissingGenesisError;
@ -761,7 +889,7 @@ fn merge_chains(
match (curr_orig.as_ref(), curr_update.as_ref()) { match (curr_orig.as_ref(), curr_update.as_ref()) {
// Update block that doesn't exist in the original chain // Update block that doesn't exist in the original chain
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => { (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(); prev_update = curr_update.take();
} }
// Original block that isn't in the update // Original block that isn't in the update
@ -813,9 +941,9 @@ fn merge_chains(
} else { } else {
// We have an invalidation height so we set the height to the updated hash and // 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. // 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(..) { for invalidated_height in potentially_invalidated_heights.drain(..) {
changeset.insert(invalidated_height, None); changeset.blocks.insert(invalidated_height, None);
} }
prev_orig_was_invalidated = true; prev_orig_was_invalidated = true;
} }

135
crates/chain/src/persist.rs Normal file
View File

@ -0,0 +1,135 @@
use core::{
future::Future,
ops::{Deref, DerefMut},
pin::Pin,
};
use alloc::boxed::Box;
/// Trait that persists the type with `Db`.
///
/// Methods of this trait should not be called directly.
pub trait PersistWith<Db>: Sized {
/// Parameters for [`PersistWith::create`].
type CreateParams;
/// Parameters for [`PersistWith::load`].
type LoadParams;
/// Error type of [`PersistWith::create`].
type CreateError;
/// Error type of [`PersistWith::load`].
type LoadError;
/// Error type of [`PersistWith::persist`].
type PersistError;
/// Create the type and initialize the `Db`.
fn create(db: &mut Db, params: Self::CreateParams) -> Result<Self, Self::CreateError>;
/// Load the type from the `Db`.
fn load(db: &mut Db, params: Self::LoadParams) -> Result<Option<Self>, Self::LoadError>;
/// Persist staged changes into `Db`.
fn persist(&mut self, db: &mut Db) -> Result<bool, Self::PersistError>;
}
type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
/// Trait that persists the type with an async `Db`.
pub trait PersistAsyncWith<Db>: Sized {
/// Parameters for [`PersistAsyncWith::create`].
type CreateParams;
/// Parameters for [`PersistAsyncWith::load`].
type LoadParams;
/// Error type of [`PersistAsyncWith::create`].
type CreateError;
/// Error type of [`PersistAsyncWith::load`].
type LoadError;
/// Error type of [`PersistAsyncWith::persist`].
type PersistError;
/// Create the type and initialize the `Db`.
fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult<Self, Self::CreateError>;
/// Load the type from `Db`.
fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult<Option<Self>, Self::LoadError>;
/// Persist staged changes into `Db`.
fn persist<'a>(&'a mut self, db: &'a mut Db) -> FutureResult<'a, bool, Self::PersistError>;
}
/// Represents a persisted `T`.
pub struct Persisted<T> {
inner: T,
}
impl<T> Persisted<T> {
/// Create a new persisted `T`.
pub fn create<Db>(db: &mut Db, params: T::CreateParams) -> Result<Self, T::CreateError>
where
T: PersistWith<Db>,
{
T::create(db, params).map(|inner| Self { inner })
}
/// Create a new persisted `T` with async `Db`.
pub async fn create_async<Db>(
db: &mut Db,
params: T::CreateParams,
) -> Result<Self, T::CreateError>
where
T: PersistAsyncWith<Db>,
{
T::create(db, params).await.map(|inner| Self { inner })
}
/// Construct a persisted `T` from `Db`.
pub fn load<Db>(db: &mut Db, params: T::LoadParams) -> Result<Option<Self>, T::LoadError>
where
T: PersistWith<Db>,
{
Ok(T::load(db, params)?.map(|inner| Self { inner }))
}
/// Contruct a persisted `T` from an async `Db`.
pub async fn load_async<Db>(
db: &mut Db,
params: T::LoadParams,
) -> Result<Option<Self>, T::LoadError>
where
T: PersistAsyncWith<Db>,
{
Ok(T::load(db, params).await?.map(|inner| Self { inner }))
}
/// Persist staged changes of `T` into `Db`.
pub fn persist<Db>(&mut self, db: &mut Db) -> Result<bool, T::PersistError>
where
T: PersistWith<Db>,
{
self.inner.persist(db)
}
/// Persist staged changes of `T` into an async `Db`.
pub async fn persist_async<'a, Db>(
&'a mut self,
db: &'a mut Db,
) -> Result<bool, T::PersistError>
where
T: PersistAsyncWith<Db>,
{
self.inner.persist(db).await
}
}
impl<T> Deref for Persisted<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for Persisted<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}

332
crates/chain/src/sqlite.rs Normal file
View File

@ -0,0 +1,332 @@
//! Module for stuff
use core::{fmt::Debug, ops::Deref, str::FromStr};
use alloc::{borrow::ToOwned, boxed::Box, string::ToString, vec::Vec};
use bitcoin::consensus::{Decodable, Encodable};
pub use rusqlite;
pub use rusqlite::Connection;
use rusqlite::OptionalExtension;
pub use rusqlite::Transaction;
use rusqlite::{
named_params,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
ToSql,
};
use crate::{Anchor, Merge};
/// Parameters for [`Persister`].
pub trait PersistParams {
/// Data type that is loaded and written to the database.
type ChangeSet: Default + Merge;
/// Initialize SQL tables.
fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()>;
/// Load all data from tables.
fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>>;
/// Write data into table(s).
fn write_changeset(
&self,
db_tx: &Transaction,
changeset: &Self::ChangeSet,
) -> rusqlite::Result<()>;
}
// TODO: Use macros
impl<A: PersistParams, B: PersistParams> PersistParams for (A, B) {
type ChangeSet = (A::ChangeSet, B::ChangeSet);
fn initialize_tables(&self, db_tx: &Transaction) -> rusqlite::Result<()> {
self.0.initialize_tables(db_tx)?;
self.1.initialize_tables(db_tx)?;
Ok(())
}
fn load_changeset(&self, db_tx: &Transaction) -> rusqlite::Result<Option<Self::ChangeSet>> {
let changeset = (
self.0.load_changeset(db_tx)?.unwrap_or_default(),
self.1.load_changeset(db_tx)?.unwrap_or_default(),
);
if changeset.is_empty() {
Ok(None)
} else {
Ok(Some(changeset))
}
}
fn write_changeset(
&self,
db_tx: &Transaction,
changeset: &Self::ChangeSet,
) -> rusqlite::Result<()> {
self.0.write_changeset(db_tx, &changeset.0)?;
self.1.write_changeset(db_tx, &changeset.1)?;
Ok(())
}
}
/// Persists data in to a relational schema based [SQLite] database file.
///
/// The changesets loaded or stored represent changes to keychain and blockchain data.
///
/// [SQLite]: https://www.sqlite.org/index.html
#[derive(Debug)]
pub struct Persister<P> {
conn: rusqlite::Connection,
params: P,
}
impl<P: PersistParams> Persister<P> {
/// Persist changeset to the database connection.
pub fn persist(&mut self, changeset: &P::ChangeSet) -> rusqlite::Result<()> {
if !changeset.is_empty() {
let db_tx = self.conn.transaction()?;
self.params.write_changeset(&db_tx, changeset)?;
db_tx.commit()?;
}
Ok(())
}
}
/// Extends [`rusqlite::Connection`] to transform into a [`Persister`].
pub trait ConnectionExt: Sized {
/// Transform into a [`Persister`].
fn into_persister<P: PersistParams>(
self,
params: P,
) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)>;
}
impl ConnectionExt for rusqlite::Connection {
fn into_persister<P: PersistParams>(
mut self,
params: P,
) -> rusqlite::Result<(Persister<P>, Option<P::ChangeSet>)> {
let db_tx = self.transaction()?;
params.initialize_tables(&db_tx)?;
let changeset = params.load_changeset(&db_tx)?;
db_tx.commit()?;
let persister = Persister { conn: self, params };
Ok((persister, changeset))
}
}
/// Table name for schemas.
pub const SCHEMAS_TABLE_NAME: &str = "bdk_schemas";
/// Initialize the schema table.
fn init_schemas_table(db_tx: &Transaction) -> rusqlite::Result<()> {
let sql = format!("CREATE TABLE IF NOT EXISTS {}( name TEXT PRIMARY KEY NOT NULL, version INTEGER NOT NULL ) STRICT", SCHEMAS_TABLE_NAME);
db_tx.execute(&sql, ())?;
Ok(())
}
/// Get schema version of `schema_name`.
fn schema_version(db_tx: &Transaction, schema_name: &str) -> rusqlite::Result<Option<u32>> {
let sql = format!(
"SELECT version FROM {} WHERE name=:name",
SCHEMAS_TABLE_NAME
);
db_tx
.query_row(&sql, named_params! { ":name": schema_name }, |row| {
row.get::<_, u32>("version")
})
.optional()
}
/// Set the `schema_version` of `schema_name`.
fn set_schema_version(
db_tx: &Transaction,
schema_name: &str,
schema_version: u32,
) -> rusqlite::Result<()> {
let sql = format!(
"REPLACE INTO {}(name, version) VALUES(:name, :version)",
SCHEMAS_TABLE_NAME,
);
db_tx.execute(
&sql,
named_params! { ":name": schema_name, ":version": schema_version },
)?;
Ok(())
}
/// Runs logic that initializes/migrates the table schemas.
pub fn migrate_schema(
db_tx: &Transaction,
schema_name: &str,
versioned_scripts: &[&[&str]],
) -> rusqlite::Result<()> {
init_schemas_table(db_tx)?;
let current_version = schema_version(db_tx, schema_name)?;
let exec_from = current_version.map_or(0_usize, |v| v as usize + 1);
let scripts_to_exec = versioned_scripts.iter().enumerate().skip(exec_from);
for (version, &script) in scripts_to_exec {
set_schema_version(db_tx, schema_name, version as u32)?;
for statement in script {
db_tx.execute(statement, ())?;
}
}
Ok(())
}
/// A wrapper so that we can impl [FromSql] and [ToSql] for multiple types.
pub struct Sql<T>(pub T);
impl<T> From<T> for Sql<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T> Deref for Sql<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromSql for Sql<bitcoin::Txid> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Txid::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Sql<bitcoin::Txid> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Sql<bitcoin::BlockHash> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::BlockHash::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Sql<bitcoin::BlockHash> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
#[cfg(feature = "miniscript")]
impl FromSql for Sql<crate::DescriptorId> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
crate::DescriptorId::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl ToSql for Sql<crate::DescriptorId> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Sql<bitcoin::Transaction> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Sql<bitcoin::Transaction> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let mut bytes = Vec::<u8>::new();
self.consensus_encode(&mut bytes).map_err(to_sql_error)?;
Ok(bytes.into())
}
}
impl FromSql for Sql<bitcoin::ScriptBuf> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(bitcoin::Script::from_bytes(value.as_bytes()?)
.to_owned()
.into())
}
}
impl ToSql for Sql<bitcoin::ScriptBuf> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.as_bytes().into())
}
}
impl FromSql for Sql<bitcoin::Amount> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into())
}
}
impl ToSql for Sql<bitcoin::Amount> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?;
Ok(amount.into())
}
}
impl<A: Anchor + serde_crate::de::DeserializeOwned> FromSql for Sql<A> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
serde_json::from_str(value.as_str()?)
.map(Sql)
.map_err(from_sql_error)
}
}
impl<A: Anchor + serde_crate::Serialize> ToSql for Sql<A> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
serde_json::to_string(&self.0)
.map(Into::into)
.map_err(to_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl FromSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
miniscript::Descriptor::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
#[cfg(feature = "miniscript")]
impl ToSql for Sql<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
impl FromSql for Sql<bitcoin::Network> {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
bitcoin::Network::from_str(value.as_str()?)
.map(Self)
.map_err(from_sql_error)
}
}
impl ToSql for Sql<bitcoin::Network> {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(self.to_string().into())
}
}
fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> FromSqlError {
FromSqlError::Other(Box::new(err))
}
fn to_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> rusqlite::Error {
rusqlite::Error::ToSqlConversionFailure(Box::new(err))
}

View File

@ -1293,6 +1293,188 @@ impl<A> ChangeSet<A> {
} }
} }
#[cfg(feature = "sqlite")]
impl<A> ChangeSet<A>
where
A: Anchor + Clone + Ord + serde::Serialize + serde::de::DeserializeOwned,
{
/// Schema name for the [`ChangeSet`].
pub const SCHEMA_NAME: &'static str = "bdk_txgraph";
/// Name of table that stores full transactions and `last_seen` timestamps.
pub const TXS_TABLE_NAME: &'static str = "bdk_txs";
/// Name of table that stores floating txouts.
pub const TXOUTS_TABLE_NAME: &'static str = "bdk_txouts";
/// Name of table that stores [`Anchor`]s.
pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors";
/// Initialize sqlite tables.
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
let schema_v0: &[&str] = &[
// full transactions
&format!(
"CREATE TABLE {} ( \
txid TEXT PRIMARY KEY NOT NULL, \
raw_tx BLOB, \
last_seen INTEGER \
) STRICT",
Self::TXS_TABLE_NAME,
),
// floating txouts
&format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL, \
vout INTEGER NOT NULL, \
value INTEGER NOT NULL, \
script BLOB NOT NULL, \
PRIMARY KEY (txid, vout) \
) STRICT",
Self::TXOUTS_TABLE_NAME,
),
// anchors
&format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL REFERENCES {} (txid), \
block_height INTEGER NOT NULL, \
block_hash TEXT NOT NULL, \
anchor BLOB NOT NULL, \
PRIMARY KEY (txid, block_height, block_hash) \
) STRICT",
Self::ANCHORS_TABLE_NAME,
Self::TXS_TABLE_NAME,
),
];
crate::sqlite::migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
}
/// Construct a [`TxGraph`] from an sqlite database.
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
Self::init_sqlite_tables(db_tx)?;
use crate::sqlite::Sql;
let mut changeset = Self::default();
let mut statement = db_tx.prepare(&format!(
"SELECT txid, raw_tx, last_seen FROM {}",
Self::TXS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Sql<Txid>>("txid")?,
row.get::<_, Option<Sql<Transaction>>>("raw_tx")?,
row.get::<_, Option<u64>>("last_seen")?,
))
})?;
for row in row_iter {
let (Sql(txid), tx, last_seen) = row?;
if let Some(Sql(tx)) = tx {
changeset.txs.insert(Arc::new(tx));
}
if let Some(last_seen) = last_seen {
changeset.last_seen.insert(txid, last_seen);
}
}
let mut statement = db_tx.prepare(&format!(
"SELECT txid, vout, value, script FROM {}",
Self::TXOUTS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Sql<Txid>>("txid")?,
row.get::<_, u32>("vout")?,
row.get::<_, Sql<Amount>>("value")?,
row.get::<_, Sql<bitcoin::ScriptBuf>>("script")?,
))
})?;
for row in row_iter {
let (Sql(txid), vout, Sql(value), Sql(script_pubkey)) = row?;
changeset.txouts.insert(
OutPoint { txid, vout },
TxOut {
value,
script_pubkey,
},
);
}
let mut statement = db_tx.prepare(&format!(
"SELECT json(anchor), txid FROM {}",
Self::ANCHORS_TABLE_NAME,
))?;
let row_iter = statement.query_map([], |row| {
Ok((
row.get::<_, Sql<A>>("json(anchor)")?,
row.get::<_, Sql<Txid>>("txid")?,
))
})?;
for row in row_iter {
let (Sql(anchor), Sql(txid)) = row?;
changeset.anchors.insert((anchor, txid));
}
Ok(changeset)
}
/// Persist `changeset` to the sqlite database.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
Self::init_sqlite_tables(db_tx)?;
use crate::rusqlite::named_params;
use crate::sqlite::Sql;
let mut statement = db_tx.prepare_cached(&format!(
"INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx",
Self::TXS_TABLE_NAME,
))?;
for tx in &self.txs {
statement.execute(named_params! {
":txid": Sql(tx.compute_txid()),
":raw_tx": Sql(tx.as_ref().clone()),
})?;
}
let mut statement = db_tx
.prepare_cached(&format!(
"INSERT INTO {}(txid, last_seen) VALUES(:txid, :last_seen) ON CONFLICT(txid) DO UPDATE SET last_seen=:last_seen",
Self::TXS_TABLE_NAME,
))?;
for (&txid, &last_seen) in &self.last_seen {
statement.execute(named_params! {
":txid": Sql(txid),
":last_seen": Some(last_seen),
})?;
}
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, vout, value, script) VALUES(:txid, :vout, :value, :script)",
Self::TXOUTS_TABLE_NAME,
))?;
for (op, txo) in &self.txouts {
statement.execute(named_params! {
":txid": Sql(op.txid),
":vout": op.vout,
":value": Sql(txo.value),
":script": Sql(txo.script_pubkey.clone()),
})?;
}
let mut statement = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))",
Self::ANCHORS_TABLE_NAME,
))?;
for (anchor, txid) in &self.anchors {
let anchor_block = anchor.anchor_block();
statement.execute(named_params! {
":txid": Sql(*txid),
":block_height": anchor_block.height,
":block_hash": Sql(anchor_block.hash),
":anchor": Sql(anchor.clone()),
})?;
}
Ok(())
}
}
impl<A: Ord> Merge for ChangeSet<A> { impl<A: Ord> Merge for ChangeSet<A> {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.

View File

@ -3,7 +3,7 @@
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap; use std::collections::HashMap;
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex}; use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor};
use bitcoin::{ use bitcoin::{
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
Sequence, Transaction, TxIn, TxOut, Txid, Witness, Sequence, Transaction, TxIn, TxOut, Txid, Witness,

View File

@ -10,7 +10,7 @@ use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
indexer::keychain_txout::KeychainTxOutIndex, indexer::keychain_txout::KeychainTxOutIndex,
local_chain::LocalChain, local_chain::LocalChain,
tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge, tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt,
}; };
use bitcoin::{ use bitcoin::{
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
@ -73,13 +73,12 @@ fn insert_relevant_txs() {
let txs = [tx_c, tx_b, tx_a]; let txs = [tx_c, tx_b, tx_a];
let changeset = indexed_tx_graph::ChangeSet { let changeset = indexed_tx_graph::ChangeSet {
graph: tx_graph::ChangeSet { tx_graph: tx_graph::ChangeSet {
txs: txs.iter().cloned().map(Arc::new).collect(), txs: txs.iter().cloned().map(Arc::new).collect(),
..Default::default() ..Default::default()
}, },
indexer: keychain_txout::ChangeSet { indexer: keychain_txout::ChangeSet {
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(), last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
keychains_added: [].into(),
}, },
}; };
@ -90,10 +89,9 @@ fn insert_relevant_txs() {
// The initial changeset will also contain info about the keychain we added // The initial changeset will also contain info about the keychain we added
let initial_changeset = indexed_tx_graph::ChangeSet { let initial_changeset = indexed_tx_graph::ChangeSet {
graph: changeset.graph, tx_graph: changeset.tx_graph,
indexer: keychain_txout::ChangeSet { indexer: keychain_txout::ChangeSet {
last_revealed: changeset.indexer.last_revealed, last_revealed: changeset.indexer.last_revealed,
keychains_added: [((), descriptor)].into(),
}, },
}; };
@ -144,16 +142,14 @@ fn test_list_owned_txouts() {
KeychainTxOutIndex::new(10), KeychainTxOutIndex::new(10),
); );
assert!(!graph assert!(graph
.index .index
.insert_descriptor("keychain_1".into(), desc_1) .insert_descriptor("keychain_1".into(), desc_1)
.unwrap() .unwrap());
.is_empty()); assert!(graph
assert!(!graph
.index .index
.insert_descriptor("keychain_2".into(), desc_2) .insert_descriptor("keychain_2".into(), desc_2)
.unwrap() .unwrap());
.is_empty());
// Get trusted and untrusted addresses // Get trusted and untrusted addresses
@ -532,8 +528,8 @@ fn test_list_owned_txouts() {
#[test] #[test]
fn test_get_chain_position() { fn test_get_chain_position() {
use bdk_chain::local_chain::CheckPoint; use bdk_chain::local_chain::CheckPoint;
use bdk_chain::spk_txout::SpkTxOutIndex;
use bdk_chain::BlockId; use bdk_chain::BlockId;
use bdk_chain::SpkTxOutIndex;
struct TestCase<A> { struct TestCase<A> {
name: &'static str, name: &'static str,

View File

@ -81,11 +81,9 @@ fn merge_changesets_check_last_revealed() {
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
let mut lhs = ChangeSet { let mut lhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: lhs_di, last_revealed: lhs_di,
}; };
let rhs = ChangeSet { let rhs = ChangeSet {
keychains_added: BTreeMap::<(), _>::new(),
last_revealed: rhs_di, last_revealed: rhs_di,
}; };
lhs.merge(rhs); lhs.merge(rhs);
@ -100,49 +98,6 @@ fn merge_changesets_check_last_revealed() {
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4)); 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] #[test]
fn test_set_all_derivation_indices() { fn test_set_all_derivation_indices() {
let external_descriptor = parse_descriptor(DESCRIPTORS[0]); let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
@ -159,7 +114,6 @@ fn test_set_all_derivation_indices() {
assert_eq!( assert_eq!(
txout_index.reveal_to_target_multi(&derive_to), txout_index.reveal_to_target_multi(&derive_to),
ChangeSet { ChangeSet {
keychains_added: BTreeMap::new(),
last_revealed: last_revealed.clone() last_revealed: last_revealed.clone()
} }
); );
@ -633,46 +587,29 @@ fn lookahead_to_target() {
} }
#[test] #[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() { fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
let desc = parse_descriptor(DESCRIPTORS[0]); let desc = parse_descriptor(DESCRIPTORS[0]);
let changesets: &[ChangeSet<TestKeychain>] = &[ let changesets: &[ChangeSet] = &[
ChangeSet { ChangeSet {
keychains_added: [(TestKeychain::Internal, desc.clone())].into(), last_revealed: [(desc.descriptor_id(), 10)].into(),
last_revealed: [].into(),
}, },
ChangeSet { ChangeSet {
keychains_added: [(TestKeychain::External, desc.clone())].into(),
last_revealed: [(desc.descriptor_id(), 12)].into(), last_revealed: [(desc.descriptor_id(), 12)].into(),
}, },
]; ];
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0); let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
indexer_a
.insert_descriptor(TestKeychain::External, desc.clone())
.expect("must insert keychain");
for changeset in changesets { for changeset in changesets {
indexer_a.apply_changeset(changeset.clone()); indexer_a.apply_changeset(changeset.clone());
} }
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0); let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
indexer_b
.insert_descriptor(TestKeychain::External, desc.clone())
.expect("must insert keychain");
let aggregate_changesets = changesets let aggregate_changesets = changesets
.iter() .iter()
.cloned() .cloned()

View File

@ -1,4 +1,4 @@
use bdk_chain::{Indexer, SpkTxOutIndex}; use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
use bitcoin::{ use bitcoin::{
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
}; };

View File

@ -2,7 +2,8 @@ use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
local_chain::LocalChain, local_chain::LocalChain,
spk_client::{FullScanRequest, SyncRequest}, spk_client::{FullScanRequest, SyncRequest},
Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex, spk_txout::SpkTxOutIndex,
Balance, ConfirmationBlockTime, IndexedTxGraph,
}; };
use bdk_electrum::BdkElectrumClient; use bdk_electrum::BdkElectrumClient;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};

View File

@ -4,11 +4,13 @@
//! used with hardware wallets. //! used with hardware wallets.
//! ```no_run //! ```no_run
//! # use bdk_wallet::bitcoin::Network; //! # use bdk_wallet::bitcoin::Network;
//! # use bdk_wallet::descriptor::Descriptor;
//! # use bdk_wallet::signer::SignerOrdering; //! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_hwi::HWISigner; //! # use bdk_hwi::HWISigner;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet}; //! # use bdk_wallet::{KeychainKind, SignOptions};
//! # use hwi::HWIClient; //! # use hwi::HWIClient;
//! # use std::sync::Arc; //! # use std::sync::Arc;
//! # use std::str::FromStr;
//! # //! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> { //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut devices = HWIClient::enumerate()?; //! let mut devices = HWIClient::enumerate()?;
@ -18,11 +20,8 @@
//! let first_device = devices.remove(0)?; //! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; //! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//! //!
//! # let mut wallet = Wallet::new( //! # let mut wallet = bdk_wallet::CreateParams::new("", "", Network::Testnet)?
//! # "", //! # .create_wallet_no_persist()?;
//! # "",
//! # Network::Testnet,
//! # )?;
//! # //! #
//! // Adding the hardware signer to the BDK wallet //! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer( //! wallet.add_signer(

View File

@ -1,17 +0,0 @@
[package]
name = "bdk_sqlite"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk_sqlite"
description = "A simple SQLite relational database client for persisting bdk_chain data."
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
authors = ["Bitcoin Dev Kit Developers"]
readme = "README.md"
[dependencies]
bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
rusqlite = { version = "0.31.0", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@ -1,8 +0,0 @@
# BDK SQLite
This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
[SQLite]: https://www.sqlite.org/index.html

View File

@ -1,69 +0,0 @@
-- schema version control
CREATE TABLE version
(
version INTEGER
) STRICT;
INSERT INTO version
VALUES (1);
-- network is the valid network for all other table data
CREATE TABLE network
(
name TEXT UNIQUE NOT NULL
) STRICT;
-- keychain is the json serialized keychain structure as JSONB,
-- descriptor is the complete descriptor string,
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
-- last revealed index is a u32
CREATE TABLE keychain
(
keychain BLOB PRIMARY KEY NOT NULL,
descriptor TEXT NOT NULL,
descriptor_id BLOB NOT NULL,
last_revealed INTEGER
) STRICT;
-- hash is block hash hex string,
-- block height is a u32,
CREATE TABLE block
(
hash TEXT PRIMARY KEY NOT NULL,
height INTEGER NOT NULL
) STRICT;
-- txid is transaction hash hex string (reversed)
-- whole_tx is a consensus encoded transaction,
-- last seen is a u64 unix epoch seconds
CREATE TABLE tx
(
txid TEXT PRIMARY KEY NOT NULL,
whole_tx BLOB,
last_seen INTEGER
) STRICT;
-- Outpoint txid hash hex string (reversed)
-- Outpoint vout
-- TxOut value as SATs
-- TxOut script consensus encoded
CREATE TABLE txout
(
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
value INTEGER NOT NULL,
script BLOB NOT NULL,
PRIMARY KEY (txid, vout)
) STRICT;
-- join table between anchor and tx
-- block hash hex string
-- anchor is a json serialized Anchor structure as JSONB,
-- txid is transaction hash hex string (reversed)
CREATE TABLE anchor_tx
(
block_hash TEXT NOT NULL,
anchor BLOB NOT NULL,
txid TEXT NOT NULL REFERENCES tx (txid),
UNIQUE (anchor, txid),
FOREIGN KEY (block_hash) REFERENCES block(hash)
) STRICT;

View File

@ -1,34 +0,0 @@
#![doc = include_str!("../README.md")]
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))]
mod schema;
mod store;
use bdk_chain::bitcoin::Network;
pub use rusqlite;
pub use store::Store;
/// Error that occurs while reading or writing change sets with the SQLite database.
#[derive(Debug)]
pub enum Error {
/// Invalid network, cannot change the one already stored in the database.
Network { expected: Network, given: Network },
/// SQLite error.
Sqlite(rusqlite::Error),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Network { expected, given } => write!(
f,
"network error trying to read or write change set, expected {}, given {}",
expected, given
),
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
}
}
}
impl std::error::Error for Error {}

View File

@ -1,96 +0,0 @@
use crate::Store;
use rusqlite::{named_params, Connection, Error};
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
const MIGRATIONS: &[&str] = &[SCHEMA_0];
/// Schema migration related functions.
impl<K, A> Store<K, A> {
/// Migrate sqlite db schema to latest version.
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
let stmts = &MIGRATIONS
.iter()
.flat_map(|stmt| {
// remove comment lines
let s = stmt
.split('\n')
.filter(|l| !l.starts_with("--") && !l.is_empty())
.collect::<Vec<_>>()
.join(" ");
// split into statements
s.split(';')
// remove extra spaces
.map(|s| {
s.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
})
// remove empty statements
.filter(|s| !s.is_empty())
.collect::<Vec<String>>();
let version = Self::get_schema_version(conn)?;
let stmts = &stmts[(version as usize)..];
// begin transaction, all migration statements and new schema version commit or rollback
let tx = conn.transaction()?;
// execute every statement and return `Some` new schema version
// if execution fails, return `Error::Rusqlite`
// if no statements executed returns `None`
let new_version = stmts
.iter()
.enumerate()
.map(|version_stmt| {
tx.execute(version_stmt.1.as_str(), [])
// map result value to next migration version
.map(|_| version_stmt.0 as i32 + version + 1)
})
.last()
.transpose()?;
// if `Some` new statement version, set new schema version
if let Some(version) = new_version {
Self::set_schema_version(&tx, version)?;
}
// commit transaction
tx.commit()?;
Ok(())
}
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
let statement = conn.prepare_cached("SELECT version FROM version");
match statement {
Err(Error::SqliteFailure(e, Some(msg))) => {
if msg == "no such table: version" {
Ok(0)
} else {
Err(Error::SqliteFailure(e, Some(msg)))
}
}
Ok(mut stmt) => {
let mut rows = stmt.query([])?;
match rows.next()? {
Some(row) => {
let version: i32 = row.get(0)?;
Ok(version)
}
None => Ok(0),
}
}
_ => Ok(0),
}
}
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
conn.execute(
"UPDATE version SET version=:version",
named_params! {":version": version},
)
}
}

View File

@ -1,734 +0,0 @@
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
use bdk_chain::bitcoin::hashes::Hash;
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
use bdk_chain::bitcoin::{BlockHash, Txid};
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use rusqlite::{named_params, Connection};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crate::Error;
use bdk_chain::CombinedChangeSet;
use bdk_chain::{
indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt,
DescriptorId, Merge,
};
/// Persists data in to a relational schema based [SQLite] database file.
///
/// The changesets loaded or stored represent changes to keychain and blockchain data.
///
/// [SQLite]: https://www.sqlite.org/index.html
pub struct Store<K, A> {
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
conn: Mutex<Connection>,
keychain_marker: PhantomData<K>,
anchor_marker: PhantomData<A>,
}
impl<K, A> Debug for Store<K, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.conn, f)
}
}
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Creates a new store from a [`Connection`].
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
Self::migrate(&mut conn)?;
Ok(Self {
conn: Mutex::new(conn),
keychain_marker: Default::default(),
anchor_marker: Default::default(),
})
}
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
let connection = self.conn.get_mut().expect("unlocked connection mutex");
connection.transaction().map_err(Error::Sqlite)
}
}
/// Network table related functions.
impl<K, A> Store<K, A> {
/// Insert [`Network`] for which all other tables data is valid.
///
/// Error if trying to insert different network value.
fn insert_network(
current_network: &Option<Network>,
db_transaction: &rusqlite::Transaction,
network_changeset: &Option<Network>,
) -> Result<(), Error> {
if let Some(network) = network_changeset {
match current_network {
// if no network change do nothing
Some(current_network) if current_network == network => Ok(()),
// if new network not the same as current, error
Some(current_network) => Err(Error::Network {
expected: *current_network,
given: *network,
}),
// insert network if none exists
None => {
let insert_network_stmt = &mut db_transaction
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
.expect("insert network statement");
let name = network.to_string();
insert_network_stmt
.execute(named_params! {":name": name })
.map_err(Error::Sqlite)?;
Ok(())
}
}
} else {
Ok(())
}
}
/// Select the valid [`Network`] for this database, or `None` if not set.
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
let mut select_network_stmt = db_transaction
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
.expect("select network statement");
let network = select_network_stmt
.query_row([], |row| {
let network = row.get_unwrap::<usize, String>(0);
let network = Network::from_str(network.as_str()).expect("valid network");
Ok(network)
})
.map_err(Error::Sqlite);
match network {
Ok(network) => Ok(Some(network)),
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(e) => Err(e),
}
}
}
/// Block table related functions.
impl<K, A> Store<K, A> {
/// Insert or delete local chain blocks.
///
/// Error if trying to insert existing block hash.
fn insert_or_delete_blocks(
db_transaction: &rusqlite::Transaction,
chain_changeset: &local_chain::ChangeSet,
) -> Result<(), Error> {
for (height, hash) in chain_changeset.iter() {
match hash {
// add new hash at height
Some(hash) => {
let insert_block_stmt = &mut db_transaction
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
.expect("insert block statement");
let hash = hash.to_string();
insert_block_stmt
.execute(named_params! {":hash": hash, ":height": height })
.map_err(Error::Sqlite)?;
}
// delete block at height
None => {
let delete_block_stmt = &mut db_transaction
.prepare_cached("DELETE FROM block WHERE height IS :height")
.expect("delete block statement");
delete_block_stmt
.execute(named_params! {":height": height })
.map_err(Error::Sqlite)?;
}
}
}
Ok(())
}
/// Select all blocks.
fn select_blocks(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
let mut select_blocks_stmt = db_transaction
.prepare_cached("SELECT height, hash FROM block")
.expect("select blocks statement");
let blocks = select_blocks_stmt
.query_map([], |row| {
let height = row.get_unwrap::<usize, u32>(0);
let hash = row.get_unwrap::<usize, String>(1);
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
Ok((height, hash))
})
.map_err(Error::Sqlite)?;
blocks
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Keychain table related functions.
///
/// The keychain objects are stored as [`JSONB`] data.
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + Send,
{
/// Insert keychain with descriptor and last active index.
///
/// If keychain exists only update last active index.
fn insert_keychains(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
let insert_keychain_stmt = &mut db_transaction
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
.expect("insert keychain statement");
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
let descriptor_id = descriptor.descriptor_id().to_byte_array();
let descriptor = descriptor.to_string();
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Update descriptor last revealed index.
fn update_last_revealed(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
let keychain_changeset = &tx_graph_changeset.indexer;
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
let update_last_revealed_stmt = &mut db_transaction
.prepare_cached(
"UPDATE keychain SET last_revealed = :last_revealed
WHERE descriptor_id = :descriptor_id",
)
.expect("update last revealed statement");
let descriptor_id = descriptor_id.to_byte_array();
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select keychains added.
fn select_keychains(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
let mut select_keychains_added_stmt = db_transaction
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
.expect("select keychains statement");
let keychains = select_keychains_added_stmt
.query_map([], |row| {
let keychain = row.get_unwrap::<usize, String>(0);
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
let descriptor = row.get_unwrap::<usize, String>(1);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
Ok((keychain, descriptor))
})
.map_err(Error::Sqlite)?;
keychains
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select descriptor last revealed indexes.
fn select_last_revealed(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
let mut select_last_revealed_stmt = db_transaction
.prepare_cached(
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
)
.expect("select last revealed statement");
let last_revealed = select_last_revealed_stmt
.query_map([], |row| {
let descriptor = row.get_unwrap::<usize, String>(0);
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
let descriptor_id = descriptor.descriptor_id();
let last_revealed = row.get_unwrap::<usize, u32>(1);
Ok((descriptor_id, last_revealed))
})
.map_err(Error::Sqlite)?;
last_revealed
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Tx (transaction) and txout (transaction output) table related functions.
impl<K, A> Store<K, A> {
/// Insert transactions.
///
/// Error if trying to insert existing txid.
fn insert_txs(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
for tx in tx_graph_changeset.graph.txs.iter() {
let insert_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
.expect("insert or update tx whole_tx statement");
let txid = tx.compute_txid().to_string();
let whole_tx = serialize(&tx);
insert_tx_stmt
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transactions.
fn select_txs(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
let mut select_tx_stmt = db_transaction
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
.expect("select tx statement");
let txs = select_tx_stmt
.query_map([], |row| {
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
Ok(Arc::new(whole_tx))
})
.map_err(Error::Sqlite)?;
txs.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Select all transactions with last_seen values.
fn select_last_seen(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<Txid, u64>, Error> {
// load tx last_seen
let mut select_last_seen_stmt = db_transaction
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
.expect("select tx last seen statement");
let last_seen = select_last_seen_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let last_seen = row.get_unwrap::<usize, u64>(1);
Ok((txid, last_seen))
})
.map_err(Error::Sqlite)?;
last_seen
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Insert txouts.
///
/// Error if trying to insert existing outpoint.
fn insert_txouts(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
for txout in tx_graph_changeset.graph.txouts.iter() {
let insert_txout_stmt = &mut db_transaction
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
.expect("insert txout statement");
let txid = txout.0.txid.to_string();
let vout = txout.0.vout;
let value = txout.1.value.to_sat();
let script = txout.1.script_pubkey.as_bytes();
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all transaction outputs.
fn select_txouts(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
// load tx outs
let mut select_txout_stmt = db_transaction
.prepare_cached("SELECT txid, vout, value, script FROM txout")
.expect("select txout statement");
let txouts = select_txout_stmt
.query_map([], |row| {
let txid = row.get_unwrap::<usize, String>(0);
let txid = Txid::from_str(&txid).expect("txid");
let vout = row.get_unwrap::<usize, u32>(1);
let outpoint = OutPoint::new(txid, vout);
let value = row.get_unwrap::<usize, u64>(2);
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
let txout = TxOut {
value: Amount::from_sat(value),
script_pubkey,
};
Ok((outpoint, txout))
})
.map_err(Error::Sqlite)?;
txouts
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
/// Update transaction last seen times.
fn update_last_seen(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
let insert_or_update_tx_stmt = &mut db_transaction
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
.expect("insert or update tx last_seen statement");
let txid = tx_last_seen.0.to_string();
let last_seen = *tx_last_seen.1;
insert_or_update_tx_stmt
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
.map_err(Error::Sqlite)?;
}
Ok(())
}
}
/// Anchor table related functions.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Insert anchors.
fn insert_anchors(
db_transaction: &rusqlite::Transaction,
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>>,
) -> Result<(), Error> {
// serde_json::to_string
for anchor in tx_graph_changeset.graph.anchors.iter() {
let insert_anchor_stmt = &mut db_transaction
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
.expect("insert anchor statement");
let block_hash = anchor.0.anchor_block().hash.to_string();
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
let txid = anchor.1.to_string();
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
.map_err(Error::Sqlite)?;
}
Ok(())
}
/// Select all anchors.
fn select_anchors(
db_transaction: &rusqlite::Transaction,
) -> Result<BTreeSet<(A, Txid)>, Error> {
// serde_json::from_str
let mut select_anchor_stmt = db_transaction
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
.expect("select anchor statement");
let anchors = select_anchor_stmt
.query_map([], |row| {
let hash = row.get_unwrap::<usize, String>(0);
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
let anchor = row.get_unwrap::<usize, String>(1);
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
// double check anchor blob block hash matches
assert_eq!(hash, anchor.anchor_block().hash);
let txid = row.get_unwrap::<usize, String>(2);
let txid = Txid::from_str(&txid).expect("txid");
Ok((anchor, txid))
})
.map_err(Error::Sqlite)?;
anchors
.into_iter()
.map(|row| row.map_err(Error::Sqlite))
.collect()
}
}
/// Functions to read and write all [`CombinedChangeSet`] data.
impl<K, A> Store<K, A>
where
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
{
/// Write the given `changeset` atomically.
pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
// no need to write anything if changeset is empty
if changeset.is_empty() {
return Ok(());
}
let db_transaction = self.db_transaction()?;
let network_changeset = &changeset.network;
let current_network = Self::select_network(&db_transaction)?;
Self::insert_network(&current_network, &db_transaction, network_changeset)?;
let chain_changeset = &changeset.chain;
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
let tx_graph_changeset = &changeset.indexed_tx_graph;
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
db_transaction.commit().map_err(Error::Sqlite)
}
/// Read the entire database and return the aggregate [`CombinedChangeSet`].
pub fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
let db_transaction = self.db_transaction()?;
let network = Self::select_network(&db_transaction)?;
let chain = Self::select_blocks(&db_transaction)?;
let keychains_added = Self::select_keychains(&db_transaction)?;
let last_revealed = Self::select_last_revealed(&db_transaction)?;
let txs = Self::select_txs(&db_transaction)?;
let last_seen = Self::select_last_seen(&db_transaction)?;
let txouts = Self::select_txouts(&db_transaction)?;
let anchors = Self::select_anchors(&db_transaction)?;
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
txs,
txouts,
anchors,
last_seen,
};
let indexer = keychain_txout::ChangeSet {
keychains_added,
last_revealed,
};
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<K>> =
indexed_tx_graph::ChangeSet { graph, indexer };
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
Ok(None)
} else {
Ok(Some(CombinedChangeSet {
chain,
indexed_tx_graph,
network,
}))
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::store::Merge;
use bdk_chain::bitcoin::consensus::encode::deserialize;
use bdk_chain::bitcoin::constants::genesis_block;
use bdk_chain::bitcoin::hashes::hex::FromHex;
use bdk_chain::bitcoin::transaction::Transaction;
use bdk_chain::bitcoin::Network::Testnet;
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
use bdk_chain::miniscript::Descriptor;
use bdk_chain::CombinedChangeSet;
use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt};
use std::str::FromStr;
use std::sync::Arc;
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
enum Keychain {
External { account: u32, name: String },
Internal { account: u32, name: String },
}
#[test]
fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, time, hash| ConfirmationBlockTime {
confirmation_time: time,
block_id: (height, hash).into(),
});
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, ConfirmationBlockTime>::new(conn)
.expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
#[test]
fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
let (test_changesets, agg_test_changesets) =
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
let conn = Connection::open_in_memory().expect("in memory connection");
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
test_changesets.iter().for_each(|changeset| {
store.write(changeset).expect("write changeset");
});
let agg_changeset = store.read().expect("aggregated changeset");
assert_eq!(agg_changeset, Some(agg_test_changesets));
}
fn create_test_changesets<A: Anchor + Copy>(
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
) -> (
Vec<CombinedChangeSet<Keychain, A>>,
CombinedChangeSet<Keychain, A>,
) {
let secp = &secp256k1::Secp256k1::signing_only();
let network_changeset = Some(Testnet);
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
let block_hash_1 =
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
.unwrap();
let block_hash_2 =
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
.unwrap();
let block_changeset = [
(0, Some(block_hash_0)),
(1, Some(block_hash_1)),
(2, Some(block_hash_2)),
]
.into();
let ext_keychain = Keychain::External {
account: 0,
name: "ext test".to_string(),
};
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
let ext_desc_id = ext_desc.descriptor_id();
let int_keychain = Keychain::Internal {
account: 0,
name: "int test".to_string(),
};
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
let int_desc_id = int_desc.descriptor_id();
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
let txout0_0 = tx0.output.first().unwrap().clone();
let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
let txout1_0 = tx1.output.first().unwrap().clone();
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
txs: [tx0.clone(), tx1.clone()].into(),
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
last_seen: [
(tx0.compute_txid(), 1598918400),
(tx1.compute_txid(), 1598919121),
(tx2.compute_txid(), 1608919121),
]
.into(),
};
let keychain_changeset = keychain_txout::ChangeSet {
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
};
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset,
indexer: keychain_changeset,
};
// test changesets to write to db
let mut changesets = Vec::new();
changesets.push(CombinedChangeSet {
chain: block_changeset,
indexed_tx_graph: graph_changeset,
network: network_changeset,
});
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
txs: [tx2.clone()].into(),
txouts: BTreeMap::default(),
anchors: BTreeSet::default(),
last_seen: [(tx2.compute_txid(), 1708919121)].into(),
};
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset2,
indexer: keychain_txout::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset2,
network: None,
});
// create changeset that adds a new anchor2 for tx0 and tx1
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
txs: BTreeSet::default(),
txouts: BTreeMap::default(),
anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
last_seen: BTreeMap::default(),
};
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>> =
indexed_tx_graph::ChangeSet {
graph: tx_graph_changeset3,
indexer: keychain_txout::ChangeSet::default(),
};
changesets.push(CombinedChangeSet {
chain: local_chain::ChangeSet::default(),
indexed_tx_graph: graph_changeset3,
network: None,
});
// aggregated test changesets
let agg_test_changesets =
changesets
.iter()
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
i.merge(cs.clone());
i
});
(changesets, agg_test_changesets)
}
}

View File

@ -19,22 +19,26 @@ bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false } 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 # Optional dependencies
bip39 = { version = "2.0", optional = true } bip39 = { version = "2.0", optional = true }
[features] [features]
default = ["std"] default = ["std", "file_store"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"] std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"] compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"] all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"] keys-bip39 = ["bip39"]
sqlite = ["bdk_chain/sqlite"]
file_store = ["bdk_file_store"]
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4" lazy_static = "1.4"
assert_matches = "1.5.0" assert_matches = "1.5.0"
tempfile = "3" 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" } bdk_file_store = { path = "../file_store" }
anyhow = "1" anyhow = "1"
rand = "^0.8" rand = "^0.8"

View File

@ -57,18 +57,17 @@ that the `Wallet` can use to update its view of the chain.
## Persistence ## 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** **Implementations**
* [`bdk_file_store`]: Stores wallet changes in a simple flat file. * [`bdk_file_store`]: Stores wallet changes in a simple flat file.
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
**Example** **Example**
<!-- compile_fail because outpoint and txout are fake variables --> <!-- compile_fail because outpoint and txout are fake variables -->
```rust,no_run ```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. // Open or create a new file store for wallet data.
let mut db = let mut db =
@ -76,21 +75,22 @@ let mut db =
.expect("create store"); .expect("create store");
// Create a wallet with initial wallet data read from the file 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 descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)"; let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets().expect("changeset loaded"); let load_params = LoadParams::with_descriptors(descriptor, change_descriptor, network)
let mut wallet = .expect("must parse descriptors");
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet) let create_params = CreateParams::new(descriptor, change_descriptor, network)
.expect("create or load wallet"); .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. // Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External); let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store. // Persist staged wallet data changes to the file store.
let staged_changeset = wallet.take_staged(); wallet.persist(&mut db).expect("persist");
if let Some(changeset) = staged_changeset {
db.append_changeset(&changeset)
.expect("must commit changes to database");
}
println!("Your new receive address is: {}", receive_address.address); println!("Your new receive address is: {}", receive_address.address);
``` ```
@ -233,7 +233,6 @@ conditions.
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html [`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`bdk_chain`]: https://docs.rs/bdk_chain/latest [`bdk_chain`]: https://docs.rs/bdk_chain/latest
[`bdk_file_store`]: https://docs.rs/bdk_file_store/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_electrum`]: https://docs.rs/bdk_electrum/latest
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest [`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest [`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest

View File

@ -21,7 +21,7 @@ use bitcoin::Network;
use miniscript::policy::Concrete; use miniscript::policy::Concrete;
use miniscript::Descriptor; 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 /// 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 /// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
@ -77,7 +77,8 @@ fn main() -> Result<(), Box<dyn Error>> {
); );
// Create a new wallet from descriptors // 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!( println!(
"First derived address from the descriptor: \n{}", "First derived address from the descriptor: \n{}",

View File

@ -281,15 +281,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
} }
} }
/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the /// Extra checks for [`ExtendedDescriptor`].
/// descriptor pub(crate) fn check_wallet_descriptor(
pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>( descriptor: &Descriptor<DescriptorPublicKey>,
inner: T, ) -> Result<(), DescriptorError> {
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards // Ensure the keys don't contain any hardened derivation steps or hardened wildcards
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| { let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(DescriptorXKey { if let DescriptorPublicKey::XPub(DescriptorXKey {
@ -316,7 +311,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
// issues // issues
descriptor.sanity_check()?; descriptor.sanity_check()?;
Ok((descriptor, keymap)) Ok(())
} }
#[doc(hidden)] #[doc(hidden)]
@ -855,22 +850,31 @@ mod test {
} }
#[test] #[test]
fn test_into_wallet_descriptor_checked() { fn test_check_wallet_descriptor() {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)"; 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)); assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)"; 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)); assert_matches!(result, Err(DescriptorError::MultiPath));
// repeated pubkeys // repeated pubkeys
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))"; 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()); assert!(result.is_err());
} }
@ -882,8 +886,10 @@ mod test {
let secp = Secp256k1::new(); 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 = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
let (descriptor, _) = let (descriptor, _) = descriptor
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap(); .into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
check_wallet_descriptor(&descriptor).expect("descriptor");
let descriptor = descriptor.at_derivation_index(0).unwrap(); let descriptor = descriptor.at_derivation_index(0).unwrap();

View File

@ -73,7 +73,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// ///
/// ``` /// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet; /// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind; /// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Pkh; /// use bdk_wallet::template::P2Pkh;
/// ///
@ -81,7 +81,8 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal = /// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; /// 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!( /// assert_eq!(
/// wallet /// wallet
@ -105,7 +106,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// ///
/// ``` /// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet; /// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind; /// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh_P2Sh; /// use bdk_wallet::template::P2Wpkh_P2Sh;
/// ///
@ -113,11 +114,12 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal = /// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new( /// let mut wallet = CreateParams::new(
/// P2Wpkh_P2Sh(key_external), /// P2Wpkh_P2Sh(key_external),
/// P2Wpkh_P2Sh(key_internal), /// P2Wpkh_P2Sh(key_internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!( /// assert_eq!(
/// wallet /// wallet
@ -142,7 +144,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// ///
/// ``` /// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet}; /// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind; /// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh; /// use bdk_wallet::template::P2Wpkh;
/// ///
@ -150,7 +152,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal = /// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; /// 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!( /// assert_eq!(
/// wallet /// wallet
@ -174,7 +178,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// ///
/// ``` /// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet; /// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind; /// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2TR; /// use bdk_wallet::template::P2TR;
/// ///
@ -182,7 +186,8 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal = /// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; /// 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!( /// assert_eq!(
/// wallet /// wallet
@ -211,15 +216,16 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip44; /// use bdk_wallet::template::Bip44;
/// ///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// 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.clone(), KeychainKind::External),
/// Bip44(key, KeychainKind::Internal), /// Bip44(key, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
@ -247,16 +253,17 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip44Public; /// use bdk_wallet::template::Bip44Public;
/// ///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; /// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// 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.clone(), fingerprint, KeychainKind::External),
/// Bip44Public(key, fingerprint, KeychainKind::Internal), /// Bip44Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
@ -284,15 +291,16 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip49; /// use bdk_wallet::template::Bip49;
/// ///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// 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.clone(), KeychainKind::External),
/// Bip49(key, KeychainKind::Internal), /// Bip49(key, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
@ -320,16 +328,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip49Public; /// use bdk_wallet::template::Bip49Public;
/// ///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; /// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// 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.clone(), fingerprint, KeychainKind::External),
/// Bip49Public(key, fingerprint, KeychainKind::Internal), /// Bip49Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
@ -357,15 +366,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip84; /// use bdk_wallet::template::Bip84;
/// ///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// 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.clone(), KeychainKind::External),
/// Bip84(key, KeychainKind::Internal), /// Bip84(key, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
@ -393,16 +403,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip84Public; /// use bdk_wallet::template::Bip84Public;
/// ///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// 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.clone(), fingerprint, KeychainKind::External),
/// Bip84Public(key, fingerprint, KeychainKind::Internal), /// Bip84Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?.create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
@ -430,15 +440,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip86; /// use bdk_wallet::template::Bip86;
/// ///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; /// 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.clone(), KeychainKind::External),
/// Bip86(key, KeychainKind::Internal), /// Bip86(key, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
@ -466,16 +477,17 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
/// ``` /// ```
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip86Public; /// use bdk_wallet::template::Bip86Public;
/// ///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; /// 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.clone(), fingerprint, KeychainKind::External),
/// Bip86Public(key, fingerprint, KeychainKind::Internal), /// Bip86Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet, /// Network::Testnet,
/// )?; /// )?
/// .create_wallet_no_persist()?;
/// ///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37"); /// 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"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");

View File

@ -36,12 +36,22 @@ pub use types::*;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::signer::SignOptions; pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder; 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; 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 { pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown") env!("CARGO_PKG_VERSION", "unknown")
} }
pub use bdk_chain as chain; pub use bdk_chain as chain;
pub(crate) use bdk_chain::collections; 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;

View File

@ -29,11 +29,12 @@
//! }"#; //! }"#;
//! //!
//! let import = FullyNodedExport::from_str(import)?; //! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new( //! let wallet = CreateParams::new(
//! &import.descriptor(), //! &import.descriptor(),
//! &import.change_descriptor().expect("change descriptor"), //! &import.change_descriptor().expect("change descriptor"),
//! Network::Testnet, //! Network::Testnet,
//! )?; //! )?
//! .create_wallet_no_persist()?;
//! # Ok::<_, Box<dyn std::error::Error>>(()) //! # Ok::<_, Box<dyn std::error::Error>>(())
//! ``` //! ```
//! //!
@ -42,11 +43,12 @@
//! # use bitcoin::*; //! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*; //! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*; //! # use bdk_wallet::*;
//! let wallet = Wallet::new( //! let wallet = CreateParams::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", //! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)", //! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
//! Network::Testnet, //! Network::Testnet,
//! )?; //! )?
//! .create_wallet_no_persist()?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); //! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
//! //!
//! println!("Exported: {}", export.to_string()); //! println!("Exported: {}", export.to_string());
@ -219,12 +221,15 @@ mod test {
use bitcoin::{transaction, BlockHash, Network, Transaction}; use bitcoin::{transaction, BlockHash, Network, Transaction};
use super::*; use super::*;
use crate::wallet::Wallet; use crate::wallet::{CreateParams, Wallet};
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
use crate::wallet::Update; use crate::wallet::Update;
use bdk_chain::TxGraph; 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 { let transaction = Transaction {
input: vec![], input: vec![],
output: vec![], output: vec![],

View File

@ -18,7 +18,7 @@
//! # use bdk_wallet::signer::SignerOrdering; //! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner; //! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::wallet::AddressIndex::New; //! # use bdk_wallet::wallet::AddressIndex::New;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet}; //! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
//! # use hwi::HWIClient; //! # use hwi::HWIClient;
//! # use std::sync::Arc; //! # use std::sync::Arc;
//! # //! #
@ -30,11 +30,7 @@
//! let first_device = devices.remove(0)?; //! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; //! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//! //!
//! # let mut wallet = Wallet::new( //! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
//! # "",
//! # None,
//! # Network::Testnet,
//! # )?;
//! # //! #
//! // Adding the hardware signer to the BDK wallet //! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer( //! wallet.add_signer(

View File

@ -12,7 +12,10 @@
//! Wallet //! Wallet
//! //!
//! This module defines the [`Wallet`]. //! This module defines the [`Wallet`].
use crate::collections::{BTreeMap, HashMap}; use crate::{
collections::{BTreeMap, HashMap},
descriptor::check_wallet_descriptor,
};
use alloc::{ use alloc::{
boxed::Box, boxed::Box,
string::{String, ToString}, string::{String, ToString},
@ -28,8 +31,8 @@ use bdk_chain::{
}, },
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::{CanonicalTx, TxGraph, TxNode}, tx_graph::{CanonicalTx, TxGraph, TxNode},
BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed, BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut,
IndexedTxGraph, Merge, Indexed, IndexedTxGraph, Merge,
}; };
use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{ use bitcoin::{
@ -38,24 +41,28 @@ use bitcoin::{
}; };
use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
use bitcoin::{constants::genesis_block, Amount}; use bitcoin::{constants::genesis_block, Amount};
use bitcoin::{ use bitcoin::{secp256k1::Secp256k1, Weight};
secp256k1::{All, Secp256k1},
Weight,
};
use core::fmt; use core::fmt;
use core::mem; use core::mem;
use core::ops::Deref; use core::ops::Deref;
use rand_core::RngCore; use rand_core::RngCore;
use descriptor::error::Error as DescriptorError; 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; use bdk_chain::tx_graph::CalculateFeeError;
pub mod coin_selection; pub mod coin_selection;
pub mod export; pub mod export;
mod params;
pub mod signer; pub mod signer;
pub mod tx_builder; pub mod tx_builder;
pub use params::*;
mod persisted;
pub use persisted::*;
pub(crate) mod utils; pub(crate) mod utils;
pub mod error; pub mod error;
@ -69,8 +76,8 @@ use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{ use crate::descriptor::{
self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, self, calc_checksum, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, IntoWalletDescriptor, Policy, XKeyUtils,
}; };
use crate::psbt::PsbtUtils; use crate::psbt::PsbtUtils;
use crate::signer::SignerError; use crate::signer::SignerError;
@ -149,7 +156,7 @@ impl From<SyncResult> for Update {
} }
/// The changes made to a wallet by applying an [`Update`]. /// The changes made to a wallet by applying an [`Update`].
pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationBlockTime>; pub type ChangeSet = bdk_chain::WalletChangeSet;
/// A derived address and the index it was found at. /// A derived address and the index it was found at.
/// For convenience this automatically derefs to `Address` /// For convenience this automatically derefs to `Address`
@ -177,34 +184,7 @@ impl fmt::Display for AddressInfo {
} }
} }
/// The error type when constructing a fresh [`Wallet`].
///
/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
///
/// [`new`]: Wallet::new
/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
#[derive(Debug)]
pub enum NewError {
/// There was problem with the passed-in descriptor(s).
Descriptor(crate::descriptor::DescriptorError),
}
impl fmt::Display for NewError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NewError::Descriptor(e) => e.fmt(f),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for NewError {}
/// The error type when loading a [`Wallet`] from a [`ChangeSet`]. /// 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)] #[derive(Debug)]
pub enum LoadError { pub enum LoadError {
/// There was a problem with the passed-in descriptor(s). /// There was a problem with the passed-in descriptor(s).
@ -215,6 +195,8 @@ pub enum LoadError {
MissingGenesis, MissingGenesis,
/// Data loaded from persistence is missing descriptor. /// Data loaded from persistence is missing descriptor.
MissingDescriptor(KeychainKind), MissingDescriptor(KeychainKind),
/// Data loaded is unexpected.
Mismatch(LoadMismatch),
} }
impl fmt::Display for LoadError { impl fmt::Display for LoadError {
@ -226,6 +208,7 @@ impl fmt::Display for LoadError {
LoadError::MissingDescriptor(k) => { LoadError::MissingDescriptor(k) => {
write!(f, "loaded data is missing descriptor for keychain {k:?}") write!(f, "loaded data is missing descriptor for keychain {k:?}")
} }
LoadError::Mismatch(mismatch) => write!(f, "data mismatch: {mismatch:?}"),
} }
} }
} }
@ -233,63 +216,34 @@ impl fmt::Display for LoadError {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for LoadError {} impl std::error::Error for LoadError {}
/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent. /// Represents a mismatch with what is loaded and what is expected from [`LoadParams`].
///
/// 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
#[derive(Debug)] #[derive(Debug)]
pub enum NewOrLoadError { pub enum LoadMismatch {
/// There is a problem with the passed-in descriptor. /// Network does not match.
Descriptor(crate::descriptor::DescriptorError), Network {
/// The loaded genesis hash does not match what was provided. /// The network that is loaded.
LoadedGenesisDoesNotMatch { loaded: Network,
/// The expected genesis block hash. /// The expected network.
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.
expected: Network, expected: Network,
/// The network type loaded from persistence.
got: Option<Network>,
}, },
/// The loaded desccriptor does not match what was provided. /// Genesis hash does not match.
LoadedDescriptorDoesNotMatch { Genesis {
/// The descriptor loaded from persistence. /// The genesis hash that is loaded.
got: Option<ExtendedDescriptor>, loaded: BlockHash,
/// The keychain of the descriptor not matching /// The expected genesis hash.
expected: BlockHash,
},
/// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match.
Descriptor {
/// Keychain identifying the descriptor.
keychain: KeychainKind, 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`]. /// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)] #[derive(Debug)]
pub enum ApplyBlockError { pub enum ApplyBlockError {
@ -324,39 +278,81 @@ impl fmt::Display for ApplyBlockError {
impl std::error::Error for ApplyBlockError {} impl std::error::Error for ApplyBlockError {}
impl Wallet { impl Wallet {
/// Initialize an empty [`Wallet`]. /// Build a new [`Wallet`].
pub fn new<E: IntoWalletDescriptor>( ///
/// 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, descriptor: E,
change_descriptor: E, change_descriptor: E,
network: Network, network: Network,
) -> Result<Self, NewError> { ) -> Result<CreateParams, DescriptorError> {
let genesis_hash = genesis_block(network).block_hash(); CreateParams::new(descriptor, change_descriptor, network)
Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
} }
/// 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 /// If you have previously created a wallet, use [`load`](Self::load) instead.
/// for syncing from alternative networks. pub fn create_with_params(params: CreateParams) -> Result<Self, DescriptorError> {
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>( let secp = params.secp;
descriptor: E, let network = params.network;
change_descriptor: E, let genesis_hash = params
network: Network, .genesis_hash
genesis_hash: BlockHash, .unwrap_or(genesis_block(network).block_hash());
) -> Result<Self, NewError> {
let secp = Secp256k1::new();
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
let (signers, change_signers) = check_wallet_descriptor(&params.descriptor)?;
create_signers(&mut index, &secp, descriptor, change_descriptor, network) check_wallet_descriptor(&params.change_descriptor)?;
.map_err(NewError::Descriptor)?; let signers = Arc::new(SignersContainer::build(
params.descriptor_keymap,
&params.descriptor,
&secp,
));
let change_signers = Arc::new(SignersContainer::build(
params.change_descriptor_keymap,
&params.change_descriptor,
&secp,
));
let index = create_indexer(
params.descriptor,
params.change_descriptor,
params.lookahead,
)?;
let descriptor = index.get_descriptor(&KeychainKind::External).cloned();
let change_descriptor = index.get_descriptor(&KeychainKind::Internal).cloned();
let indexed_graph = IndexedTxGraph::new(index); let indexed_graph = IndexedTxGraph::new(index);
let indexed_graph_changeset = indexed_graph.initial_changeset();
let staged = ChangeSet { let stage = ChangeSet {
chain: chain_changeset, descriptor,
indexed_tx_graph: indexed_graph.initial_changeset(), change_descriptor,
local_chain: chain_changeset,
tx_graph: indexed_graph_changeset.tx_graph,
indexer: indexed_graph_changeset.indexer,
network: Some(network), network: Some(network),
}; };
@ -366,11 +362,79 @@ impl Wallet {
network, network,
chain, chain,
indexed_graph, indexed_graph,
stage: staged, stage,
secp, 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`]. /// Load [`Wallet`] from the given previously persisted [`ChangeSet`].
/// ///
/// Note that the descriptor secret keys are not persisted to the db; this means that after /// Note that the descriptor secret keys are not persisted to the db; this means that after
@ -382,68 +446,102 @@ impl Wallet {
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # use bdk_wallet::Wallet; /// # use bdk_wallet::Wallet;
/// # use bdk_wallet::signer::{SignersContainer, SignerOrdering}; /// # use bitcoin::Network;
/// # use bdk_wallet::descriptor::Descriptor; /// # use bdk_wallet::{LoadParams, KeychainKind, PersistedWallet};
/// # use bitcoin::key::Secp256k1; /// use bdk_chain::sqlite::Connection;
/// # use bdk_wallet::KeychainKind;
/// use bdk_sqlite::{Store, rusqlite::Connection};
/// # /// #
/// # fn main() -> Result<(), anyhow::Error> { /// # fn main() -> anyhow::Result<()> {
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); /// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db"); /// # let file_path = temp_dir.path().join("store.db");
/// let conn = Connection::open(file_path).expect("must open connection"); /// const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// let mut db = Store::new(conn).expect("must create db"); /// const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
/// let secp = Secp256k1::new();
/// ///
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap(); /// let mut conn = Connection::open(file_path)?;
/// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap(); /// 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");
/// ///
/// 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()));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
/// pub fn load_with_params(
/// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the changeset: ChangeSet,
/// passed-in descriptors to the [`Wallet`]. params: LoadParams,
pub fn load_from_changeset(changeset: ChangeSet) -> Result<Self, LoadError> { ) -> Result<Option<Self>, LoadError> {
if changeset.is_empty() {
return Ok(None);
}
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let network = changeset.network.ok_or(LoadError::MissingNetwork)?; let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
let chain = let chain = LocalChain::from_changeset(changeset.local_chain)
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?; .map_err(|_| LoadError::MissingGenesis)?;
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
let descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::External)
.ok_or(LoadError::MissingDescriptor(KeychainKind::External))?
.clone();
let change_descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::Internal)
.ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?
.clone();
let (signers, change_signers) = let descriptor = changeset
create_signers(&mut index, &secp, descriptor, change_descriptor, network) .descriptor
.expect("Can't fail: we passed in valid descriptors, recovered from the changeset"); .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?;
let change_descriptor = changeset
.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 = 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); 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(); let stage = ChangeSet::default();
Ok(Wallet { Ok(Some(Wallet {
signers, signers,
change_signers, change_signers,
chain, chain,
@ -451,146 +549,7 @@ impl Wallet {
stage, stage,
network, network,
secp, 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. /// Get the Bitcoin network the wallet is using.
@ -642,17 +601,15 @@ impl Wallet {
/// calls to this method before closing the wallet. For example: /// calls to this method before closing the wallet. For example:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # use bdk_wallet::wallet::{Wallet, ChangeSet}; /// # use bdk_wallet::{LoadParams, ChangeSet, KeychainKind};
/// # use bdk_wallet::KeychainKind; /// use bdk_chain::sqlite::Connection;
/// use bdk_sqlite::{rusqlite::Connection, Store}; /// let mut conn = Connection::open_in_memory().expect("must open connection");
/// let conn = Connection::open_in_memory().expect("must open connection"); /// let mut wallet = LoadParams::new()
/// let mut db = Store::new(conn).expect("must create store"); /// .load_wallet(&mut conn)
/// # let changeset = ChangeSet::default(); /// .expect("database is okay")
/// # let mut wallet = Wallet::load_from_changeset(changeset).expect("load wallet"); /// .expect("database has data");
/// let next_address = wallet.reveal_next_address(KeychainKind::External); /// let next_address = wallet.reveal_next_address(KeychainKind::External);
/// if let Some(changeset) = wallet.take_staged() { /// wallet.persist(&mut conn).expect("write is okay");
/// db.write(&changeset)?;
/// }
/// ///
/// // Now it's safe to show the user their next address! /// // Now it's safe to show the user their next address!
/// println!("Next address: {}", next_address.address); /// println!("Next address: {}", next_address.address);
@ -666,7 +623,7 @@ impl Wallet {
.reveal_next_spk(&keychain) .reveal_next_spk(&keychain)
.expect("keychain must exist"); .expect("keychain must exist");
stage.merge(indexed_tx_graph::ChangeSet::from(index_changeset).into()); stage.merge(index_changeset.into());
AddressInfo { AddressInfo {
index, index,
@ -1110,16 +1067,38 @@ impl Wallet {
signers.add_external(signer.id(&self.secp), ordering, signer); 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 /// Get the signers
/// ///
/// ## Example /// ## Example
/// ///
/// ``` /// ```
/// # use bdk_wallet::{Wallet, KeychainKind}; /// # use bdk_wallet::{CreateParams, KeychainKind};
/// # use bdk_wallet::bitcoin::Network; /// # use bdk_wallet::bitcoin::Network;
/// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)"; /// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)";
/// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)"; /// 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()) { /// 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/* /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
/// println!("secret_key: {}", secret_key); /// println!("secret_key: {}", secret_key);
@ -2424,25 +2403,23 @@ fn new_local_utxo(
} }
} }
fn create_signers<E: IntoWalletDescriptor>( fn create_indexer(
index: &mut KeychainTxOutIndex<KeychainKind>, descriptor: ExtendedDescriptor,
secp: &Secp256k1<All>, change_descriptor: ExtendedDescriptor,
descriptor: E, lookahead: u32,
change_descriptor: E, ) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
network: Network, let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead);
) -> 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
.insert_descriptor(KeychainKind::External, descriptor)
.expect("this is the first descriptor we're inserting");
let (descriptor, keymap) = change_descriptor; // let (descriptor, keymap) = descriptor;
let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); // let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
let _ = index assert!(indexer
.insert_descriptor(KeychainKind::Internal, descriptor) .insert_descriptor(KeychainKind::External, descriptor)
.expect("first descriptor introduced must succeed"));
// 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| { .map_err(|e| {
use bdk_chain::indexer::keychain_txout::InsertDescriptorError; use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
match e { match e {
@ -2453,9 +2430,9 @@ fn create_signers<E: IntoWalletDescriptor>(
unreachable!("this is the first time we're assigning internal") 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. /// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
@ -2476,16 +2453,18 @@ macro_rules! doctest_wallet {
() => {{ () => {{
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph};
use $crate::wallet::{Update, Wallet}; use $crate::wallet::{Update, CreateParams};
use $crate::KeychainKind; use $crate::KeychainKind;
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
let mut wallet = Wallet::new( let mut wallet = CreateParams::new(
descriptor, descriptor,
change_descriptor, change_descriptor,
Network::Regtest, Network::Regtest,
) )
.unwrap()
.create_wallet_no_persist()
.unwrap(); .unwrap();
let address = wallet.peek_address(KeychainKind::External, 0).address; let address = wallet.peek_address(KeychainKind::External, 0).address;
let tx = Transaction { let tx = Transaction {

View File

@ -0,0 +1,217 @@
use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
use bitcoin::{BlockHash, Network};
use miniscript::descriptor::KeyMap;
use crate::{
descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
KeychainKind, Wallet,
};
use super::{utils::SecpCtx, ChangeSet, LoadError, PersistedWallet};
/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
#[derive(Debug, Clone)]
#[must_use]
pub struct CreateParams {
pub(crate) descriptor: ExtendedDescriptor,
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor: ExtendedDescriptor,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) network: Network,
pub(crate) genesis_hash: Option<BlockHash>,
pub(crate) lookahead: u32,
pub(crate) secp: SecpCtx,
}
impl CreateParams {
/// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
///
/// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<Self, DescriptorError> {
let secp = SecpCtx::default();
let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(&secp, network)?;
let (change_descriptor, change_descriptor_keymap) =
change_descriptor.into_wallet_descriptor(&secp, network)?;
Ok(Self {
descriptor,
descriptor_keymap,
change_descriptor,
change_descriptor_keymap,
network,
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
secp,
})
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Use a custom `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Create [`PersistedWallet`] with the given `Db`.
pub fn create_wallet<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
where
Wallet: PersistWith<Db, CreateParams = Self>,
{
PersistedWallet::create(db, self)
}
/// Create [`PersistedWallet`] with the given async `Db`.
pub async fn create_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
where
Wallet: PersistAsyncWith<Db, CreateParams = Self>,
{
PersistedWallet::create_async(db, self).await
}
/// Create [`Wallet`] without persistence.
pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
Wallet::create_with_params(self)
}
}
/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
#[must_use]
#[derive(Debug, Clone)]
pub struct LoadParams {
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) lookahead: u32,
pub(crate) check_network: Option<Network>,
pub(crate) check_genesis_hash: Option<BlockHash>,
pub(crate) check_descriptor: Option<ExtendedDescriptor>,
pub(crate) check_change_descriptor: Option<ExtendedDescriptor>,
pub(crate) secp: SecpCtx,
}
impl LoadParams {
/// Construct parameters with default values.
///
/// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new() -> Self {
Self {
descriptor_keymap: KeyMap::default(),
change_descriptor_keymap: KeyMap::default(),
lookahead: DEFAULT_LOOKAHEAD,
check_network: None,
check_genesis_hash: None,
check_descriptor: None,
check_change_descriptor: None,
secp: SecpCtx::new(),
}
}
/// Construct parameters with `network` check.
pub fn with_network(network: Network) -> Self {
Self {
check_network: Some(network),
..Default::default()
}
}
/// Construct parameters with descriptor checks.
pub fn with_descriptors<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<Self, DescriptorError> {
let mut params = Self::with_network(network);
let secp = &params.secp;
let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(secp, network)?;
params.check_descriptor = Some(descriptor);
params.descriptor_keymap = descriptor_keymap;
let (change_descriptor, change_descriptor_keymap) =
change_descriptor.into_wallet_descriptor(secp, network)?;
params.check_change_descriptor = Some(change_descriptor);
params.change_descriptor_keymap = change_descriptor_keymap;
Ok(params)
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Check for a `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.check_genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Load [`PersistedWallet`] with the given `Db`.
pub fn load_wallet<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
where
Wallet: PersistWith<Db, LoadParams = Self>,
{
PersistedWallet::load(db, self)
}
/// Load [`PersistedWallet`] with the given async `Db`.
pub async fn load_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
where
Wallet: PersistAsyncWith<Db, LoadParams = Self>,
{
PersistedWallet::load_async(db, self).await
}
/// Load [`Wallet`] without persistence.
pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
Wallet::load_with_params(changeset, self)
}
}
impl Default for LoadParams {
fn default() -> Self {
Self::new()
}
}

View File

@ -0,0 +1,180 @@
use core::fmt;
use crate::wallet::{ChangeSet, CreateParams, LoadError, LoadParams};
use crate::{descriptor::DescriptorError, Wallet};
use bdk_chain::{Merge, PersistWith};
/// Represents a persisted wallet.
pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
#[cfg(feature = "sqlite")]
impl<'c> PersistWith<bdk_chain::sqlite::Transaction<'c>> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::sqlite::Transaction<'c>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
changeset
.persist_to_sqlite(db)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
conn: &mut bdk_chain::sqlite::Transaction<'c>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset = ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
if changeset.is_empty() {
return Ok(None);
}
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
&mut self,
conn: &mut bdk_chain::sqlite::Transaction,
) -> Result<bool, Self::PersistError> {
if let Some(changeset) = self.take_staged() {
changeset.persist_to_sqlite(conn)?;
return Ok(true);
}
Ok(false)
}
}
#[cfg(feature = "sqlite")]
impl PersistWith<bdk_chain::sqlite::Connection> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::sqlite::Connection,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
let wallet = PersistWith::create(&mut db_tx, params)?;
db_tx.commit().map_err(CreateWithPersistError::Persist)?;
Ok(wallet)
}
fn load(
db: &mut bdk_chain::sqlite::Connection,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
let wallet_opt = PersistWith::load(&mut db_tx, params)?;
db_tx.commit().map_err(LoadWithPersistError::Persist)?;
Ok(wallet_opt)
}
fn persist(
&mut self,
db: &mut bdk_chain::sqlite::Connection,
) -> Result<bool, Self::PersistError> {
let mut db_tx = db.transaction()?;
let has_changes = PersistWith::persist(self, &mut db_tx)?;
db_tx.commit()?;
Ok(has_changes)
}
}
#[cfg(feature = "file_store")]
impl PersistWith<bdk_file_store::Store<ChangeSet>> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<std::io::Error>;
type LoadError = LoadWithPersistError<bdk_file_store::AggregateChangesetsError<ChangeSet>>;
type PersistError = std::io::Error;
fn create(
db: &mut bdk_file_store::Store<ChangeSet>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
db: &mut bdk_file_store::Store<ChangeSet>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset = db
.aggregate_changesets()
.map_err(LoadWithPersistError::Persist)?
.unwrap_or_default();
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
&mut self,
db: &mut bdk_file_store::Store<ChangeSet>,
) -> Result<bool, Self::PersistError> {
if let Some(changeset) = self.take_staged() {
db.append_changeset(&changeset)?;
return Ok(true);
}
Ok(false)
}
}
/// Error type for [`PersistedWallet::load`].
#[derive(Debug)]
pub enum LoadWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot construct [`Wallet`].
InvalidChangeSet(LoadError),
}
impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
/// Error type for [`PersistedWallet::create`].
#[derive(Debug)]
pub enum CreateWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot contruct [`Wallet`].
Descriptor(DescriptorError),
}
impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::Descriptor(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}

View File

@ -69,7 +69,8 @@
//! //!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)"; //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)"; //! 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( //! wallet.add_signer(
//! KeychainKind::External, //! KeychainKind::External,
//! SignerOrdering(200), //! SignerOrdering(200),

View File

@ -1,7 +1,7 @@
#![allow(unused)] #![allow(unused)]
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{ use bdk_wallet::{
wallet::{Update, Wallet}, wallet::{CreateParams, Update, Wallet},
KeychainKind, LocalOutput, KeychainKind, LocalOutput,
}; };
use bitcoin::{ use bitcoin::{
@ -16,7 +16,11 @@ use std::str::FromStr;
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000 /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
/// sats are the transaction fee. /// sats are the transaction fee.
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) { 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 receive_address = wallet.peek_address(KeychainKind::External, 0).address;
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
.expect("address") .expect("address")

View File

@ -3,18 +3,17 @@ extern crate alloc;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use anyhow::Context;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use bdk_chain::collections::BTreeMap;
use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime}; 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::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::psbt::PsbtUtils;
use bdk_wallet::signer::{SignOptions, SignerError}; use bdk_wallet::signer::{SignOptions, SignerError};
use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::wallet::error::CreateTxError; use bdk_wallet::wallet::error::CreateTxError;
use bdk_wallet::wallet::tx_builder::AddForeignUtxoError; 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 bdk_wallet::KeychainKind;
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;
use bitcoin::key::Secp256k1; use bitcoin::key::Secp256k1;
@ -102,46 +101,44 @@ const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test] #[test]
fn load_recovers_wallet() -> anyhow::Result<()> { fn wallet_is_persisted() -> anyhow::Result<()> {
fn run<Db, New, Recover, Read, Write>( fn run<Db, CreateDb, OpenDb>(
filename: &str, filename: &str,
create_new: New, create_db: CreateDb,
recover: Recover, open_db: OpenDb,
read: Read,
write: Write,
) -> anyhow::Result<()> ) -> anyhow::Result<()>
where where
New: Fn(&Path) -> anyhow::Result<Db>, CreateDb: Fn(&Path) -> anyhow::Result<Db>,
Recover: Fn(&Path) -> anyhow::Result<Db>, OpenDb: Fn(&Path) -> anyhow::Result<Db>,
Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>, Wallet: PersistWith<Db, CreateParams = CreateParams, LoadParams = LoadParams>,
Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>, <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 temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename); 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 // create new wallet
let wallet_spk_index = { let wallet_spk_index = {
let mut wallet = let mut db = create_db(&file_path)?;
Wallet::new(desc, change_desc, Network::Testnet).expect("must init wallet"); let mut wallet = CreateParams::new(external_desc, internal_desc, Network::Testnet)?
.create_wallet(&mut db)?;
wallet.reveal_next_address(KeychainKind::External); wallet.reveal_next_address(KeychainKind::External);
// persist new wallet changes // persist new wallet changes
let mut db = create_new(&file_path).expect("must create db"); assert!(wallet.persist(&mut db)?, "must write");
if let Some(changeset) = wallet.take_staged() {
write(&mut db, &changeset)?;
}
wallet.spk_index().clone() wallet.spk_index().clone()
}; };
// recover wallet // recover wallet
{ {
// load persisted wallet changes let mut db = open_db(&file_path).context("failed to recover db")?;
let db = &mut recover(&file_path).expect("must recover db"); let wallet =
let changeset = read(db).expect("must recover wallet").expect("changeset"); 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.network(), Network::Testnet);
assert_eq!( assert_eq!(
wallet.spk_index().keychains().collect::<Vec<_>>(), wallet.spk_index().keychains().collect::<Vec<_>>(),
@ -154,7 +151,8 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
assert_eq!( assert_eq!(
*wallet.public_descriptor(KeychainKind::External), *wallet.public_descriptor(KeychainKind::External),
desc.into_wallet_descriptor(&secp, wallet.network()) external_desc
.into_wallet_descriptor(&secp, wallet.network())
.unwrap() .unwrap()
.0 .0
); );
@ -167,166 +165,11 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
"store.db", "store.db",
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?), |path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?), |path| Ok(bdk_file_store::Store::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( run::<bdk_chain::sqlite::Connection, _, _>(
"store.sqlite", "store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?), |path| Ok(bdk_chain::sqlite::Connection::open(path)?),
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?), |path| Ok(bdk_chain::sqlite::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(
"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)?),
)?; )?;
Ok(()) Ok(())
@ -336,14 +179,11 @@ fn new_or_load() -> anyhow::Result<()> {
fn test_error_external_and_internal_are_the_same() { fn test_error_external_and_internal_are_the_same() {
// identical descriptors should fail to create wallet // identical descriptors should fail to create wallet
let desc = get_test_wpkh(); 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!( assert!(
matches!( matches!(&err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
&err,
Err(NewError::Descriptor(
DescriptorError::ExternalAndInternalAreTheSame
))
),
"expected same descriptors error, got {:?}", "expected same descriptors error, got {:?}",
err, err,
); );
@ -351,14 +191,11 @@ fn test_error_external_and_internal_are_the_same() {
// public + private of same descriptor should fail to create wallet // public + private of same descriptor should fail to create wallet
let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/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!( assert!(
matches!( matches!(err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
err,
Err(NewError::Descriptor(
DescriptorError::ExternalAndInternalAreTheSame
))
),
"expected same descriptors error, got {:?}", "expected same descriptors error, got {:?}",
err, err,
); );
@ -1316,8 +1153,11 @@ fn test_create_tx_policy_path_required() {
#[test] #[test]
fn test_create_tx_policy_path_no_csv() { fn test_create_tx_policy_path_no_csv() {
let (desc, change_desc) = get_test_wpkh_with_change_desc(); let (descriptor, change_descriptor) = get_test_wpkh_with_change_desc();
let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).expect("wallet"); let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Regtest)
.expect("must parse")
.create_wallet_no_persist()
.expect("wallet");
let tx = Transaction { let tx = Transaction {
version: transaction::Version::non_standard(0), version: transaction::Version::non_standard(0),
@ -2927,9 +2767,12 @@ fn test_sign_nonstandard_sighash() {
#[test] #[test]
fn test_unused_address() { fn test_unused_address() {
let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_desc = get_test_wpkh(); let change_descriptor = get_test_wpkh();
let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).expect("wallet"); 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 // `list_unused_addresses` should be empty if we haven't revealed any
assert!(wallet assert!(wallet
@ -2956,8 +2799,11 @@ fn test_unused_address() {
#[test] #[test]
fn test_next_unused_address() { fn test_next_unused_address() {
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change = get_test_wpkh(); let change_descriptor = get_test_wpkh();
let mut wallet = Wallet::new(descriptor, change, Network::Testnet).expect("wallet"); 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!(wallet.derivation_index(KeychainKind::External), None);
assert_eq!( assert_eq!(
@ -3002,9 +2848,12 @@ fn test_next_unused_address() {
#[test] #[test]
fn test_peek_address_at_index() { fn test_peek_address_at_index() {
let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_desc = get_test_wpkh(); let change_descriptor = get_test_wpkh();
let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).unwrap(); let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("wallet");
assert_eq!( assert_eq!(
wallet.peek_address(KeychainKind::External, 1).to_string(), wallet.peek_address(KeychainKind::External, 1).to_string(),
@ -3039,8 +2888,11 @@ fn test_peek_address_at_index() {
#[test] #[test]
fn test_peek_address_at_index_not_derivable() { fn test_peek_address_at_index_not_derivable() {
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", let wallet = CreateParams::new(
get_test_wpkh(), Network::Testnet).unwrap(); "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
get_test_wpkh(),
Network::Testnet,
).unwrap().create_wallet_no_persist().unwrap();
assert_eq!( assert_eq!(
wallet.peek_address(KeychainKind::External, 1).to_string(), wallet.peek_address(KeychainKind::External, 1).to_string(),
@ -3060,8 +2912,11 @@ fn test_peek_address_at_index_not_derivable() {
#[test] #[test]
fn test_returns_index_and_address() { fn test_returns_index_and_address() {
let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", let mut wallet = CreateParams::new(
get_test_wpkh(), Network::Testnet).unwrap(); "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
get_test_wpkh(),
Network::Testnet,
).unwrap().create_wallet_no_persist().unwrap();
// new index 0 // new index 0
assert_eq!( assert_eq!(
@ -3127,11 +2982,13 @@ fn test_sending_to_bip350_bech32m_address() {
fn test_get_address() { fn test_get_address() {
use bdk_wallet::descriptor::template::Bip84; use bdk_wallet::descriptor::template::Bip84;
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let wallet = Wallet::new( let wallet = CreateParams::new(
Bip84(key, KeychainKind::External), Bip84(key, KeychainKind::External),
Bip84(key, KeychainKind::Internal), Bip84(key, KeychainKind::Internal),
Network::Regtest, Network::Regtest,
) )
.unwrap()
.create_wallet_no_persist()
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -3160,7 +3017,10 @@ fn test_get_address() {
#[test] #[test]
fn test_reveal_addresses() { fn test_reveal_addresses() {
let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); 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 keychain = KeychainKind::External;
let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap(); let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap();
@ -3181,11 +3041,13 @@ fn test_get_address_no_reuse() {
use std::collections::HashSet; use std::collections::HashSet;
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); 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::External),
Bip84(key, KeychainKind::Internal), Bip84(key, KeychainKind::Internal),
Network::Regtest, Network::Regtest,
) )
.unwrap()
.create_wallet_no_persist()
.unwrap(); .unwrap();
let mut used_set = HashSet::new(); let mut used_set = HashSet::new();
@ -3655,11 +3517,13 @@ fn test_taproot_sign_derive_index_from_psbt() {
let mut psbt = builder.finish().unwrap(); let mut psbt = builder.finish().unwrap();
// re-create the wallet with an empty db // 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_xprv(),
get_test_tr_single_sig(), get_test_tr_single_sig(),
Network::Regtest, Network::Regtest,
) )
.unwrap()
.create_wallet_no_persist()
.unwrap(); .unwrap();
// signing with an empty db means that we will only look at the psbt to infer the // signing with an empty db means that we will only look at the psbt to infer the
@ -3760,7 +3624,10 @@ fn test_taproot_sign_non_default_sighash() {
#[test] #[test]
fn test_spend_coinbase() { fn test_spend_coinbase() {
let (desc, change_desc) = get_test_wpkh_with_change_desc(); 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; let confirmation_height = 5;
wallet wallet
@ -4014,6 +3881,7 @@ fn test_taproot_load_descriptor_duplicated_keys() {
/// [#1483]: https://github.com/bitcoindevkit/bdk/issues/1483 /// [#1483]: https://github.com/bitcoindevkit/bdk/issues/1483
/// [#1486]: https://github.com/bitcoindevkit/bdk/pull/1486 /// [#1486]: https://github.com/bitcoindevkit/bdk/pull/1486
#[test] #[test]
#[cfg(debug_assertions)]
#[should_panic( #[should_panic(
expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0" expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0"
)] )]

View File

@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
type ChangeSet = ( type ChangeSet = (
local_chain::ChangeSet, local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>, indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
); );
#[derive(Debug)] #[derive(Debug)]

View File

@ -30,7 +30,7 @@ use clap::{Parser, Subcommand};
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>; pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
pub type KeychainChangeSet<A> = ( pub type KeychainChangeSet<A> = (
local_chain::ChangeSet, local_chain::ChangeSet,
indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet<Keychain>>, indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
); );
#[derive(Parser)] #[derive(Parser)]
@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain {
} }
pub struct CreateTxChange { pub struct CreateTxChange {
pub index_changeset: keychain_txout::ChangeSet<Keychain>, pub index_changeset: keychain_txout::ChangeSet,
pub change_keychain: Keychain, pub change_keychain: Keychain,
pub index: u32, pub index: u32,
} }

View File

@ -100,7 +100,7 @@ pub struct ScanOptions {
type ChangeSet = ( type ChangeSet = (
local_chain::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<()> { fn main() -> anyhow::Result<()> {

View File

@ -22,11 +22,11 @@ use example_cli::{
}; };
const DB_MAGIC: &[u8] = b"bdk_example_esplora"; 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 = ( type ChangeSet = (
local_chain::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)] #[derive(Subcommand, Debug, Clone)]
@ -84,7 +84,7 @@ impl EsploraArgs {
Network::Bitcoin => "https://blockstream.info/api", Network::Bitcoin => "https://blockstream.info/api",
Network::Testnet => "https://blockstream.info/testnet/api", Network::Testnet => "https://blockstream.info/testnet/api",
Network::Regtest => "http://localhost:3002", Network::Regtest => "http://localhost:3002",
Network::Signet => "https://mempool.space/signet/api", Network::Signet => "http://signet.bitcoindevkit.net",
_ => panic!("unsupported network"), _ => panic!("unsupported network"),
}); });
@ -96,7 +96,7 @@ impl EsploraArgs {
#[derive(Parser, Debug, Clone, PartialEq)] #[derive(Parser, Debug, Clone, PartialEq)]
pub struct ScanOptions { pub struct ScanOptions {
/// Max number of concurrent esplora server requests. /// Max number of concurrent esplora server requests.
#[clap(long, default_value = "1")] #[clap(long, default_value = "5")]
pub parallel_requests: usize, pub parallel_requests: usize,
} }

View File

@ -4,7 +4,7 @@ version = "0.2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bdk_wallet = { path = "../../crates/wallet" } bdk_wallet = { path = "../../crates/wallet", feature = ["file_store"] }
bdk_electrum = { path = "../../crates/electrum" } bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1" anyhow = "1"

View File

@ -1,53 +1,52 @@
const DB_MAGIC: &str = "bdk_wallet_electrum_example"; use bdk_wallet::wallet::CreateParams;
const SEND_AMOUNT: Amount = Amount::from_sat(5000); use bdk_wallet::wallet::LoadParams;
const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5;
use anyhow::anyhow;
use std::io::Write; use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use bdk_electrum::electrum_client; use bdk_electrum::electrum_client;
use bdk_electrum::BdkElectrumClient; use bdk_electrum::BdkElectrumClient;
use bdk_file_store::Store; use bdk_file_store::Store;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::bitcoin::{Address, Amount}; use bdk_wallet::bitcoin::{Address, Amount};
use bdk_wallet::chain::collections::HashSet; use bdk_wallet::chain::collections::HashSet;
use bdk_wallet::{bitcoin::Network, Wallet};
use bdk_wallet::{KeychainKind, SignOptions}; 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> { 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 = let mut db =
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; 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 load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
let changeset = db let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
.aggregate_changesets() let mut wallet = match load_params.load_wallet(&mut db)? {
.map_err(|e| anyhow!("load changes error: {}", e))?; Some(wallet) => wallet,
let mut wallet = Wallet::new_or_load( None => create_params.create_wallet(&mut db)?,
external_descriptor, };
internal_descriptor,
changeset,
Network::Testnet,
)?;
let address = wallet.next_unused_address(KeychainKind::External); let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut db)?;
db.append_changeset(&changeset)?;
}
println!("Generated Address: {}", address); println!("Generated Address: {}", address);
let balance = wallet.balance(); let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total()); println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); print!("Syncing...");
let client = BdkElectrumClient::new(electrum_client::Client::new( let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
"ssl://electrum.blockstream.info:60002",
)?);
// Populate the electrum client's transaction cache so it doesn't redownload transaction we // Populate the electrum client's transaction cache so it doesn't redownload transaction we
// already have. // already have.
client.populate_tx_cache(&wallet); client.populate_tx_cache(wallet.tx_graph());
let request = wallet let request = wallet
.start_full_scan() .start_full_scan()
@ -71,9 +70,7 @@ fn main() -> Result<(), anyhow::Error> {
println!(); println!();
wallet.apply_update(update)?; wallet.apply_update(update)?;
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut db)?;
db.append_changeset(&changeset)?;
}
let balance = wallet.balance(); let balance = wallet.balance();
println!("Wallet balance after syncing: {} sats", balance.total()); println!("Wallet balance after syncing: {} sats", balance.total());

View File

@ -6,8 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
bdk_wallet = { path = "../../crates/wallet" } bdk_wallet = { path = "../../crates/wallet", features = ["sqlite"] }
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] } bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
bdk_sqlite = { path = "../../crates/sqlite" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1" anyhow = "1"

View File

@ -1,76 +1,55 @@
use std::{collections::BTreeSet, io::Write, str::FromStr}; use std::{collections::BTreeSet, io::Write};
use anyhow::Ok;
use bdk_esplora::{esplora_client, EsploraAsyncExt}; use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{ use bdk_wallet::{
bitcoin::{Address, Amount, Network, Script}, bitcoin::{Amount, Network},
KeychainKind, SignOptions, Wallet, rusqlite::Connection,
wallet::{CreateParams, LoadParams},
KeychainKind, SignOptions,
}; };
use bdk_sqlite::{rusqlite::Connection, Store};
const SEND_AMOUNT: Amount = Amount::from_sat(5000); const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50; const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: 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] #[tokio::main]
async fn main() -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> {
let db_path = "bdk-esplora-async-example.sqlite"; let mut conn = Connection::open(DB_PATH)?;
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( let load_params = LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
external_descriptor, let create_params = CreateParams::new(EXTERNAL_DESC, INTERNAL_DESC, NETWORK)?;
internal_descriptor, let mut wallet = match load_params.load_wallet(&mut conn)? {
changeset, Some(wallet) => wallet,
Network::Signet, None => create_params.create_wallet(&mut conn)?,
)?; };
let address = wallet.next_unused_address(KeychainKind::External); let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut conn)?;
db.write(&changeset)?; println!("Next unused address: ({}) {}", address.index, address);
}
println!("Generated Address: {}", address);
let balance = wallet.balance(); let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total()); println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); print!("Syncing...");
let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?; let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static { let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
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(); let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| { move |keychain, spk_i, _| {
match once.insert(keychain) { if once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain), print!("\nScanning keychain [{:?}] ", keychain);
false => print!(" {:<3}", spk_i),
} }
print!(" {:<3}", spk_i);
std::io::stdout().flush().expect("must flush") std::io::stdout().flush().expect("must flush")
} }
}) });
.inspect_spks_for_keychain(
KeychainKind::External,
generate_inspect(KeychainKind::External),
)
.inspect_spks_for_keychain(
KeychainKind::Internal,
generate_inspect(KeychainKind::Internal),
);
let mut update = client let mut update = client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
@ -79,9 +58,7 @@ async fn main() -> Result<(), anyhow::Error> {
let _ = update.graph_update.update_last_seen_unconfirmed(now); let _ = update.graph_update.update_last_seen_unconfirmed(now);
wallet.apply_update(update)?; wallet.apply_update(update)?;
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut conn)?;
db.write(&changeset)?;
}
println!(); println!();
let balance = wallet.balance(); let balance = wallet.balance();
@ -95,12 +72,9 @@ async fn main() -> Result<(), anyhow::Error> {
std::process::exit(0); std::process::exit(0);
} }
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Signet)?;
let mut tx_builder = wallet.build_tx(); let mut tx_builder = wallet.build_tx();
tx_builder tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) .add_recipient(address.script_pubkey(), SEND_AMOUNT)
.enable_rbf(); .enable_rbf();
let mut psbt = tx_builder.finish()?; let mut psbt = tx_builder.finish()?;

View File

@ -1,52 +1,56 @@
const DB_MAGIC: &str = "bdk_wallet_esplora_example"; use std::{collections::BTreeSet, io::Write};
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 bdk_esplora::{esplora_client, EsploraExt}; use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store; use bdk_file_store::Store;
use bdk_wallet::{ use bdk_wallet::{
bitcoin::{Address, Amount, Network}, bitcoin::{Amount, Network},
KeychainKind, SignOptions, Wallet, wallet::{CreateParams, LoadParams},
KeychainKind, SignOptions,
}; };
fn main() -> Result<(), anyhow::Error> { const DB_MAGIC: &str = "bdk_wallet_esplora_example";
let db_path = std::env::temp_dir().join("bdk-esplora-example"); const DB_PATH: &str = "bdk-example-esplora-blocking.db";
let mut db = const SEND_AMOUNT: Amount = Amount::from_sat(5000);
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; const STOP_GAP: usize = 5;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; const PARALLEL_REQUESTS: usize = 5;
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets()?;
let mut wallet = Wallet::new_or_load( const NETWORK: Network = Network::Signet;
external_descriptor, const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
internal_descriptor, const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
changeset, const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
Network::Testnet,
)?; fn main() -> Result<(), anyhow::Error> {
let mut db =
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); let address = wallet.next_unused_address(KeychainKind::External);
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut db)?;
db.append_changeset(&changeset)?; println!(
} "Next unused address: ({}) {}",
println!("Generated Address: {}", address); address.index, address.address
);
let balance = wallet.balance(); let balance = wallet.balance();
println!("Wallet balance before syncing: {} sats", balance.total()); println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); print!("Syncing...");
let client = let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({ let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new(); let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| { move |keychain, spk_i, _| {
match once.insert(keychain) { if once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain), print!("\nScanning keychain [{:?}] ", keychain);
false => print!(" {:<3}", spk_i), }
}; print!(" {:<3}", spk_i);
std::io::stdout().flush().expect("must flush") std::io::stdout().flush().expect("must flush")
} }
}); });
@ -72,12 +76,9 @@ fn main() -> Result<(), anyhow::Error> {
std::process::exit(0); std::process::exit(0);
} }
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Testnet)?;
let mut tx_builder = wallet.build_tx(); let mut tx_builder = wallet.build_tx();
tx_builder tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) .add_recipient(address.script_pubkey(), SEND_AMOUNT)
.enable_rbf(); .enable_rbf();
let mut psbt = tx_builder.finish()?; let mut psbt = tx_builder.finish()?;

View File

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
bdk_wallet = { path = "../../crates/wallet" } bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" } bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }

View File

@ -5,7 +5,7 @@ use bdk_bitcoind_rpc::{
use bdk_file_store::Store; use bdk_file_store::Store;
use bdk_wallet::{ use bdk_wallet::{
bitcoin::{Block, Network, Transaction}, bitcoin::{Block, Network, Transaction},
wallet::Wallet, wallet::{CreateParams, LoadParams},
}; };
use clap::{self, Parser}; use clap::{self, Parser};
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant}; use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
@ -90,14 +90,14 @@ fn main() -> anyhow::Result<()> {
DB_MAGIC.as_bytes(), DB_MAGIC.as_bytes(),
args.db_path, args.db_path,
)?; )?;
let changeset = db.aggregate_changesets()?;
let mut wallet = Wallet::new_or_load( let load_params =
&args.descriptor, LoadParams::with_descriptors(&args.descriptor, &args.change_descriptor, args.network)?;
&args.change_descriptor, let create_params = CreateParams::new(&args.descriptor, &args.change_descriptor, args.network)?;
changeset, let mut wallet = match load_params.load_wallet(&mut db)? {
args.network, Some(wallet) => wallet,
)?; None => create_params.create_wallet(&mut db)?,
};
println!( println!(
"Loaded wallet in {}s", "Loaded wallet in {}s",
start_load_wallet.elapsed().as_secs_f32() start_load_wallet.elapsed().as_secs_f32()
@ -146,9 +146,7 @@ fn main() -> anyhow::Result<()> {
let connected_to = block_emission.connected_to(); let connected_to = block_emission.connected_to();
let start_apply_block = Instant::now(); let start_apply_block = Instant::now();
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?; wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut db)?;
db.append_changeset(&changeset)?;
}
let elapsed = start_apply_block.elapsed().as_secs_f32(); let elapsed = start_apply_block.elapsed().as_secs_f32();
println!( println!(
"Applied block {} at height {} in {}s", "Applied block {} at height {} in {}s",
@ -158,9 +156,7 @@ fn main() -> anyhow::Result<()> {
Emission::Mempool(mempool_emission) => { Emission::Mempool(mempool_emission) => {
let start_apply_mempool = Instant::now(); let start_apply_mempool = Instant::now();
wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time))); wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
if let Some(changeset) = wallet.take_staged() { wallet.persist(&mut db)?;
db.append_changeset(&changeset)?;
}
println!( println!(
"Applied unconfirmed transactions in {}s", "Applied unconfirmed transactions in {}s",
start_apply_mempool.elapsed().as_secs_f32() start_apply_mempool.elapsed().as_secs_f32()