Skip to content

Commit 5a65422

Browse files
committed
feat: Add exclude_unconfirmed and exclude_below_confirmations
These are convenience methods on `TxBuilder` that can be used to filter out outpoints which do not meet a given confirmation threshold.
1 parent 4d3116a commit 5a65422

File tree

1 file changed

+129
-0
lines changed

1 file changed

+129
-0
lines changed

wallet/src/wallet/tx_builder.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,45 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
459459
self
460460
}
461461

462+
/// Excludes any outpoints whose enclosing transaction has fewer than `min_confirms`
463+
/// confirmations.
464+
///
465+
/// `min_confirms` is the minimum number of confirmations a transaction must have in order for
466+
/// its outpoints to remain spendable.
467+
/// - Passing `0` will include all transactions (no filtering).
468+
/// - Passing `1` will exclude all unconfirmed transactions (equivalent to
469+
/// `exclude_unconfirmed`).
470+
/// - Passing `6` will only allow outpoints from transactions with at least 6 confirmations.
471+
///
472+
/// If you chain this with other filtering methods, the final set of unspendable outpoints will
473+
/// be the union of all filters.
474+
pub fn exclude_below_confirmations(&mut self, min_confirms: u32) -> &mut Self {
475+
let tip_height = self.wallet.latest_checkpoint().height();
476+
let to_exclude = self
477+
.wallet
478+
.list_unspent()
479+
.filter(|utxo| {
480+
utxo.chain_position
481+
.confirmation_height_upper_bound()
482+
.map_or(0, |h| tip_height.saturating_add(1).saturating_sub(h))
483+
< min_confirms
484+
})
485+
.map(|utxo| utxo.outpoint);
486+
for op in to_exclude {
487+
self.params.unspendable.insert(op);
488+
}
489+
self
490+
}
491+
492+
/// Exclude outpoints whose enclosing transaction is unconfirmed.
493+
///
494+
/// This is a shorthand for [`exclude_below_confirmations(1)`].
495+
///
496+
/// [`exclude_below_confirmations(1)`]: Self::exclude_below_confirmations
497+
pub fn exclude_unconfirmed(&mut self) -> &mut Self {
498+
self.exclude_below_confirmations(1)
499+
}
500+
462501
/// Sign with a specific sig hash
463502
///
464503
/// **Use this option very carefully**
@@ -1088,6 +1127,96 @@ mod test {
10881127
assert_eq!(filtered[0].keychain, KeychainKind::Internal);
10891128
}
10901129

1130+
#[test]
1131+
fn test_exclude_unconfirmed() {
1132+
use crate::test_utils::*;
1133+
use bdk_chain::BlockId;
1134+
use bitcoin::{hashes::Hash, BlockHash, Network};
1135+
1136+
let mut wallet = Wallet::create_single(get_test_tr_single_sig())
1137+
.network(Network::Regtest)
1138+
.create_wallet_no_persist()
1139+
.unwrap();
1140+
let recipient = wallet.next_unused_address(KeychainKind::External).address;
1141+
1142+
insert_checkpoint(
1143+
&mut wallet,
1144+
BlockId {
1145+
height: 1,
1146+
hash: BlockHash::all_zeros(),
1147+
},
1148+
);
1149+
insert_checkpoint(
1150+
&mut wallet,
1151+
BlockId {
1152+
height: 2,
1153+
hash: BlockHash::all_zeros(),
1154+
},
1155+
);
1156+
receive_output(
1157+
&mut wallet,
1158+
Amount::ONE_BTC,
1159+
ReceiveTo::Block(chain::ConfirmationBlockTime {
1160+
block_id: BlockId {
1161+
height: 1,
1162+
hash: BlockHash::all_zeros(),
1163+
},
1164+
confirmation_time: 1,
1165+
}),
1166+
);
1167+
receive_output(
1168+
&mut wallet,
1169+
Amount::ONE_BTC * 2,
1170+
ReceiveTo::Block(chain::ConfirmationBlockTime {
1171+
block_id: BlockId {
1172+
height: 2,
1173+
hash: BlockHash::all_zeros(),
1174+
},
1175+
confirmation_time: 2,
1176+
}),
1177+
);
1178+
receive_output(&mut wallet, Amount::ONE_BTC * 3, ReceiveTo::Mempool(100));
1179+
1180+
// Exclude nothing.
1181+
{
1182+
let mut builder = wallet.build_tx();
1183+
builder
1184+
.fee_rate(FeeRate::ZERO)
1185+
.exclude_below_confirmations(0)
1186+
.drain_wallet()
1187+
.drain_to(recipient.script_pubkey());
1188+
let tx = builder.finish().unwrap();
1189+
let output = tx.unsigned_tx.output.first().expect("must have one output");
1190+
assert_eq!(output.value, Amount::ONE_BTC * 6);
1191+
}
1192+
1193+
// Exclude < 1 conf (a.k.a exclude unconfirmed).
1194+
{
1195+
let mut builder = wallet.build_tx();
1196+
builder
1197+
.fee_rate(FeeRate::ZERO)
1198+
.exclude_below_confirmations(1)
1199+
.drain_wallet()
1200+
.drain_to(recipient.script_pubkey());
1201+
let tx = builder.finish().unwrap();
1202+
let output = tx.unsigned_tx.output.first().expect("must have one output");
1203+
assert_eq!(output.value, Amount::ONE_BTC * 3);
1204+
}
1205+
1206+
// Exclude < 2 conf (a.k.a need at least 2 conf)
1207+
{
1208+
let mut builder = wallet.build_tx();
1209+
builder
1210+
.fee_rate(FeeRate::ZERO)
1211+
.exclude_below_confirmations(2)
1212+
.drain_wallet()
1213+
.drain_to(recipient.script_pubkey());
1214+
let tx = builder.finish().unwrap();
1215+
let output = tx.unsigned_tx.output.first().expect("must have one output");
1216+
assert_eq!(output.value, Amount::ONE_BTC);
1217+
}
1218+
}
1219+
10911220
#[test]
10921221
fn test_build_fee_bump_remove_change_output_single_desc() {
10931222
use crate::test_utils::*;

0 commit comments

Comments
 (0)