Skip to content

Commit 0aa4c3f

Browse files
achow101Claude Code
authored andcommitted
Merge bitcoin#25730: RPC: listunspent, add "include immature coinbase" flag
fa84df1 scripted-diff: wallet: rename AvailableCoinsParams members to snake_case (furszy) 61c2265 wallet: group AvailableCoins filtering parameters in a single struct (furszy) f0f6a35 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 bitcoin#25728. ACKs for top commit: danielabrozzoni: reACK fa84df1 achow101: ACK fa84df1 aureleoules: reACK fa84df1 kouloumos: reACK fa84df1 theStack: Code-review ACK fa84df1 Tree-SHA512: 0f3544cb8cfd0378a5c74594480f78e9e919c6cfb73a83e0f3112f8a0132a9147cf846f999eab522cea9ef5bd3ffd60690ea2ca367dde457b0554d7f38aec792
1 parent e3d61f2 commit 0aa4c3f

File tree

5 files changed

+60
-44
lines changed

5 files changed

+60
-44
lines changed

doc/release-notes-25730.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
RPC Wallet
2+
----------
3+
4+
- RPC `listunspent` now has a new argument `include_immature_coinbase`
5+
to include coinbase UTXOs that don't meet the minimum spendability
6+
depth requirement (which before were silently skipped). (#25730)

src/wallet/rpc/coins.cpp

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ RPCHelpMan listunspent()
527527
{"coinType", RPCArg::Type::NUM, RPCArg::Default{0}, "Filter coinTypes as follows:\n"
528528
"0=ALL_COINS, 1=ONLY_FULLY_MIXED, 2=ONLY_READY_TO_MIX, 3=ONLY_NONDENOMINATED,\n"
529529
"4=ONLY_MASTERNODE_COLLATERAL, 5=ONLY_COINJOIN_COLLATERAL" },
530+
{"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase UTXOs"},
530531
},
531532
"query_options"},
532533
},
@@ -598,10 +599,8 @@ RPCHelpMan listunspent()
598599
include_unsafe = request.params[3].get_bool();
599600
}
600601

601-
CAmount nMinimumAmount = 0;
602-
CAmount nMaximumAmount = MAX_MONEY;
603-
CAmount nMinimumSumAmount = MAX_MONEY;
604-
uint64_t nMaximumCount = 0;
602+
CoinFilterParams filter_coins;
603+
filter_coins.min_amount = 0;
605604
CCoinControl coinControl(CoinType::ALL_COINS);
606605

607606
if (!request.params[4].isNull()) {
@@ -613,7 +612,8 @@ RPCHelpMan listunspent()
613612
"maximumAmount",
614613
"minimumSumAmount",
615614
"maximumCount",
616-
"coinType"
615+
"coinType",
616+
"include_immature_coinbase"
617617
};
618618

619619
for (const auto& key : options.getKeys()) {
@@ -623,16 +623,16 @@ RPCHelpMan listunspent()
623623
}
624624

625625
if (options.exists("minimumAmount"))
626-
nMinimumAmount = AmountFromValue(options["minimumAmount"]);
626+
filter_coins.min_amount = AmountFromValue(options["minimumAmount"]);
627627

628628
if (options.exists("maximumAmount"))
629-
nMaximumAmount = AmountFromValue(options["maximumAmount"]);
629+
filter_coins.max_amount = AmountFromValue(options["maximumAmount"]);
630630

631631
if (options.exists("minimumSumAmount"))
632-
nMinimumSumAmount = AmountFromValue(options["minimumSumAmount"]);
632+
filter_coins.min_sum_amount = AmountFromValue(options["minimumSumAmount"]);
633633

634634
if (options.exists("maximumCount"))
635-
nMaximumCount = options["maximumCount"].getInt<int64_t>();
635+
filter_coins.max_count = options["maximumCount"].getInt<int64_t>();
636636

637637
if (options.exists("coinType")) {
638638
int64_t nCoinType = options["coinType"].getInt<int64_t>();
@@ -643,6 +643,10 @@ RPCHelpMan listunspent()
643643

644644
coinControl.nCoinType = static_cast<CoinType>(nCoinType);
645645
}
646+
647+
if (options.exists("include_immature_coinbase")) {
648+
filter_coins.include_immature_coinbase = options["include_immature_coinbase"].get_bool();
649+
}
646650
}
647651

