Skip to content

Commit 59fdc6b

Browse files
committed
WIP: clean up "coin control" logic and add grouping
1 parent 3e1290b commit 59fdc6b

File tree

6 files changed

+415
-165
lines changed

6 files changed

+415
-165
lines changed

src/create_psbt.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
use std::vec::Vec;
2-
31
use bitcoin::{absolute, transaction};
42
use miniscript::{bitcoin, psbt::PsbtExt};
53

6-
use crate::{Finalizer, Input, Output};
4+
use crate::{Finalizer, Input, Selection};
75

86
/// Parameters for creating a psbt.
97
#[derive(Debug, Clone)]
108
pub struct CreatePsbtParams {
11-
/// Inputs to fund the tx.
12-
///
13-
/// It is up to the caller to not duplicate inputs, spend from 2 conflicting txs, spend from
14-
/// invalid inputs, etc.
15-
pub inputs: Vec<Input>,
16-
/// Outputs.
17-
pub outputs: Vec<Output>,
9+
/// Inputs and outputs to fund the tx.
10+
pub selection: Selection,
1811

1912
/// Use a specific [`transaction::Version`].
2013
pub version: transaction::Version,
@@ -30,12 +23,12 @@ pub struct CreatePsbtParams {
3023
pub mandate_full_tx_for_segwit_v0: bool,
3124
}
3225

33-
impl Default for CreatePsbtParams {
34-
fn default() -> Self {
26+
impl CreatePsbtParams {
27+
/// With default values.
28+
pub fn new(selection: Selection) -> Self {
3529
Self {
30+
selection,
3631
version: transaction::Version::TWO,
37-
inputs: Default::default(),
38-
outputs: Default::default(),
3932
fallback_locktime: absolute::LockTime::ZERO,
4033
mandate_full_tx_for_segwit_v0: true,
4134
}
@@ -117,13 +110,15 @@ pub fn create_psbt(
117110
version: params.version,
118111
lock_time: accumulate_max_locktime(
119112
params
113+
.selection
120114
.inputs
121115
.iter()
122116
.filter_map(|input| input.plan().absolute_timelock),
123117
params.fallback_locktime,
124118
)
125119
.ok_or(CreatePsbtError::LockTypeMismatch)?,
126120
input: params
121+
.selection
127122
.inputs
128123
.iter()
129124
.map(|input| bitcoin::TxIn {
@@ -135,11 +130,16 @@ pub fn create_psbt(
135130
..Default::default()
136131
})
137132
.collect(),
138-
output: params.outputs.iter().map(|output| output.txout()).collect(),
133+
output: params
134+
.selection
135+
.outputs
136+
.iter()
137+
.map(|output| output.txout())
138+
.collect(),
139139
})
140140
.map_err(CreatePsbtError::Psbt)?;
141141

142-
for (plan_input, psbt_input) in params.inputs.iter().zip(psbt.inputs.iter_mut()) {
142+
for (plan_input, psbt_input) in params.selection.inputs.iter().zip(psbt.inputs.iter_mut()) {
143143
let txout = plan_input.prev_txout();
144144

145145
plan_input.plan().update_psbt_input(psbt_input);
@@ -168,7 +168,7 @@ pub fn create_psbt(
168168
}
169169
}
170170
}
171-
for (output_index, output) in params.outputs.iter().enumerate() {
171+
for (output_index, output) in params.selection.outputs.iter().enumerate() {
172172
if let Some(desc) = output.descriptor() {
173173
psbt.update_output_with_descriptor(output_index, desc)
174174
.map_err(CreatePsbtError::OutputUpdate)?;
@@ -177,6 +177,7 @@ pub fn create_psbt(
177177

178178
let finalizer = Finalizer {
179179
plans: params
180+
.selection
180181
.inputs
181182
.into_iter()
182183
.map(|input| (input.prev_outpoint(), input.plan().clone()))

src/create_selection.rs

Lines changed: 51 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,18 @@
11
use core::fmt::{Debug, Display};
2-
use std::collections::{BTreeMap, HashSet};
2+
use std::collections::HashSet;
33
use std::vec::Vec;
44

5-
use bdk_chain::KeychainIndexed;
6-
use bdk_chain::{local_chain::LocalChain, Anchor, TxGraph};
75
use bdk_coin_select::float::Ordf32;
86
use bdk_coin_select::metrics::LowestFee;
97
use bdk_coin_select::{
108
Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, NoBnbSolution, Target, TargetFee,
119
TargetOutputs,
1210
};
13-
use bitcoin::{absolute, Amount, OutPoint, TxOut};
11+
use bitcoin::{Amount, OutPoint, TxOut};
1412
use miniscript::bitcoin;
15-
use miniscript::{plan::Assets, Descriptor, DescriptorPublicKey, ForEachKey};
1613

1714
use crate::{DefiniteDescriptor, Input, InputGroup, Output};
1815

19-
/// Error
20-
#[derive(Debug)]
21-
pub enum GetCandidateInputsError<K> {
22-
/// Descriptor is missing for keychain K.
23-
MissingDescriptor(K),
24-
/// Cannot plan descriptor. Missing assets?
25-
CannotPlan(DefiniteDescriptor),
26-
}
27-
28-
impl<K: Debug> Display for GetCandidateInputsError<K> {
29-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30-
match self {
31-
GetCandidateInputsError::MissingDescriptor(k) => {
32-
write!(f, "missing descriptor for keychain {:?}", k)
33-
}
34-
GetCandidateInputsError::CannotPlan(descriptor) => {
35-
write!(f, "cannot plan input with descriptor {}", descriptor)
36-
}
37-
}
38-
}
39-
}
40-
41-
#[cfg(feature = "std")]
42-
impl<K: Debug> std::error::Error for GetCandidateInputsError<K> {}
43-
44-
/// Get candidate inputs.
45-
///
46-
/// This does not do any UTXO filtering or grouping.
47-
pub fn get_candidate_inputs<A: Anchor, K: Clone + Ord + core::fmt::Debug>(
48-
tx_graph: &TxGraph<A>,
49-
chain: &LocalChain,
50-
outpoints: impl IntoIterator<Item = KeychainIndexed<K, OutPoint>>,
51-
owned_descriptors: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
52-
additional_assets: Assets,
53-
) -> Result<Vec<Input>, GetCandidateInputsError<K>> {
54-
let tip = chain.tip().block_id();
55-
56-
let mut pks = vec![];
57-
for desc in owned_descriptors.values() {
58-
desc.for_each_key(|k| {
59-
pks.extend(k.clone().into_single_keys());
60-
true
61-
});
62-
}
63-
64-
let assets = Assets::new()
65-
.after(absolute::LockTime::from_height(tip.height).expect("must be valid height"))
66-
.add(pks)
67-
.add(additional_assets);
68-
69-
tx_graph
70-
.filter_chain_unspents(chain, tip, outpoints)
71-
.map(
72-
move |((k, i), txo)| -> Result<_, GetCandidateInputsError<K>> {
73-
let descriptor = owned_descriptors
74-
.get(&k)
75-
.ok_or(GetCandidateInputsError::MissingDescriptor(k))?
76-
.at_derivation_index(i)
77-
// TODO: Is this safe?
78-
.expect("derivation index must not overflow");
79-
80-
let plan = match descriptor.desc_type().segwit_version() {
81-
Some(_) => descriptor.plan(&assets),
82-
None => descriptor.plan_mall(&assets),
83-
}
84-
.map_err(GetCandidateInputsError::CannotPlan)?;
85-
86-
// BDK cannot spend from floating txouts so we will always have the full tx.
87-
let tx = tx_graph
88-
.get_tx(txo.outpoint.txid)
89-
.expect("must have full tx");
90-
91-
let input = Input::from_prev_tx(plan, tx, txo.outpoint.vout as _)
92-
.expect("tx must have output");
93-
Ok(input)
94-
},
95-
)
96-
.collect()
97-
}
98-
9916
/// Parameters for creating tx.
10017
#[derive(Debug, Clone)]
10118
pub struct CreateSelectionParams {
@@ -108,7 +25,6 @@ pub struct CreateSelectionParams {
10825
/// To derive change output.
10926
///
11027
/// Will error if this is unsatisfiable descriptor.
111-
///
11228
pub change_descriptor: DefiniteDescriptor,
11329

11430
/// Feerate target!
@@ -124,13 +40,37 @@ pub struct CreateSelectionParams {
12440
pub max_rounds: usize,
12541
}
12642

43+
impl CreateSelectionParams {
44+
/// With default params.
45+
pub fn new(
46+
input_candidates: Vec<InputGroup>,
47+
change_descriptor: DefiniteDescriptor,
48+
target_outputs: Vec<Output>,
49+
target_feerate: bitcoin::FeeRate,
50+
) -> Self {
51+
Self {
52+
input_candidates,
53+
must_spend: HashSet::new(),
54+
change_descriptor,
55+
target_feerate,
56+
long_term_feerate: None,
57+
target_outputs,
58+
max_rounds: 100_000,
59+
}
60+
}
61+
}
62+
12763
/// Final selection of inputs and outputs.
12864
#[derive(Debug, Clone)]
12965
pub struct Selection {
13066
/// Inputs in this selection.
13167
pub inputs: Vec<Input>,
13268
/// Outputs in this selection.
13369
pub outputs: Vec<Output>,
70+
}
71+
72+
/// Selection Metrics.
73+
pub struct SelectionMetrics {
13474
/// Selection score.
13575
pub score: Ordf32,
13676
/// Whether there is a change output in this selection.
@@ -159,7 +99,9 @@ impl Display for CreateSelectionError {
15999
impl std::error::Error for CreateSelectionError {}
160100

161101
/// TODO
162-
pub fn create_selection(params: CreateSelectionParams) -> Result<Selection, CreateSelectionError> {
102+
pub fn create_selection(
103+
params: CreateSelectionParams,
104+
) -> Result<(Selection, SelectionMetrics), CreateSelectionError> {
163105
fn convert_feerate(feerate: bitcoin::FeeRate) -> bdk_coin_select::FeeRate {
164106
FeeRate::from_sat_per_wu(feerate.to_sat_per_kwu() as f32 / 1000.0)
165107
}
@@ -239,23 +181,27 @@ pub fn create_selection(params: CreateSelectionParams) -> Result<Selection, Crea
239181
.map_err(CreateSelectionError::NoSolution)?;
240182

241183
let maybe_drain = selector.drain(target, change_policy);
242-
Ok(Selection {
243-
inputs: selector
244-
.apply_selection(&must_spend.into_iter().chain(may_spend).collect::<Vec<_>>())
245-
.flat_map(|group| group.inputs())
246-
.cloned()
247-
.collect::<Vec<Input>>(),
248-
outputs: {
249-
let mut outputs = params.target_outputs;
250-
if maybe_drain.is_some() {
251-
outputs.push(Output::with_descriptor(
252-
params.change_descriptor,
253-
Amount::from_sat(maybe_drain.value),
254-
));
255-
}
256-
outputs
184+
Ok((
185+
Selection {
186+
inputs: selector
187+
.apply_selection(&must_spend.into_iter().chain(may_spend).collect::<Vec<_>>())
188+
.flat_map(|group| group.inputs())
189+
.cloned()
190+
.collect::<Vec<Input>>(),
191+
outputs: {
192+
let mut outputs = params.target_outputs;
193+
if maybe_drain.is_some() {
194+
outputs.push(Output::with_descriptor(
195+
params.change_descriptor,
196+
Amount::from_sat(maybe_drain.value),
197+
));
198+
}
199+
outputs
200+
},
201+
},
202+
SelectionMetrics {
203+
score,
204+
has_change: maybe_drain.is_some(),
257205
},
258-
score,
259-
has_change: maybe_drain.is_some(),
260-
})
206+
))
261207
}

0 commit comments

Comments
 (0)