diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 1de36cc74..6dd180e84 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -209,6 +209,13 @@ fn main() { .long("min_conf") .default_value("1") .takes_value(true)) + .arg(Arg::with_name("selection_strategy") + .help("Coin/Output selection strategy.") + .short("s") + .long("selection") + .possible_values(&["default", "smallest"]) + .default_value("default") + .takes_value(true)) .arg(Arg::with_name("dest") .help("Send the transaction to the provided server") .short("d") @@ -397,9 +404,12 @@ fn wallet_command(wallet_args: &ArgMatches) { .expect("Could not parse amount as a number with optional decimal point."); let minimum_confirmations: u64 = send_args .value_of("minimum_confirmations") - .unwrap_or("1") + .unwrap() .parse() .expect("Could not parse minimum_confirmations as a whole number."); + let selection_strategy = send_args + .value_of("selection_strategy") + .expect("Selection strategy required"); let mut dest = "stdout"; if let Some(d) = send_args.value_of("dest") { dest = d; @@ -410,6 +420,7 @@ fn wallet_command(wallet_args: &ArgMatches) { amount, minimum_confirmations, dest.to_string(), + (selection_strategy == "default"), ); match result { Ok(_) => {}, //success messaged logged internally @@ -425,7 +436,7 @@ fn wallet_command(wallet_args: &ArgMatches) { .expect("Could not parse amount as number with optional decimal point."); let minimum_confirmations: u64 = send_args .value_of("minimum_confirmations") - .unwrap_or("1") + .unwrap() .parse() .expect("Could not parse minimum_confirmations as a whole number."); wallet::issue_burn_tx(&wallet_config, &keychain, amount, minimum_confirmations) diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index a583a3a4f..7f249a33b 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -36,6 +36,7 @@ pub fn issue_send_tx( amount: u64, minimum_confirmations: u64, dest: String, + selection_strategy: bool, ) -> Result<(), Error> { checker::refresh_outputs(config, keychain)?; @@ -52,6 +53,7 @@ pub fn issue_send_tx( current_height, minimum_confirmations, lock_height, + selection_strategy, )?; let partial_tx = build_partial_tx(amount, blind_sum, tx); @@ -79,12 +81,19 @@ fn build_send_tx( current_height: u64, minimum_confirmations: u64, lock_height: u64, + default_strategy: bool, ) -> Result<(Transaction, BlindingFactor), Error> { let key_id = keychain.clone().root_key_id(); // select some spendable coins from the wallet let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - wallet_data.select(key_id.clone(), current_height, minimum_confirmations) + wallet_data.select( + key_id.clone(), + amount, + current_height, + minimum_confirmations, + default_strategy, + ) })?; // build transaction skeleton with inputs and change @@ -124,9 +133,11 @@ pub fn issue_burn_tx( // select some spendable coins from the wallet let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - wallet_data.select(key_id.clone(), current_height, minimum_confirmations) + wallet_data.select(key_id.clone(), amount, current_height, minimum_confirmations, false) })?; + debug!(LOGGER, "selected some coins - {}", coins.len()); + let mut parts = inputs_and_change(&coins, config, keychain, key_id, amount)?; // add burn output and fees diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 06fe6a7bc..145d5cc32 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -499,8 +499,67 @@ impl WalletData { self.outputs.get(&key_id.to_hex()) } - /// Select spendable coins from the wallet + /// Select spendable coins from the wallet. + /// Default strategy is "aggressive". + /// Non-default strategy is "smallest_first". + /// When we introduce additional strategies we should pass something other than a bool in. pub fn select( + &self, + root_key_id: keychain::Identifier, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + default_strategy: bool, + ) -> Vec { + if default_strategy { + self.select_aggressive( + root_key_id, + current_height, + minimum_confirmations, + ) + } else { + self.select_smallest_first( + root_key_id, + amount, + current_height, + minimum_confirmations, + ) + } + } + + // Selects the smallest number of outputs after ordering them by value. + // Reduces "dust" but leaves larger outputs unspent if possible. + fn select_smallest_first( + &self, + root_key_id: keychain::Identifier, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + ) -> Vec { + let mut eligible = self.outputs + .values() + .filter(|out| { + out.root_key_id == root_key_id + && out.eligible_to_spend(current_height, minimum_confirmations) + }) + .cloned() + .collect::>(); + + eligible.sort_by_key(|out| out.value); + + let mut total_amount = 0; + eligible.iter() + .take_while(|out| { + let res = total_amount < amount; + total_amount += out.value; + res + }) + .cloned() + .collect::>() + } + + // Selects all eligible outputs to spend to reduce UTXO set as much as possible (the default). + fn select_aggressive( &self, root_key_id: keychain::Identifier, current_height: u64, @@ -512,7 +571,7 @@ impl WalletData { out.root_key_id == root_key_id && out.eligible_to_spend(current_height, minimum_confirmations) }) - .map(|out| out.clone()) + .cloned() .collect() }