Skip to content

Commit 0aa065b

Browse files
committed
wallet: return accurate error messages from Coin Selection
and not the general "Insufficient funds" when the wallet actually have funds. Two new error messages: 1) If the selection result exceeds the maximum transaction weight, we now will return: "The inputs size exceeds the maximum weight". 2) If the user preselected inputs and disallowed the automatic coin selection process (no other inputs are allowed), we now will return: "The preselected coins total amount does not cover the transaction target".
1 parent 7e8340a commit 0aa065b

File tree

5 files changed

+59
-26
lines changed

5 files changed

+59
-26
lines changed

src/wallet/spend.cpp

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -510,15 +510,20 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
510510
return groups_out;
511511
}
512512

513+
// Returns true if the result contains an error and the message is not empty
514+
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
515+
513516
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
514517
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
515518
{
516519
// Run coin selection on each OutputType and compute the Waste Metric
517520
std::vector<SelectionResult> results;
518521
for (const auto& it : available_coins.coins) {
519-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, it.second, coin_selection_params)}) {
520-
results.push_back(*result);
521-
}
522+
auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, it.second, coin_selection_params)};
523+
// If any specific error message appears here, then something particularly wrong happened.
524+
if (HasErrorMsg(result)) return result; // So let's return the specific error.
525+
// Append the favorable result.
526+
if (result) results.push_back(*result);
522527
}
523528
// If we have at least one solution for funding the transaction without mixing, choose the minimum one according to waste metric
524529
// and return the result
@@ -528,9 +533,7 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
528533
// over all available coins, which would allow mixing.
529534
// If TypesCount() <= 1, there is nothing to mix.
530535
if (allow_mixed_output_types && available_coins.TypesCount() > 1) {
531-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.All(), coin_selection_params)}) {
532-
return result;
533-
}
536+
return ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.All(), coin_selection_params);
534537
}
535538
// Either mixing is not allowed and we couldn't find a solution from any single OutputType, or mixing was allowed and we still couldn't
536539
// find a solution using all available coins
@@ -571,7 +574,8 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
571574
});
572575

573576
if (eligible_results.empty()) {
574-
return util::Error();
577+
return util::Error{_("The inputs size exceeds the maximum weight. "
578+
"Please try sending a smaller amount or manually consolidating your wallet's UTXOs")};
575579
}
576580

577581
// Choose the result with the least waste
@@ -588,7 +592,10 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
588592
CAmount selection_target = nTargetValue - pre_set_inputs.total_amount;
589593

590594
// Return if automatic coin selection is disabled, and we don't cover the selection target
591-
if (!coin_control.m_allow_other_inputs && selection_target > 0) return util::Error();
595+
if (!coin_control.m_allow_other_inputs && selection_target > 0) {
596+
return util::Error{_("The preselected coins total amount does not cover the transaction target. "
597+
"Please allow other inputs to be automatically selected or include more coins manually")};
598+
}
592599

593600
// Return if we can cover the target only with the preset inputs
594601
if (selection_target <= 0) {
@@ -681,13 +688,23 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
681688
}
682689
}
683690

684-
// Walk-through the filters until the solution gets found
691+
// Walk-through the filters until the solution gets found.
692+
// If no solution is found, return the first detailed error (if any).
693+
// future: add "error level" so the worst one can be picked instead.
694+
std::vector<util::Result<SelectionResult>> res_detailed_errors;
685695
for (const auto& select_filter : ordered_filters) {
686696
if (auto res{AttemptSelection(wallet, value_to_select, select_filter.filter, available_coins,
687-
coin_selection_params, select_filter.allow_mixed_output_types)}) return res;
697+
coin_selection_params, select_filter.allow_mixed_output_types)}) {
698+
return res; // result found
699+
} else {
700+
// If any specific error message appears here, then something particularly wrong might have happened.
701+
// Save the error and continue the selection process. So if no solutions gets found, we can return
702+
// the detailed error to the upper layers.
703+
if (HasErrorMsg(res)) res_detailed_errors.emplace_back(res);
704+
}
688705
}
689706
// Coin Selection failed.
690-
return util::Result<SelectionResult>(util::Error());
707+
return res_detailed_errors.empty() ? util::Result<SelectionResult>(util::Error()) : res_detailed_errors.front();
691708
}();
692709

693710
return res;
@@ -916,7 +933,9 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
916933
// Choose coins to use
917934
auto select_coins_res = SelectCoins(wallet, available_coins, preset_inputs, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
918935
if (!select_coins_res) {
919-
return util::Error{_("Insufficient funds")};
936+
// 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds".
937+
const bilingual_str& err = util::ErrorString(select_coins_res);
938+
return util::Error{err.empty() ?_("Insufficient funds") : err};
920939
}
921940
const SelectionResult& result = *select_coins_res;
922941
TRACE5(coin_selection, selected_coins, wallet.GetName().c_str(), GetAlgorithmName(result.GetAlgo()).c_str(), result.GetTarget(), result.GetWaste(), result.GetSelectedValue());

src/wallet/spend.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
119119
* param@[in] coin_selection_params Parameters for the coin selection
120120
* param@[in] allow_mixed_output_types Relax restriction that SelectionResults must be of the same OutputType
121121
* returns If successful, a SelectionResult containing the input set
122-
* If failed, a nullopt
122+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
123+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
124+
* result that surpassed the tx max weight size).
123125
*/
124126
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
125127
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
@@ -135,7 +137,9 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
135137
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
136138
* param@[in] coin_selection_params Parameters for the coin selection
137139
* returns If successful, a SelectionResult containing the input set
138-
* If failed, a nullopt
140+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
141+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
142+
* result that surpassed the tx max weight size).
139143
*/
140144
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
141145
const CoinSelectionParams& coin_selection_params);
@@ -175,7 +179,9 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
175179
* param@[in] coin_selection_params Parameters for this coin selection such as feerates, whether to avoid partial spends,
176180
* and whether to subtract the fee from the outputs.
177181
* returns If successful, a SelectionResult containing the selected coins
178-
* If failed, a nullopt.
182+
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
183+
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
184+
* result that surpassed the tx max weight size).
179185
*/
180186
util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
181187
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);

