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"
2 changes: 2 additions & 0 deletions src/persist_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ where
local_chain: local_chain_changeset,
tx_graph: tx_graph_changeset,
indexer: keychain_txout_changeset,
locked_outpoints: Default::default(),
};

// persist and load
Expand Down Expand Up @@ -216,6 +217,7 @@ where
local_chain: local_chain_changeset,
tx_graph: tx_graph_changeset,
indexer: keychain_txout_changeset,
locked_outpoints: Default::default(),
};

// 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
@@ -1,17 +1,20 @@
use bdk_chain::{
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge,
};
use bitcoin::{OutPoint, Txid};
use miniscript::{Descriptor, DescriptorPublicKey};
use serde::{Deserialize, Serialize};

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

/// A change set for [`Wallet`]
use crate::locked_outpoints;

/// 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 +117,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 +147,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 +162,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 +172,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 +188,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 Down Expand Up @@ -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!(
"REPLACE 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 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()
}
}
79 changes: 70 additions & 9 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mod changeset;
pub mod coin_selection;
pub mod error;
pub mod export;
pub mod locked_outpoints;
mod params;
mod persisted;
pub mod signer;
Expand Down Expand Up @@ -109,6 +110,7 @@ pub struct Wallet {
stage: ChangeSet,
network: Network,
secp: SecpCtx,
locked_outpoints: HashSet<OutPoint>,
}

/// An update to [`Wallet`].
Expand Down Expand Up @@ -227,9 +229,9 @@ pub enum LoadMismatch {
/// Keychain identifying the descriptor.
keychain: KeychainKind,
/// The loaded descriptor.
loaded: Option<ExtendedDescriptor>,
loaded: Option<Box<ExtendedDescriptor>>,
/// The expected descriptor.
expected: Option<ExtendedDescriptor>,
expected: Option<Box<ExtendedDescriptor>>,
},
}

Expand Down Expand Up @@ -473,6 +475,8 @@ impl Wallet {
None => (None, Arc::new(SignersContainer::new())),
};

let locked_outpoints = HashSet::new();

let mut stage = ChangeSet {
descriptor: Some(descriptor.clone()),
change_descriptor: change_descriptor.clone(),
Expand All @@ -499,6 +503,7 @@ impl Wallet {
indexed_graph,
stage,
secp,
locked_outpoints,
})
}

Expand Down Expand Up @@ -597,8 +602,8 @@ impl Wallet {
if descriptor.descriptor_id() != exp_desc.descriptor_id() {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::External,
loaded: Some(descriptor),
expected: Some(exp_desc),
loaded: Some(Box::new(descriptor)),
expected: Some(Box::new(exp_desc)),
}));
}
if params.extract_keys {
Expand All @@ -607,7 +612,7 @@ impl Wallet {
} else {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::External,
loaded: Some(descriptor),
loaded: Some(Box::new(descriptor)),
expected: None,
}));
}
Expand All @@ -627,7 +632,7 @@ impl Wallet {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::Internal,
loaded: None,
expected: Some(exp_desc),
expected: Some(Box::new(exp_desc)),
}));
}
}
Expand All @@ -641,7 +646,7 @@ impl Wallet {
None => {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::Internal,
loaded: Some(desc),
loaded: Some(Box::new(desc)),
expected: None,
}))
}
Expand All @@ -653,8 +658,8 @@ impl Wallet {
if desc.descriptor_id() != exp_desc.descriptor_id() {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::Internal,
loaded: Some(desc),
expected: Some(exp_desc),
loaded: Some(Box::new(desc)),
expected: Some(Box::new(exp_desc)),
}));
}
if params.extract_keys {
Expand All @@ -674,6 +679,14 @@ impl Wallet {
None => Arc::new(SignersContainer::new()),
};

// Apply locked outpoints
let locked_outpoints = changeset.locked_outpoints.outpoints;
let locked_outpoints = locked_outpoints
.into_iter()
.filter(|&(_op, is_locked)| is_locked)
.map(|(op, _)| op)
.collect();

let mut stage = ChangeSet::default();

let indexed_graph = make_indexed_graph(
Expand All @@ -695,6 +708,7 @@ impl Wallet {
stage,
network,
secp,
locked_outpoints,
}))
}

Expand Down Expand Up @@ -2140,6 +2154,8 @@ impl Wallet {
CanonicalizationParams::default(),
self.indexed_graph.index.outpoints().iter().cloned(),
)
// Filter out locked outpoints
.filter(|(_, txo)| !self.is_outpoint_locked(txo.outpoint))
// only create LocalOutput if UTxO is mature
.filter_map(move |((k, i), full_txo)| {
full_txo
Expand Down Expand Up @@ -2408,6 +2424,51 @@ impl Wallet {
&self.chain
}

/// List the locked outpoints.
pub fn list_locked_outpoints(&self) -> impl Iterator<Item = OutPoint> + '_ {
self.locked_outpoints.iter().copied()
}

/// List unspent outpoints that are currently locked.
pub fn list_locked_unspent(&self) -> impl Iterator<Item = OutPoint> + '_ {
self.list_unspent()
.filter(|output| self.is_outpoint_locked(output.outpoint))
.map(|output| output.outpoint)
}

/// Whether the `outpoint` is locked. See [`Wallet::lock_outpoint`] for more.
pub fn is_outpoint_locked(&self, outpoint: OutPoint) -> bool {
self.locked_outpoints.contains(&outpoint)
}

/// Lock a wallet output identified by the given `outpoint`.
///
/// A locked UTXO will not be selected as an input to fund a transaction. This is useful
/// for excluding or reserving candidate inputs during transaction creation.
///
/// **You must persist the staged change for the lock status to be persistent**. To unlock a
/// previously locked outpoint, see [`Wallet::unlock_outpoint`].
pub fn lock_outpoint(&mut self, outpoint: OutPoint) {
if self.locked_outpoints.insert(outpoint) {
let changeset = locked_outpoints::ChangeSet {
outpoints: [(outpoint, true)].into(),
};
self.stage.merge(changeset.into());
}
}

/// Unlock the wallet output of the specified `outpoint`.
///
/// **You must persist the staged change for the lock status to be persistent**.
pub fn unlock_outpoint(&mut self, outpoint: OutPoint) {
if self.locked_outpoints.remove(&outpoint) {
let changeset = locked_outpoints::ChangeSet {
outpoints: [(outpoint, false)].into(),
};
self.stage.merge(changeset.into());
}
}

/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
/// `prev_blockhash` of the block's header.
///
Expand Down
Loading