1use crate::wallet::utils::IsDust;
105use crate::Utxo;
106use crate::WeightedUtxo;
107use bitcoin::{Amount, FeeRate, SignedAmount};
108
109use alloc::vec::Vec;
110use bitcoin::consensus::encode::serialize;
111use bitcoin::TxIn;
112use bitcoin::{Script, Weight};
113
114use core::convert::TryInto;
115use core::fmt::{self, Formatter};
116use rand_core::RngCore;
117
118use super::utils::shuffle_slice;
119pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection<SingleRandomDraw>;
122
123#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct InsufficientFunds {
128 pub needed: Amount,
130 pub available: Amount,
132}
133
134impl fmt::Display for InsufficientFunds {
135 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
136 write!(
137 f,
138 "Insufficient funds: {} available of {} needed",
139 self.available, self.needed
140 )
141 }
142}
143
144#[cfg(feature = "std")]
145impl std::error::Error for InsufficientFunds {}
146
147#[derive(Debug)]
148pub enum Excess {
150 NoChange {
152 dust_threshold: Amount,
154 remaining_amount: Amount,
156 change_fee: Amount,
158 },
159 Change {
161 amount: Amount,
163 fee: Amount,
165 },
166}
167
168#[derive(Debug)]
170pub struct CoinSelectionResult {
171 pub selected: Vec<Utxo>,
173 pub fee_amount: Amount,
175 pub excess: Excess,
177}
178
179impl CoinSelectionResult {
180 pub fn selected_amount(&self) -> Amount {
182 self.selected.iter().map(|u| u.txout().value).sum()
183 }
184
185 pub fn local_selected_amount(&self) -> Amount {
187 self.selected
188 .iter()
189 .filter_map(|u| match u {
190 Utxo::Local(_) => Some(u.txout().value),
191 _ => None,
192 })
193 .sum()
194 }
195}
196
197pub trait CoinSelectionAlgorithm: core::fmt::Debug {
204 fn coin_select<R: RngCore>(
217 &self,
218 required_utxos: Vec<WeightedUtxo>,
219 optional_utxos: Vec<WeightedUtxo>,
220 fee_rate: FeeRate,
221 target_amount: Amount,
222 drain_script: &Script,
223 rand: &mut R,
224 ) -> Result<CoinSelectionResult, InsufficientFunds>;
225}
226
227#[derive(Debug, Default, Clone, Copy)]
232pub struct LargestFirstCoinSelection;
233
234impl CoinSelectionAlgorithm for LargestFirstCoinSelection {
235 fn coin_select<R: RngCore>(
236 &self,
237 required_utxos: Vec<WeightedUtxo>,
238 mut optional_utxos: Vec<WeightedUtxo>,
239 fee_rate: FeeRate,
240 target_amount: Amount,
241 drain_script: &Script,
242 _: &mut R,
243 ) -> Result<CoinSelectionResult, InsufficientFunds> {
244 let utxos = {
247 optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
248 required_utxos
249 .into_iter()
250 .map(|utxo| (true, utxo))
251 .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
252 };
253
254 select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
255 }
256}
257
258#[derive(Debug, Default, Clone, Copy)]
264pub struct OldestFirstCoinSelection;
265
266impl CoinSelectionAlgorithm for OldestFirstCoinSelection {
267 fn coin_select<R: RngCore>(
268 &self,
269 required_utxos: Vec<WeightedUtxo>,
270 mut optional_utxos: Vec<WeightedUtxo>,
271 fee_rate: FeeRate,
272 target_amount: Amount,
273 drain_script: &Script,
274 _: &mut R,
275 ) -> Result<CoinSelectionResult, InsufficientFunds> {
276 let utxos = {
281 optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
282 Utxo::Local(local) => (false, Some(local.chain_position)),
283 Utxo::Foreign { .. } => (true, None),
284 });
285
286 required_utxos
287 .into_iter()
288 .map(|utxo| (true, utxo))
289 .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
290 };
291
292 select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
293 }
294}
295
296pub fn decide_change(remaining_amount: Amount, fee_rate: FeeRate, drain_script: &Script) -> Excess {
302 let drain_output_len = serialize(drain_script).len() + 8usize;
304 let change_fee =
305 fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred");
306 let drain_val = remaining_amount.checked_sub(change_fee).unwrap_or_default();
307
308 if drain_val.is_dust(drain_script) {
309 let dust_threshold = drain_script.minimal_non_dust();
310 Excess::NoChange {
311 dust_threshold,
312 change_fee,
313 remaining_amount,
314 }
315 } else {
316 Excess::Change {
317 amount: drain_val,
318 fee: change_fee,
319 }
320 }
321}
322
323fn select_sorted_utxos(
324 utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
325 fee_rate: FeeRate,
326 target_amount: Amount,
327 drain_script: &Script,
328) -> Result<CoinSelectionResult, InsufficientFunds> {
329 let mut selected_amount = Amount::ZERO;
330 let mut fee_amount = Amount::ZERO;
331 let selected = utxos
332 .scan(
333 (&mut selected_amount, &mut fee_amount),
334 |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
335 if must_use || **selected_amount < target_amount + **fee_amount {
336 **fee_amount += fee_rate
337 * TxIn::default()
338 .segwit_weight()
339 .checked_add(weighted_utxo.satisfaction_weight)
340 .expect("`Weight` addition should not cause an integer overflow");
341 **selected_amount += weighted_utxo.utxo.txout().value;
342 Some(weighted_utxo.utxo)
343 } else {
344 None
345 }
346 },
347 )
348 .collect::<Vec<_>>();
349
350 let amount_needed_with_fees = target_amount + fee_amount;
351 if selected_amount < amount_needed_with_fees {
352 return Err(InsufficientFunds {
353 needed: amount_needed_with_fees,
354 available: selected_amount,
355 });
356 }
357
358 let remaining_amount = selected_amount - amount_needed_with_fees;
359
360 let excess = decide_change(remaining_amount, fee_rate, drain_script);
361
362 Ok(CoinSelectionResult {
363 selected,
364 fee_amount,
365 excess,
366 })
367}
368
369#[derive(Debug, Clone)]
370struct OutputGroup {
372 weighted_utxo: WeightedUtxo,
373 fee: Amount,
375 effective_value: SignedAmount,
377}
378
379impl OutputGroup {
380 fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
381 let fee = fee_rate
382 * TxIn::default()
383 .segwit_weight()
384 .checked_add(weighted_utxo.satisfaction_weight)
385 .expect("`Weight` addition should not cause an integer overflow");
386 let effective_value = weighted_utxo
387 .utxo
388 .txout()
389 .value
390 .to_signed()
391 .expect("signed amount")
392 - fee.to_signed().expect("signed amount");
393 OutputGroup {
394 weighted_utxo,
395 fee,
396 effective_value,
397 }
398 }
399}
400
401#[derive(Debug, Clone)]
405pub struct BranchAndBoundCoinSelection<Cs = SingleRandomDraw> {
406 size_of_change: u64,
407 fallback_algorithm: Cs,
408}
409
410#[derive(Debug)]
412enum BnbError {
413 NoExactMatch,
416 TotalTriesExceeded,
419}
420
421impl<Cs: Default> Default for BranchAndBoundCoinSelection<Cs> {
422 fn default() -> Self {
423 Self {
424 size_of_change: 8 + 1 + 22,
426 fallback_algorithm: Cs::default(),
427 }
428 }
429}
430
431impl<Cs> BranchAndBoundCoinSelection<Cs> {
432 pub fn new(size_of_change: u64, fallback_algorithm: Cs) -> Self {
434 Self {
435 size_of_change,
436 fallback_algorithm,
437 }
438 }
439}
440
441const BNB_TOTAL_TRIES: usize = 100_000;
442
443impl<Cs: CoinSelectionAlgorithm> CoinSelectionAlgorithm for BranchAndBoundCoinSelection<Cs> {
444 fn coin_select<R: RngCore>(
445 &self,
446 required_utxos: Vec<WeightedUtxo>,
447 optional_utxos: Vec<WeightedUtxo>,
448 fee_rate: FeeRate,
449 target_amount: Amount,
450 drain_script: &Script,
451 rand: &mut R,
452 ) -> Result<CoinSelectionResult, InsufficientFunds> {
453 let required_ogs: Vec<OutputGroup> = required_utxos
455 .iter()
456 .map(|u| OutputGroup::new(u.clone(), fee_rate))
457 .collect();
458
459 let optional_ogs: Vec<OutputGroup> = optional_utxos
462 .iter()
463 .map(|u| OutputGroup::new(u.clone(), fee_rate))
464 .filter(|u| u.effective_value.is_positive())
465 .collect();
466
467 let curr_value = required_ogs
468 .iter()
469 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
470
471 let curr_available_value = optional_ogs
472 .iter()
473 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
474
475 let cost_of_change = (Weight::from_vb(self.size_of_change).expect("overflow occurred")
476 * fee_rate)
477 .to_signed()
478 .expect("signed amount");
479
480 let total_value: Result<Amount, _> = (curr_available_value + curr_value).try_into();
492 match total_value {
493 Ok(v) if v >= target_amount => {}
494 _ => {
495 let (utxo_fees, utxo_value) = required_ogs.iter().chain(optional_ogs.iter()).fold(
498 (Amount::ZERO, Amount::ZERO),
499 |(mut fees, mut value), utxo| {
500 fees += utxo.fee;
501 value += utxo.weighted_utxo.utxo.txout().value;
502 (fees, value)
503 },
504 );
505
506 return Err(InsufficientFunds {
508 needed: target_amount + utxo_fees,
509 available: utxo_value,
510 });
511 }
512 }
513
514 let signed_target_amount = target_amount
515 .try_into()
516 .expect("Bitcoin amount to fit into i64");
517
518 if curr_value > signed_target_amount {
519 let remaining_amount = (curr_value - signed_target_amount)
523 .to_unsigned()
524 .expect("remaining amount can't be negative");
525
526 let excess = decide_change(remaining_amount, fee_rate, drain_script);
527
528 return Ok(calculate_cs_result(vec![], required_ogs, excess));
529 }
530
531 match self.bnb(
532 required_ogs,
533 optional_ogs,
534 curr_value,
535 curr_available_value,
536 signed_target_amount,
537 cost_of_change,
538 drain_script,
539 fee_rate,
540 ) {
541 Ok(r) => Ok(r),
542 Err(_) => self.fallback_algorithm.coin_select(
543 required_utxos,
544 optional_utxos,
545 fee_rate,
546 target_amount,
547 drain_script,
548 rand,
549 ),
550 }
551 }
552}
553
554impl<Cs> BranchAndBoundCoinSelection<Cs> {
555 #[allow(clippy::too_many_arguments)]
558 fn bnb(
559 &self,
560 required_utxos: Vec<OutputGroup>,
561 mut optional_utxos: Vec<OutputGroup>,
562 mut curr_value: SignedAmount,
563 mut curr_available_value: SignedAmount,
564 target_amount: SignedAmount,
565 cost_of_change: SignedAmount,
566 drain_script: &Script,
567 fee_rate: FeeRate,
568 ) -> Result<CoinSelectionResult, BnbError> {
569 let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
574
575 optional_utxos.sort_unstable_by_key(|a| a.effective_value);
577 optional_utxos.reverse();
578
579 let mut best_selection = Vec::new();
581 let mut best_selection_value = None;
582
583 for _ in 0..BNB_TOTAL_TRIES {
585 let mut backtrack = false;
587 if curr_value + curr_available_value < target_amount
591 || curr_value > target_amount + cost_of_change
592 {
593 backtrack = true;
594 } else if curr_value >= target_amount {
595 backtrack = true;
598
599 if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
602 best_selection.clone_from(¤t_selection);
603 best_selection_value = Some(curr_value);
604 }
605
606 if curr_value == target_amount {
608 break;
609 }
610 }
611
612 if backtrack {
614 while let Some(false) = current_selection.last() {
617 current_selection.pop();
618 curr_available_value += optional_utxos[current_selection.len()].effective_value;
619 }
620
621 if current_selection.last_mut().is_none() {
622 if best_selection.is_empty() {
626 return Err(BnbError::NoExactMatch);
627 }
628 break;
629 }
630
631 if let Some(c) = current_selection.last_mut() {
632 *c = false;
634 }
635
636 let utxo = &optional_utxos[current_selection.len() - 1];
637 curr_value -= utxo.effective_value;
638 } else {
639 let utxo = &optional_utxos[current_selection.len()];
641
642 curr_available_value -= utxo.effective_value;
644
645 current_selection.push(true);
647 curr_value += utxo.effective_value;
648 }
649 }
650
651 if best_selection.is_empty() {
653 return Err(BnbError::TotalTriesExceeded);
654 }
655
656 let selected_utxos = optional_utxos
658 .into_iter()
659 .zip(best_selection)
660 .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
661 .collect::<Vec<OutputGroup>>();
662
663 let selected_amount = best_selection_value.unwrap();
664
665 let remaining_amount = (selected_amount - target_amount)
669 .to_unsigned()
670 .expect("valid unsigned");
671
672 let excess = decide_change(remaining_amount, fee_rate, drain_script);
673
674 Ok(calculate_cs_result(selected_utxos, required_utxos, excess))
675 }
676}
677
678#[derive(Debug, Clone, Copy, Default)]
680pub struct SingleRandomDraw;
681
682impl CoinSelectionAlgorithm for SingleRandomDraw {
683 fn coin_select<R: RngCore>(
684 &self,
685 required_utxos: Vec<WeightedUtxo>,
686 mut optional_utxos: Vec<WeightedUtxo>,
687 fee_rate: FeeRate,
688 target_amount: Amount,
689 drain_script: &Script,
690 rand: &mut R,
691 ) -> Result<CoinSelectionResult, InsufficientFunds> {
692 let utxos = {
694 shuffle_slice(&mut optional_utxos, rand);
695
696 required_utxos
697 .into_iter()
698 .map(|utxo| (true, utxo))
699 .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
700 };
701
702 select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
704 }
705}
706
707fn calculate_cs_result(
708 mut selected_utxos: Vec<OutputGroup>,
709 mut required_utxos: Vec<OutputGroup>,
710 excess: Excess,
711) -> CoinSelectionResult {
712 selected_utxos.append(&mut required_utxos);
713 let fee_amount = selected_utxos.iter().map(|u| u.fee).sum();
714 let selected = selected_utxos
715 .into_iter()
716 .map(|u| u.weighted_utxo.utxo)
717 .collect::<Vec<_>>();
718
719 CoinSelectionResult {
720 selected,
721 fee_amount,
722 excess,
723 }
724}
725
726#[cfg(test)]
727mod test {
728 use assert_matches::assert_matches;
729 use bitcoin::hashes::Hash;
730 use bitcoin::{psbt, OutPoint, Sequence};
731 use chain::{ChainPosition, ConfirmationBlockTime};
732 use core::str::FromStr;
733 use rand::rngs::StdRng;
734 use std::boxed::Box;
735
736 use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
737
738 use super::*;
739 use crate::types::*;
740
741 use rand::prelude::SliceRandom;
742 use rand::{thread_rng, Rng, RngCore, SeedableRng};
743
744 const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33;
747
748 const FEE_AMOUNT: Amount = Amount::from_sat(50);
749
750 fn unconfirmed_utxo(value: Amount, index: u32, last_seen: u64) -> WeightedUtxo {
751 utxo(
752 value,
753 index,
754 ChainPosition::Unconfirmed {
755 first_seen: Some(last_seen),
756 last_seen: Some(last_seen),
757 },
758 )
759 }
760
761 fn confirmed_utxo(
762 value: Amount,
763 index: u32,
764 confirmation_height: u32,
765 confirmation_time: u64,
766 ) -> WeightedUtxo {
767 utxo(
768 value,
769 index,
770 ChainPosition::Confirmed {
771 anchor: ConfirmationBlockTime {
772 block_id: chain::BlockId {
773 height: confirmation_height,
774 hash: bitcoin::BlockHash::all_zeros(),
775 },
776 confirmation_time,
777 },
778 transitively: None,
779 },
780 )
781 }
782
783 fn foreign_utxo(value: Amount, index: u32) -> WeightedUtxo {
784 assert!(index < 10);
785 let outpoint = OutPoint::from_str(&format!(
786 "000000000000000000000000000000000000000000000000000000000000000{index}:0"
787 ))
788 .unwrap();
789 WeightedUtxo {
790 utxo: Utxo::Foreign {
791 outpoint,
792 sequence: Sequence(0xFFFFFFFD),
793 psbt_input: Box::new(psbt::Input {
794 witness_utxo: Some(TxOut {
795 value,
796 script_pubkey: ScriptBuf::new(),
797 }),
798 non_witness_utxo: None,
799 ..Default::default()
800 }),
801 },
802 satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
803 }
804 }
805
806 fn utxo(
807 value: Amount,
808 index: u32,
809 chain_position: ChainPosition<ConfirmationBlockTime>,
810 ) -> WeightedUtxo {
811 assert!(index < 10);
812 let outpoint = OutPoint::from_str(&format!(
813 "000000000000000000000000000000000000000000000000000000000000000{index}:0"
814 ))
815 .unwrap();
816 WeightedUtxo {
817 satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
818 utxo: Utxo::Local(LocalOutput {
819 outpoint,
820 txout: TxOut {
821 value,
822 script_pubkey: ScriptBuf::new(),
823 },
824 keychain: KeychainKind::External,
825 is_spent: false,
826 derivation_index: 42,
827 chain_position,
828 }),
829 }
830 }
831
832 fn get_test_utxos() -> Vec<WeightedUtxo> {
833 vec![
834 unconfirmed_utxo(Amount::from_sat(100_000), 0, 0),
835 unconfirmed_utxo(FEE_AMOUNT - Amount::from_sat(40), 1, 0),
836 unconfirmed_utxo(Amount::from_sat(200_000), 2, 0),
837 ]
838 }
839
840 fn get_oldest_first_test_utxos() -> Vec<WeightedUtxo> {
841 let utxo1 = confirmed_utxo(Amount::from_sat(120_000), 1, 1, 1231006505);
843 let utxo2 = confirmed_utxo(Amount::from_sat(80_000), 2, 2, 1231006505);
844 let utxo3 = confirmed_utxo(Amount::from_sat(300_000), 3, 3, 1231006505);
845 vec![utxo1, utxo2, utxo3]
846 }
847
848 fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
849 let mut res = Vec::new();
850 for i in 0..utxos_number {
851 res.push(WeightedUtxo {
852 satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
853 utxo: Utxo::Local(LocalOutput {
854 outpoint: OutPoint::from_str(&format!(
855 "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{i}"
856 ))
857 .unwrap(),
858 txout: TxOut {
859 value: Amount::from_sat(rng.gen_range(0..200000000)),
860 script_pubkey: ScriptBuf::new(),
861 },
862 keychain: KeychainKind::External,
863 is_spent: false,
864 derivation_index: rng.next_u32(),
865 chain_position: if rng.gen_bool(0.5) {
866 ChainPosition::Confirmed {
867 anchor: ConfirmationBlockTime {
868 block_id: chain::BlockId {
869 height: rng.next_u32(),
870 hash: BlockHash::all_zeros(),
871 },
872 confirmation_time: rng.next_u64(),
873 },
874 transitively: None,
875 }
876 } else {
877 ChainPosition::Unconfirmed {
878 first_seen: Some(1),
879 last_seen: Some(1),
880 }
881 },
882 }),
883 });
884 }
885 res
886 }
887
888 fn generate_same_value_utxos(utxos_value: Amount, utxos_number: usize) -> Vec<WeightedUtxo> {
889 (0..utxos_number)
890 .map(|i| WeightedUtxo {
891 satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
892 utxo: Utxo::Local(LocalOutput {
893 outpoint: OutPoint::from_str(&format!(
894 "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{i}"
895 ))
896 .unwrap(),
897 txout: TxOut {
898 value: utxos_value,
899 script_pubkey: ScriptBuf::new(),
900 },
901 keychain: KeychainKind::External,
902 is_spent: false,
903 derivation_index: 42,
904 chain_position: ChainPosition::Unconfirmed {
905 first_seen: Some(1),
906 last_seen: Some(1),
907 },
908 }),
909 })
910 .collect()
911 }
912
913 fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut [WeightedUtxo]) -> Amount {
914 let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
915 utxos.shuffle(&mut rng);
916 utxos[..utxos_picked_len]
917 .iter()
918 .map(|u| u.utxo.txout().value)
919 .sum()
920 }
921
922 fn calc_target_amount(utxos: &[WeightedUtxo], fee_rate: FeeRate) -> Amount {
923 utxos
924 .iter()
925 .cloned()
926 .map(|utxo| OutputGroup::new(utxo, fee_rate).effective_value)
927 .sum::<SignedAmount>()
928 .to_unsigned()
929 .expect("unsigned amount")
930 }
931
932 #[test]
933 fn test_largest_first_coin_selection_success() {
934 let utxos = get_test_utxos();
935 let drain_script = ScriptBuf::default();
936 let target_amount = Amount::from_sat(250_000) + FEE_AMOUNT;
937
938 let result = LargestFirstCoinSelection
939 .coin_select(
940 utxos,
941 vec![],
942 FeeRate::from_sat_per_vb_unchecked(1),
943 target_amount,
944 &drain_script,
945 &mut thread_rng(),
946 )
947 .unwrap();
948
949 assert_eq!(result.selected.len(), 3);
950 assert_eq!(result.selected_amount(), Amount::from_sat(300_010));
951 assert_eq!(result.fee_amount, Amount::from_sat(204));
952 }
953
954 #[test]
955 fn test_largest_first_coin_selection_use_all() {
956 let utxos = get_test_utxos();
957 let drain_script = ScriptBuf::default();
958 let target_amount = Amount::from_sat(20_000) + FEE_AMOUNT;
959
960 let result = LargestFirstCoinSelection
961 .coin_select(
962 utxos,
963 vec![],
964 FeeRate::from_sat_per_vb_unchecked(1),
965 target_amount,
966 &drain_script,
967 &mut thread_rng(),
968 )
969 .unwrap();
970
971 assert_eq!(result.selected.len(), 3);
972 assert_eq!(result.selected_amount(), Amount::from_sat(300_010));
973 assert_eq!(result.fee_amount, Amount::from_sat(204));
974 }
975
976 #[test]
977 fn test_largest_first_coin_selection_use_only_necessary() {
978 let utxos = get_test_utxos();
979 let drain_script = ScriptBuf::default();
980 let target_amount = Amount::from_sat(20_000) + FEE_AMOUNT;
981
982 let result = LargestFirstCoinSelection
983 .coin_select(
984 vec![],
985 utxos,
986 FeeRate::from_sat_per_vb_unchecked(1),
987 target_amount,
988 &drain_script,
989 &mut thread_rng(),
990 )
991 .unwrap();
992
993 assert_eq!(result.selected.len(), 1);
994 assert_eq!(result.selected_amount(), Amount::from_sat(200_000));
995 assert_eq!(result.fee_amount, Amount::from_sat(68));
996 }
997
998 #[test]
999 fn test_largest_first_coin_selection_insufficient_funds() {
1000 let utxos = get_test_utxos();
1001 let drain_script = ScriptBuf::default();
1002 let target_amount = Amount::from_sat(500_000) + FEE_AMOUNT;
1003
1004 let result = LargestFirstCoinSelection.coin_select(
1005 vec![],
1006 utxos,
1007 FeeRate::from_sat_per_vb_unchecked(1),
1008 target_amount,
1009 &drain_script,
1010 &mut thread_rng(),
1011 );
1012 assert!(matches!(result, Err(InsufficientFunds { .. })));
1013 }
1014
1015 #[test]
1016 fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
1017 let utxos = get_test_utxos();
1018 let drain_script = ScriptBuf::default();
1019 let target_amount = Amount::from_sat(250_000) + FEE_AMOUNT;
1020
1021 let result = LargestFirstCoinSelection.coin_select(
1022 vec![],
1023 utxos,
1024 FeeRate::from_sat_per_vb_unchecked(1000),
1025 target_amount,
1026 &drain_script,
1027 &mut thread_rng(),
1028 );
1029 assert!(matches!(result, Err(InsufficientFunds { .. })));
1030 }
1031
1032 #[test]
1033 fn test_oldest_first_coin_selection_success() {
1034 let utxos = get_oldest_first_test_utxos();
1035 let drain_script = ScriptBuf::default();
1036 let target_amount = Amount::from_sat(180_000) + FEE_AMOUNT;
1037
1038 let result = OldestFirstCoinSelection
1039 .coin_select(
1040 vec![],
1041 utxos,
1042 FeeRate::from_sat_per_vb_unchecked(1),
1043 target_amount,
1044 &drain_script,
1045 &mut thread_rng(),
1046 )
1047 .unwrap();
1048
1049 assert_eq!(result.selected.len(), 2);
1050 assert_eq!(result.selected_amount(), Amount::from_sat(200_000));
1051 assert_eq!(result.fee_amount, Amount::from_sat(136));
1052 }
1053
1054 #[test]
1055 fn test_oldest_first_coin_selection_use_all() {
1056 let utxos = get_oldest_first_test_utxos();
1057 let drain_script = ScriptBuf::default();
1058 let target_amount = Amount::from_sat(20_000) + FEE_AMOUNT;
1059
1060 let result = OldestFirstCoinSelection
1061 .coin_select(
1062 utxos,
1063 vec![],
1064 FeeRate::from_sat_per_vb_unchecked(1),
1065 target_amount,
1066 &drain_script,
1067 &mut thread_rng(),
1068 )
1069 .unwrap();
1070
1071 assert_eq!(result.selected.len(), 3);
1072 assert_eq!(result.selected_amount(), Amount::from_sat(500_000));
1073 assert_eq!(result.fee_amount, Amount::from_sat(204));
1074 }
1075
1076 #[test]
1077 fn test_oldest_first_coin_selection_uses_all_optional_with_foreign_utxo_locals_sorted_first() {
1078 let utxos = get_oldest_first_test_utxos();
1079 let mut all_utxos = vec![foreign_utxo(Amount::from_sat(120_000), 1)];
1080 all_utxos.extend_from_slice(&utxos);
1081 let drain_script = ScriptBuf::default();
1082 let target_amount = Amount::from_sat(619_000) + FEE_AMOUNT;
1083
1084 let result = OldestFirstCoinSelection
1085 .coin_select(
1086 vec![],
1087 all_utxos,
1088 FeeRate::from_sat_per_vb_unchecked(1),
1089 target_amount,
1090 &drain_script,
1091 &mut thread_rng(),
1092 )
1093 .unwrap();
1094
1095 assert_eq!(result.selected.len(), 4);
1096 assert_eq!(result.selected_amount(), Amount::from_sat(620_000));
1097 assert_eq!(result.fee_amount, Amount::from_sat(272));
1098 assert!(matches!(result.selected[3], Utxo::Foreign { .. }));
1099 }
1100
1101 #[test]
1102 fn test_oldest_first_coin_selection_uses_only_all_optional_local_utxos_not_a_single_foreign() {
1103 let utxos = get_oldest_first_test_utxos();
1104 let mut all_utxos = vec![foreign_utxo(Amount::from_sat(120_000), 1)];
1105 all_utxos.extend_from_slice(&utxos);
1106 let drain_script = ScriptBuf::default();
1107 let target_amount = Amount::from_sat(499_000) + FEE_AMOUNT;
1108
1109 let result = OldestFirstCoinSelection
1110 .coin_select(
1111 vec![],
1112 all_utxos,
1113 FeeRate::from_sat_per_vb_unchecked(1),
1114 target_amount,
1115 &drain_script,
1116 &mut thread_rng(),
1117 )
1118 .unwrap();
1119
1120 assert_eq!(result.selected.len(), 3);
1121 assert_eq!(result.selected_amount(), Amount::from_sat(500_000));
1122 assert_eq!(result.fee_amount, Amount::from_sat(204));
1123 assert!(result
1124 .selected
1125 .iter()
1126 .all(|utxo| matches!(utxo, Utxo::Local(..))));
1127 }
1128
1129 #[test]
1130 fn test_oldest_first_coin_selection_use_only_necessary() {
1131 let utxos = get_oldest_first_test_utxos();
1132 let drain_script = ScriptBuf::default();
1133 let target_amount = Amount::from_sat(20_000) + FEE_AMOUNT;
1134
1135 let result = OldestFirstCoinSelection
1136 .coin_select(
1137 vec![],
1138 utxos,
1139 FeeRate::from_sat_per_vb_unchecked(1),
1140 target_amount,
1141 &drain_script,
1142 &mut thread_rng(),
1143 )
1144 .unwrap();
1145
1146 assert_eq!(result.selected.len(), 1);
1147 assert_eq!(result.selected_amount(), Amount::from_sat(120_000));
1148 assert_eq!(result.fee_amount, Amount::from_sat(68));
1149 }
1150
1151 #[test]
1152 fn test_oldest_first_coin_selection_insufficient_funds() {
1153 let utxos = get_oldest_first_test_utxos();
1154 let drain_script = ScriptBuf::default();
1155 let target_amount = Amount::from_sat(600_000) + FEE_AMOUNT;
1156
1157 let result = OldestFirstCoinSelection.coin_select(
1158 vec![],
1159 utxos,
1160 FeeRate::from_sat_per_vb_unchecked(1),
1161 target_amount,
1162 &drain_script,
1163 &mut thread_rng(),
1164 );
1165 assert!(matches!(result, Err(InsufficientFunds { .. })));
1166 }
1167
1168 #[test]
1169 fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
1170 let utxos = get_oldest_first_test_utxos();
1171
1172 let target_amount =
1173 utxos.iter().map(|wu| wu.utxo.txout().value).sum::<Amount>() - Amount::from_sat(50);
1174 let drain_script = ScriptBuf::default();
1175
1176 let result = OldestFirstCoinSelection.coin_select(
1177 vec![],
1178 utxos,
1179 FeeRate::from_sat_per_vb_unchecked(1000),
1180 target_amount,
1181 &drain_script,
1182 &mut thread_rng(),
1183 );
1184 assert!(matches!(result, Err(InsufficientFunds { .. })));
1185 }
1186
1187 #[test]
1188 fn test_bnb_coin_selection_success() {
1189 let utxos = generate_same_value_utxos(Amount::from_sat(100_000), 20);
1192 let drain_script = ScriptBuf::default();
1193 let target_amount = Amount::from_sat(250_000) + FEE_AMOUNT;
1194
1195 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1196 .coin_select(
1197 vec![],
1198 utxos,
1199 FeeRate::from_sat_per_vb_unchecked(1),
1200 target_amount,
1201 &drain_script,
1202 &mut thread_rng(),
1203 )
1204 .unwrap();
1205
1206 assert_eq!(result.selected.len(), 3);
1207 assert_eq!(result.selected_amount(), Amount::from_sat(300_000));
1208 assert_eq!(result.fee_amount, Amount::from_sat(204));
1209 }
1210
1211 #[test]
1212 fn test_bnb_coin_selection_required_are_enough() {
1213 let utxos = get_test_utxos();
1214 let drain_script = ScriptBuf::default();
1215 let target_amount = Amount::from_sat(20_000) + FEE_AMOUNT;
1216
1217 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1218 .coin_select(
1219 utxos.clone(),
1220 utxos,
1221 FeeRate::from_sat_per_vb_unchecked(1),
1222 target_amount,
1223 &drain_script,
1224 &mut thread_rng(),
1225 )
1226 .unwrap();
1227
1228 assert_eq!(result.selected.len(), 3);
1229 assert_eq!(result.selected_amount(), Amount::from_sat(300_010));
1230 assert_eq!(result.fee_amount, Amount::from_sat(204));
1231 }
1232
1233 #[test]
1234 fn test_bnb_coin_selection_optional_are_enough() {
1235 let utxos = get_test_utxos();
1236 let drain_script = ScriptBuf::default();
1237 let fee_rate = FeeRate::BROADCAST_MIN;
1238 let target_amount = calc_target_amount(&[utxos[0].clone(), utxos[2].clone()], fee_rate);
1240
1241 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1242 .coin_select(
1243 vec![],
1244 utxos,
1245 fee_rate,
1246 target_amount,
1247 &drain_script,
1248 &mut thread_rng(),
1249 )
1250 .unwrap();
1251
1252 assert_eq!(result.selected.len(), 2);
1253 assert_eq!(result.selected_amount(), Amount::from_sat(300000));
1254 assert_eq!(result.fee_amount, Amount::from_sat(136));
1255 }
1256
1257 #[test]
1258 fn test_single_random_draw_function_success() {
1259 let seed = [0; 32];
1260 let mut rng: StdRng = SeedableRng::from_seed(seed);
1261 let mut utxos = generate_random_utxos(&mut rng, 300);
1262 let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
1263 let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1264 let drain_script = ScriptBuf::default();
1265
1266 let result = SingleRandomDraw.coin_select(
1267 vec![],
1268 utxos,
1269 fee_rate,
1270 target_amount,
1271 &drain_script,
1272 &mut thread_rng(),
1273 );
1274
1275 assert!(
1276 matches!(result, Ok(CoinSelectionResult {selected, fee_amount, ..})
1277 if selected.iter().map(|u| u.txout().value).sum::<Amount>() > target_amount
1278 && fee_amount == Amount::from_sat(selected.len() as u64 * 68)
1279 )
1280 );
1281 }
1282
1283 #[test]
1284 fn test_single_random_draw_function_error() {
1285 let seed = [0; 32];
1286 let mut rng: StdRng = SeedableRng::from_seed(seed);
1287
1288 let utxos = get_test_utxos();
1290 let target_amount = Amount::from_sat(300_000) + FEE_AMOUNT;
1291 let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1292 let drain_script = ScriptBuf::default();
1293
1294 let result = SingleRandomDraw.coin_select(
1295 vec![],
1296 utxos,
1297 fee_rate,
1298 target_amount,
1299 &drain_script,
1300 &mut rng,
1301 );
1302
1303 assert!(matches!(result, Err(InsufficientFunds {needed, available})
1304 if needed == Amount::from_sat(300_254) && available == Amount::from_sat(300_010)));
1305 }
1306
1307 #[test]
1308 fn test_bnb_coin_selection_required_not_enough() {
1309 let utxos = get_test_utxos();
1310
1311 let required = vec![utxos[0].clone()];
1312 let mut optional = utxos[1..].to_vec();
1313 optional.push(utxo(
1314 Amount::from_sat(500_000),
1315 3,
1316 ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
1317 first_seen: Some(1),
1318 last_seen: Some(1),
1319 },
1320 ));
1321
1322 let amount = required
1324 .iter()
1325 .map(|u| u.utxo.txout().value)
1326 .sum::<Amount>();
1327 assert_eq!(amount, Amount::from_sat(100_000));
1328 let amount = optional
1329 .iter()
1330 .map(|u| u.utxo.txout().value)
1331 .sum::<Amount>();
1332 assert!(amount > Amount::from_sat(150_000));
1333 let drain_script = ScriptBuf::default();
1334
1335 let fee_rate = FeeRate::BROADCAST_MIN;
1336 let target_amount = calc_target_amount(&[utxos[0].clone(), utxos[2].clone()], fee_rate);
1338
1339 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1340 .coin_select(
1341 required,
1342 optional,
1343 fee_rate,
1344 target_amount,
1345 &drain_script,
1346 &mut thread_rng(),
1347 )
1348 .unwrap();
1349
1350 assert_eq!(result.selected.len(), 2);
1351 assert_eq!(result.selected_amount(), Amount::from_sat(300_000));
1352 assert_eq!(result.fee_amount, Amount::from_sat(136));
1353 }
1354
1355 #[test]
1356 fn test_bnb_coin_selection_insufficient_funds() {
1357 let utxos = get_test_utxos();
1358 let drain_script = ScriptBuf::default();
1359 let target_amount = Amount::from_sat(500_000) + FEE_AMOUNT;
1360
1361 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1362 vec![],
1363 utxos,
1364 FeeRate::from_sat_per_vb_unchecked(1),
1365 target_amount,
1366 &drain_script,
1367 &mut thread_rng(),
1368 );
1369
1370 assert!(matches!(result, Err(InsufficientFunds { .. })));
1371 }
1372
1373 #[test]
1374 fn test_bnb_coin_selection_insufficient_funds_high_fees() {
1375 let utxos = get_test_utxos();
1376 let drain_script = ScriptBuf::default();
1377 let target_amount = Amount::from_sat(250_000) + FEE_AMOUNT;
1378
1379 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1380 vec![],
1381 utxos,
1382 FeeRate::from_sat_per_vb_unchecked(1000),
1383 target_amount,
1384 &drain_script,
1385 &mut thread_rng(),
1386 );
1387 assert!(matches!(result, Err(InsufficientFunds { .. })));
1388 }
1389
1390 #[test]
1391 fn test_bnb_coin_selection_check_fee_rate() {
1392 let utxos = get_test_utxos();
1393 let drain_script = ScriptBuf::default();
1394 let fee_rate = FeeRate::BROADCAST_MIN;
1395 let target_amount = calc_target_amount(&utxos[0..1], fee_rate);
1397
1398 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1399 .coin_select(
1400 vec![],
1401 utxos,
1402 fee_rate,
1403 target_amount,
1404 &drain_script,
1405 &mut thread_rng(),
1406 )
1407 .unwrap();
1408
1409 assert_eq!(result.selected.len(), 1);
1410 assert_eq!(result.selected_amount(), Amount::from_sat(100_000));
1411 let input_weight =
1412 TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64;
1413 let result_feerate = result.fee_amount / Weight::from_wu(input_weight);
1415 assert_eq!(result_feerate, fee_rate);
1416 }
1417
1418 #[test]
1419 fn test_bnb_coin_selection_exact_match() {
1420 let seed = [0; 32];
1421 let mut rng: StdRng = SeedableRng::from_seed(seed);
1422
1423 for _i in 0..200 {
1424 let mut optional_utxos = generate_random_utxos(&mut rng, 16);
1425 let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
1426 let drain_script = ScriptBuf::default();
1427 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1428 .coin_select(
1429 vec![],
1430 optional_utxos,
1431 FeeRate::ZERO,
1432 target_amount,
1433 &drain_script,
1434 &mut thread_rng(),
1435 )
1436 .unwrap();
1437 assert_eq!(result.selected_amount(), target_amount);
1438 }
1439 }
1440
1441 #[test]
1442 fn test_bnb_function_no_exact_match() {
1443 let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
1444 let utxos: Vec<OutputGroup> = get_test_utxos()
1445 .into_iter()
1446 .map(|u| OutputGroup::new(u, fee_rate))
1447 .collect();
1448
1449 let curr_available_value = utxos
1450 .iter()
1451 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
1452
1453 let size_of_change = 31;
1454 let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate)
1455 .to_signed()
1456 .unwrap();
1457
1458 let drain_script = ScriptBuf::default();
1459 let target_amount = SignedAmount::from_sat(20_000) + FEE_AMOUNT.to_signed().unwrap();
1460 let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb(
1461 vec![],
1462 utxos,
1463 SignedAmount::ZERO,
1464 curr_available_value,
1465 target_amount,
1466 cost_of_change,
1467 &drain_script,
1468 fee_rate,
1469 );
1470 assert!(matches!(result, Err(BnbError::NoExactMatch)));
1471 }
1472
1473 #[test]
1474 fn test_bnb_function_tries_exceeded() {
1475 let fee_rate = FeeRate::from_sat_per_vb_unchecked(10);
1476 let utxos: Vec<OutputGroup> = generate_same_value_utxos(Amount::from_sat(100_000), 100_000)
1477 .into_iter()
1478 .map(|u| OutputGroup::new(u, fee_rate))
1479 .collect();
1480
1481 let curr_available_value = utxos
1482 .iter()
1483 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
1484
1485 let size_of_change = 31;
1486 let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate)
1487 .to_signed()
1488 .unwrap();
1489 let target_amount = SignedAmount::from_sat(20_000) + FEE_AMOUNT.to_signed().unwrap();
1490
1491 let drain_script = ScriptBuf::default();
1492
1493 let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb(
1494 vec![],
1495 utxos,
1496 SignedAmount::ZERO,
1497 curr_available_value,
1498 target_amount,
1499 cost_of_change,
1500 &drain_script,
1501 fee_rate,
1502 );
1503 assert!(matches!(result, Err(BnbError::TotalTriesExceeded)));
1504 }
1505
1506 #[test]
1508 fn test_bnb_function_almost_exact_match_with_fees() {
1509 let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1510 let size_of_change = 31;
1511 let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate)
1512 .to_signed()
1513 .unwrap();
1514
1515 let utxos: Vec<_> = generate_same_value_utxos(Amount::from_sat(50_000), 10)
1516 .into_iter()
1517 .map(|u| OutputGroup::new(u, fee_rate))
1518 .collect();
1519
1520 let curr_value = SignedAmount::ZERO;
1521
1522 let curr_available_value = utxos
1523 .iter()
1524 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
1525
1526 let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.to_sat() + 5;
1529 let target_amount = SignedAmount::from_sat(target_amount);
1530
1531 let drain_script = ScriptBuf::default();
1532
1533 let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw)
1534 .bnb(
1535 vec![],
1536 utxos,
1537 curr_value,
1538 curr_available_value,
1539 target_amount,
1540 cost_of_change,
1541 &drain_script,
1542 fee_rate,
1543 )
1544 .unwrap();
1545 assert_eq!(result.selected_amount(), Amount::from_sat(100_000));
1546 assert_eq!(result.fee_amount, Amount::from_sat(136));
1547 }
1548
1549 #[test]
1551 fn test_bnb_function_exact_match_more_utxos() {
1552 let seed = [0; 32];
1553 let mut rng: StdRng = SeedableRng::from_seed(seed);
1554 let fee_rate = FeeRate::ZERO;
1555
1556 for _ in 0..200 {
1557 let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
1558 .into_iter()
1559 .map(|u| OutputGroup::new(u, fee_rate))
1560 .collect();
1561
1562 let curr_value = SignedAmount::ZERO;
1563
1564 let curr_available_value = optional_utxos
1565 .iter()
1566 .fold(SignedAmount::ZERO, |acc, x| acc + x.effective_value);
1567
1568 let target_amount =
1569 optional_utxos[3].effective_value + optional_utxos[23].effective_value;
1570
1571 let drain_script = ScriptBuf::default();
1572
1573 let result = BranchAndBoundCoinSelection::<SingleRandomDraw>::default()
1574 .bnb(
1575 vec![],
1576 optional_utxos,
1577 curr_value,
1578 curr_available_value,
1579 target_amount,
1580 SignedAmount::ZERO,
1581 &drain_script,
1582 fee_rate,
1583 )
1584 .unwrap();
1585 assert_eq!(
1586 result.selected_amount(),
1587 target_amount.to_unsigned().unwrap()
1588 );
1589 }
1590 }
1591
1592 #[test]
1593 fn test_bnb_exclude_negative_effective_value() {
1594 let utxos = get_test_utxos();
1595 let drain_script = ScriptBuf::default();
1596
1597 let selection = BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1598 vec![],
1599 utxos,
1600 FeeRate::from_sat_per_vb_unchecked(10),
1601 Amount::from_sat(500_000),
1602 &drain_script,
1603 &mut thread_rng(),
1604 );
1605
1606 assert_matches!(
1607 selection,
1608 Err(InsufficientFunds {
1609 available,
1610 ..
1611 }) if available.to_sat() == 300_000
1612 );
1613 }
1614
1615 #[test]
1616 fn test_bnb_include_negative_effective_value_when_required() {
1617 let utxos = get_test_utxos();
1618 let drain_script = ScriptBuf::default();
1619
1620 let (required, optional) = utxos.into_iter().partition(
1621 |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000),
1622 );
1623
1624 let selection = BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1625 required,
1626 optional,
1627 FeeRate::from_sat_per_vb_unchecked(10),
1628 Amount::from_sat(500_000),
1629 &drain_script,
1630 &mut thread_rng(),
1631 );
1632
1633 assert_matches!(
1634 selection,
1635 Err(InsufficientFunds {
1636 available,
1637 ..
1638 }) if available.to_sat() == 300_010
1639 );
1640 }
1641
1642 #[test]
1643 fn test_bnb_sum_of_effective_value_negative() {
1644 let utxos = get_test_utxos();
1645 let drain_script = ScriptBuf::default();
1646
1647 let selection = BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1648 utxos,
1649 vec![],
1650 FeeRate::from_sat_per_vb_unchecked(10_000),
1651 Amount::from_sat(500_000),
1652 &drain_script,
1653 &mut thread_rng(),
1654 );
1655
1656 assert_matches!(
1657 selection,
1658 Err(InsufficientFunds {
1659 available,
1660 ..
1661 }) if available.to_sat() == 300_010
1662 );
1663 }
1664
1665 #[test]
1666 fn test_bnb_fallback_algorithm() {
1667 let optional_utxos = get_oldest_first_test_utxos();
1670 let feerate = FeeRate::BROADCAST_MIN;
1671 let target_amount = Amount::from_sat(190_000);
1672 let drain_script = ScriptBuf::new();
1673 let bnb_with_oldest_first =
1675 BranchAndBoundCoinSelection::new(8 + 1 + 22, OldestFirstCoinSelection);
1676 let res = bnb_with_oldest_first
1677 .coin_select(
1678 vec![],
1679 optional_utxos,
1680 feerate,
1681 target_amount,
1682 &drain_script,
1683 &mut thread_rng(),
1684 )
1685 .unwrap();
1686 assert_eq!(res.selected_amount(), Amount::from_sat(200_000));
1687 }
1688
1689 #[test]
1690 fn test_deterministic_coin_selection_picks_same_utxos() {
1691 enum CoinSelectionAlgo {
1692 BranchAndBound,
1693 OldestFirst,
1694 LargestFirst,
1695 }
1696
1697 struct TestCase<'a> {
1698 name: &'a str,
1699 coin_selection_algo: CoinSelectionAlgo,
1700 exp_vouts: &'a [u32],
1701 }
1702
1703 let test_cases = [
1704 TestCase {
1705 name: "branch and bound",
1706 coin_selection_algo: CoinSelectionAlgo::BranchAndBound,
1707 exp_vouts: &[29, 28, 27],
1710 },
1711 TestCase {
1712 name: "oldest first",
1713 coin_selection_algo: CoinSelectionAlgo::OldestFirst,
1714 exp_vouts: &[0, 1, 2],
1715 },
1716 TestCase {
1717 name: "largest first",
1718 coin_selection_algo: CoinSelectionAlgo::LargestFirst,
1719 exp_vouts: &[29, 28, 27],
1720 },
1721 ];
1722
1723 let optional = generate_same_value_utxos(Amount::from_sat(100_000), 30);
1724 let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
1725 let target_amount = calc_target_amount(&optional[0..3], fee_rate);
1726 assert_eq!(target_amount, Amount::from_sat(299_796));
1727 let drain_script = ScriptBuf::default();
1728
1729 for tc in test_cases {
1730 let optional = optional.clone();
1731
1732 let result = match tc.coin_selection_algo {
1733 CoinSelectionAlgo::BranchAndBound => {
1734 BranchAndBoundCoinSelection::<SingleRandomDraw>::default().coin_select(
1735 vec![],
1736 optional,
1737 fee_rate,
1738 target_amount,
1739 &drain_script,
1740 &mut thread_rng(),
1741 )
1742 }
1743 CoinSelectionAlgo::OldestFirst => OldestFirstCoinSelection.coin_select(
1744 vec![],
1745 optional,
1746 fee_rate,
1747 target_amount,
1748 &drain_script,
1749 &mut thread_rng(),
1750 ),
1751 CoinSelectionAlgo::LargestFirst => LargestFirstCoinSelection.coin_select(
1752 vec![],
1753 optional,
1754 fee_rate,
1755 target_amount,
1756 &drain_script,
1757 &mut thread_rng(),
1758 ),
1759 };
1760
1761 assert!(result.is_ok(), "coin_select failed {}", tc.name);
1762 let result = result.unwrap();
1763 assert!(matches!(result.excess, Excess::NoChange { .. },));
1764 assert_eq!(
1765 result.selected.len(),
1766 3,
1767 "wrong selected len for {}",
1768 tc.name
1769 );
1770 assert_eq!(
1771 result.selected_amount(),
1772 Amount::from_sat(300_000),
1773 "wrong selected amount for {}",
1774 tc.name
1775 );
1776 assert_eq!(
1777 result.fee_amount,
1778 Amount::from_sat(204),
1779 "wrong fee amount for {}",
1780 tc.name
1781 );
1782 let vouts = result
1783 .selected
1784 .iter()
1785 .map(|utxo| utxo.outpoint().vout)
1786 .collect::<Vec<u32>>();
1787 assert_eq!(vouts, tc.exp_vouts, "wrong selected vouts for {}", tc.name);
1788 }
1789 }
1790}