test/functional/rpc_psbt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def run_test(self):
120120

121121
# If inputs are specified, do not automatically add more:
122122
utxo1 = self.nodes[0].listunspent()[0]
123-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
123+
assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. "
124+
"Please allow other inputs to be automatically selected or include more coins manually",
125+
self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
124126

125127
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
126128
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)

test/functional/wallet_fundrawtransaction.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
)
2828
from test_framework.wallet_util import bytes_to_wif
2929

30+
ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \
31+
"Please allow other inputs to be automatically selected or include more coins manually"
3032

3133
def get_unspent(listunspent, amount):
3234
for utx in listunspent:
@@ -328,7 +330,7 @@ def test_coin_selection(self):
328330
assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex'])
329331

330332
# Should fail without add_inputs:
331-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
333+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
332334
# add_inputs is enabled by default
333335
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
334336

@@ -360,7 +362,7 @@ def test_two_vin(self):
360362
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
361363

362364
# Should fail without add_inputs:
363-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
365+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
364366
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
365367

366368
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
@@ -394,7 +396,7 @@ def test_two_vin_two_vout(self):
394396
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
395397

396398
# Should fail without add_inputs:
397-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
399+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
398400
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
399401

400402
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
@@ -987,7 +989,9 @@ def test_transaction_too_large(self):
987989
outputs[recipient.getnewaddress()] = 0.1
988990
wallet.sendmany("", outputs)
989991
self.generate(self.nodes[0], 10)
990-
assert_raises_rpc_error(-4, "Insufficient funds", recipient.fundrawtransaction, rawtx)
992+
assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. "
993+
"Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
994+
recipient.fundrawtransaction, rawtx)
991995
self.nodes[0].unloadwallet("large")
992996

993997
def test_external_inputs(self):
@@ -1128,7 +1132,7 @@ def test_add_inputs_default_value(self):
11281132
}
11291133
]
11301134
}
1131-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{addr1: 8}], options=options)
1135+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.send, outputs=[{addr1: 8}], options=options)
11321136

11331137
# Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount)
11341138
options["add_inputs"] = True
@@ -1156,7 +1160,7 @@ def test_add_inputs_default_value(self):
11561160

11571161
# 6. Explicit add_inputs=false, no preset inputs:
11581162
options = {"add_inputs": False}
1159-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.send, outputs=[{addr1: 3}], options=options)
1163+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.send, outputs=[{addr1: 3}], options=options)
11601164

11611165
################################################
11621166

@@ -1173,7 +1177,7 @@ def test_add_inputs_default_value(self):
11731177
"vout": 1 # change position was hardcoded to index 0
11741178
}]
11751179
outputs = {self.nodes[1].getnewaddress(): 8}
1176-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, inputs=inputs, outputs=outputs)
1180+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.walletcreatefundedpsbt, inputs=inputs, outputs=outputs)
11771181

11781182
# Case (3), Explicit add_inputs=true and preset inputs (with preset inputs not-covering the target amount)
11791183
options["add_inputs"] = True
@@ -1200,7 +1204,7 @@ def test_add_inputs_default_value(self):
12001204

12011205
# Case (6). Explicit add_inputs=false, no preset inputs:
12021206
options = {"add_inputs": False}
1203-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, inputs=[], outputs=outputs, options=options)
1207+
assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.walletcreatefundedpsbt, inputs=[], outputs=outputs, options=options)
12041208

12051209
self.nodes[2].unloadwallet("test_preset_inputs")
12061210

test/functional/wallet_send.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,12 @@ def run_test(self):
412412
assert res["complete"]
413413
utxo1 = w0.listunspent()[0]
414414
assert_equal(utxo1["amount"], 50)
415+
ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \
416+
"Please allow other inputs to be automatically selected or include more coins manually"
415417
self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1],
416-
expect_error=(-4, "Insufficient funds"))
418+
expect_error=(-4, ERR_NOT_ENOUGH_PRESET_INPUTS))
417419
self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1], add_inputs=False,
418-
expect_error=(-4, "Insufficient funds"))
420+
expect_error=(-4, ERR_NOT_ENOUGH_PRESET_INPUTS))
419421
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=51, inputs=[utxo1], add_inputs=True, add_to_wallet=False)
420422
assert res["complete"]
421423

0 commit comments

Comments
 (0)