Skip to content

Commit e997379

Browse files
committed
wallet: Add ConfirmationSpendPolicy to spend only [un]confirmed outputs
1 parent d9b0e5e commit e997379

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

crates/wallet/src/wallet/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1998,6 +1998,7 @@ impl Wallet {
19981998
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
19991999
let TxParams {
20002000
change_policy,
2001+
confirmation_policy,
20012002
unspendable,
20022003
utxos,
20032004
drain_wallet,
@@ -2068,6 +2069,7 @@ impl Wallet {
20682069
let mut i = 0;
20692070
may_spend.retain(|u| {
20702071
let retain = (self.keychains().count() == 1 || change_policy.is_satisfied_by(&u.0))
2072+
&& confirmation_policy.is_satisfied_by(&u.0)
20712073
&& !unspendable.contains(&u.0.outpoint)
20722074
&& satisfies_confirmed[i];
20732075
i += 1;

crates/wallet/src/wallet/tx_builder.rs

+48
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub(crate) struct TxParams {
134134
pub(crate) sequence: Option<Sequence>,
135135
pub(crate) version: Option<Version>,
136136
pub(crate) change_policy: ChangeSpendPolicy,
137+
pub(crate) confirmation_policy: ConfirmationSpendPolicy,
137138
pub(crate) only_witness_utxo: bool,
138139
pub(crate) add_global_xpubs: bool,
139140
pub(crate) include_output_redeem_witness_script: bool,
@@ -493,6 +494,31 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
493494
self
494495
}
495496

497+
/// Only spend confirmed outputs
498+
///
499+
/// This effectively adds all the unconfirmed outputs to the "unspendable" list. See
500+
/// [`TxBuilder::unspendable`].
501+
pub fn only_spend_confirmed(&mut self) -> &mut Self {
502+
self.params.confirmation_policy = ConfirmationSpendPolicy::OnlyConfirmed;
503+
self
504+
}
505+
506+
/// Only spend unconfirmed outputs
507+
///
508+
/// This effectively adds all the confirmed outputs to the "unspendable" list. See
509+
/// [`TxBuilder::unspendable`].
510+
pub fn only_spend_unconfirmed(&mut self) -> &mut Self {
511+
self.params.confirmation_policy = ConfirmationSpendPolicy::OnlyUnconfirmed;
512+
self
513+
}
514+
515+
/// Set a specific [`ConfirmationSpendPolicy`]. See [`TxBuilder::only_spend_confirmed`] and
516+
/// [`TxBuilder::only_spend_unconfirmed`] for some shortcuts.
517+
pub fn confirmation_policy(&mut self, confirmation_policy: ConfirmationSpendPolicy) -> &mut Self {
518+
self.params.confirmation_policy = confirmation_policy;
519+
self
520+
}
521+
496522
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from
497523
/// SegWit descriptors.
498524
///
@@ -818,6 +844,28 @@ impl ChangeSpendPolicy {
818844
}
819845
}
820846

847+
/// Policy regarding the use of unconfirmed outputs when creating a transaction
848+
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
849+
pub enum ConfirmationSpendPolicy {
850+
/// Use both confirmed and unconfirmed outputs (default)
851+
#[default]
852+
UnconfirmedAllowed,
853+
/// Only use confirmed outputs (see [`TxBuilder::only_spend_confirmed`])
854+
OnlyConfirmed,
855+
/// Only use unconfirmed outputs (see [`TxBuilder::only_spend_unconfirmed`])
856+
OnlyUnconfirmed,
857+
}
858+
859+
impl ConfirmationSpendPolicy {
860+
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
861+
match self {
862+
ConfirmationSpendPolicy::UnconfirmedAllowed => true,
863+
ConfirmationSpendPolicy::OnlyConfirmed => utxo.chain_position.is_confirmed(),
864+
ConfirmationSpendPolicy::OnlyUnconfirmed => !utxo.chain_position.is_confirmed(),
865+
}
866+
}
867+
}
868+
821869
#[cfg(test)]
822870
mod test {
823871
const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\

crates/wallet/tests/wallet.rs

+86
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,92 @@ fn test_create_tx_change_policy() {
766766
));
767767
}
768768

769+
#[test]
770+
fn test_create_tx_confirmation_policy() {
771+
let (mut wallet, funding_txid) = get_funded_wallet_wpkh();
772+
// confirm the funding tx
773+
let anchor = ConfirmationBlockTime {
774+
block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(),
775+
confirmation_time: 200,
776+
};
777+
insert_anchor(&mut wallet, funding_txid, anchor);
778+
assert_eq!(wallet.balance().confirmed, Amount::from_sat(50_000));
779+
780+
let confirmed_tx = Transaction {
781+
input: vec![],
782+
output: vec![TxOut {
783+
script_pubkey: wallet
784+
.next_unused_address(KeychainKind::External)
785+
.script_pubkey(),
786+
value: Amount::from_sat(25_000),
787+
}],
788+
version: transaction::Version::non_standard(0),
789+
lock_time: absolute::LockTime::ZERO,
790+
};
791+
let confirmed_txid = confirmed_tx.compute_txid();
792+
insert_tx(&mut wallet, confirmed_tx);
793+
let anchor = ConfirmationBlockTime {
794+
block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(),
795+
confirmation_time: 200,
796+
};
797+
insert_anchor(&mut wallet, confirmed_txid, anchor);
798+
let unconfirmed_tx = Transaction {
799+
input: vec![],
800+
output: vec![TxOut {
801+
script_pubkey: wallet
802+
.next_unused_address(KeychainKind::External)
803+
.script_pubkey(),
804+
value: Amount::from_sat(25_000),
805+
}],
806+
version: transaction::Version::non_standard(0),
807+
lock_time: absolute::LockTime::ZERO,
808+
};
809+
let unconfirmed_txid = unconfirmed_tx.compute_txid();
810+
insert_tx(&mut wallet, unconfirmed_tx);
811+
812+
let addr = wallet.next_unused_address(KeychainKind::External);
813+
814+
let mut builder = wallet.build_tx();
815+
builder
816+
.add_recipient(addr.script_pubkey(), Amount::from_sat(51_000))
817+
.only_spend_confirmed();
818+
let ret = builder.finish().unwrap();
819+
assert_eq!(ret.unsigned_tx.input.len(), 2);
820+
assert!(ret.unsigned_tx.input.iter().find(|i| i.previous_output.txid == confirmed_txid).is_some());
821+
assert!(ret.unsigned_tx.input.iter().find(|i| i.previous_output.txid == unconfirmed_txid).is_none());
822+
823+
let mut builder = wallet.build_tx();
824+
builder
825+
.add_recipient(addr.script_pubkey(), Amount::from_sat(24_000))
826+
.only_spend_unconfirmed();
827+
let ret = builder.finish().unwrap();
828+
assert_eq!(ret.unsigned_tx.input.len(), 1);
829+
assert!(ret.unsigned_tx.input.iter().find(|i| i.previous_output.txid == unconfirmed_txid).is_some());
830+
assert!(ret.unsigned_tx.input.iter().find(|i| i.previous_output.txid == confirmed_txid).is_none());
831+
832+
let mut builder = wallet.build_tx();
833+
builder
834+
.add_recipient(addr.script_pubkey(), Amount::from_sat(76_000))
835+
.only_spend_confirmed();
836+
assert!(matches!(
837+
builder.finish(),
838+
Err(CreateTxError::CoinSelection(
839+
coin_selection::InsufficientFunds { .. }
840+
)),
841+
));
842+
843+
let mut builder = wallet.build_tx();
844+
builder
845+
.add_recipient(addr.script_pubkey(), Amount::from_sat(76_000))
846+
.only_spend_unconfirmed();
847+
assert!(matches!(
848+
builder.finish(),
849+
Err(CreateTxError::CoinSelection(
850+
coin_selection::InsufficientFunds { .. }
851+
)),
852+
));
853+
}
854+
769855
#[test]
770856
fn test_create_tx_default_sequence() {
771857
let (mut wallet, _) = get_funded_wallet_wpkh();

0 commit comments

Comments
 (0)