Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
# TODO fix, see: https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
enum-variant-size-threshold = 1032
# TODO fix, see: https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err
large-error-threshold = 993
msrv = "1.85.0"
19 changes: 17 additions & 2 deletions src/persist_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
keychain_txout::{self},
local_chain, tx_graph, ConfirmationBlockTime, DescriptorExt, Merge, SpkIterator,
},
locked_outpoints,
miniscript::descriptor::{Descriptor, DescriptorPublicKey},
ChangeSet, WalletPersister,
};
Expand Down Expand Up @@ -113,11 +114,13 @@ where
confirmation_time: 1755317160,
};

let outpoint = OutPoint::new(hash!("Rust"), 0);

let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
txs: [tx1.clone()].into(),
txouts: [
(
OutPoint::new(hash!("Rust"), 0),
outpoint,
TxOut {
value: Amount::from_sat(1300),
script_pubkey: spk_at_index(&descriptor, 4),
Expand Down Expand Up @@ -157,13 +160,18 @@ where
.into(),
};

let locked_outpoints_changeset = locked_outpoints::ChangeSet {
outpoints: [(outpoint, true)].into(),
};

let mut changeset = ChangeSet {
descriptor: Some(descriptor.clone()),
change_descriptor: Some(change_descriptor.clone()),
network: Some(Network::Testnet),
local_chain: local_chain_changeset,
tx_graph: tx_graph_changeset,
indexer: keychain_txout_changeset,
locked_outpoints: locked_outpoints_changeset,
};

// persist and load
Expand All @@ -184,10 +192,12 @@ where
confirmation_time: 1755317760,
};

let outpoint = OutPoint::new(hash!("Bitcoin_fixes_things"), 1);

let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
txs: [tx2.clone()].into(),
txouts: [(
OutPoint::new(hash!("Bitcoin_fixes_things"), 0),
outpoint,
TxOut {
value: Amount::from_sat(10000),
script_pubkey: spk_at_index(&descriptor, 21),
Expand All @@ -209,13 +219,18 @@ where
.into(),
};

let locked_outpoints_changeset = locked_outpoints::ChangeSet {
outpoints: [(outpoint, true)].into(),
};

let changeset_new = ChangeSet {
descriptor: None,
change_descriptor: None,
network: None,
local_chain: local_chain_changeset,
tx_graph: tx_graph_changeset,
indexer: keychain_txout_changeset,
locked_outpoints: locked_outpoints_changeset,
};

// persist, load and check if same as merged
Expand Down
80 changes: 77 additions & 3 deletions src/wallet/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ use bdk_chain::{
use miniscript::{Descriptor, DescriptorPublicKey};
use serde::{Deserialize, Serialize};

use crate::locked_outpoints;

type IndexedTxGraphChangeSet =
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>;

/// A change set for [`Wallet`]
/// A change set for [`Wallet`].
///
/// ## Definition
///
/// The change set is responsible for transmiting data between the persistent storage layer and the
/// The change set is responsible for transmitting data between the persistent storage layer and the
/// core library components. Specifically, it serves two primary functions:
///
/// 1) Recording incremental changes to the in-memory representation that need to be persisted to
Expand Down Expand Up @@ -114,6 +116,8 @@ pub struct ChangeSet {
pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
/// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
pub indexer: keychain_txout::ChangeSet,
/// Changes to locked outpoints.
pub locked_outpoints: locked_outpoints::ChangeSet,
}

impl Merge for ChangeSet {
Expand Down Expand Up @@ -142,6 +146,9 @@ impl Merge for ChangeSet {
self.network = other.network;
}

// merge locked outpoints
self.locked_outpoints.merge(other.locked_outpoints);

Merge::merge(&mut self.local_chain, other.local_chain);
Merge::merge(&mut self.tx_graph, other.tx_graph);
Merge::merge(&mut self.indexer, other.indexer);
Expand All @@ -154,6 +161,7 @@ impl Merge for ChangeSet {
&& self.local_chain.is_empty()
&& self.tx_graph.is_empty()
&& self.indexer.is_empty()
&& self.locked_outpoints.is_empty()
}
}

Expand All @@ -163,6 +171,8 @@ impl ChangeSet {
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";
/// Name of table to store wallet locked outpoints.
pub const WALLET_OUTPOINT_LOCK_TABLE_NAME: &'static str = "bdk_wallet_locked_outpoints";

/// Get v0 sqlite [ChangeSet] schema
pub fn schema_v0() -> alloc::string::String {
Expand All @@ -177,12 +187,24 @@ impl ChangeSet {
)
}

/// Get v1 sqlite [`ChangeSet`] schema. Schema v1 adds a table for locked outpoints.
pub fn schema_v1() -> alloc::string::String {
format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL, \
vout INTEGER NOT NULL, \
PRIMARY KEY(txid, vout) \
) STRICT;",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
)
}

/// Initialize sqlite tables for wallet tables.
pub fn init_sqlite_tables(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<()> {
crate::rusqlite_impl::migrate_schema(
db_tx,
Self::WALLET_SCHEMA_NAME,
&[&Self::schema_v0()],
&[&Self::schema_v0(), &Self::schema_v1()],
)?;

bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?;
Expand All @@ -194,6 +216,7 @@ impl ChangeSet {

/// Recover a [`ChangeSet`] from sqlite database.
pub fn from_sqlite(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<Self> {
use bitcoin::{OutPoint, Txid};
use chain::rusqlite::OptionalExtension;
use chain::Impl;

Expand All @@ -220,6 +243,24 @@ impl ChangeSet {
changeset.network = network.map(Impl::into_inner);
}

// Select locked outpoints.
let mut stmt = db_tx.prepare(&format!(
"SELECT txid, vout FROM {}",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
))?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, Impl<Txid>>("txid")?,
row.get::<_, u32>("vout")?,
))
})?;
let locked_outpoints = &mut changeset.locked_outpoints.outpoints;
for row in rows {
let (Impl(txid), vout) = row?;
let outpoint = OutPoint::new(txid, vout);
locked_outpoints.insert(outpoint, true);
}

changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?;
Expand Down Expand Up @@ -268,6 +309,30 @@ impl ChangeSet {
})?;
}

// Insert or delete locked outpoints.
let mut insert_stmt = db_tx.prepare_cached(&format!(
"INSERT OR IGNORE INTO {}(txid, vout) VALUES(:txid, :vout)",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME
))?;
let mut delete_stmt = db_tx.prepare_cached(&format!(
"DELETE FROM {} WHERE txid=:txid AND vout=:vout",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
))?;
for (&outpoint, &is_locked) in &self.locked_outpoints.outpoints {
let bitcoin::OutPoint { txid, vout } = outpoint;
if is_locked {
insert_stmt.execute(named_params! {
":txid": Impl(txid),
":vout": vout,
})?;
} else {
delete_stmt.execute(named_params! {
":txid": Impl(txid),
":vout": vout,
})?;
}
}

self.local_chain.persist_to_sqlite(db_tx)?;
self.tx_graph.persist_to_sqlite(db_tx)?;
self.indexer.persist_to_sqlite(db_tx)?;
Expand Down Expand Up @@ -311,3 +376,12 @@ impl From<keychain_txout::ChangeSet> for ChangeSet {
}
}
}

impl From<locked_outpoints::ChangeSet> for ChangeSet {
fn from(locked_outpoints: locked_outpoints::ChangeSet) -> Self {
Self {
locked_outpoints,
..Default::default()
}
}
}
26 changes: 26 additions & 0 deletions src/wallet/locked_outpoints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Module containing the locked outpoints change set.

use bdk_chain::Merge;
use bitcoin::OutPoint;
use serde::{Deserialize, Serialize};

use crate::collections::BTreeMap;

/// Represents changes to locked outpoints.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ChangeSet {
/// The lock status of an outpoint, `true == is_locked`.
pub outpoints: BTreeMap<OutPoint, bool>,
}

impl Merge for ChangeSet {
fn merge(&mut self, other: Self) {
// Extend self with other. Any entries in `self` that share the same
// outpoint are overwritten.
self.outpoints.extend(other.outpoints);
}

fn is_empty(&self) -> bool {
self.outpoints.is_empty()
}
}
Loading