1use std::thread;
2
3use crate::{
4 algorithms::{
5 bnb::select_coin_bnb, coingrinder::select_coin_coingrinder, fifo::select_coin_fifo,
6 lowestlarger::select_coin_lowestlarger,
7 },
8 types::{CoinSelectionOpt, OutputGroup, SelectionAlgorithm, SelectionError, SelectionOutput},
9 utils::insufficient_funds,
10};
11
12type CoinSelectionFn =
14 fn(&[OutputGroup], &CoinSelectionOpt) -> Result<SelectionOutput, SelectionError>;
15
16const ALGORITHMS: [(SelectionAlgorithm, CoinSelectionFn); 4] = [
18 (SelectionAlgorithm::BranchAndBound, select_coin_bnb),
19 (SelectionAlgorithm::CoinGrinder, select_coin_coingrinder),
20 (SelectionAlgorithm::Fifo, select_coin_fifo),
21 (SelectionAlgorithm::LowestLarger, select_coin_lowestlarger),
22];
23
24pub fn select_coin(
32 inputs: &[OutputGroup],
33 options: &CoinSelectionOpt,
34) -> Result<Vec<(SelectionAlgorithm, SelectionOutput)>, SelectionError> {
35 let outcomes: Vec<(SelectionAlgorithm, Result<SelectionOutput, SelectionError>)> =
37 thread::scope(|scope| {
38 let handles: Vec<_> = ALGORITHMS
39 .into_iter()
40 .map(|(name, algo)| scope.spawn(move || (name, algo(inputs, options))))
41 .collect();
42 handles
43 .into_iter()
44 .map(|handle| {
46 handle.join().unwrap_or((
47 SelectionAlgorithm::BranchAndBound,
48 Err(SelectionError::NoSolutionFound),
49 ))
50 })
51 .collect()
52 });
53
54 let mut results = Vec::new();
55 for (name, outcome) in outcomes {
56 match outcome {
57 Ok(result) => results.push((name, result)),
58 Err(
59 error @ (SelectionError::NonPositiveTarget
60 | SelectionError::NonPositiveFeeRate
61 | SelectionError::AbnormallyHighFeeRate),
62 ) => return Err(error),
63 Err(SelectionError::InsufficientFunds { .. } | SelectionError::NoSolutionFound) => {
64 continue
65 }
66 }
67 }
68
69 if results.is_empty() {
70 return Err(insufficient_funds(inputs, options));
71 }
72
73 results.sort_by_key(|(_, output)| {
75 let total_input_count = output
76 .selected_inputs
77 .iter()
78 .map(|&idx| inputs[idx].input_count)
79 .sum::<usize>();
80 (
81 total_input_count,
82 output.selected_inputs.len(),
83 output.waste.0,
84 )
85 });
86 Ok(results)
87}
88
89#[cfg(test)]
90mod test {
91
92 use crate::{
93 algorithms::{
94 bnb::select_coin_bnb, coingrinder::select_coin_coingrinder, fifo::select_coin_fifo,
95 lowestlarger::select_coin_lowestlarger,
96 },
97 selectcoin::select_coin,
98 types::{
99 basic_output_group, CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError,
100 SelectionOutput,
101 },
102 utils::calculate_fee,
103 };
104
105 fn setup_basic_output_groups() -> Vec<OutputGroup> {
106 vec![
107 basic_output_group(1_500_000, 50),
108 basic_output_group(2_000_000, 200),
109 basic_output_group(3_000_000, 300),
110 basic_output_group(2_500_000, 100),
111 basic_output_group(4_000_000, 150),
112 basic_output_group(500_000, 250),
113 basic_output_group(6_000_000, 120),
114 basic_output_group(70_000, 50),
115 basic_output_group(800_000, 60),
116 basic_output_group(900_000, 70),
117 basic_output_group(100_000, 80),
118 basic_output_group(1_000_000, 90),
119 ]
120 }
121
122 fn setup_options(target_value: u64) -> CoinSelectionOpt {
123 CoinSelectionOpt {
124 target_value,
125 target_feerate: 2.0, long_term_feerate: Some(0.4),
127 min_absolute_fee: 0,
128 base_weight: 10,
129 change_weight: 50,
130 change_cost: 10,
131 min_change_value: 500,
132 excess_strategy: ExcessStrategy::ToChange,
133 }
134 }
135
136 fn assert_covers_target(
138 inputs: &[OutputGroup],
139 options: &CoinSelectionOpt,
140 selected: &[usize],
141 ) {
142 let value: u64 = selected.iter().map(|&i| inputs[i].value).sum();
143 let selected_weight: u64 = selected.iter().map(|&i| inputs[i].weight).sum();
144 let total_fee = calculate_fee(
145 options.base_weight + selected_weight,
146 options.target_feerate,
147 )
148 .max(options.min_absolute_fee);
149 assert!(
150 value >= options.target_value + total_fee,
151 "selection {:?} (value {}) does not cover target {} + fee {}",
152 selected,
153 value,
154 options.target_value,
155 total_fee
156 );
157 }
158
159 #[test]
160 fn test_select_coin_rejects_zero_target() {
161 let inputs = setup_basic_output_groups();
162 let options = setup_options(0);
163 let result = select_coin(&inputs, &options);
164 assert!(matches!(result, Err(SelectionError::NonPositiveTarget)));
165 }
166
167 #[test]
168 fn test_select_coin_successful() {
169 let inputs = setup_basic_output_groups();
170 let options = setup_options(654321);
171 let result = select_coin(&inputs, &options);
172 assert!(result.is_ok());
173 let ranked = result.unwrap();
174 let best = &ranked[0].1;
175 assert!(!best.selected_inputs.is_empty());
176 assert_covers_target(&inputs, &options, &best.selected_inputs);
177 }
178
179 #[test]
180 fn test_select_coin_insufficient_funds() {
181 let inputs = setup_basic_output_groups();
182 let options = setup_options(999_999_999); let result = select_coin(&inputs, &options);
184 assert!(matches!(
185 result,
186 Err(SelectionError::InsufficientFunds {
187 available: 22_370_000,
188 required: 1_000_003_059,
189 })
190 ));
191 }
192
193 #[test]
194 fn test_select_coin_keeps_success_when_another_algorithm_is_insufficient() {
195 let inputs = vec![basic_output_group(1_000, 0)];
196 let mut options = setup_options(1_000);
197 options.target_feerate = 1.0;
198 options.long_term_feerate = Some(1.0);
199 options.base_weight = 0;
200 options.change_cost = 20;
201 options.min_change_value = 100;
202
203 let ranked = select_coin(&inputs, &options).expect("BnB should find the exact match");
204 assert_eq!(ranked[0].1.selected_inputs, vec![0]);
205 }
206
207 #[test]
210 fn test_select_coin_returns_minimum_inputs() {
211 let inputs = setup_basic_output_groups();
212 let options = setup_options(654321);
213
214 let individual_min_inputs = [
215 select_coin_bnb(&inputs, &options),
216 select_coin_coingrinder(&inputs, &options),
217 select_coin_fifo(&inputs, &options),
218 select_coin_lowestlarger(&inputs, &options),
219 ]
220 .into_iter()
221 .filter_map(|r| r.ok())
222 .map(|r| r.selected_inputs.len())
223 .min()
224 .expect("at least one algorithm should succeed");
225
226 let ranked = select_coin(&inputs, &options).expect("selection should succeed");
227 let chosen = &ranked[0].1;
228 assert_eq!(
229 chosen.selected_inputs.len(),
230 individual_min_inputs,
231 "wrapper did not return the minimum-input selection"
232 );
233 assert_covers_target(&inputs, &options, &chosen.selected_inputs);
234 }
235
236 #[test]
239 fn test_select_coin_orders_best_first() {
240 let inputs = setup_basic_output_groups();
241 let options = setup_options(654321);
242
243 let ranked = select_coin(&inputs, &options).expect("selection should succeed");
244 assert!(!ranked.is_empty());
245
246 let key = |output: &SelectionOutput| {
248 let total_input_count: usize = output
249 .selected_inputs
250 .iter()
251 .map(|&idx| inputs[idx].input_count)
252 .sum();
253 (
254 total_input_count,
255 output.selected_inputs.len(),
256 output.waste.0,
257 )
258 };
259 let keys: Vec<_> = ranked.iter().map(|(_, output)| key(output)).collect();
260 assert!(
261 keys.windows(2).all(|w| w[0] <= w[1]),
262 "ranked results are not ordered best-first: {keys:?}"
263 );
264 }
265}