1#![deny(non_upper_case_globals)]
7#![deny(non_camel_case_types)]
8#![deny(non_snake_case)]
9#![deny(unused_mut)]
10#![deny(missing_docs)]
11#![cfg_attr(docsrs, feature(doc_cfg))]
13
14mod branch_and_bound;
15mod coin_grinder;
16mod single_random_draw;
17
18mod weighted_utxo;
19
20pub use crate::weighted_utxo::WeightedUtxo;
21
22pub mod errors;
24
25use bitcoin_units::{Amount, FeeRate, SignedAmount, Weight};
26#[cfg(feature = "rand")]
27#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
28use rand::thread_rng;
29
30pub use crate::branch_and_bound::branch_and_bound;
31pub use crate::coin_grinder::coin_grinder;
32use crate::errors::{OverflowError, SelectionError};
33#[cfg(feature = "rand")]
34#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
35pub use crate::single_random_draw::single_random_draw;
36
37pub(crate) type Return<'a> = Result<(u32, Vec<&'a WeightedUtxo>), SelectionError>;
38
39const CHANGE_LOWER: Amount = Amount::from_sat_u32(50_000);
41
42pub(crate) fn effective_value(
56 fee_rate: FeeRate,
57 weight: Weight,
58 value: Amount,
59) -> Option<SignedAmount> {
60 let signed_input_fee: SignedAmount = fee_rate.to_fee(weight).to_signed();
61 let eff_value = (value.to_signed() - signed_input_fee).unwrap();
62 Some(eff_value)
63}
64
65#[cfg(feature = "rand")]
90#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
91pub fn select_coins<'a, T: IntoIterator<Item = &'a WeightedUtxo> + std::marker::Copy>(
92 target: Amount,
93 cost_of_change: Amount,
94 max_weight: Weight,
95 weighted_utxos: T,
96) -> Return<'a> {
97 let bnb_result = branch_and_bound(target, cost_of_change, max_weight, weighted_utxos);
98
99 if bnb_result.is_err() {
100 single_random_draw(target, max_weight, &mut thread_rng(), weighted_utxos)
101 } else {
102 bnb_result
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use std::str::FromStr;
109
110 use arbitrary::{Arbitrary, Result, Unstructured};
111 use arbtest::arbtest;
112 use bitcoin_units::{Amount, Weight};
113
114 use super::*;
115 use crate::SelectionError::{InsufficentFunds, Overflow, ProgramError};
116
117 pub fn build_pool() -> Vec<WeightedUtxo> {
118 let amts = [27_336, 238, 9_225, 20_540, 35_590, 49_463, 6_331, 35_548, 50_363, 28_009];
119 let weight = Weight::ZERO;
120 let fee_rate = FeeRate::ZERO;
121 let lt_fee_rate = FeeRate::ZERO;
122
123 let utxos: Vec<_> = amts
124 .iter()
125 .filter_map(|a| {
126 let amt = Amount::from_sat_u32(*a);
127 WeightedUtxo::new(amt, weight, fee_rate, lt_fee_rate)
128 })
129 .collect();
130
131 utxos
132 }
133
134 pub fn assert_ref_eq(inputs: Vec<&WeightedUtxo>, expected: Vec<WeightedUtxo>) {
135 let expected_ref: Vec<&WeightedUtxo> = expected.iter().collect();
136 assert_eq!(inputs, expected_ref);
137 }
138
139 pub fn assert_target_selection(
140 utxos: &Vec<&WeightedUtxo>,
141 target: Amount,
142 upper_bound: Option<Amount>,
143 ) {
144 let utxos: Vec<WeightedUtxo> = utxos.iter().map(|&u| u.clone()).collect();
145 let eff_value_sum = Selection::effective_value_sum(&utxos).unwrap();
146 assert!(eff_value_sum >= target);
147
148 if let Some(ub) = upper_bound {
149 assert!(eff_value_sum <= ub);
150 }
151 }
152
153 pub(crate) fn parse_fee_rate(f: &str) -> FeeRate {
154 let rate_parts: Vec<_> = f.split(" ").collect();
155 let rate = rate_parts[0].parse::<u32>().unwrap();
156
157 match rate_parts.len() {
158 1 => {
159 assert!(rate == 0, "Try adding sat/kwu or sat/vB to fee_rate");
160 FeeRate::ZERO
161 }
162
163 2 => match rate_parts[1] {
164 "sat/kwu" => FeeRate::from_sat_per_kwu(rate),
165 "sat/vB" => FeeRate::from_sat_per_vb(rate),
166 "0" => FeeRate::ZERO,
167 _ => panic!("only support sat/kwu or sat/vB rates"),
168 },
169
170 _ => panic!("number, space then rate not parsed. example: 10 sat/kwu"),
171 }
172 }
173
174 #[derive(Debug)]
175 pub struct Selection {
176 pub utxos: Vec<WeightedUtxo>,
177 pub fee_rate: FeeRate,
178 pub long_term_fee_rate: FeeRate,
179 }
180
181 impl<'a> Arbitrary<'a> for Selection {
182 fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
183 let init: Vec<(Amount, Weight)> = Vec::arbitrary(u)?;
184 let fee_rate = FeeRate::arbitrary(u)?;
185 let long_term_fee_rate = FeeRate::arbitrary(u)?;
186 let utxos: Vec<WeightedUtxo> = init
187 .iter()
188 .filter_map(|i| WeightedUtxo::new(i.0, i.1, fee_rate, long_term_fee_rate))
189 .collect();
190
191 Ok(Selection { utxos, fee_rate, long_term_fee_rate })
192 }
193 }
194
195 fn parse_weight(weight: &str) -> Weight {
197 let size_parts: Vec<_> = weight.split(" ").collect();
198 let size_int = size_parts[0].parse::<u64>().unwrap();
199 match size_parts[1] {
200 "wu" => Weight::from_wu(size_int),
201 "vB" => Weight::from_vb(size_int).unwrap(),
202 _ => panic!("only support wu or vB sizes"),
203 }
204 }
205
206 impl Selection {
207 pub fn new(utxos: &[&str], fee_rate: FeeRate, long_term_fee_rate: FeeRate) -> Selection {
208 let utxos: Vec<_> = utxos
209 .iter()
210 .filter_map(|u| {
211 let val_with_size: Vec<_> = u.split("/").collect();
212 let weight = parse_weight(val_with_size[1]);
213 let val = val_with_size[0];
214
215 let abs_val = if val.starts_with("e") {
216 let val = val.replace("e(", "").replace(")", "");
217 let eff_value = SignedAmount::from_str(&val).unwrap();
218 compute_absolute_value(eff_value, weight, fee_rate)
219 } else {
220 Amount::from_str(val).unwrap()
221 };
222
223 WeightedUtxo::new(abs_val, weight, fee_rate, long_term_fee_rate)
224 })
225 .collect();
226
227 Selection { utxos, fee_rate, long_term_fee_rate }
228 }
229
230 fn effective_value_sum(utxos: &[WeightedUtxo]) -> Option<Amount> {
231 utxos.iter().map(|u| u.effective_value()).try_fold(Amount::ZERO, Amount::checked_add)
232 }
233
234 pub fn available_value(&self) -> Option<Amount> { Self::effective_value_sum(&self.utxos) }
235
236 pub fn weight_total(&self) -> Option<Weight> {
237 self.utxos.iter().map(|u| u.weight()).try_fold(Weight::ZERO, Weight::checked_add)
238 }
239 }
240
241 pub fn compute_absolute_value(
242 effective_value: SignedAmount,
243 weight: Weight,
244 fee_rate: FeeRate,
245 ) -> Amount {
246 let signed_fee = fee_rate.fee_wu(weight).unwrap().to_signed();
247 let signed_absolute_value = (effective_value + signed_fee).unwrap();
248 signed_absolute_value.to_unsigned().unwrap()
249 }
250
251 #[test]
252 fn select_coins_no_solution() {
253 let target = Amount::from_sat_u32(255432);
255 let cost_of_change = Amount::ZERO;
256 let max_weight = Weight::from_wu(40_000);
257 let pool = build_pool(); let result = select_coins(target, cost_of_change, max_weight, &pool);
260
261 match result {
262 Err(crate::SelectionError::InsufficentFunds) => {}
263 _ => panic!("un-expected result: {:?}", result),
264 }
265 }
266
267 #[test]
268 fn select_coins_srd_solution() {
269 let target = (Amount::from_sat_u32(255432) - CHANGE_LOWER).unwrap();
270 let cost_of_change = Amount::ZERO;
271 let max_weight = Weight::from_wu(40_000);
272 let pool = build_pool();
273
274 let result = select_coins(target, cost_of_change, max_weight, &pool);
275 let (_iterations, utxos) = result.unwrap();
276 let sum: Amount = utxos
277 .into_iter()
278 .map(|u| u.value())
279 .try_fold(Amount::ZERO, Amount::checked_add)
280 .unwrap();
281 assert!(sum > target);
282 }
283
284 #[test]
285 fn select_coins_bnb_solution() {
286 let target = Amount::from_sat_u32(255432);
287 let max_weight = Weight::from_wu(40_000);
288 let pool = build_pool();
289
290 let cost_of_change = Amount::from_sat_u32(7211);
295
296 let result = select_coins(target, cost_of_change, max_weight, &pool);
297 let (iterations, utxos) = result.unwrap();
298 let sum: Amount = utxos
299 .into_iter()
300 .map(|u| u.value())
301 .try_fold(Amount::ZERO, Amount::checked_add)
302 .unwrap();
303 assert!(sum > target);
304 assert!(sum <= (target + cost_of_change).unwrap());
305 assert_eq!(16, iterations);
306 }
307
308 #[test]
309 fn select_coins_proptest() {
310 arbtest(|u| {
311 let candidate_selection = Selection::arbitrary(u)?;
312 let target = Amount::arbitrary(u)?;
313 let cost_of_change = Amount::arbitrary(u)?;
314 let max_weight = Weight::arbitrary(u)?;
315
316 let candidate_utxos = candidate_selection.utxos.clone();
317 let result = select_coins(target, cost_of_change, max_weight, &candidate_utxos);
318
319 match result {
320 Ok((i, utxos)) => {
321 assert!(i > 0);
322 crate::tests::assert_target_selection(&utxos, target, None);
323 }
324 Err(InsufficentFunds) => {
325 let available_value = candidate_selection.available_value().unwrap();
326 assert!(available_value < (target + CHANGE_LOWER).unwrap());
327 }
328 Err(Overflow(_)) => {
329 let available_value = candidate_selection.available_value();
330 let weight_total = candidate_selection.weight_total();
331 assert!(
332 available_value.is_none()
333 || weight_total.is_none()
334 || target.checked_add(CHANGE_LOWER).is_none()
335 );
336 }
337 Err(ProgramError) => panic!("un-expected program error"),
338 _ => {}
339 }
340
341 Ok(())
342 });
343 }
344}