1use std::collections::HashSet;
4use std::{cmp, fmt, mem, slice};
5
6use anyhow::Context;
7use bitcoin::Amount;
8use log::trace;
9
10use ark::VtxoId;
11use ark::fees::VtxoFeeInfo;
12use bitcoin_ext::BlockHeight;
13
14use crate::WalletVtxo;
15
16pub type FeeAmount = Amount;
18
19const MAX_FEE_ITERATIONS: usize = 100;
22
23#[derive(Debug, Clone, Default)]
43pub struct InputSelection<F = ()> {
44 pub max_inputs: Option<usize>,
46 pub exclude: HashSet<VtxoId>,
48
49 fee_scheme: F,
50}
51
52impl<F> InputSelection<F> {
53 pub fn max_inputs(mut self, max_inputs: usize) -> Self {
55 self.max_inputs = Some(max_inputs);
56 self
57 }
58
59 pub fn exclude(mut self, exclude: VtxoId) -> Self {
61 self.exclude.insert(exclude);
62 self
63 }
64
65 pub fn exclude_many(mut self, exclude: impl IntoIterator<Item = VtxoId>) -> Self {
67 self.exclude.extend(exclude);
68 self
69 }
70}
71
72impl InputSelection {
73 pub fn new() -> InputSelection {
75 Default::default()
76 }
77
78 pub fn fee_scheme<F>(self, tip: BlockHeight, calc_fee: F) -> InputSelection<FeeScheme<F>>
86 where
87 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<FeeAmount>,
88 {
89 InputSelection {
90 max_inputs: self.max_inputs,
91 exclude: self.exclude,
92 fee_scheme: FeeScheme { tip, calc_fee },
93 }
94 }
95
96 pub fn select(
104 &self,
105 vtxos: Vec<WalletVtxo>,
106 amount: Amount,
107 ) -> anyhow::Result<Vec<WalletVtxo>> {
108 let mut scanner = InputScanner::new(self, vtxos);
109 scanner.cover_amount(amount)?;
110 Ok(scanner.into_selected())
111 }
112}
113
114impl<F> InputSelection<FeeScheme<F>>
115where
116 F: for<'a> Fn(Amount, SelectedFeeInfos<'a>) -> anyhow::Result<FeeAmount>,
117{
118 pub fn select(
128 &self,
129 vtxos: Vec<WalletVtxo>,
130 amount: Amount,
131 ) -> anyhow::Result<(Vec<WalletVtxo>, FeeAmount)> {
132 let mut scanner = InputScanner::new(self, vtxos);
133
134 let mut fee = Amount::ZERO;
139 for _ in 0..MAX_FEE_ITERATIONS {
140 let required = amount.checked_add(fee)
141 .context("Amount + fee overflow")?;
142
143 scanner.cover_amount(required)
144 .context("Could not find enough suitable VTXOs to cover payment + fees")?;
145 fee = (self.fee_scheme.calc_fee)(
146 amount, scanner.selected_fee_infos(self.fee_scheme.tip),
147 )?;
148
149 let new_required = amount.checked_add(fee)
150 .context("Amount + fee overflow")?;
151 if new_required <= scanner.total() {
152 trace!("Selected vtxos to cover amount + fee: amount = {}, fee = {}, total inputs = {}",
153 amount, fee, scanner.total(),
154 );
155 return Ok((scanner.into_selected(), fee));
156 }
157 trace!("VTXO sum of {} did not exceed amount {} and fee {}, iterating again",
158 scanner.total(), amount, fee,
159 );
160 }
161 bail!("Fee calculation did not converge after maximum iterations")
162 }
163}
164
165#[derive(Clone)]
167pub struct FeeScheme<F> {
168 tip: BlockHeight,
169 calc_fee: F,
170}
171
172impl<F> fmt::Debug for FeeScheme<F> {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.debug_struct("FeeScheme")
175 .field("tip", &self.tip)
176 .finish_non_exhaustive()
177 }
178}
179
180struct InputScanner {
187 candidates: Vec<WalletVtxo>,
189 cursor: usize,
191 selected: Vec<usize>,
193 total: Amount,
195 max_inputs: usize,
197}
198
199impl InputScanner {
200 fn new<F>(selection: &InputSelection<F>, mut candidates: Vec<WalletVtxo>) -> InputScanner {
203 candidates.retain(|v| !selection.exclude.contains(&v.id()));
204 candidates.sort_by_key(|v| v.expiry_height());
205
206 let max_inputs = selection.max_inputs.unwrap_or(usize::MAX);
207 let capacity = max_inputs.min(candidates.len());
208 InputScanner {
209 candidates,
210 cursor: 0,
211 selected: Vec::with_capacity(capacity),
212 total: Amount::ZERO,
213 max_inputs,
214 }
215 }
216
217 fn cover_amount(&mut self, amount: Amount) -> anyhow::Result<()> {
229 while self.total < amount {
230 let Some(vtxo) = self.candidates.get(self.cursor) else {
231 if self.candidates.len() > self.max_inputs {
232 bail!("Insufficient money available. Needed {} but the best {} inputs \
233 only amount to {}", amount, self.max_inputs, self.total,
234 );
235 }
236 bail!("Insufficient money available. Needed {} but {} is available",
237 amount, self.total,
238 );
239 };
240
241 if self.selected.len() < self.max_inputs {
243 self.total = self.total.checked_add(vtxo.amount()).context("total overflow")?;
244 self.selected.push(self.cursor);
245 } else {
246 if let Some(pos) = self.position_to_replace(vtxo.amount()) {
248 let evicted = mem::replace(&mut self.selected[pos], self.cursor);
249 self.total = self.total.checked_sub(self.candidates[evicted].amount())
250 .context("total deduction overflow")?;
251 self.total = self.total.checked_add(vtxo.amount())
252 .context("total addition overflow")?;
253 }
254 }
255 self.cursor += 1;
256 }
257 Ok(())
258 }
259
260 fn position_to_replace(&self, candidate_amount: Amount) -> Option<usize> {
265 let (pos, &idx) = self.selected.iter().enumerate()
268 .min_by_key(|&(_, &idx)| (self.candidates[idx].amount(), cmp::Reverse(idx)))?;
269
270 if candidate_amount > self.candidates[idx].amount() {
271 Some(pos)
272 } else {
273 None
274 }
275 }
276
277 fn total(&self) -> Amount {
278 self.total
279 }
280
281 fn selected_fee_infos(&self, tip: BlockHeight) -> SelectedFeeInfos<'_> {
283 SelectedFeeInfos {
284 selected: self.selected.iter(),
285 candidates: &self.candidates,
286 tip,
287 }
288 }
289
290 fn into_selected(self) -> Vec<WalletVtxo> {
292 let InputScanner { candidates, mut selected, .. } = self;
293 selected.sort();
294 let mut selected = selected.into_iter().peekable();
295 candidates.into_iter().enumerate()
296 .filter(|(idx, _)| selected.next_if_eq(idx).is_some())
297 .map(|(_, vtxo)| vtxo)
298 .collect()
299 }
300}
301
302pub struct SelectedFeeInfos<'a> {
307 selected: slice::Iter<'a, usize>,
308 candidates: &'a [WalletVtxo],
309 tip: BlockHeight,
310}
311
312impl Iterator for SelectedFeeInfos<'_> {
313 type Item = VtxoFeeInfo;
314
315 fn next(&mut self) -> Option<VtxoFeeInfo> {
316 let vtxo = &self.candidates[*self.selected.next()?];
317 Some(VtxoFeeInfo::from_vtxo_and_tip(vtxo, self.tip))
318 }
319
320 fn size_hint(&self) -> (usize, Option<usize>) {
321 self.selected.size_hint()
322 }
323}
324
325#[cfg(test)]
326mod test {
327 use super::*;
328
329 use bitcoin::Weight;
330
331 use ark::test_util::dummy::DummyTestVtxoSpec;
332
333 use crate::vtxo::state::VtxoState;
334
335 fn dummy_wallet_vtxo(sats: u64, expiry_height: BlockHeight) -> WalletVtxo {
340 let amount = Amount::from_sat(sats);
341 let fee = Amount::from_sat(330);
342 let (_, vtxo) = DummyTestVtxoSpec {
343 amount: amount + fee,
344 fee,
345 expiry_height,
346 ..Default::default()
347 }.build();
348 assert_eq!(vtxo.amount(), amount);
349 WalletVtxo {
350 vtxo: vtxo.into_bare(),
351 state: VtxoState::Spendable,
352 exit_depth: 0,
353 exit_tx_weight: Weight::ZERO,
354 registered: false,
355 }
356 }
357
358 fn amounts(vtxos: &[WalletVtxo]) -> Vec<u64> {
359 vtxos.iter().map(|v| v.amount().to_sat()).collect()
360 }
361
362 #[test]
363 fn covers_soonest_expiring_first() {
364 let vtxos = vec![
366 dummy_wallet_vtxo(30_000, 300),
367 dummy_wallet_vtxo(10_000, 100),
368 dummy_wallet_vtxo(20_000, 200),
369 ];
370 let selection = InputSelection::new();
371
372 let selected = selection.select(vtxos.clone(), Amount::from_sat(25_000)).unwrap();
373 assert_eq!(amounts(&selected), [10_000, 20_000]);
374
375 let selected = selection.select(vtxos.clone(), Amount::from_sat(60_000)).unwrap();
376 assert_eq!(amounts(&selected), [10_000, 20_000, 30_000]);
377
378 let err = selection.select(vtxos, Amount::from_sat(60_001)).unwrap_err();
379 assert!(err.to_string().contains("Insufficient money"), "{}", err);
380 assert!(!err.to_string().contains("inputs"), "{}", err);
381 }
382
383 #[test]
384 fn max_inputs_replaces_smallest_selected() {
385 let vtxos = vec![
386 dummy_wallet_vtxo(10_000, 100),
387 dummy_wallet_vtxo(20_000, 200),
388 dummy_wallet_vtxo(30_000, 300),
389 ];
390 let selection = InputSelection::new()
391 .max_inputs(2);
392
393 let selected = selection.select(vtxos.clone(), Amount::from_sat(25_000)).unwrap();
395 assert_eq!(amounts(&selected), [10_000, 20_000]);
396
397 let selected = selection.select(vtxos.clone(), Amount::from_sat(40_000)).unwrap();
400 assert_eq!(amounts(&selected), [20_000, 30_000]);
401
402 let err = selection.select(vtxos, Amount::from_sat(50_001)).unwrap_err();
404 assert!(err.to_string().contains("best 2 inputs"), "{}", err);
405 }
406
407 #[test]
408 fn max_inputs_evicts_latest_expiring_on_equal_amounts() {
409 let soonest = dummy_wallet_vtxo(10_000, 100);
410 let soonest_id = soonest.id();
411 let vtxos = vec![
412 soonest,
413 dummy_wallet_vtxo(10_000, 200),
414 dummy_wallet_vtxo(30_000, 300),
415 ];
416
417 let selected = InputSelection::new()
418 .max_inputs(2)
419 .select(vtxos, Amount::from_sat(40_000)).unwrap();
420
421 assert_eq!(amounts(&selected), [10_000, 30_000]);
423 assert_eq!(selected[0].id(), soonest_id);
424 }
425
426 #[test]
427 fn exclusions_are_never_selected() {
428 let vtxos = vec![
429 dummy_wallet_vtxo(10_000, 100),
430 dummy_wallet_vtxo(20_000, 200),
431 dummy_wallet_vtxo(30_000, 300),
432 ];
433 let excluded = vtxos[1].id();
434
435 let selected = InputSelection::new()
436 .exclude(excluded)
437 .select(vtxos, Amount::from_sat(20_000)).unwrap();
438
439 assert_eq!(amounts(&selected), [10_000, 30_000]);
440 assert!(selected.iter().all(|v| v.id() != excluded));
441 }
442
443 #[test]
444 fn with_fee_resumes_the_scan_as_the_fee_grows() {
445 let vtxos = vec![
446 dummy_wallet_vtxo(10_000, 100),
447 dummy_wallet_vtxo(20_000, 200),
448 ];
449
450 let (selected, fee) = InputSelection::new()
453 .fee_scheme(0, |_, _| Ok(Amount::from_sat(500)))
454 .select(vtxos, Amount::from_sat(9_800)).unwrap();
455
456 assert_eq!(amounts(&selected), [10_000, 20_000]);
457 assert_eq!(fee, Amount::from_sat(500));
458 }
459
460 #[test]
461 fn with_fee_respects_max_inputs() {
462 let vtxos = vec![
463 dummy_wallet_vtxo(10_000, 100),
464 dummy_wallet_vtxo(20_000, 200),
465 ];
466
467 let (selected, fee) = InputSelection::new()
470 .max_inputs(1)
471 .fee_scheme(0, |_, _| Ok(Amount::from_sat(500)))
472 .select(vtxos, Amount::from_sat(9_800)).unwrap();
473
474 assert_eq!(amounts(&selected), [20_000]);
475 assert_eq!(fee, Amount::from_sat(500));
476 }
477}