aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Chow <github@achow101.com>2022-11-15 19:40:25 -0500
committerAndrew Chow <github@achow101.com>2022-11-15 19:53:04 -0500
commitf0c646f026e652082e798800136dc06c734fdab6 (patch)
treef6018207a3b233a1c54b4cf944e91c21ede4f6cf
parent5602cc7ccf4a51ad52dadc495b732f54b43ceb99 (diff)
parentfa84df1f033a5d1a8342ea941eca0b5ef73d78e7 (diff)
Merge bitcoin/bitcoin#25730: RPC: listunspent, add "include immature coinbase" flag
fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 scripted-diff: wallet: rename AvailableCoinsParams members to snake_case (furszy) 61c2265629fdf11a2cc266fad54ceb0a1247bb5e wallet: group AvailableCoins filtering parameters in a single struct (furszy) f0f6a3577bef2e9ebd084fe35850e4e9580128a9 RPC: listunspent, add "include immature coinbase" flag (furszy) Pull request description: Simple PR; adds a "include_immature_coinbase" flag to `listunspent` to include the immature coinbase UTXOs on the response. Requested by #25728. ACKs for top commit: danielabrozzoni: reACK fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 achow101: ACK fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 aureleoules: reACK fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 kouloumos: reACK fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 theStack: Code-review ACK fa84df1f033a5d1a8342ea941eca0b5ef73d78e7 Tree-SHA512: 0f3544cb8cfd0378a5c74594480f78e9e919c6cfb73a83e0f3112f8a0132a9147cf846f999eab522cea9ef5bd3ffd60690ea2ca367dde457b0554d7f38aec792
-rw-r--r--doc/release-notes-25730.md6
-rw-r--r--src/bench/wallet_create_tx.cpp5
-rw-r--r--src/wallet/rpc/coins.cpp22
-rw-r--r--src/wallet/rpc/spend.cpp4
-rw-r--r--src/wallet/spend.cpp39
-rw-r--r--src/wallet/spend.h25
-rwxr-xr-xtest/functional/wallet_balance.py10
7 files changed, 65 insertions, 46 deletions
diff --git a/doc/release-notes-25730.md b/doc/release-notes-25730.md
new file mode 100644
index 0000000000..33393cf314
--- /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/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp
index 207b22c584..8f5c50872b 100644
--- a/src/bench/wallet_create_tx.cpp
+++ b/src/bench/wallet_create_tx.cpp
@@ -111,9 +111,10 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type
CAmount target = 0;
if (preset_inputs) {
// Select inputs, each has 49 BTC
+ wallet::CoinFilterParams filter_coins;
+ filter_coins.max_count = preset_inputs->num_of_internal_inputs;
const auto& res = WITH_LOCK(wallet.cs_wallet,
- return wallet::AvailableCoins(wallet, nullptr, std::nullopt, 1, MAX_MONEY,
- MAX_MONEY, preset_inputs->num_of_internal_inputs));
+ return wallet::AvailableCoins(wallet, /*coinControl=*/nullptr, /*feerate=*/std::nullopt, filter_coins));
for (int i=0; i < preset_inputs->num_of_internal_inputs; i++) {
const auto& coin{res.coins.at(output_type)[i]};
target += coin.txout.nValue;
diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp
index 9c0c953a7a..6021e4bf4f 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"}},
},
@@ -590,10 +591,8 @@ RPCHelpMan listunspent()
include_unsafe = request.params[3].get_bool();
}
- CAmount nMinimumAmount = 0;
- CAmount nMaximumAmount = MAX_MONEY;
- CAmount nMinimumSumAmount = MAX_MONEY;
- uint64_t nMaximumCount = 0;
+ CoinFilterParams filter_coins;
+ filter_coins.min_amount = 0;
if (!request.params[4].isNull()) {
const UniValue& options = request.params[4].get_obj();
@@ -604,20 +603,25 @@ RPCHelpMan listunspent()
{"maximumAmount", UniValueType()},
{"minimumSumAmount", UniValueType()},
{"maximumCount", UniValueType(UniValue::VNUM)},
+ {"include_immature_coinbase", UniValueType(UniValue::VBOOL)}
},
true, true);
if (options.exists("minimumAmount"))
- nMinimumAmount = AmountFromValue(options["minimumAmount"]);
+ filter_coins.min_amount = AmountFromValue(options["minimumAmount"]);
if (options.exists("maximumAmount"))
- nMaximumAmount = AmountFromValue(options["maximumAmount"]);
+ filter_coins.max_amount = AmountFromValue(options["maximumAmount"]);
if (options.exists("minimumSumAmount"))
- nMinimumSumAmount = AmountFromValue(options["minimumSumAmount"]);
+ filter_coins.min_sum_amount = AmountFromValue(options["minimumSumAmount"]);
if (options.exists("maximumCount"))
- nMaximumCount = options["maximumCount"].getInt<int64_t>();
+ filter_coins.max_count = options["maximumCount"].getInt<int64_t>();
+
+ if (options.exists("include_immature_coinbase")) {
+ filter_coins.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 +637,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, filter_coins).All();
}
LOCK(pwallet->cs_wallet);
diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index f43cc8fb42..0fa693e7e7 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -1385,7 +1385,9 @@ RPCHelpMan sendall()
total_input_value += tx->tx->vout[input.prevout.n].nValue;
}
} else {
- for (const COutput& output : AvailableCoins(*pwallet, &coin_control, fee_rate, /*nMinimumAmount=*/0).All()) {
+ CoinFilterParams coins_params;
+ coins_params.min_amount = 0;
+ for (const COutput& output : AvailableCoins(*pwallet, &coin_control, fee_rate, coins_params).All()) {
CHECK_NONFATAL(output.input_bytes > 0);
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
continue;
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index 644b2b587c..8c0d56a1cb 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -191,11 +191,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
CoinsResult AvailableCoins(const CWallet& wallet,
const CCoinControl* coinControl,
std::optional<CFeeRate> feerate,
- const CAmount& nMinimumAmount,
- const CAmount& nMaximumAmount,
- const CAmount& nMinimumSumAmount,
- const uint64_t nMaximumCount,
- bool only_spendable)
+ const CoinFilterParams& params)
{
AssertLockHeld(wallet.cs_wallet);
@@ -213,7 +209,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
const uint256& wtxid = entry.first;
const CWalletTx& wtx = entry.second;
- if (wallet.IsTxImmatureCoinBase(wtx))
+ if (wallet.IsTxImmatureCoinBase(wtx) && !params.include_immature_coinbase)
continue;
int nDepth = wallet.GetTxDepthInMainChain(wtx);
@@ -272,7 +268,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
const CTxOut& output = wtx.tx->vout[i];
const COutPoint outpoint(wtxid, i);
- if (output.nValue < nMinimumAmount || output.nValue > nMaximumAmount)
+ if (output.nValue < params.min_amount || output.nValue > params.max_amount)
continue;
// Skip manually selected coins (the caller can fetch them directly)
@@ -304,7 +300,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
// Filter by spendable outputs only
- if (!spendable && only_spendable) continue;
+ if (!spendable && params.only_spendable) continue;
// Obtain script type
std::vector<std::vector<uint8_t>> script_solutions;
@@ -328,14 +324,14 @@ CoinsResult AvailableCoins(const CWallet& wallet,
// Cache total amount as we go
result.total_amount += output.nValue;
// Checks the sum amount of all UTXO's.
- if (nMinimumSumAmount != MAX_MONEY) {
- if (result.total_amount >= nMinimumSumAmount) {
+ if (params.min_sum_amount != MAX_MONEY) {
+ if (result.total_amount >= params.min_sum_amount) {
return result;
}
}
// Checks the maximum number of UTXO's.
- if (nMaximumCount > 0 && result.Size() >= nMaximumCount) {
+ if (params.max_count > 0 && result.Size() >= params.max_count) {
return result;
}
}
@@ -344,21 +340,16 @@ 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, CoinFilterParams params)
{
- return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, /*only_spendable=*/false);
+ params.only_spendable = false;
+ return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, params);
}
CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl)
{
LOCK(wallet.cs_wallet);
- return AvailableCoins(wallet, coinControl,
- /*feerate=*/ std::nullopt,
- /*nMinimumAmount=*/ 1,
- /*nMaximumAmount=*/ MAX_MONEY,
- /*nMinimumSumAmount=*/ MAX_MONEY,
- /*nMaximumCount=*/ 0
- ).total_amount;
+ return AvailableCoins(wallet, coinControl).total_amount;
}
const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output)
@@ -897,13 +888,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
// allowed (coins automatically selected by the wallet)
CoinsResult available_coins;
if (coin_control.m_allow_other_inputs) {
- available_coins = AvailableCoins(wallet,
- &coin_control,
- coin_selection_params.m_effective_feerate,
- 1, /*nMinimumAmount*/
- MAX_MONEY, /*nMaximumAmount*/
- MAX_MONEY, /*nMinimumSumAmount*/
- 0); /*nMaximumCount*/
+ available_coins = AvailableCoins(wallet, &coin_control, coin_selection_params.m_effective_feerate);
}
// Choose coins to use
diff --git a/src/wallet/spend.h b/src/wallet/spend.h
index b66bb3797c..ba2c6638c8 100644
--- a/src/wallet/spend.h
+++ b/src/wallet/spend.h
@@ -55,23 +55,34 @@ struct CoinsResult {
CAmount total_amount{0};
};
+struct CoinFilterParams {
+ // Outputs below the minimum amount will not get selected
+ CAmount min_amount{1};
+ // Outputs above the maximum amount will not get selected
+ CAmount max_amount{MAX_MONEY};
+ // Return outputs until the minimum sum amount is covered
+ CAmount min_sum_amount{MAX_MONEY};
+ // Maximum number of outputs that can be returned
+ uint64_t max_count{0};
+ // By default, return only spendable outputs
+ bool only_spendable{true};
+ // By default, do not include immature coinbase outputs
+ bool include_immature_coinbase{false};
+};
+
/**
* Populate the CoinsResult struct with vectors of available COutputs, organized by OutputType.
*/
CoinsResult AvailableCoins(const CWallet& wallet,
const CCoinControl* coinControl = nullptr,
std::optional<CFeeRate> feerate = std::nullopt,
- const CAmount& nMinimumAmount = 1,
- 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);
+ const CoinFilterParams& params = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
/**
- * Wrapper function for AvailableCoins which skips the `feerate` parameter. Use this function
+ * Wrapper function for AvailableCoins which skips the `feerate` and `CoinFilterParams::only_spendable` parameters. 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, CoinFilterParams params = {}) 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 ec58ace4a2..60da22ca26 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)