From f0f6a3577bef2e9ebd084fe35850e4e9580128a9 Mon Sep 17 00:00:00 2001 From: furszy Date: Thu, 28 Jul 2022 10:25:29 -0300 Subject: [PATCH] RPC: listunspent, add "include immature coinbase" flag so we can return the immature coinbase UTXOs as well. --- doc/release-notes-25730.md | 6 ++++++ src/wallet/rpc/coins.cpp | 9 ++++++++- src/wallet/spend.cpp | 9 +++++---- src/wallet/spend.h | 5 +++-- test/functional/wallet_balance.py | 10 ++++++++++ 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 doc/release-notes-25730.md diff --git a/doc/release-notes-25730.md b/doc/release-notes-25730.md new file mode 100644 index 00000000000..33393cf3149 --- /dev/null +++ b/doc/release-notes-25730.md @@ -0,0 +1,6 @@ +RPC Wallet +---------- + +- RPC `listunspent` now has a new argument `include_immature_coinbase` + to include coinbase UTXOs that don't meet the minimum spendability + depth requirement (which before were silently skipped). (#25730) \ No newline at end of file diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp index 9c0c953a7a4..da961b97ef8 100644 --- a/src/wallet/rpc/coins.cpp +++ b/src/wallet/rpc/coins.cpp @@ -515,6 +515,7 @@ RPCHelpMan listunspent() {"maximumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Maximum value of each UTXO in " + CURRENCY_UNIT + ""}, {"maximumCount", RPCArg::Type::NUM, RPCArg::DefaultHint{"unlimited"}, "Maximum number of UTXOs"}, {"minimumSumAmount", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"unlimited"}, "Minimum sum value of all UTXOs in " + CURRENCY_UNIT + ""}, + {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase UTXOs"} }, RPCArgOptions{.oneline_description="query_options"}}, }, @@ -594,6 +595,7 @@ RPCHelpMan listunspent() CAmount nMaximumAmount = MAX_MONEY; CAmount nMinimumSumAmount = MAX_MONEY; uint64_t nMaximumCount = 0; + bool include_immature_coinbase{false}; if (!request.params[4].isNull()) { const UniValue& options = request.params[4].get_obj(); @@ -604,6 +606,7 @@ RPCHelpMan listunspent() {"maximumAmount", UniValueType()}, {"minimumSumAmount", UniValueType()}, {"maximumCount", UniValueType(UniValue::VNUM)}, + {"include_immature_coinbase", UniValueType(UniValue::VBOOL)} }, true, true); @@ -618,6 +621,10 @@ RPCHelpMan listunspent() if (options.exists("maximumCount")) nMaximumCount = options["maximumCount"].getInt(); + + if (options.exists("include_immature_coinbase")) { + include_immature_coinbase = options["include_immature_coinbase"].get_bool(); + } } // Make sure the results are valid at least up to the most recent block @@ -633,7 +640,7 @@ RPCHelpMan listunspent() cctl.m_max_depth = nMaxDepth; cctl.m_include_unsafe_inputs = include_unsafe; LOCK(pwallet->cs_wallet); - vecOutputs = AvailableCoinsListUnspent(*pwallet, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount).All(); + vecOutputs = AvailableCoinsListUnspent(*pwallet, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, include_immature_coinbase).All(); } LOCK(pwallet->cs_wallet); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 644b2b587c3..03d260ea968 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -195,7 +195,8 @@ CoinsResult AvailableCoins(const CWallet& wallet, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount, - bool only_spendable) + bool only_spendable, + bool include_immature_coinbase) { AssertLockHeld(wallet.cs_wallet); @@ -213,7 +214,7 @@ CoinsResult AvailableCoins(const CWallet& wallet, const uint256& wtxid = entry.first; const CWalletTx& wtx = entry.second; - if (wallet.IsTxImmatureCoinBase(wtx)) + if (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase) continue; int nDepth = wallet.GetTxDepthInMainChain(wtx); @@ -344,9 +345,9 @@ CoinsResult AvailableCoins(const CWallet& wallet, return result; } -CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount) +CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount, bool include_immature_coinbase) { - return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, /*only_spendable=*/false); + return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, /*only_spendable=*/false, include_immature_coinbase); } CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl) diff --git a/src/wallet/spend.h b/src/wallet/spend.h index b66bb3797cd..1d4570dcbb7 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -65,13 +65,14 @@ CoinsResult AvailableCoins(const CWallet& wallet, const CAmount& nMaximumAmount = MAX_MONEY, const CAmount& nMinimumSumAmount = MAX_MONEY, const uint64_t nMaximumCount = 0, - bool only_spendable = true) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + bool only_spendable = true, + bool include_immature_coinbase = false) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); /** * Wrapper function for AvailableCoins which skips the `feerate` parameter. Use this function * to list all available coins (e.g. listunspent RPC) while not intending to fund a transaction. */ -CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl = nullptr, const CAmount& nMinimumAmount = 1, const CAmount& nMaximumAmount = MAX_MONEY, const CAmount& nMinimumSumAmount = MAX_MONEY, const uint64_t nMaximumCount = 0) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl = nullptr, const CAmount& nMinimumAmount = 1, const CAmount& nMaximumAmount = MAX_MONEY, const CAmount& nMinimumSumAmount = MAX_MONEY, const uint64_t nMaximumCount = 0, bool include_immature_coinbase = false) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl = nullptr); diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index ec58ace4a28..60da22ca263 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -77,8 +77,18 @@ class WalletTest(BitcoinTestFramework): self.log.info("Mining blocks ...") self.generate(self.nodes[0], 1) self.generate(self.nodes[1], 1) + + # Verify listunspent returns immature coinbase if 'include_immature_coinbase' is set + assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1) + assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 0) + self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, ADDRESS_WATCHONLY) + # Verify listunspent returns all immature coinbases if 'include_immature_coinbase' is set + # For now, only the legacy wallet will see the coinbases going to the imported 'ADDRESS_WATCHONLY' + assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 1 if self.options.descriptors else 2) + assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1 if self.options.descriptors else COINBASE_MATURITY + 2) + if not self.options.descriptors: # Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 50)