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