rust_coinselect/algorithms/
srd.rs

1use crate::{
2    types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput, WasteMetric},
3    utils::{calculate_fee, calculate_waste},
4};
5use rand::{seq::SliceRandom, thread_rng};
6
7/// Performs coin selection using a single random draw.
8///
9/// Returns `NoSolutionFound` if no solution is found.
10pub fn select_coin_srd(
11    inputs: &[OutputGroup],
12    options: &CoinSelectionOpt,
13) -> Result<SelectionOutput, SelectionError> {
14    // In out put we need to specify the indexes of the inputs in the given order
15    // So keep track of the indexes when randomiz ing the vec
16    let mut randomized_inputs: Vec<_> = inputs.iter().enumerate().collect();
17    let base_fees = calculate_fee(options.base_weight, options.target_feerate).unwrap_or_default();
18    let target =
19        options.target_value + options.min_change_value + base_fees.max(options.min_absolute_fee);
20
21    // Randomize the inputs order to simulate the random draw
22    let mut rng = thread_rng();
23    randomized_inputs.shuffle(&mut rng);
24
25    let mut accumulated_value = 0;
26    let mut selected_inputs = Vec::new();
27    let mut accumulated_weight = 0;
28    let mut estimated_fee = 0;
29    let mut _input_counts = 0;
30
31    for (index, input) in randomized_inputs {
32        selected_inputs.push(index);
33        accumulated_value += input.value;
34        accumulated_weight += input.weight;
35        _input_counts += input.input_count;
36
37        estimated_fee = calculate_fee(accumulated_weight, options.target_feerate)?;
38
39        if accumulated_value >= target + estimated_fee {
40            break;
41        }
42    }
43
44    if accumulated_value < target + estimated_fee {
45        return Err(SelectionError::InsufficientFunds);
46    }
47    let waste = calculate_waste(
48        options,
49        accumulated_value,
50        accumulated_weight,
51        estimated_fee,
52    );
53
54    Ok(SelectionOutput {
55        selected_inputs,
56        waste: WasteMetric(waste),
57    })
58}
59
60#[cfg(test)]
61mod test {
62
63    use crate::{
64        algorithms::srd::select_coin_srd,
65        types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
66    };
67
68    fn setup_basic_output_groups() -> Vec<OutputGroup> {
69        vec![
70            OutputGroup {
71                value: 1000,
72                weight: 100,
73                input_count: 1,
74                creation_sequence: None,
75            },
76            OutputGroup {
77                value: 2000,
78                weight: 200,
79                input_count: 1,
80                creation_sequence: None,
81            },
82            OutputGroup {
83                value: 3000,
84                weight: 300,
85                input_count: 1,
86                creation_sequence: None,
87            },
88        ]
89    }
90
91    fn setup_output_groups_withsequence() -> Vec<OutputGroup> {
92        vec![
93            OutputGroup {
94                value: 1000,
95                weight: 100,
96                input_count: 1,
97                creation_sequence: Some(1),
98            },
99            OutputGroup {
100                value: 2000,
101                weight: 200,
102                input_count: 1,
103                creation_sequence: Some(5000),
104            },
105            OutputGroup {
106                value: 3000,
107                weight: 300,
108                input_count: 1,
109                creation_sequence: Some(1001),
110            },
111            OutputGroup {
112                value: 1500,
113                weight: 150,
114                input_count: 1,
115                creation_sequence: None,
116            },
117        ]
118    }
119
120    fn setup_options(target_value: u64) -> CoinSelectionOpt {
121        CoinSelectionOpt {
122            target_value,
123            target_feerate: 0.4, // Simplified feerate
124            long_term_feerate: Some(0.4),
125            min_absolute_fee: 0,
126            base_weight: 10,
127            change_weight: 50,
128            change_cost: 10,
129            avg_input_weight: 20,
130            avg_output_weight: 10,
131            min_change_value: 500,
132            excess_strategy: ExcessStrategy::ToChange,
133        }
134    }
135
136    fn test_successful_selection() {
137        let mut inputs = setup_basic_output_groups();
138        let mut options = setup_options(2500);
139        let mut result = select_coin_srd(&inputs, &options);
140        assert!(result.is_ok());
141        let mut selection_output = result.unwrap();
142        assert!(!selection_output.selected_inputs.is_empty());
143
144        inputs = setup_output_groups_withsequence();
145        options = setup_options(500);
146        result = select_coin_srd(&inputs, &options);
147        assert!(result.is_ok());
148        selection_output = result.unwrap();
149        assert!(!selection_output.selected_inputs.is_empty());
150    }
151
152    fn test_insufficient_funds() {
153        let inputs = setup_basic_output_groups();
154        let options = setup_options(7000); // Set a target value higher than the sum of all inputs
155        let result = select_coin_srd(&inputs, &options);
156        assert!(matches!(result, Err(SelectionError::InsufficientFunds)));
157    }
158
159    #[test]
160    fn test_srd() {
161        test_successful_selection();
162        test_insufficient_funds();
163    }
164}