-
Notifications
You must be signed in to change notification settings - Fork 30
feat!: Persist utxo lock status #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ValuedMammal
wants to merge
1
commit into
bitcoindevkit:master
Choose a base branch
from
ValuedMammal:feat/lock-unspent
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+302
−1
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 locked_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.locked_outpoints.extend(other.locked_outpoints); | ||
} | ||
|
||
fn is_empty(&self) -> bool { | ||
self.locked_outpoints.is_empty() | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -109,6 +110,7 @@ pub struct Wallet { | |
stage: ChangeSet, | ||
network: Network, | ||
secp: SecpCtx, | ||
locked_outpoints: BTreeMap<OutPoint, UtxoLock>, | ||
} | ||
|
||
/// An update to [`Wallet`]. | ||
|
@@ -449,6 +451,7 @@ impl Wallet { | |
let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned(); | ||
let indexed_graph = IndexedTxGraph::new(index); | ||
let indexed_graph_changeset = indexed_graph.initial_changeset(); | ||
let locked_outpoints = BTreeMap::new(); | ||
|
||
let stage = ChangeSet { | ||
descriptor, | ||
|
@@ -457,6 +460,7 @@ impl Wallet { | |
tx_graph: indexed_graph_changeset.tx_graph, | ||
indexer: indexed_graph_changeset.indexer, | ||
network: Some(network), | ||
..Default::default() | ||
}; | ||
|
||
Ok(Wallet { | ||
|
@@ -467,6 +471,7 @@ impl Wallet { | |
indexed_graph, | ||
stage, | ||
secp, | ||
locked_outpoints, | ||
}) | ||
} | ||
|
||
|
@@ -654,6 +659,21 @@ impl Wallet { | |
indexed_graph.apply_changeset(changeset.indexer.into()); | ||
indexed_graph.apply_changeset(changeset.tx_graph.into()); | ||
|
||
// Apply locked outpoints | ||
let locked_outpoints = changeset.locked_outpoints.locked_outpoints; | ||
let locked_outpoints = locked_outpoints | ||
.into_iter() | ||
.map(|(outpoint, is_locked)| { | ||
( | ||
outpoint, | ||
UtxoLock { | ||
outpoint, | ||
is_locked, | ||
}, | ||
) | ||
}) | ||
.collect(); | ||
|
||
let stage = ChangeSet::default(); | ||
|
||
Ok(Some(Wallet { | ||
|
@@ -664,6 +684,7 @@ impl Wallet { | |
stage, | ||
network, | ||
secp, | ||
locked_outpoints, | ||
})) | ||
} | ||
|
||
|
@@ -2108,6 +2129,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 | ||
|
@@ -2376,6 +2399,82 @@ impl Wallet { | |
&self.chain | ||
} | ||
|
||
/// Get a reference to the locked outpoints. | ||
pub fn locked_outpoints(&self) -> &BTreeMap<OutPoint, UtxoLock> { | ||
&self.locked_outpoints | ||
} | ||
|
||
/// 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 | ||
.get(&outpoint) | ||
.map_or(false, |u| u.is_locked) | ||
} | ||
|
||
/// 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) { | ||
use crate::collections::btree_map; | ||
let lock_value = true; | ||
let mut changeset = locked_outpoints::ChangeSet::default(); | ||
|
||
// If the lock status changed, update the entry and record the change | ||
// in the changeset. | ||
match self.locked_outpoints.entry(outpoint) { | ||
btree_map::Entry::Occupied(mut e) => { | ||
let utxo = e.get_mut(); | ||
if !utxo.is_locked { | ||
utxo.is_locked = lock_value; | ||
changeset.locked_outpoints.insert(outpoint, lock_value); | ||
} | ||
} | ||
btree_map::Entry::Vacant(e) => { | ||
e.insert(UtxoLock { | ||
outpoint, | ||
is_locked: lock_value, | ||
}); | ||
changeset.locked_outpoints.insert(outpoint, lock_value); | ||
} | ||
}; | ||
|
||
self.stage.merge(changeset.into()); | ||
} | ||
|
||
/// Unlock the wallet output of the specified `outpoint`. | ||
ValuedMammal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// | ||
/// **You must persist the staged changes.** | ||
pub fn unlock_outpoint(&mut self, outpoint: OutPoint) { | ||
use crate::collections::btree_map; | ||
let lock_value = false; | ||
let mut changeset = locked_outpoints::ChangeSet::default(); | ||
|
||
match self.locked_outpoints.entry(outpoint) { | ||
btree_map::Entry::Vacant(..) => {} | ||
btree_map::Entry::Occupied(mut e) => { | ||
// If the utxo is currently locked, update the lock value and stage | ||
// the change. | ||
let utxo = e.get_mut(); | ||
if utxo.is_locked { | ||
utxo.is_locked = lock_value; | ||
changeset.locked_outpoints.insert(outpoint, lock_value); | ||
self.stage.merge(changeset.into()); | ||
} | ||
} | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about an |
||
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the | ||
/// `prev_blockhash` of the block's header. | ||
/// | ||
|
@@ -2579,6 +2678,20 @@ impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime>> for Wallet { | |
} | ||
} | ||
|
||
/// Records the lock status of a wallet UTXO. | ||
/// | ||
/// Only a single [`UtxoLock`] may be assigned to a particular outpoint at a time. Refer to the docs | ||
/// for [`Wallet::lock_outpoint`]. Note that the lock status is user-defined and does not take | ||
/// into account any timelocks encoded by the descriptor. | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] | ||
#[non_exhaustive] | ||
pub struct UtxoLock { | ||
/// Outpoint. | ||
pub outpoint: OutPoint, | ||
/// Whether the outpoint is locked. | ||
pub is_locked: bool, | ||
} | ||
|
||
/// Deterministically generate a unique name given the descriptors defining the wallet | ||
/// | ||
/// Compatible with [`wallet_name_from_descriptor`] | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛏️ a nit, it's a little confusing to refer to the outpoints as "unspent" here and "outpoints" in the other new functions. Since we already have the "list_unspent" function would it make sense to use "unspent" for all the related functions (ie. locked_unspent, is_unspent_locked, lock_unspent, unlock_unspent)? Even though we all know what "outpoints" mean for those unfamiliar with bitcoin internals it's not a very descriptive term.