1use crate::{
2 algorithms::{
3 bnb::select_coin_bnb,
4 fifo::select_coin_fifo,
5 knapsack::select_coin_knapsack,
6 leastchange::select_coin_bnb_leastchange,
7 lowestlarger::select_coin_lowestlarger,
8 },
10 types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput},
11};
12
13type CoinSelectionFn =
17 fn(&[OutputGroup], &CoinSelectionOpt) -> Result<SelectionOutput, SelectionError>;
18
19pub fn select_coin(
20 inputs: &[OutputGroup],
21 options: &CoinSelectionOpt,
22) -> Result<SelectionOutput, SelectionError> {
23 if options.target_value == 0 {
24 return Err(SelectionError::NonPositiveTarget);
25 }
26
27 let mut results = vec![];
28
29 let mut sorted_inputs = inputs.to_vec();
30 sorted_inputs.sort_by(|a, b| a.value.cmp(&b.value));
31
32 let algorithms: Vec<(&str, CoinSelectionFn)> = vec![
33 ("bnb", select_coin_bnb),
34 ("fifo", select_coin_fifo),
36 ("lowestlarger", select_coin_lowestlarger),
37 ("knapsack", select_coin_knapsack),
38 ("leastchange", select_coin_bnb_leastchange), ];
40
41 for (algo_name, algo) in algorithms {
42 if let Ok(result) = algo(inputs, options) {
43 let input_amount = result
44 .selected_inputs
45 .iter()
46 .map(|&idx| inputs[idx].value)
47 .sum::<u64>();
48 let change = input_amount.saturating_sub(options.target_value);
49 results.push((result, change, algo_name));
50 }
51 }
52
53 if results.is_empty() {
54 return Err(SelectionError::InsufficientFunds);
55 }
56
57 let best_result = results
58 .into_iter()
59 .min_by(|a, b| {
60 a.1.cmp(&b.1) .then_with(|| {
62 a.0.waste
63 .0
64 .partial_cmp(&b.0.waste.0)
65 .unwrap_or(std::cmp::Ordering::Equal)
66 }) .then_with(|| a.0.selected_inputs.len().cmp(&b.0.selected_inputs.len()))
68 })
70 .map(|(result, _, _)| result)
71 .expect("No selection results found");
72
73 Ok(best_result)
74}
75
76#[cfg(test)]
77mod test {
78
79 use crate::{
80 selectcoin::select_coin,
81 types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
82 };
83
84 fn setup_basic_output_groups() -> Vec<OutputGroup> {
85 vec![
86 OutputGroup {
87 value: 1_500_000,
88 weight: 50,
89 input_count: 1,
90 creation_sequence: None,
91 },
92 OutputGroup {
93 value: 2_000_000,
94 weight: 200,
95 input_count: 1,
96 creation_sequence: None,
97 },
98 OutputGroup {
99 value: 3_000_000,
100 weight: 300,
101 input_count: 1,
102 creation_sequence: None,
103 },
104 OutputGroup {
105 value: 2_500_000,
106 weight: 100,
107 input_count: 1,
108 creation_sequence: None,
109 },
110 OutputGroup {
111 value: 4_000_000,
112 weight: 150,
113 input_count: 1,
114 creation_sequence: None,
115 },
116 OutputGroup {
117 value: 500_000,
118 weight: 250,
119 input_count: 1,
120 creation_sequence: None,
121 },
122 OutputGroup {
123 value: 6_000_000,
124 weight: 120,
125 input_count: 1,
126 creation_sequence: None,
127 },
128 OutputGroup {
129 value: 70_000,
130 weight: 50,
131 input_count: 1,
132 creation_sequence: None,
133 },
134 OutputGroup {
135 value: 800_000,
136 weight: 60,
137 input_count: 1,
138 creation_sequence: None,
139 },
140 OutputGroup {
141 value: 900_000,
142 weight: 70,
143 input_count: 1,
144 creation_sequence: None,
145 },
146 OutputGroup {
147 value: 100_000,
148 weight: 80,
149 input_count: 1,
150 creation_sequence: None,
151 },
152 OutputGroup {
153 value: 1_000_000,
154 weight: 90,
155 input_count: 1,
156 creation_sequence: None,
157 },
158 ]
159 }
160
161 fn setup_options(target_value: u64) -> CoinSelectionOpt {
162 CoinSelectionOpt {
163 target_value,
164 target_feerate: 2.0, long_term_feerate: Some(0.4),
166 min_absolute_fee: 0,
167 base_weight: 10,
168 change_weight: 50,
169 change_cost: 10,
170 avg_input_weight: 20,
171 avg_output_weight: 10,
172 min_change_value: 500,
173 excess_strategy: ExcessStrategy::ToChange,
174 }
175 }
176
177 #[test]
178 fn test_select_coin_successful() {
179 let inputs = setup_basic_output_groups();
180 let options = setup_options(654321);
181 let result = select_coin(&inputs, &options);
182 assert!(result.is_ok());
183 let selection_output = result.unwrap();
184 assert!(!selection_output.selected_inputs.is_empty());
185
186 let selected_values = selection_output
187 .selected_inputs
188 .iter()
189 .map(|&idx| inputs[idx].value)
190 .collect::<Vec<_>>();
191 eprintln!("Best Result : {:?}", selected_values);
192 }
193
194 #[test]
195 fn test_select_coin_insufficient_funds() {
196 let inputs = setup_basic_output_groups();
197 let options = setup_options(999_999_999); let result = select_coin(&inputs, &options);
199 assert!(matches!(result, Err(SelectionError::InsufficientFunds)));
200 }
201
202 #[test]
203 fn test_select_coin_equals_lowest_larger() {
204 let inputs = vec![
206 OutputGroup {
207 value: 500,
208 weight: 50,
209 input_count: 1,
210 creation_sequence: None,
211 },
212 OutputGroup {
213 value: 1500,
214 weight: 100,
215 input_count: 1,
216 creation_sequence: None,
217 },
218 OutputGroup {
219 value: 2000,
220 weight: 200,
221 input_count: 1,
222 creation_sequence: None,
223 },
224 OutputGroup {
225 value: 1000,
226 weight: 75,
227 input_count: 1,
228 creation_sequence: None,
229 },
230 ];
231
232 let options = CoinSelectionOpt {
234 target_value: 1600, target_feerate: 0.4,
236 long_term_feerate: Some(0.4),
237 min_absolute_fee: 0,
238 base_weight: 10,
239 change_weight: 50,
240 change_cost: 10,
241 avg_input_weight: 50,
242 avg_output_weight: 25,
243 min_change_value: 500,
244 excess_strategy: ExcessStrategy::ToChange,
245 };
246
247 let selection_result = select_coin(&inputs, &options).unwrap();
249
250 let expected_inputs = vec![2]; let mut selection_inputs = selection_result.selected_inputs.clone();
255 let mut expected_inputs_sorted = expected_inputs.clone();
256 selection_inputs.sort();
257 expected_inputs_sorted.sort();
258 }
259
260 #[test]
261 fn test_select_coin_equals_knapsack() {
262 let inputs = vec![
264 OutputGroup {
265 value: 1500,
266 weight: 1,
267 input_count: 1,
268 creation_sequence: None,
269 },
270 OutputGroup {
271 value: 2500,
272 weight: 1,
273 input_count: 1,
274 creation_sequence: None,
275 },
276 OutputGroup {
277 value: 3000,
278 weight: 1,
279 input_count: 1,
280 creation_sequence: None,
281 },
282 OutputGroup {
283 value: 1000,
284 weight: 1,
285 input_count: 1,
286 creation_sequence: None,
287 },
288 OutputGroup {
289 value: 500,
290 weight: 1,
291 input_count: 1,
292 creation_sequence: None,
293 },
294 ];
295
296 let options = CoinSelectionOpt {
298 target_value: 4000, target_feerate: 1.0,
300 min_absolute_fee: 0,
301 base_weight: 1,
302 change_weight: 1,
303 change_cost: 1,
304 avg_input_weight: 1,
305 avg_output_weight: 1,
306 min_change_value: 500,
307 long_term_feerate: Some(0.5),
308 excess_strategy: ExcessStrategy::ToChange,
309 };
310
311 let selection_result = select_coin(&inputs, &options).unwrap();
312
313 let expected_inputs = vec![1, 3]; let mut selection_inputs = selection_result.selected_inputs.clone();
323 let mut expected_inputs_sorted = expected_inputs.clone();
324 selection_inputs.sort();
325 expected_inputs_sorted.sort();
326 }
327
328 #[test]
329 fn test_select_coin_equals_fifo() {
330 fn create_fifo_inputs(values: Vec<u64>) -> Vec<OutputGroup> {
332 values
333 .into_iter()
334 .map(|value| OutputGroup {
335 value,
336 weight: 100,
337 input_count: 1,
338 creation_sequence: None,
339 })
340 .collect()
341 }
342
343 let options_case = CoinSelectionOpt {
344 target_value: 250000,
345 target_feerate: 1.0,
346 min_absolute_fee: 0,
347 base_weight: 100,
348 change_weight: 10,
349 change_cost: 20,
350 avg_input_weight: 10,
351 avg_output_weight: 10,
352 min_change_value: 400,
353 long_term_feerate: Some(0.5),
354 excess_strategy: ExcessStrategy::ToChange,
355 };
356
357 let inputs_case = create_fifo_inputs(vec![80000, 70000, 60000, 50000, 40000, 30000]);
358
359 let result_case = select_coin(&inputs_case, &options_case).unwrap();
360 let expected_case = vec![0, 1, 2, 3]; assert_eq!(result_case.selected_inputs, expected_case);
362 }
363
364 #[test]
365 fn test_select_coin_equals_bnb() {
366 let inputs = vec![
367 OutputGroup {
368 value: 150000,
369 weight: 100,
370 input_count: 1,
371 creation_sequence: None,
372 },
373 OutputGroup {
374 value: 250000,
375 weight: 100,
376 input_count: 1,
377 creation_sequence: None,
378 },
379 OutputGroup {
380 value: 300000,
381 weight: 100,
382 input_count: 1,
383 creation_sequence: None,
384 },
385 OutputGroup {
386 value: 100000,
387 weight: 100,
388 input_count: 1,
389 creation_sequence: None,
390 },
391 OutputGroup {
392 value: 50000,
393 weight: 100,
394 input_count: 1,
395 creation_sequence: None,
396 },
397 ];
398 let opt = CoinSelectionOpt {
399 target_value: 500000,
400 target_feerate: 1.0,
401 min_absolute_fee: 0,
402 base_weight: 100,
403 change_weight: 10,
404 change_cost: 20,
405 avg_input_weight: 10,
406 avg_output_weight: 10,
407 min_change_value: 400,
408 long_term_feerate: Some(0.5),
409 excess_strategy: ExcessStrategy::ToChange,
410 };
411 let ans = select_coin(&inputs, &opt);
412
413 if let Ok(selection_output) = ans {
414 let mut selected_inputs = selection_output.selected_inputs.clone();
415 selected_inputs.sort();
416
417 let expected_solution = vec![1, 2];
424 assert_eq!(
425 selected_inputs, expected_solution,
426 "Expected solution {:?}, but got {:?}",
427 expected_solution, selected_inputs
428 );
429 }
430 }
431
432 #[test]
433 fn test_select_coin_equals_leastchange_bnb() {
434 let inputs = vec![
436 OutputGroup {
437 value: 1000,
438 weight: 10,
439 input_count: 1,
440 creation_sequence: None,
441 },
442 OutputGroup {
443 value: 2000,
444 weight: 10,
445 input_count: 1,
446 creation_sequence: None,
447 },
448 OutputGroup {
449 value: 3000,
450 weight: 10,
451 input_count: 1,
452 creation_sequence: None,
453 },
454 OutputGroup {
455 value: 4000,
456 weight: 10,
457 input_count: 1,
458 creation_sequence: None,
459 },
460 OutputGroup {
461 value: 5500,
462 weight: 10,
463 input_count: 1,
464 creation_sequence: None,
465 },
466 ];
467
468 let options = CoinSelectionOpt {
469 target_value: 12000,
470 target_feerate: 1.0,
471 min_absolute_fee: 0,
472 base_weight: 100,
473 change_weight: 10,
474 change_cost: 20,
475 avg_input_weight: 10,
476 avg_output_weight: 10,
477 min_change_value: 400,
478 long_term_feerate: Some(0.5),
479 excess_strategy: ExcessStrategy::ToChange,
480 };
481
482 let result = select_coin(&inputs, &options);
483 assert!(result.is_ok());
484 let selection_output = result.unwrap();
485 assert!(!selection_output.selected_inputs.is_empty());
486
487 let mut selected = selection_output.selected_inputs.clone();
488 selected.sort();
489 assert_eq!(selected, vec![0, 2, 3, 4]);
490 }
491}