648652
// Make sure the results are valid at least up to the most recent block
@@ -658,7 +662,7 @@ RPCHelpMan listunspent()
658662
coinControl.m_include_unsafe_inputs = include_unsafe;
659663

660664
LOCK(pwallet->cs_wallet);
661-
vecOutputs = AvailableCoinsListUnspent(*pwallet, &coinControl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount).all();
665+
vecOutputs = AvailableCoinsListUnspent(*pwallet, &coinControl, filter_coins).all();
662666
}
663667

664668
LOCK(pwallet->cs_wallet);

src/wallet/spend.cpp

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,7 @@ void CoinsResult::clear()
100100
CoinsResult AvailableCoins(const CWallet& wallet,
101101
const CCoinControl* coinControl,
102102
std::optional<CFeeRate> feerate,
103-
const CAmount& nMinimumAmount,
104-
const CAmount& nMaximumAmount,
105-
const CAmount& nMinimumSumAmount,
106-
const uint64_t nMaximumCount,
107-
bool only_spendable)
103+
const CoinFilterParams& params)
108104
{
109105
AssertLockHeld(wallet.cs_wallet);
110106

@@ -123,7 +119,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
123119
const uint256& wtxid = pwtx->GetHash();
124120
const CWalletTx& wtx = *pwtx;
125121

126-
if (wallet.IsTxImmatureCoinBase(wtx))
122+
if (wallet.IsTxImmatureCoinBase(wtx) && !params.include_immature_coinbase)
127123
continue;
128124

129125
int nDepth = wallet.GetTxDepthInMainChain(wtx);
@@ -182,7 +178,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
182178
} // no default case, so the compiler can warn about missing cases
183179
if (!found) continue;
184180

185-
if (output.nValue < nMinimumAmount || output.nValue > nMaximumAmount)
181+
if (output.nValue < params.min_amount || output.nValue > params.max_amount)
186182
continue;
187183

188184
if (coinControl && coinControl->HasSelected() && !coinControl->m_allow_other_inputs && !coinControl->IsSelected(outpoint))
@@ -213,7 +209,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
213209
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
214210

215211
// Filter by spendable outputs only
216-
if (!spendable && only_spendable) continue;
212+
if (!spendable && params.only_spendable) continue;
217213

218214
// When parsing a scriptPubKey, Solver returns the parsed pubkeys or hashes (depending on the script)
219215
// We don't need those here, so we are leaving them in return_values_unused
@@ -250,14 +246,14 @@ CoinsResult AvailableCoins(const CWallet& wallet,
250246
// Cache total amount as we go
251247
result.total_amount += output.nValue;
252248
// Checks the sum amount of all UTXO's.
253-
if (nMinimumSumAmount != MAX_MONEY) {
254-
if (result.total_amount >= nMinimumSumAmount) {
249+
if (params.min_sum_amount != MAX_MONEY) {
250+
if (result.total_amount >= params.min_sum_amount) {
255251
return result;
256252
}
257253
}
258254

259255
// Checks the maximum number of UTXO's.
260-
if (nMaximumCount > 0 && result.size() >= nMaximumCount) {
256+
if (params.max_count > 0 && result.size() >= params.max_count) {
261257
return result;
262258
}
263259
}
@@ -266,21 +262,16 @@ CoinsResult AvailableCoins(const CWallet& wallet,
266262
return result;
267263
}
268264

269-
CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, const CAmount& nMinimumAmount, const CAmount& nMaximumAmount, const CAmount& nMinimumSumAmount, const uint64_t nMaximumCount)
265+
CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl, CoinFilterParams params)
270266
{
271-
return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, /*only_spendable=*/false);
267+
params.only_spendable = false;
268+
return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, params);
272269
}
273270

