diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -75,6 +75,34 @@ max_ancestors(max_ancestors_), max_descendants(max_descendants_) {} }; +struct OutputGroup { + std::vector m_outputs; + bool m_from_me{true}; + Amount m_value = Amount::zero(); + int m_depth{999}; + size_t m_ancestors{0}; + size_t m_descendants{0}; + Amount effective_value = Amount::zero(); + Amount fee = Amount::zero(); + Amount long_term_fee = Amount::zero(); + + OutputGroup() {} + OutputGroup(std::vector &&outputs, bool from_me, Amount value, + int depth, size_t ancestors, size_t descendants) + : m_outputs(std::move(outputs)), m_from_me(from_me), m_value(value), + m_depth(depth), m_ancestors(ancestors), m_descendants(descendants) {} + OutputGroup(const CInputCoin &output, int depth, bool from_me, + size_t ancestors, size_t descendants) + : OutputGroup() { + Insert(output, depth, from_me, ancestors, descendants); + } + void Insert(const CInputCoin &output, int depth, bool from_me, + size_t ancestors, size_t descendants); + std::vector::iterator Discard(const CInputCoin &output); + bool + EligibleForSpending(const CoinEligibilityFilter &eligibility_filter) const; +}; + bool SelectCoinsBnB(std::vector &utxo_pool, const Amount &target_value, const Amount &cost_of_change, std::set &out_set, Amount &value_ret, diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -330,3 +330,48 @@ return true; } + +/****************************************************************************** + + OutputGroup + + ******************************************************************************/ + +void OutputGroup::Insert(const CInputCoin &output, int depth, bool from_me, + size_t ancestors, size_t descendants) { + m_outputs.push_back(output); + m_from_me &= from_me; + m_value += output.effective_value; + m_depth = std::min(m_depth, depth); + // m_ancestors is currently the max ancestor count for all coins in the + // group; however, this is not ideal, as a wallet will consider e.g. thirty + // 2-ancestor coins as having two ancestors, when in reality it has 60 + // ancestors. + m_ancestors = std::max(m_ancestors, ancestors); + // m_descendants is the count as seen from the top ancestor, not the + // descendants as seen from the coin itself; thus, this value is accurate + m_descendants = std::max(m_descendants, descendants); + effective_value = m_value; +} + +std::vector::iterator +OutputGroup::Discard(const CInputCoin &output) { + auto it = m_outputs.begin(); + while (it != m_outputs.end() && it->outpoint != output.outpoint) { + ++it; + } + if (it == m_outputs.end()) { + return it; + } + m_value -= output.effective_value; + effective_value -= output.effective_value; + return m_outputs.erase(it); +} + +bool OutputGroup::EligibleForSpending( + const CoinEligibilityFilter &eligibility_filter) const { + return m_depth >= (m_from_me ? eligibility_filter.conf_mine + : eligibility_filter.conf_theirs) && + m_ancestors <= eligibility_filter.max_ancestors && + m_descendants <= eligibility_filter.max_descendants; +} diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -890,6 +890,8 @@ bool &bnb_used) const; bool IsSpent(const TxId &txid, uint32_t n) const; + std::vector GroupOutputs(const std::vector &outputs, + bool single_coin) const; bool IsLockedCoin(const TxId &txid, uint32_t n) const; void LockCoin(const COutPoint &output); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4640,3 +4640,44 @@ assert(false); } } + +std::vector +CWallet::GroupOutputs(const std::vector &outputs, + bool single_coin) const { + std::vector groups; + std::map gmap; + CTxDestination dst; + for (const auto &output : outputs) { + if (output.fSpendable) { + CInputCoin input_coin = output.GetInputCoin(); + + size_t ancestors, descendants; + g_mempool.GetTransactionAncestry(output.tx->GetId(), ancestors, + descendants); + if (!single_coin && + ExtractDestination(output.tx->tx->vout[output.i].scriptPubKey, + dst)) { + // Limit output groups to no more than 10 entries, to protect + // against inadvertently creating a too-large transaction + // when using -avoidpartialspends + if (gmap[dst].m_outputs.size() >= 10) { + groups.push_back(gmap[dst]); + gmap.erase(dst); + } + gmap[dst].Insert(input_coin, output.nDepth, + output.tx->IsFromMe(ISMINE_ALL), ancestors, + descendants); + } else { + groups.emplace_back(input_coin, output.nDepth, + output.tx->IsFromMe(ISMINE_ALL), ancestors, + descendants); + } + } + } + if (!single_coin) { + for (const auto &it : gmap) { + groups.push_back(it.second); + } + } + return groups; +}