From 9b593eb55c8bf3618cb92793cb7dc078fa7750b7 Mon Sep 17 00:00:00 2001 From: Stanimal Date: Thu, 23 Jun 2022 11:42:49 +0200 Subject: [PATCH] feat(wallet): improvements to UTXO selection --- .../output_manager_service/input_selection.rs | 126 +++++++++++++++ .../wallet/src/output_manager_service/mod.rs | 25 +-- .../src/output_manager_service/service.rs | 148 +++++------------- .../storage/database/backend.rs | 5 +- .../storage/database/mod.rs | 7 +- .../storage/sqlite_db/mod.rs | 11 +- .../storage/sqlite_db/output_sql.rs | 105 ++++++++----- 7 files changed, 255 insertions(+), 172 deletions(-) create mode 100644 base_layer/wallet/src/output_manager_service/input_selection.rs diff --git a/base_layer/wallet/src/output_manager_service/input_selection.rs b/base_layer/wallet/src/output_manager_service/input_selection.rs new file mode 100644 index 0000000000..026ebe5616 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/input_selection.rs @@ -0,0 +1,126 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + fmt, + fmt::{Display, Formatter}, +}; + +use tari_common_types::types::PublicKey; + +use crate::output_manager_service::storage::models::DbUnblindedOutput; + +#[derive(Debug, Clone, Default)] +pub struct UtxoSelectionCriteria { + pub filter: UtxoSelectionFilter, + pub ordering: UtxoSelectionOrdering, +} + +impl UtxoSelectionCriteria { + pub fn largest_first() -> Self { + Self { + filter: UtxoSelectionFilter::Standard, + ordering: UtxoSelectionOrdering::LargestFirst, + } + } + + pub fn for_token(unique_id: Vec, parent_public_key: Option) -> Self { + Self { + filter: UtxoSelectionFilter::TokenOutput { + unique_id, + parent_public_key, + }, + ordering: UtxoSelectionOrdering::Default, + } + } +} + +impl Display for UtxoSelectionCriteria { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "filter: {}, ordering: {}", self.filter, self.ordering) + } +} + +/// UTXO selection ordering +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UtxoSelectionOrdering { + /// The Default ordering is heuristic and depends on the requested value and the value of the available UTXOs. + /// If the requested value is larger than the largest available UTXO, we select LargerFirst as inputs, otherwise + /// SmallestFirst. + Default, + /// Start from the smallest UTXOs and work your way up until the amount is covered. Main benefit + /// is removing small UTXOs from the blockchain, con is that it costs more in fees + SmallestFirst, + /// A strategy that selects the largest UTXOs first. Preferred when the amount is large + LargestFirst, +} + +impl Default for UtxoSelectionOrdering { + fn default() -> Self { + UtxoSelectionOrdering::Default + } +} + +impl Display for UtxoSelectionOrdering { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UtxoSelectionOrdering::SmallestFirst => write!(f, "Smallest"), + UtxoSelectionOrdering::LargestFirst => write!(f, "Largest"), + UtxoSelectionOrdering::Default => write!(f, "Default"), + } + } +} + +#[derive(Debug, Clone)] +pub enum UtxoSelectionFilter { + /// Select OutputType::Standard or OutputType::Coinbase outputs only + Standard, + /// Select matching token outputs. This will be deprecated in future. + TokenOutput { + unique_id: Vec, + parent_public_key: Option, + }, + /// Selects specific outputs. All outputs must be exist and be spendable. + SpecificOutputs { outputs: Vec }, +} + +impl Default for UtxoSelectionFilter { + fn default() -> Self { + UtxoSelectionFilter::Standard + } +} + +impl Display for UtxoSelectionFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + UtxoSelectionFilter::Standard => { + write!(f, "Standard") + }, + UtxoSelectionFilter::TokenOutput { .. } => { + write!(f, "TokenOutput{{..}}") + }, + UtxoSelectionFilter::SpecificOutputs { outputs } => { + write!(f, "Specific({} output(s))", outputs.len()) + }, + } + } +} diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index 6b0238ac47..14f9a7b549 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -20,7 +20,20 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::sync::Arc; +pub mod config; +pub mod error; +pub mod handle; + +mod input_selection; +pub use input_selection::{UtxoSelectionCriteria, UtxoSelectionFilter, UtxoSelectionOrdering}; + +mod recovery; +pub mod resources; +pub mod service; +pub mod storage; +mod tasks; + +use std::{marker::PhantomData, sync::Arc}; use futures::future; use log::*; @@ -47,16 +60,6 @@ use crate::{ }, }; -pub mod config; -pub mod error; -pub mod handle; -mod recovery; -pub mod resources; -pub mod service; -pub mod storage; -mod tasks; -use std::marker::PhantomData; - const LOG_TARGET: &str = "wallet::output_manager_service::initializer"; pub struct OutputManagerServiceInitializer diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 35e8b90b88..772b6ae135 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{convert::TryInto, fmt, fmt::Display, sync::Arc}; +use std::{convert::TryInto, fmt, sync::Arc}; use blake2::Digest; use diesel::result::{DatabaseErrorKind, Error as DieselError}; @@ -80,6 +80,7 @@ use crate::{ PublicRewindKeys, RecoveredOutput, }, + input_selection::UtxoSelectionCriteria, recovery::StandardUtxoRecoverer, resources::{OutputManagerKeyManagerBranch, OutputManagerResources}, storage::{ @@ -840,9 +841,7 @@ where fee_per_gram, num_outputs, metadata_byte_size * num_outputs, - None, - None, - None, + UtxoSelectionCriteria::default(), ) .await?; @@ -887,16 +886,15 @@ where recipient_covenant.consensus_encode_exact_size(), ); + // TODO: Some(unique_id) means select the unique_id AND use the features of UTXOs with the unique_id. These + // should be able to be specified independently. + let selection_criteria = match unique_id.as_ref() { + Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key.as_ref().cloned()), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self - .select_utxos( - amount, - fee_per_gram, - 1, - metadata_byte_size, - None, - unique_id.as_ref(), - parent_public_key.as_ref(), - ) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) .await?; // TODO: improve this logic #LOGGED @@ -1100,15 +1098,19 @@ where .consensus_encode_exact_size() }) }); + + let selection_criteria = match spending_unique_id { + Some(unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), spending_parent_public_key.cloned()), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self .select_utxos( total_value, fee_per_gram, outputs.len(), metadata_byte_size, - None, - spending_unique_id, - spending_parent_public_key, + selection_criteria, ) .await?; let offset = PrivateKey::random(&mut OsRng); @@ -1265,16 +1267,13 @@ where covenant.consensus_encode_exact_size(), ); + let selection_criteria = match unique_id { + Some(ref unique_id) => UtxoSelectionCriteria::for_token(unique_id.clone(), parent_public_key), + None => UtxoSelectionCriteria::default(), + }; + let input_selection = self - .select_utxos( - amount, - fee_per_gram, - 1, - metadata_byte_size, - None, - unique_id.as_ref(), - parent_public_key.as_ref(), - ) + .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, selection_criteria) .await?; let offset = PrivateKey::random(&mut OsRng); @@ -1426,47 +1425,23 @@ where /// Select which unspent transaction outputs to use to send a transaction of the specified amount. Use the specified /// selection strategy to choose the outputs. It also determines if a change output is required. - #[allow(clippy::too_many_lines)] async fn select_utxos( &mut self, amount: MicroTari, fee_per_gram: MicroTari, num_outputs: usize, output_metadata_byte_size: usize, - strategy: Option, - unique_id: Option<&Vec>, - parent_public_key: Option<&PublicKey>, + selection_criteria: UtxoSelectionCriteria, ) -> Result { - let token = match unique_id { - Some(unique_id) => { - debug!(target: LOG_TARGET, "Looking for {:?}", unique_id); - // todo: new method to fetch by unique asset id - let uo = self.resources.db.fetch_all_unspent_outputs()?; - if let Some(token_id) = uo.into_iter().find(|x| match &x.unblinded_output.features.unique_id { - Some(token_unique_id) => { - debug!(target: LOG_TARGET, "Comparing with {:?}", token_unique_id); - token_unique_id == unique_id && - x.unblinded_output.features.parent_public_key.as_ref() == parent_public_key - }, - _ => false, - }) { - Some(token_id) - } else { - return Err(OutputManagerError::TokenUniqueIdNotFound); - } - }, - _ => None, - }; debug!( target: LOG_TARGET, - "select_utxos amount: {}, token : {:?}, fee_per_gram: {}, num_outputs: {}, output_metadata_byte_size: {}, \ - strategy: {:?}", + "select_utxos amount: {}, fee_per_gram: {}, num_outputs: {}, output_metadata_byte_size: {}, \ + selection_criteria: {:?}", amount, - token, fee_per_gram, num_outputs, output_metadata_byte_size, - strategy + selection_criteria ); let mut utxos = Vec::new(); @@ -1474,44 +1449,19 @@ where let mut fee_without_change = MicroTari::from(0); let mut fee_with_change = MicroTari::from(0); let fee_calc = self.get_fee_calc(); - if let Some(token) = token { - utxos_total_value = token.unblinded_output.value; - utxos.push(token); - } // Attempt to get the chain tip height let chain_metadata = self.base_node_service.get_chain_metadata().await?; - let (connected, tip_height) = match &chain_metadata { - Some(metadata) => (true, Some(metadata.height_of_longest_chain())), - None => (false, None), - }; - - // If no strategy was specified and no metadata is available, then make sure to use MaturitythenSmallest - let strategy = match (strategy, connected) { - (Some(s), _) => s, - (None, false) => UTXOSelectionStrategy::MaturityThenSmallest, - (None, true) => UTXOSelectionStrategy::Default, // use the selection heuristic next - }; - // Heuristic for selection strategy: Default to MaturityThenSmallest, but if the amount is greater than - // the largest UTXO, use Largest UTXOs first. - // let strategy = match (strategy, uo.is_empty()) { - // (Some(s), _) => s, - // (None, true) => UTXOSelectionStrategy::Smallest, - // (None, false) => { - // let largest_utxo = &uo[uo.len() - 1]; - // if amount > largest_utxo.unblinded_output.value { - // UTXOSelectionStrategy::Largest - // } else { - // UTXOSelectionStrategy::MaturityThenSmallest - // } - // }, - // }; - warn!(target: LOG_TARGET, "select_utxos selection strategy: {}", strategy); + warn!( + target: LOG_TARGET, + "select_utxos selection criteria: {}", selection_criteria + ); + let tip_height = chain_metadata.as_ref().map(|m| m.height_of_longest_chain()); let uo = self .resources .db - .fetch_unspent_outputs_for_spending(strategy, amount, tip_height)?; + .fetch_unspent_outputs_for_spending(selection_criteria, amount, tip_height)?; trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); // Assumes that default Outputfeatures are used for change utxo @@ -1619,9 +1569,7 @@ where fee_per_gram, output_count, output_count * metadata_byte_size, - Some(UTXOSelectionStrategy::Largest), - None, - None, + UtxoSelectionCriteria::largest_first(), ) .await?; @@ -2080,32 +2028,6 @@ where } } -/// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction -#[derive(Debug, PartialEq, Eq)] -pub enum UTXOSelectionStrategy { - // Start from the smallest UTXOs and work your way up until the amount is covered. Main benefit - // is removing small UTXOs from the blockchain, con is that it costs more in fees - Smallest, - // Start from oldest maturity to reduce the likelihood of grabbing locked up UTXOs - MaturityThenSmallest, - // A strategy that selects the largest UTXOs first. Preferred when the amount is large - Largest, - // Heuristic for selection strategy: MaturityThenSmallest, but if the amount is greater than - // the largest UTXO, use Largest UTXOs first - Default, -} - -impl Display for UTXOSelectionStrategy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - UTXOSelectionStrategy::Smallest => write!(f, "Smallest"), - UTXOSelectionStrategy::MaturityThenSmallest => write!(f, "MaturityThenSmallest"), - UTXOSelectionStrategy::Largest => write!(f, "Largest"), - UTXOSelectionStrategy::Default => write!(f, "Default"), - } - } -} - /// This struct holds the detailed balance of the Output Manager Service. #[derive(Debug, Clone, PartialEq)] pub struct Balance { diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index 69e74f001b..add101d77e 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -10,7 +10,8 @@ use tari_core::transactions::transaction_components::{OutputFlags, TransactionOu use crate::output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ database::{DbKey, DbValue, OutputBackendQuery, WriteOperation}, models::DbUnblindedOutput, @@ -109,7 +110,7 @@ pub trait OutputManagerBackend: Send + Sync + Clone { fn add_unvalidated_output(&self, output: DbUnblindedOutput, tx_id: TxId) -> Result<(), OutputManagerStorageError>; fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: u64, current_tip_height: Option, ) -> Result, OutputManagerStorageError>; diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index a550d61048..0e55dafacd 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -41,7 +41,8 @@ use tari_utilities::hex::Hex; use crate::output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, OutputStatus, @@ -250,13 +251,13 @@ where T: OutputManagerBackend + 'static /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. pub fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: MicroTari, tip_height: Option, ) -> Result, OutputManagerStorageError> { let utxos = self .db - .fetch_unspent_outputs_for_spending(strategy, amount.as_u64(), tip_height)?; + .fetch_unspent_outputs_for_spending(selection_criteria, amount.as_u64(), tip_height)?; Ok(utxos) } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index 31cf388ab3..1b73c7160d 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -43,12 +43,13 @@ use tokio::time::Instant; use crate::{ output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + service::Balance, storage::{ database::{DbKey, DbKeyValuePair, DbValue, OutputBackendQuery, OutputManagerBackend, WriteOperation}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, OutputStatus, }, + UtxoSelectionCriteria, }, schema::{known_one_sided_payment_scripts, outputs, outputs::columns}, storage::sqlite_utilities::wallet_db_connection::WalletDbConnection, @@ -1188,18 +1189,14 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. fn fetch_unspent_outputs_for_spending( &self, - strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: u64, tip_height: Option, ) -> Result, OutputManagerStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); - let tip = match tip_height { - Some(v) => v as i64, - None => i64::MAX, - }; - let mut outputs = OutputSql::fetch_unspent_outputs_for_spending(strategy, amount, tip, &conn)?; + let mut outputs = OutputSql::fetch_unspent_outputs_for_spending(selection_criteria, amount, tip_height, &conn)?; for o in &mut outputs { self.decrypt_if_necessary(o)?; } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index 53e4b90c41..8ea2a6103b 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -45,13 +45,16 @@ use tari_utilities::hash::Hashable; use crate::{ output_manager_service::{ error::OutputManagerStorageError, - service::{Balance, UTXOSelectionStrategy}, + input_selection::UtxoSelectionCriteria, + service::Balance, storage::{ database::{OutputBackendQuery, SortDirection}, models::DbUnblindedOutput, sqlite_db::{UpdateOutput, UpdateOutputSql}, OutputStatus, }, + UtxoSelectionFilter, + UtxoSelectionOrdering, }, schema::outputs, util::{ @@ -178,54 +181,84 @@ impl OutputSql { /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. #[allow(clippy::cast_sign_loss)] pub fn fetch_unspent_outputs_for_spending( - mut strategy: UTXOSelectionStrategy, + selection_criteria: UtxoSelectionCriteria, amount: u64, - tip_height: i64, + tip_height: Option, conn: &SqliteConnection, ) -> Result, OutputManagerStorageError> { - if strategy == UTXOSelectionStrategy::Default { - // lets get the max value for all utxos - let max: Vec = outputs::table - .filter(outputs::status.eq(OutputStatus::Unspent as i32)) - .filter(outputs::script_lock_height.le(tip_height)) - .filter(outputs::maturity.le(tip_height)) - .filter(outputs::features_unique_id.is_null()) - .filter(outputs::features_parent_public_key.is_null()) - .order(outputs::value.desc()) - .select(outputs::value) - .limit(1) - .load(conn)?; - if max.is_empty() { - strategy = UTXOSelectionStrategy::Smallest - } else if amount > max[0] as u64 { - strategy = UTXOSelectionStrategy::Largest - } else { - strategy = UTXOSelectionStrategy::MaturityThenSmallest - } - } - let mut query = outputs::table .into_boxed() .filter(outputs::status.eq(OutputStatus::Unspent as i32)) - .filter(outputs::script_lock_height.le(tip_height)) - .filter(outputs::maturity.le(tip_height)) - .filter(outputs::features_unique_id.is_null()) - .filter(outputs::features_parent_public_key.is_null()) .order_by(outputs::spending_priority.desc()); - match strategy { - UTXOSelectionStrategy::Smallest => { - query = query.then_order_by(outputs::value.asc()); + + match selection_criteria.filter { + UtxoSelectionFilter::Standard => { + query = query + .filter(outputs::features_unique_id.is_null()) + .filter(outputs::features_parent_public_key.is_null()); }, - UTXOSelectionStrategy::MaturityThenSmallest => { + UtxoSelectionFilter::TokenOutput { + parent_public_key, + unique_id, + } => { query = query - .then_order_by(outputs::maturity.asc()) - .then_order_by(outputs::value.asc()); + .filter(outputs::features_unique_id.eq(unique_id)) + .filter(outputs::features_parent_public_key.eq(parent_public_key.as_ref().map(|pk| pk.to_vec()))); + }, + UtxoSelectionFilter::SpecificOutputs { outputs } => { + query = query.filter(outputs::hash.eq_any(outputs.into_iter().map(|o| o.hash))) + }, + } + + match selection_criteria.ordering { + UtxoSelectionOrdering::SmallestFirst => { + query = query.then_order_by(outputs::value.asc()); }, - UTXOSelectionStrategy::Largest => { + UtxoSelectionOrdering::LargestFirst => { query = query.then_order_by(outputs::value.desc()); }, - UTXOSelectionStrategy::Default => {}, + UtxoSelectionOrdering::Default => { + let i64_tip_height = tip_height.and_then(|h| i64::try_from(h).ok()).unwrap_or(i64::MAX); + // lets get the max value for all utxos + let max: Option = outputs::table + .filter(outputs::status.eq(OutputStatus::Unspent as i32)) + .filter(outputs::script_lock_height.le(i64_tip_height)) + .filter(outputs::maturity.le(i64_tip_height)) + .filter(outputs::features_unique_id.is_null()) + .filter(outputs::features_parent_public_key.is_null()) + .order(outputs::value.desc()) + .select(outputs::value) + .first(conn) + .optional()?; + match max { + Some(max) if amount > max as u64 => { + // Want to reduce the number of inputs to reduce fees + query = query.then_order_by(outputs::value.desc()); + }, + Some(_) => { + // Use the smaller utxos to make up this transaction. + query = query.then_order_by(outputs::value.asc()); + }, + None => { + // No spendable UTXOs? + query = query.then_order_by(outputs::value.asc()); + }, + } + }, }; + match tip_height { + Some(tip_height) => { + let i64_tip_height = i64::try_from(tip_height).unwrap_or(i64::MAX); + query = query + .filter(outputs::script_lock_height.le(i64_tip_height)) + .filter(outputs::maturity.le(i64_tip_height)); + }, + None => { + // If we don't know the current tip height, order by maturity ASC to reduce the chances of a locked + // output being used. + query = query.then_order_by(outputs::maturity.asc()); + }, + } Ok(query.load(conn)?) }