rust_coinselect/
selectcoin.rs

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        // srd::select_coin_srd,
9    },
10    types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput},
11};
12
13/// The global coin selection API that applies all algorithms and produces the result with the lowest [WasteMetric].
14///
15/// At least one selection solution should be found.
16type 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        // ("srd", select_coin_srd),
35        ("fifo", select_coin_fifo),
36        ("lowestlarger", select_coin_lowestlarger),
37        ("knapsack", select_coin_knapsack),
38        ("leastchange", select_coin_bnb_leastchange), // Future algorithms can be added here
39    ];
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) // Compare change amount first (a.1 vs b.1)
61                .then_with(|| {
62                    a.0.waste
63                        .0
64                        .partial_cmp(&b.0.waste.0)
65                        .unwrap_or(std::cmp::Ordering::Equal)
66                }) // Then compare waste
67                .then_with(|| a.0.selected_inputs.len().cmp(&b.0.selected_inputs.len()))
68            // Finally compare number of inputs
69        })
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, // Simplified feerate
165            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); // Set a target value higher than the sum of all inputs
198        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        // Define the inputs such that the lowest_larger algorithm should be optimal
205        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        // Define the target selection options
233        let options = CoinSelectionOpt {
234            target_value: 1600, // Target value which lowest_larger can satisfy
235            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        // Call the select_coin function, which should internally use the lowest_larger algorithm
248        let selection_result = select_coin(&inputs, &options).unwrap();
249
250        // Deterministically choose a result based on how lowest_larger would select
251        let expected_inputs = vec![2]; // Example choice based on lowest_larger logic
252
253        // Sort the selected inputs to ignore the order
254        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        // Define inputs that are best suited for knapsack algorithm to match the target value with minimal waste
263        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        // Define the target selection options
297        let options = CoinSelectionOpt {
298            target_value: 4000, // Set a target that knapsack can match efficiently
299            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        // Deterministically choose a result with justification
314        // Here, we assume that the `select_coin` function internally chooses the most efficient set
315        // of inputs that meet the `target_value` while minimizing waste. This selection is deterministic
316        // given the same inputs and options. Therefore, the following assertions are based on
317        // the assumption that the chosen inputs are correct and optimized.
318
319        let expected_inputs = vec![1, 3]; // Example deterministic choice, adjust as needed
320
321        // Sort the selected inputs to ignore the order
322        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        // Helper function to create OutputGroups
331        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]; // Indexes of oldest UTXOs that sum to target
361        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            // The expected solution is vec![1, 2] because the combined value of the selected inputs
418            // (250000 + 300000) meets the target value of 500000 with minimal excess. This selection
419            // minimizes waste and adheres to the constraints of the coin selection algorithm, which
420            // aims to find the most optimal solution.
421            // Branch and Bound also gives a better time complexity, referenced from Mark Erhardt's Master Thesis.
422
423            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        // Inputs designed so that only one combination gives minimal change
435        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}