rust_coinselect/algorithms/
lowestlarger.rs1use crate::{
2 types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput, WasteMetric},
3 utils::{calculate_fee, calculate_waste, effective_value},
4};
5
6pub fn select_coin_lowestlarger(
10 inputs: &[OutputGroup],
11 options: &CoinSelectionOpt,
12) -> Result<SelectionOutput, SelectionError> {
13 let mut accumulated_value: u64 = 0;
14 let mut accumulated_weight: u64 = 0;
15 let mut selected_inputs: Vec<usize> = Vec::new();
16 let mut estimated_fees: u64 = 0;
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 let mut sorted_inputs: Vec<_> = inputs.iter().enumerate().collect();
22 sorted_inputs.sort_by_key(|(_, input)| effective_value(input, options.target_feerate));
23
24 let index = sorted_inputs.partition_point(|(_, input)| {
25 input.value
26 <= (target + calculate_fee(input.weight, options.target_feerate).unwrap_or_default())
27 });
28
29 for (idx, input) in sorted_inputs.iter().take(index).rev() {
30 accumulated_value += input.value;
31 accumulated_weight += input.weight;
32 estimated_fees = calculate_fee(accumulated_weight, options.target_feerate)?;
33 selected_inputs.push(*idx);
34
35 if accumulated_value >= (target + estimated_fees) {
36 break;
37 }
38 }
39
40 if accumulated_value < (target + estimated_fees) {
41 for (idx, input) in sorted_inputs.iter().skip(index) {
42 accumulated_value += input.value;
43 accumulated_weight += input.weight;
44 estimated_fees = calculate_fee(accumulated_weight, options.target_feerate)?;
45 selected_inputs.push(*idx);
46
47 if accumulated_value >= (target + estimated_fees.max(options.min_absolute_fee)) {
48 break;
49 }
50 }
51 }
52
53 if accumulated_value < (target + estimated_fees) {
54 Err(SelectionError::InsufficientFunds)
55 } else {
56 let waste: f32 = calculate_waste(
57 options,
58 accumulated_value,
59 accumulated_weight,
60 estimated_fees,
61 );
62 Ok(SelectionOutput {
63 selected_inputs,
64 waste: WasteMetric(waste),
65 })
66 }
67}
68
69#[cfg(test)]
70mod test {
71
72 use crate::{
73 algorithms::lowestlarger::select_coin_lowestlarger,
74 types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
75 };
76
77 fn setup_lowestlarger_output_groups() -> Vec<OutputGroup> {
78 vec![
79 OutputGroup {
80 value: 100,
81 weight: 100,
82 input_count: 1,
83 creation_sequence: None,
84 },
85 OutputGroup {
86 value: 1500,
87 weight: 200,
88 input_count: 1,
89 creation_sequence: None,
90 },
91 OutputGroup {
92 value: 3400,
93 weight: 300,
94 input_count: 1,
95 creation_sequence: None,
96 },
97 OutputGroup {
98 value: 2200,
99 weight: 150,
100 input_count: 1,
101 creation_sequence: None,
102 },
103 OutputGroup {
104 value: 1190,
105 weight: 200,
106 input_count: 1,
107 creation_sequence: None,
108 },
109 OutputGroup {
110 value: 3300,
111 weight: 100,
112 input_count: 1,
113 creation_sequence: None,
114 },
115 OutputGroup {
116 value: 1000,
117 weight: 190,
118 input_count: 1,
119 creation_sequence: None,
120 },
121 OutputGroup {
122 value: 2000,
123 weight: 210,
124 input_count: 1,
125 creation_sequence: None,
126 },
127 OutputGroup {
128 value: 3000,
129 weight: 300,
130 input_count: 1,
131 creation_sequence: None,
132 },
133 OutputGroup {
134 value: 2250,
135 weight: 250,
136 input_count: 1,
137 creation_sequence: None,
138 },
139 OutputGroup {
140 value: 190,
141 weight: 220,
142 input_count: 1,
143 creation_sequence: None,
144 },
145 OutputGroup {
146 value: 1750,
147 weight: 170,
148 input_count: 1,
149 creation_sequence: None,
150 },
151 ]
152 }
153
154 fn setup_options(target_value: u64) -> CoinSelectionOpt {
155 CoinSelectionOpt {
156 target_value,
157 target_feerate: 0.4, long_term_feerate: Some(0.4),
159 min_absolute_fee: 0,
160 base_weight: 10,
161 change_weight: 50,
162 change_cost: 10,
163 avg_input_weight: 20,
164 avg_output_weight: 10,
165 min_change_value: 500,
166 excess_strategy: ExcessStrategy::ToChange,
167 }
168 }
169
170 #[test]
171 fn test_lowestlarger_successful() {
172 let inputs = setup_lowestlarger_output_groups();
173 let options = setup_options(20000);
174 let result = select_coin_lowestlarger(&inputs, &options);
175 assert!(result.is_ok());
176 let selection_output = result.unwrap();
177 assert!(!selection_output.selected_inputs.is_empty());
178 }
179
180 #[test]
181 fn test_lowestlarger_insufficient() {
182 let inputs = setup_lowestlarger_output_groups();
183 let options = setup_options(40000);
184 let result = select_coin_lowestlarger(&inputs, &options);
185 assert!(matches!(result, Err(SelectionError::InsufficientFunds)));
186 }
187}