rust_coinselect/algorithms/
leastchange.rs1use std::vec;
2
3use crate::{
4 types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput, WasteMetric},
5 utils::{calculate_fee, calculate_waste, effective_value},
6};
7
8struct BnBState {
10 index: usize,
11 current_eff_value: u64,
12 current_selection: Vec<usize>,
13 current_count: usize,
14 current_weight: u64,
15}
16
17pub fn select_coin_bnb_leastchange(
19 inputs: &[OutputGroup],
20 options: &CoinSelectionOpt,
21) -> Result<SelectionOutput, SelectionError> {
22 let mut best: Option<(Vec<usize>, u64, usize)> = None; let base_fees = calculate_fee(options.base_weight, options.target_feerate).unwrap_or_default();
24 let target =
25 options.target_value + options.min_change_value + base_fees.max(options.min_absolute_fee);
26
27 let mut filtered = inputs
29 .iter()
30 .enumerate()
31 .filter_map(
32 |(i, inp)| match effective_value(inp, options.target_feerate) {
33 Ok(net_value) if net_value > 0 => Some((i, inp.value, inp.weight)),
34 _ => None,
35 },
36 )
37 .collect::<Vec<_>>();
38
39 filtered.sort_by(|(_, a, _), (_, b, _)| b.cmp(a));
41
42 let n = filtered.len();
44 let mut remaining_net = vec![0; n + 1];
45 for i in (0..n).rev() {
46 remaining_net[i] = remaining_net[i + 1] + filtered[i].1;
47 }
48
49 let mut stack = vec![BnBState {
51 index: 0,
52 current_eff_value: 0,
53 current_selection: Vec::new(),
54 current_count: 0,
55 current_weight: 0,
56 }];
57
58 while let Some(state) = stack.pop() {
59 if state.index >= n {
60 continue;
61 }
62
63 if state.current_eff_value + remaining_net[state.index] < target {
65 continue;
66 }
67
68 stack.push(BnBState {
69 index: state.index + 1,
70 current_eff_value: state.current_eff_value,
71 current_selection: state.current_selection.clone(),
72 current_count: state.current_count,
73 current_weight: state.current_weight,
74 });
75
76 let (orig_idx, net_value, weight) = filtered[state.index];
77 let new_eff_value = state.current_eff_value + net_value;
78 let mut new_selection = state.current_selection.clone();
79 new_selection.push(orig_idx);
80 let new_count = state.current_count + 1;
81 let new_weight = state.current_weight + weight;
82
83 let estimated_fees = calculate_fee(new_weight, options.target_feerate).unwrap_or(0);
85 let required_value = target + estimated_fees;
86 if new_eff_value >= required_value {
87 let change = new_eff_value - required_value;
88 let update = match best {
89 None => true,
90 Some((_, best_change, best_count)) => {
91 change < best_change || (change == best_change && new_count < best_count)
92 }
93 };
94 if update {
95 best = Some((new_selection, change, new_count));
96 }
97 } else {
98 stack.push(BnBState {
99 index: state.index + 1,
100 current_eff_value: new_eff_value,
101 current_selection: new_selection,
102 current_count: new_count,
103 current_weight: new_weight,
104 });
105 }
106 }
107
108 if let Some((selected_inputs, _change, _count)) = best {
109 let total_value: u64 = selected_inputs.iter().map(|&i| inputs[i].value).sum();
110 let total_weight: u64 = selected_inputs.iter().map(|&i| inputs[i].weight).sum();
111 let estimated_fees = calculate_fee(total_weight, options.target_feerate).unwrap_or(0);
112 let waste = calculate_waste(options, total_value, total_weight, estimated_fees);
113
114 Ok(SelectionOutput {
115 selected_inputs,
116 waste: WasteMetric(waste),
117 })
118 } else {
119 Err(SelectionError::InsufficientFunds)
120 }
121}
122
123#[cfg(test)]
124mod test {
125
126 use crate::{
127 algorithms::leastchange::select_coin_bnb_leastchange,
128 types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
129 };
130
131 fn setup_leastchange_output_groups() -> Vec<OutputGroup> {
132 vec![
133 OutputGroup {
134 value: 100,
135 weight: 100,
136 input_count: 1,
137 creation_sequence: None,
138 },
139 OutputGroup {
140 value: 1500,
141 weight: 200,
142 input_count: 1,
143 creation_sequence: None,
144 },
145 OutputGroup {
146 value: 3400,
147 weight: 300,
148 input_count: 1,
149 creation_sequence: None,
150 },
151 OutputGroup {
152 value: 2200,
153 weight: 150,
154 input_count: 1,
155 creation_sequence: None,
156 },
157 OutputGroup {
158 value: 1190,
159 weight: 200,
160 input_count: 1,
161 creation_sequence: None,
162 },
163 OutputGroup {
164 value: 1200,
165 weight: 200,
166 input_count: 1,
167 creation_sequence: None,
168 },
169 OutputGroup {
170 value: 3300,
171 weight: 100,
172 input_count: 1,
173 creation_sequence: None,
174 },
175 OutputGroup {
176 value: 1000,
177 weight: 190,
178 input_count: 1,
179 creation_sequence: None,
180 },
181 OutputGroup {
182 value: 2000,
183 weight: 210,
184 input_count: 1,
185 creation_sequence: None,
186 },
187 OutputGroup {
188 value: 3000,
189 weight: 300,
190 input_count: 1,
191 creation_sequence: None,
192 },
193 OutputGroup {
194 value: 2250,
195 weight: 250,
196 input_count: 1,
197 creation_sequence: None,
198 },
199 OutputGroup {
200 value: 190,
201 weight: 220,
202 input_count: 1,
203 creation_sequence: None,
204 },
205 OutputGroup {
206 value: 1750,
207 weight: 170,
208 input_count: 1,
209 creation_sequence: None,
210 },
211 ]
212 }
213
214 fn setup_options(target_value: u64) -> CoinSelectionOpt {
215 CoinSelectionOpt {
216 target_value,
217 target_feerate: 5.0, long_term_feerate: Some(1.0),
219 min_absolute_fee: 100,
220 base_weight: 10,
221 change_weight: 50,
222 change_cost: 10,
223 avg_input_weight: 20,
224 avg_output_weight: 10,
225 min_change_value: 100,
226 excess_strategy: ExcessStrategy::ToRecipient,
227 }
228 }
229
230 #[test]
231 fn test_leastchange_successful() {
232 let inputs = setup_leastchange_output_groups();
233 let options = setup_options(5432);
234 let result = select_coin_bnb_leastchange(&inputs, &options);
235 let selection_output = result.unwrap();
237 assert!(!selection_output.selected_inputs.is_empty());
238 let mut selected = selection_output.selected_inputs.clone();
239 selected.sort();
240 assert_eq!(selected, vec![4, 5, 6, 8, 9]);
241 }
242
243 #[test]
244 fn test_leastchange_insufficient() {
245 let inputs = setup_leastchange_output_groups();
246 let options = setup_options(40000);
247 let result = select_coin_bnb_leastchange(&inputs, &options);
248 assert!(matches!(result, Err(SelectionError::InsufficientFunds)));
249 }
250}