diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d78b66..bf1eedc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ # 0.1.0 - 2022-12-17 * Initial release. + +# 0.2.0 - 2023-06-03 + +- Add Single Random Draw module and a basic error type. diff --git a/Cargo.toml b/Cargo.toml index 5bc2626..ab8cb3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,19 @@ homepage = "https://github.com/rust-bitcoin/rust-bitcoin-coin-selection/" license = "CC0-1.0" name = "rust-bitcoin-coin-selection" repository = "https://github.com/rust-bitcoin/rust-bitcoin-coin-selection/" -version = "0.1.0" +version = "0.2.0" # documentation = "https://docs.rs/bitcoin-coin-selection/" description = "Libary providing utility functions to efficiently select a set of UTXOs." keywords = ["crypto", "bitcoin"] readme = "README.md" [dependencies] +bitcoin = { git="https://github.com/yancyribbens/rust-bitcoin", branch="master" } rand = {version = "0.8.5", default-features = false, optional = true} [dev-dependencies] +rust-bitcoin-coin-selection = {path = ".", features = ["rand"]} rand = "0.8.5" + +[patch.crates-io] +bitcoin_hashes = { git="https://github.com/yancyribbens/rust-bitcoin.git" } diff --git a/clippy.toml b/clippy.toml index 799264e..11d46a7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.41.1" +msrv = "1.48.0" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..010cc54 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,26 @@ +/// Error types. +use bitcoin::FeeRate; +use bitcoin::Weight; +use std::error::Error as E; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Multiplication(Weight, FeeRate), + SizeMismatch(usize, usize), +} + +impl E for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Multiplication(weight, fee_rate) => { + write!(f, "{} * {} exceeds u64 Max", weight, fee_rate) + }, + Error::SizeMismatch(input_count, output_count) => { + write!(f, "the number of inputs: {} and outputs: {} does not match", input_count, output_count) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d4e143d..46a926b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ #![deny(non_snake_case)] #![deny(unused_mut)] #![deny(missing_docs)] - // Experimental features we need. #![cfg_attr(bench, feature(test))] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -19,8 +18,12 @@ extern crate test; use std::cmp::Reverse; -#[cfg(any(test, feature = "rand"))] -use rand::{seq::SliceRandom, thread_rng}; +mod errors; +mod single_random_draw; + +use crate::errors::Error; +use crate::single_random_draw::select_coins_srd; +use rand::thread_rng; /// Trait that a UTXO struct must implement to be used as part of the coin selection /// algorithm. @@ -29,54 +32,37 @@ pub trait Utxo: Clone { fn get_value(&self) -> u64; } +// https://github.com/bitcoin/bitcoin/blob/f722a9bd132222d9d5cd503b5af25c905b205cdb/src/wallet/coinselection.h#L20 +const CHANGE_LOWER: Amount = Amount::from_sat(50_000); + +use bitcoin::Amount; +use bitcoin::FeeRate; +use bitcoin::TxOut; +use bitcoin::TxIn; + /// Select coins first using BnB algorithm similar to what is done in bitcoin /// core see: , /// and falls back on a random UTXO selection. Returns none if the target cannot /// be reached with the given utxo pool. /// Requires compilation with the "rand" feature. -#[cfg(any(test, feature = "rand"))] +#[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] +// removeing utxo_pool param next release +#[allow(clippy::too_many_arguments)] pub fn select_coins( - target: u64, + target: Amount, cost_of_change: u64, - utxo_pool: &mut [T], -) -> Option> { - match select_coins_bnb(target, cost_of_change, utxo_pool) { - Some(res) => Some(res), - None => select_coins_random(target, utxo_pool), + fee_rate: FeeRate, + utxos: &mut [TxOut], + inputs: &mut [TxIn], + utxo_pool: &mut [T] +) -> Result, Error> { + match select_coins_bnb(target.to_sat(), cost_of_change, utxo_pool) { + Some(_res) => Ok(Vec::new()), + None => select_coins_srd(target, fee_rate, utxos, inputs, &mut thread_rng()), } } -/// Randomly select coins for the given target by shuffling the utxo pool and -/// taking UTXOs until the given target is reached, or returns None if the target -/// cannot be reached with the given utxo pool. -/// Requires compilation with the "rand" feature. -#[cfg(any(test, feature = "rand"))] -#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] -pub fn select_coins_random(target: u64, utxo_pool: &mut [T]) -> Option> { - utxo_pool.shuffle(&mut thread_rng()); - - let mut sum = 0; - - let res = utxo_pool - .iter() - .take_while(|x| { - if sum >= target { - return false; - } - sum += x.get_value(); - true - }) - .cloned() - .collect(); - - if sum >= target { - return Some(res); - } - - None -} - /// Select coins using BnB algorithm similar to what is done in bitcoin /// core see: /// Returns None if BnB doesn't find a solution. @@ -153,183 +139,146 @@ fn find_solution( #[cfg(test)] mod tests { - use crate::*; - - const ONE_BTC: u64 = 100000000; - const TWO_BTC: u64 = 2 * 100000000; - const THREE_BTC: u64 = 3 * 100000000; - const FOUR_BTC: u64 = 4 * 100000000; - - const UTXO_POOL: [MinimalUtxo; 4] = [ - MinimalUtxo { value: ONE_BTC }, - MinimalUtxo { value: TWO_BTC }, - MinimalUtxo { value: THREE_BTC }, - MinimalUtxo { value: FOUR_BTC }, - ]; - - const COST_OF_CHANGE: u64 = 50000000; - - #[derive(Clone, Debug, Eq, PartialEq)] - struct MinimalUtxo { - value: u64, - } - - impl Utxo for MinimalUtxo { - fn get_value(&self) -> u64 { - self.value - } - } - - #[test] - fn find_solution_1_btc() { - let utxo_match = find_solution(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_2_btc() { - let utxo_match = find_solution(TWO_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, true, false]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_3_btc() { - let utxo_match = find_solution(THREE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_4_btc() { - let utxo_match = find_solution(FOUR_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_5_btc() { - let five_btc = FOUR_BTC + ONE_BTC; - let utxo_match = find_solution(five_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, true, false]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_6_btc() { - let six_btc = FOUR_BTC + TWO_BTC; - let utxo_match = find_solution(six_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_7_btc() { - let seven_btc = FOUR_BTC + THREE_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, false, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_8_btc() { - let seven_btc = FOUR_BTC + THREE_BTC + ONE_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_9_btc() { - let seven_btc = FOUR_BTC + THREE_BTC + TWO_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, true, false]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_10_btc() { - let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; - let utxo_match = find_solution(ten_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_11_btc_not_possible() { - let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; - let utxo_match = find_solution(ten_btc + ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn find_solution_with_large_cost_of_change() { - let utxo_match = - find_solution(ONE_BTC * 9 / 10, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_with_no_cost_of_change() { - let utxo_match = find_solution(ONE_BTC * 9 / 10, 0, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn find_solution_with_not_input_fee() { - let utxo_match = find_solution(ONE_BTC + 1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn select_coins_bnb_with_match() { - select_coins_bnb(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - } - - #[test] - fn select_coins_bnb_with_no_match() { - let utxo_match = select_coins_bnb(1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn select_coins_random_draw_with_solution() { - let utxo_match = select_coins_random(ONE_BTC, &mut UTXO_POOL.clone()); - utxo_match.expect("Did not properly randomly select coins"); - } - - #[test] - fn select_coins_random_draw_no_solution() { - let utxo_match = select_coins_random(11 * ONE_BTC, &mut UTXO_POOL.clone()); - assert!(utxo_match.is_none()); - } - - #[test] - fn select_coins_bnb_match_with_random() { - let utxo_match = select_coins(1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - utxo_match.expect("Did not use random selection"); - } - - #[test] - fn select_coins_random_test() { - let mut test_utxo_pool = vec![MinimalUtxo { value: 5000000000 }]; - - let utxo_match = - select_coins(100000358, 20, &mut test_utxo_pool).expect("Did not find match"); - - assert_eq!(1, utxo_match.len()); - } - - #[test] - fn select_coins_random_fail_test() { - let mut test_utxo_pool = vec![MinimalUtxo { value: 5000000000 }]; - - let utxo_match = select_coins(5000000358, 20, &mut test_utxo_pool); - - assert!(utxo_match.is_none()); - } + use crate::*; + + const ONE_BTC: u64 = 100000000; + const TWO_BTC: u64 = 2 * 100000000; + const THREE_BTC: u64 = 3 * 100000000; + const FOUR_BTC: u64 = 4 * 100000000; + + const UTXO_POOL: [MinimalUtxo; 4] = [ + MinimalUtxo { value: ONE_BTC }, + MinimalUtxo { value: TWO_BTC }, + MinimalUtxo { value: THREE_BTC }, + MinimalUtxo { value: FOUR_BTC }, + ]; + + const COST_OF_CHANGE: u64 = 50000000; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct MinimalUtxo { + value: u64, + } + + impl Utxo for MinimalUtxo { + fn get_value(&self) -> u64 { + self.value + } + } + + #[test] + fn find_solution_1_btc() { + let utxo_match = find_solution(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, false, false, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_2_btc() { + let utxo_match = find_solution(TWO_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, false, true, false]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_3_btc() { + let utxo_match = find_solution(THREE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, false, true, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_4_btc() { + let utxo_match = find_solution(FOUR_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, true, false, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_5_btc() { + let five_btc = FOUR_BTC + ONE_BTC; + let utxo_match = find_solution(five_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, true, true, false]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_6_btc() { + let six_btc = FOUR_BTC + TWO_BTC; + let utxo_match = find_solution(six_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, true, true, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_7_btc() { + let seven_btc = FOUR_BTC + THREE_BTC; + let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![true, false, true, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_8_btc() { + let seven_btc = FOUR_BTC + THREE_BTC + ONE_BTC; + let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![true, true, false, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_9_btc() { + let seven_btc = FOUR_BTC + THREE_BTC + TWO_BTC; + let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![true, true, true, false]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_10_btc() { + let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; + let utxo_match = find_solution(ten_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![true, true, true, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_11_btc_not_possible() { + let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; + let utxo_match = find_solution(ten_btc + ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + assert_eq!(None, utxo_match); + } + + #[test] + fn find_solution_with_large_cost_of_change() { + let utxo_match = + find_solution(ONE_BTC * 9 / 10, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + let expected_bool_vec = vec![false, false, false, true]; + assert_eq!(expected_bool_vec, utxo_match); + } + + #[test] + fn find_solution_with_no_cost_of_change() { + let utxo_match = find_solution(ONE_BTC * 9 / 10, 0, &mut UTXO_POOL.clone()); + assert_eq!(None, utxo_match); + } + + #[test] + fn find_solution_with_not_input_fee() { + let utxo_match = find_solution(ONE_BTC + 1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + assert_eq!(None, utxo_match); + } + + #[test] + fn select_coins_bnb_with_match() { + select_coins_bnb(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + } + + #[test] + fn select_coins_bnb_with_no_match() { + let utxo_match = select_coins_bnb(1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + assert_eq!(None, utxo_match); + } } #[cfg(bench)] diff --git a/src/single_random_draw.rs b/src/single_random_draw.rs new file mode 100644 index 0000000..2f2e9b1 --- /dev/null +++ b/src/single_random_draw.rs @@ -0,0 +1,249 @@ +//! This library provides efficient algorithms to compose a set of unspent transaction outputs +//! (UTXOs). + +use crate::errors::Error; +use crate::CHANGE_LOWER; +use bitcoin::blockdata::transaction; +use bitcoin::Amount; +use bitcoin::FeeRate; +use bitcoin::TxIn; +use bitcoin::TxOut; +use rand::seq::SliceRandom; + +/// Calculates the effective_value of an input. +/// +/// Returns `Ok(None)` if the effective_value is negative. If the effective_value is positive, return `Ok(Some(Amount))`. +/// +/// ## Errors +/// +/// Returns `Err(Error::Multiplication)` if `FeeRate` * `Weight` overflows. +/// +fn get_effective_value( + utxo: &TxOut, + input: &TxIn, + fee_rate: FeeRate, +) -> Result, Error> { + let weight_predict = transaction::InputWeightPrediction::new( + input.script_sig.len(), + input.witness.iter().map(|elem| elem.len()), + ); + + let input_weight = transaction::predict_weight(vec![weight_predict], vec![]); + let input_fee: Option = fee_rate.checked_mul_by_weight(input_weight); + + match input_fee { + Some(f) => Ok(utxo.value.checked_sub(f)), + None => Err(Error::Multiplication(input_weight, fee_rate)), + } +} + +/// Randomly select coins for the given target by shuffling the UTXO pool and +/// taking UTXOs until the given target is reached. +/// +/// The fee_rate can have an impact on the selection process since the fee +/// must be paid for in addition to the target. However, the total fee +/// is dependant on the number of UTXOs consumed and the new inputs created. +/// The selection strategy therefore calculates the fees of what is known +/// ahead of time (the number of UTXOs create and the transaction header), +/// and then then for each new input, the effective_value is tracked which +/// deducts the fee for each individual input at selection time. For more +/// discussion see the following: +/// +/// https://bitcoin.stackexchange.com/questions/103654/calculating-fee-based-on-fee-rate-for-bitcoin-transaction/114847#114847 +/// +/// ## Parameters +/// /// +/// /// * `target` - target value to send to recipient. +/// /// * `fee_rate` - ratio of transaction amount per size. +/// /// * `utxos` - UTXOs from which to sum the target amount. +/// /// * `inputs` - the order corresponds to the ordering of the UTXOs where the first input consumes the first UTXO and so on. +/// /// * `transaction` - the transaction to populate with resulting inputs and outputs. +/// /// * `script_pubkey` - needed to compose the transaction outputs. +/// /// * `rng` - used primarily by tests to make the selection deterministic. +/// +pub fn select_coins_srd( + target: Amount, + fee_rate: FeeRate, + utxos: &mut [TxOut], + inputs: &mut [TxIn], + rng: &mut R, +) -> Result, Error> { + let mut result: Vec = Vec::new(); + + utxos.shuffle(rng); + + if inputs.len() != utxos.len() { + return Err(Error::SizeMismatch(inputs.len(), utxos.len())); + } + + let zipped = utxos.iter().zip(inputs.iter()); + + let threshold = target + CHANGE_LOWER; + let mut value = Amount::ZERO; + + for (utxo, input) in zipped { + // note: I'd prefer to use filter() and filter all inputs that have a negative + // effective_value, however, an error message is returned if a multiplication + // overflow occurs while the caclulating effective_value. + let effective_value: Option = get_effective_value(utxo, input, fee_rate)?; + + // skip if effective_value is negative. + match effective_value { + Some(e) => value += e, + None => continue, + } + + result.push(utxo.clone()); + + if value >= threshold { + return Ok(result); + } + } + + Ok(Vec::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::single_random_draw::select_coins_srd; + use crate::CHANGE_LOWER; + use bitcoin::Amount; + use bitcoin::ScriptBuf; + use core::str::FromStr; + use rand::rngs::mock::StepRng; + + const FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(10); + + fn create_outputs() -> Vec { + vec![ + TxOut { value: Amount::from_str("1 cBTC").unwrap(), script_pubkey: ScriptBuf::new() }, + TxOut { value: Amount::from_str("2 cBTC").unwrap(), script_pubkey: ScriptBuf::new() }, + ] + } + + fn create_inputs() -> Vec { + vec![TxIn::default(), TxIn::default()] + } + + fn get_rng() -> StepRng { + // [1, 2] + // let mut vec: Vec = (1..3).collect(); + // let mut rng = StepRng::new(0, 0); + // + // [2, 1] + // vec.shuffle(&mut rng); + + // shuffle() will always result in the order described above when a constant + // is used as the rng. The first is removed from the beginning and added to + // the end while the remaining elements keep their order. + StepRng::new(0, 0) + } + + #[test] + fn select_coins_srd_with_solution() { + let target: Amount = Amount::from_str("1.5 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result = select_coins_srd(target, FEE_RATE, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert_eq!(vec![utxos[0].clone()], result); + } + + #[test] + fn select_coins_srd_no_solution() { + let target: Amount = Amount::from_str("4 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result = select_coins_srd(target, FEE_RATE, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert!(result.is_empty()); + } + + #[test] + fn select_coins_srd_all_solution() { + let target: Amount = Amount::from_str("2.5 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result = + select_coins_srd(target, FeeRate::ZERO, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert_eq!(utxos.clone(), result); + } + + #[test] + fn select_coins_skip_negative_effective_value() { + let target: Amount = Amount::from_str("1 cBTC").unwrap() - CHANGE_LOWER; + + let mut utxos: Vec = vec![TxOut { + value: Amount::from_str("1 sat").unwrap(), + script_pubkey: ScriptBuf::new(), + }]; + + let mut inputs: Vec = vec![TxIn::default()]; + + let result = select_coins_srd(target, FEE_RATE, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert!(result.is_empty()); + } + + #[test] + fn select_coins_srd_fee_rate_error() { + let target: Amount = Amount::from_str("2 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result: Error = + select_coins_srd(target, FeeRate::MAX, &mut utxos, &mut inputs, &mut get_rng()) + .expect_err("expected error"); + + assert_eq!(result.to_string(), "204 * 18446744073709551615 exceeds u64 Max"); + } + + #[test] + fn select_coins_srd_change_output_too_small() { + let target: Amount = Amount::from_str("3 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result = select_coins_srd(target, FEE_RATE, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert!(result.is_empty()); + } + + #[test] + fn select_coins_srd_with_high_fee() { + let target: Amount = Amount::from_str("1.905 cBTC").unwrap(); + //The high fee_rate will cause both utxos to be consumed + //instead of just one. + let fee_rate: FeeRate = FeeRate::from_sat_per_kwu(250); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = create_inputs(); + + let result = select_coins_srd(target, fee_rate, &mut utxos, &mut inputs, &mut get_rng()) + .expect("unexpected error"); + + assert_eq!(utxos.clone(), result); + } + + #[test] + fn select_coins_srd_size_mismatch() { + let target: Amount = Amount::from_str("2 cBTC").unwrap(); + let mut utxos: Vec = create_outputs(); + let mut inputs: Vec = vec![]; + + let result: Error = + select_coins_srd(target, FEE_RATE, &mut utxos, &mut inputs, &mut get_rng()) + .expect_err("expected error"); + + assert_eq!(result.to_string(), "the number of inputs: 0 and outputs: 2 does not match"); + } +}