1use crate::types::{
2 CoinSelectionOpt, EffectiveValue, ExcessStrategy, OutputGroup, SelectionError, Weight,
3};
4use std::{collections::HashSet, fmt, ops::Deref};
5
6#[derive(Debug, Clone)]
7pub(crate) struct PreparedOutputGroup {
8 output_group: OutputGroup,
9 pub index: usize,
10}
11
12impl Deref for PreparedOutputGroup {
13 type Target = OutputGroup;
14
15 fn deref(&self) -> &Self::Target {
16 &self.output_group
17 }
18}
19
20pub(crate) fn prepare_output_groups(
22 inputs: &[OutputGroup],
23 options: &CoinSelectionOpt,
24) -> Result<Vec<PreparedOutputGroup>> {
25 if options.target_value == 0 {
26 return Err(SelectionError::NonPositiveTarget);
27 }
28 if options.target_feerate <= 0.0
29 || options
30 .long_term_feerate
31 .is_some_and(|feerate| feerate <= 0.0)
32 {
33 return Err(SelectionError::NonPositiveFeeRate);
34 }
35 if options.target_feerate > 1000.0
36 || options
37 .long_term_feerate
38 .is_some_and(|feerate| feerate > 1000.0)
39 {
40 return Err(SelectionError::AbnormallyHighFeeRate);
41 }
42
43 let mut prepared = Vec::with_capacity(inputs.len());
44 for (index, input) in inputs.iter().enumerate() {
45 let effective_value = input
46 .value
47 .saturating_sub(calculate_fee(input.weight, options.target_feerate));
48 if effective_value >= options.min_change_value {
49 let mut output_group = input.clone();
50 output_group.value = effective_value;
51 prepared.push(PreparedOutputGroup {
52 output_group,
53 index,
54 });
55 }
56 }
57 if prepared.is_empty() {
58 return Err(insufficient_funds(inputs, options));
59 }
60 Ok(prepared)
61}
62
63pub(crate) fn insufficient_funds(
65 inputs: &[OutputGroup],
66 options: &CoinSelectionOpt,
67) -> SelectionError {
68 let available = inputs
69 .iter()
70 .fold(0u64, |total, input| total.saturating_add(input.value));
71 let base_fee =
72 calculate_fee(options.base_weight, options.target_feerate).max(options.min_absolute_fee);
73 let total_input_fee = inputs
74 .iter()
75 .map(|input| calculate_fee(input.weight, options.target_feerate))
76 .sum::<u64>();
77 let required = options
78 .target_value
79 .saturating_add(base_fee)
80 .saturating_add(total_input_fee);
81
82 SelectionError::InsufficientFunds {
83 available,
84 required,
85 }
86}
87
88#[inline]
92pub fn calculate_fee_and_waste(
93 options: &CoinSelectionOpt,
94 accumulated_effective_value: u64,
95 accumulated_weight: u64,
96) -> Result<(u64, i64)> {
97 let base_fee = calculate_fee(
98 options.base_weight + options.change_weight,
99 options.target_feerate,
100 )
101 .max(options.min_absolute_fee);
102 let input_fee = calculate_fee(accumulated_weight, options.target_feerate);
103 let long_term_feerate = options.long_term_feerate.unwrap_or(options.target_feerate);
104 let fee_difference = (options.target_feerate - long_term_feerate) as f64;
105 let mut waste = (accumulated_weight as f64 * fee_difference).round() as i64;
106 let excess = accumulated_effective_value.saturating_sub(options.target_value + base_fee);
107 if options.excess_strategy == ExcessStrategy::ToChange && excess >= options.min_change_value {
108 waste += options.change_cost as i64;
110 } else {
111 waste += excess as i64;
113 }
114 Ok((base_fee + input_fee, waste))
115}
116
117pub fn calculate_accumulated_weight(
122 smaller_coins: &[(usize, EffectiveValue, Weight)],
123 selected_inputs: &HashSet<usize>,
124) -> u64 {
125 let mut accumulated_weight: u64 = 0;
126 for &(index, _value, weight) in smaller_coins {
127 if selected_inputs.contains(&index) {
128 accumulated_weight += weight;
129 }
130 }
131 accumulated_weight
132}
133
134impl fmt::Display for SelectionError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 SelectionError::NonPositiveFeeRate => write!(f, "Negative fee rate"),
138 SelectionError::NonPositiveTarget => write!(f, "Target value must be positive"),
139 SelectionError::AbnormallyHighFeeRate => write!(f, "Abnormally high fee rate"),
140 SelectionError::InsufficientFunds {
141 available,
142 required,
143 } => write!(
144 f,
145 "Insufficient funds: available {available} sats, required {required} sats"
146 ),
147 SelectionError::NoSolutionFound => write!(f, "No solution could be derived"),
148 }
149 }
150}
151
152impl std::error::Error for SelectionError {}
153
154type Result<T> = std::result::Result<T, SelectionError>;
155
156#[inline]
157pub fn calculate_fee(weight: u64, rate: f32) -> u64 {
158 (weight as f32 * rate).ceil() as u64
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::types::{CoinSelectionOpt, ExcessStrategy};
165
166 fn setup_options(target_value: u64) -> CoinSelectionOpt {
167 CoinSelectionOpt {
168 target_value,
169 target_feerate: 0.4, long_term_feerate: Some(0.4),
171 min_absolute_fee: 0,
172 base_weight: 10,
173 change_weight: 50,
174 change_cost: 10,
175 min_change_value: 500,
176 excess_strategy: ExcessStrategy::ToChange,
177 }
178 }
179
180 #[test]
190 fn test_calculate_fee_and_waste() {
191 struct TestVector {
192 options: CoinSelectionOpt,
193 accumulated_value: u64,
194 accumulated_weight: u64,
195 fee: u64,
196 result: i64,
197 }
198
199 let options = setup_options(100).clone();
200 let test_vectors = [
201 TestVector {
203 options: options.clone(),
204 accumulated_value: 1000,
205 accumulated_weight: 50,
206 fee: 24,
207 result: options.change_cost as i64,
208 },
209 TestVector {
211 options: CoinSelectionOpt {
212 excess_strategy: ExcessStrategy::ToFee,
213 ..options
214 },
215 accumulated_value: 1000,
216 accumulated_weight: 50,
217 fee: 24,
218 result: 896,
219 },
220 TestVector {
222 options: CoinSelectionOpt {
223 target_value: 1000,
224 excess_strategy: ExcessStrategy::ToFee,
225 ..options
226 },
227 accumulated_value: 200,
228 accumulated_weight: 50,
229 fee: 24,
230 result: 0,
231 },
232 ];
233
234 for vector in test_vectors {
235 let (fee, waste) = calculate_fee_and_waste(
236 &vector.options,
237 vector.accumulated_value,
238 vector.accumulated_weight,
239 )
240 .unwrap();
241
242 assert_eq!(fee, vector.fee);
243 assert_eq!(waste, vector.result)
244 }
245 }
246}