274271
CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl)
275272
{
276273
LOCK(wallet.cs_wallet);
277-
return AvailableCoins(wallet, coinControl,
278-
/*feerate=*/ std::nullopt,
279-
/*nMinimumAmount=*/ 1,
280-
/*nMaximumAmount=*/ MAX_MONEY,
281-
/*nMinimumSumAmount=*/ MAX_MONEY,
282-
/*nMaximumCount=*/ 0
283-
).total_amount;
274+
return AvailableCoins(wallet, coinControl).total_amount;
284275
}
285276

286277
const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output)
@@ -894,13 +885,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
894885
}
895886

896887
// Get available coins
897-
auto available_coins = AvailableCoins(wallet,
898-
&coin_control,
899-
coin_selection_params.m_effective_feerate,
900-
1, /*nMinimumAmount*/
901-
MAX_MONEY, /*nMaximumAmount*/
902-
MAX_MONEY, /*nMinimumSumAmount*/
903-
0); /*nMaximumCount*/
888+
auto available_coins = AvailableCoins(wallet, &coin_control, coin_selection_params.m_effective_feerate);
904889

905890
// Choose coins to use
906891
std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);

src/wallet/spend.h

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,34 @@ struct CoinsResult {
5555
CAmount total_amount{0};
5656
};
5757

58+
struct CoinFilterParams {
59+
// Outputs below the minimum amount will not get selected
60+
CAmount min_amount{1};
61+
// Outputs above the maximum amount will not get selected
62+
CAmount max_amount{MAX_MONEY};
63+
// Return outputs until the minimum sum amount is covered
64+
CAmount min_sum_amount{MAX_MONEY};
65+
// Maximum number of outputs that can be returned
66+
uint64_t max_count{0};
67+
// By default, return only spendable outputs
68+
bool only_spendable{true};
69+
// By default, do not include immature coinbase outputs
70+
bool include_immature_coinbase{false};
71+
};
72+
5873
/**
5974
* Populate the CoinsResult struct with vectors of available COutputs, organized by OutputType.
6075
*/
6176
CoinsResult AvailableCoins(const CWallet& wallet,
6277
const CCoinControl* coinControl = nullptr,
6378
std::optional<CFeeRate> feerate = std::nullopt,
64-
const CAmount& nMinimumAmount = 1,
65-
const CAmount& nMaximumAmount = MAX_MONEY,
66-
const CAmount& nMinimumSumAmount = MAX_MONEY,
67-
const uint64_t nMaximumCount = 0,
68-
bool only_spendable = true) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
79+
const CoinFilterParams& params = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
6980

7081
/**
71-
* Wrapper function for AvailableCoins which skips the `feerate` parameter. Use this function
82+
* Wrapper function for AvailableCoins which skips the `feerate` and `CoinFilterParams::only_spendable` parameters. Use this function
7283
* to list all available coins (e.g. listunspent RPC) while not intending to fund a transaction.
7384
*/
74-
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);
85+
CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl = nullptr, CoinFilterParams params = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
7586

7687
CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl = nullptr);
7788

test/functional/wallet_balance.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,18 @@ def run_test(self):
7878
self.log.info("Mining blocks ...")
7979
self.generate(self.nodes[0], 1)
8080
self.generate(self.nodes[1], 1)
81+
82+
# Verify listunspent returns immature coinbase if 'include_immature_coinbase' is set
83+
assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1)
84+
assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 0)
85+
8186
self.generatetoaddress(self.nodes[1], COINBASE_MATURITY + 1, ADDRESS_WATCHONLY)
8287

88+
# Verify listunspent returns all immature coinbases if 'include_immature_coinbase' is set
89+
# For now, only the legacy wallet will see the coinbases going to the imported 'ADDRESS_WATCHONLY'
90+
assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': False})), 1 if self.options.descriptors else 2)
91+
assert_equal(len(self.nodes[0].listunspent(query_options={'include_immature_coinbase': True})), 1 if self.options.descriptors else COINBASE_MATURITY + 2)
92+
8393
if not self.options.descriptors:
8494
# Tests legacy watchonly behavior which is not present (and does not need to be tested) in descriptor wallets
8595
assert_equal(self.nodes[0].getbalances()['mine']['trusted'], 500)

0 commit comments

Comments
 (0)