Skip to content

wallet: Add ConfirmationSpendPolicy to spend only [un]confirmed outputs #1855

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
wants to merge 2 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions crates/wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2026,6 +2026,8 @@ impl Wallet {
self.keychains().count() == 1
|| params.change_policy.is_satisfied_by(local_output)
})
// only add utxos that match the confirmation policy
.filter(|local_output| params.confirmation_policy.is_satisfied_by(local_output))
// only add to optional UTxOs those marked as spendable
.filter(|local_output| !params.unspendable.contains(&local_output.outpoint))
// if bumping fees only add to optional UTxOs those confirmed
Expand Down
70 changes: 70 additions & 0 deletions crates/wallet/src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub(crate) struct TxParams {
pub(crate) sequence: Option<Sequence>,
pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) confirmation_policy: ConfirmationSpendPolicy,
pub(crate) only_witness_utxo: bool,
pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool,
Expand Down Expand Up @@ -513,6 +514,43 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
self
}

/// Only spend confirmed outputs
///
/// This effectively adds all the unconfirmed outputs to the "unspendable" list. See
/// [`TxBuilder::unspendable`].
pub fn only_spend_confirmed(&mut self) -> &mut Self {
self.params.confirmation_policy = ConfirmationSpendPolicy::OnlyConfirmed;
self
}

/// Only spend outputs confirmed before or on the given block height
///
/// This effectively adds all the outputs not confirmed before or on the
/// given height to the "unspendable" list. See [`TxBuilder::unspendable`].
pub fn only_spend_confirmed_since(&mut self, height: u32) -> &mut Self {
self.params.confirmation_policy = ConfirmationSpendPolicy::OnlyConfirmedSince { height };
self
}

/// Only spend unconfirmed outputs
///
/// This effectively adds all the confirmed outputs to the "unspendable" list. See
/// [`TxBuilder::unspendable`].
pub fn only_spend_unconfirmed(&mut self) -> &mut Self {
self.params.confirmation_policy = ConfirmationSpendPolicy::OnlyUnconfirmed;
self
}

/// Set a specific [`ConfirmationSpendPolicy`]. See [`TxBuilder::only_spend_confirmed`] and
/// [`TxBuilder::only_spend_unconfirmed`] for some shortcuts.
pub fn confirmation_policy(
&mut self,
confirmation_policy: ConfirmationSpendPolicy,
) -> &mut Self {
self.params.confirmation_policy = confirmation_policy;
self
}

/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from
/// SegWit descriptors.
///
Expand Down Expand Up @@ -838,6 +876,38 @@ impl ChangeSpendPolicy {
}
}

/// Policy regarding the use of unconfirmed outputs when creating a transaction
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum ConfirmationSpendPolicy {
/// Use both confirmed and unconfirmed outputs (default)
#[default]
UnconfirmedAllowed,
/// Only use confirmed outputs (see [`TxBuilder::only_spend_confirmed`])
OnlyConfirmed,
/// Only use outputs confirmed since `height` (see [`TxBuilder::only_spend_confirmed`])
OnlyConfirmedSince {
/// The height at which the outputs should be confirmed
height: u32,
},
/// Only use unconfirmed outputs (see [`TxBuilder::only_spend_unconfirmed`])
OnlyUnconfirmed,
}

impl ConfirmationSpendPolicy {
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
match self {
ConfirmationSpendPolicy::UnconfirmedAllowed => true,
ConfirmationSpendPolicy::OnlyConfirmed => utxo.chain_position.is_confirmed(),
ConfirmationSpendPolicy::OnlyConfirmedSince { height } => utxo
.chain_position
.confirmation_height_upper_bound()
.map(|h| h <= *height)
.unwrap_or(false),
ConfirmationSpendPolicy::OnlyUnconfirmed => !utxo.chain_position.is_confirmed(),
}
}
}

#[cfg(test)]
mod test {
const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\
Expand Down
127 changes: 127 additions & 0 deletions crates/wallet/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,133 @@ fn test_create_tx_change_policy() {
));
}

#[test]
fn test_create_tx_confirmation_policy() {
let (mut wallet, funding_txid) = get_funded_wallet_wpkh();
assert_eq!(wallet.balance().confirmed, Amount::from_sat(50_000));
insert_checkpoint(
&mut wallet,
BlockId {
height: 3_000,
hash: BlockHash::all_zeros(),
},
);

let confirmed_tx = Transaction {
input: vec![],
output: vec![TxOut {
script_pubkey: wallet
.next_unused_address(KeychainKind::External)
.script_pubkey(),
value: Amount::from_sat(25_000),
}],
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
let confirmed_txid = confirmed_tx.compute_txid();
insert_tx(&mut wallet, confirmed_tx);
let anchor = ConfirmationBlockTime {
block_id: wallet.latest_checkpoint().get(3_000).unwrap().block_id(),
confirmation_time: 200,
};
insert_anchor(&mut wallet, confirmed_txid, anchor);
let unconfirmed_tx = Transaction {
input: vec![],
output: vec![TxOut {
script_pubkey: wallet
.next_unused_address(KeychainKind::External)
.script_pubkey(),
value: Amount::from_sat(25_000),
}],
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
let unconfirmed_txid = unconfirmed_tx.compute_txid();
insert_tx(&mut wallet, unconfirmed_tx);

let addr = wallet.next_unused_address(KeychainKind::External);

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(51_000))
.only_spend_confirmed();
let ret = builder.finish().unwrap();
assert_eq!(ret.unsigned_tx.input.len(), 2);
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == funding_txid));
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == confirmed_txid));

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(51_000))
.only_spend_confirmed_since(3_000);
let ret = builder.finish().unwrap();
assert_eq!(ret.unsigned_tx.input.len(), 2);
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == funding_txid));
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == confirmed_txid));

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
.only_spend_confirmed_since(2_500);
let ret = builder.finish().unwrap();
assert_eq!(ret.unsigned_tx.input.len(), 1);
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == funding_txid));

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(24_000))
.only_spend_unconfirmed();
let ret = builder.finish().unwrap();
assert_eq!(ret.unsigned_tx.input.len(), 1);
assert!(ret
.unsigned_tx
.input
.iter()
.any(|i| i.previous_output.txid == unconfirmed_txid));

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(76_000))
.only_spend_confirmed();
assert!(matches!(
builder.finish(),
Err(CreateTxError::CoinSelection(
coin_selection::InsufficientFunds { .. }
)),
));

let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(76_000))
.only_spend_unconfirmed();
assert!(matches!(
builder.finish(),
Err(CreateTxError::CoinSelection(
coin_selection::InsufficientFunds { .. }
)),
));
}

#[test]
fn test_create_tx_default_sequence() {
let (mut wallet, _) = get_funded_wallet_wpkh();
Expand Down
Loading