Skip to content

Commit d5006d3

Browse files
committed
feat(selector): allow multiple change sources with ChangeDescriptor
Previously, the `change_descriptor` field only accepted a definite descriptor to derive the change output script pubkey. This worked for most cases but failed for scenarios - such as silent payments - where a definite descriptor is not yet available. To address this, introduce the `ChangeDescriptor` enum, which supports: - The standard case of providing a definite descriptor. - An alternative where the script pubkey and its maximum satisfaction weight can be specified directly. This makes change output generation flexible enough to handle both standard and exceptional workflows.
1 parent 4f8a213 commit d5006d3

File tree

2 files changed

+54
-11
lines changed

2 files changed

+54
-11
lines changed

examples/synopsis.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn main() -> anyhow::Result<()> {
6060
recipient_addr.script_pubkey(),
6161
Amount::from_sat(21_000_000),
6262
)],
63-
internal.at_derivation_index(0)?,
63+
bdk_tx::ChangeDescriptor::Definite(internal.at_derivation_index(0)?),
6464
bdk_tx::ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
6565
),
6666
)?;
@@ -134,7 +134,9 @@ fn main() -> anyhow::Result<()> {
134134
// be less wasteful to have no output, however that will not be a valid tx).
135135
// If you only want to fee bump, put the original txs' recipients here.
136136
target_outputs: vec![],
137-
change_descriptor: internal.at_derivation_index(1)?,
137+
change_descriptor: bdk_tx::ChangeDescriptor::Definite(
138+
internal.at_derivation_index(1)?,
139+
),
138140
change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
139141
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
140142
replace: Some(rbf_params),

src/selector.rs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bdk_coin_select::{
22
ChangePolicy, DrainWeights, InsufficientFunds, Replace, Target, TargetFee, TargetOutputs,
33
};
4-
use bitcoin::{Amount, FeeRate, Transaction, TxOut, Weight};
4+
use bitcoin::{Amount, FeeRate, ScriptBuf, Transaction, TxOut, Weight};
55
use miniscript::bitcoin;
66

77
use crate::{cs_feerate, DefiniteDescriptor, InputCandidates, InputGroup, Output, Selection};
@@ -15,10 +15,51 @@ pub struct Selector<'c> {
1515
target_outputs: Vec<Output>,
1616
target: Target,
1717
change_policy: bdk_coin_select::ChangePolicy,
18-
change_descriptor: DefiniteDescriptor,
18+
change_descriptor: ChangeDescriptor,
1919
inner: bdk_coin_select::CoinSelector<'c>,
2020
}
2121

22+
/// Data source to create a change output if needed
23+
#[derive(Debug, Clone)]
24+
pub enum ChangeDescriptor {
25+
/// Build change output from descriptor
26+
Definite(DefiniteDescriptor),
27+
/// Build change output without descriptor
28+
Manual {
29+
/// The final script pubkey the change output should have
30+
script_pubkey: ScriptBuf,
31+
/// The maximum weight to satisfy the script pubkey
32+
max_weight_to_satisfy_wu: u64,
33+
},
34+
}
35+
36+
/// Wrapper methods to unify interface for change outputs
37+
impl ChangeDescriptor {
38+
/// Get the maximum weight to satisfy the script pubkey of the resultant output
39+
pub fn get_max_weight_to_satisfy_wu(&self) -> Result<u64, miniscript::Error> {
40+
use ChangeDescriptor::*;
41+
42+
match self {
43+
Definite(definite_descriptor) => definite_descriptor
44+
.max_weight_to_satisfy()
45+
.map(|weight| weight.to_wu()),
46+
Manual {
47+
max_weight_to_satisfy_wu,
48+
..
49+
} => Ok(*max_weight_to_satisfy_wu),
50+
}
51+
}
52+
53+
/// Get the script pubkey the change output should have
54+
pub fn get_script_pubkey(&self) -> ScriptBuf {
55+
use ChangeDescriptor::*;
56+
match self {
57+
Definite(definite_descriptor) => definite_descriptor.script_pubkey(),
58+
Manual { script_pubkey, .. } => script_pubkey.clone(),
59+
}
60+
}
61+
}
62+
2263
/// Parameters for creating tx.
2364
///
2465
/// TODO: Create a builder interface on this that does checks. I.e.
@@ -42,7 +83,7 @@ pub struct SelectorParams {
4283
/// To derive change output.
4384
///
4485
/// Will error if this is unsatisfiable descriptor.
45-
pub change_descriptor: DefiniteDescriptor,
86+
pub change_descriptor: ChangeDescriptor,
4687

4788
/// The policy to determine whether we create a change output.
4889
pub change_policy: ChangePolicyType,
@@ -140,7 +181,7 @@ impl SelectorParams {
140181
pub fn new(
141182
target_feerate: bitcoin::FeeRate,
142183
target_outputs: Vec<Output>,
143-
change_descriptor: DefiniteDescriptor,
184+
change_descriptor: ChangeDescriptor,
144185
change_policy: ChangePolicyType,
145186
) -> Self {
146187
Self {
@@ -179,12 +220,12 @@ impl SelectorParams {
179220
pub fn to_cs_change_weights(&self) -> Result<bdk_coin_select::DrainWeights, miniscript::Error> {
180221
Ok(DrainWeights {
181222
output_weight: (TxOut {
182-
script_pubkey: self.change_descriptor.script_pubkey(),
223+
script_pubkey: self.change_descriptor.get_script_pubkey(),
183224
value: Amount::ZERO,
184225
})
185226
.weight()
186227
.to_wu(),
187-
spend_weight: self.change_descriptor.max_weight_to_satisfy()?.to_wu(),
228+
spend_weight: self.change_descriptor.get_max_weight_to_satisfy_wu()?,
188229
n_outputs: 1,
189230
})
190231
}
@@ -198,7 +239,7 @@ impl SelectorParams {
198239
let change_weights = self.to_cs_change_weights()?;
199240
let dust_value = self
200241
.change_descriptor
201-
.script_pubkey()
242+
.get_script_pubkey()
202243
.minimal_non_dust()
203244
.to_sat();
204245
Ok(match self.change_policy {
@@ -358,8 +399,8 @@ impl<'c> Selector<'c> {
358399
outputs: {
359400
let mut outputs = self.target_outputs.clone();
360401
if maybe_change.is_some() {
361-
outputs.push(Output::with_descriptor(
362-
self.change_descriptor.clone(),
402+
outputs.push(Output::with_script(
403+
self.change_descriptor.get_script_pubkey(),
363404
Amount::from_sat(maybe_change.value),
364405
));
365406
}

0 commit comments

Comments
 (0)