diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 675cf623337..f9cc8595681 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -1208,7 +1208,25 @@ static util::Result CreateTransactionInternal( if (!select_coins_res) { // 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds". const bilingual_str& err = util::ErrorString(select_coins_res); - return util::Error{err.empty() ?_("Insufficient funds") : err}; + if (!err.empty()) return util::Error{err}; + + // Check if we have enough balance but cannot cover the fees + CAmount available_balance = preset_inputs.GetTotalAmount() + available_coins.GetTotalAmount(); + // Note: if SelectCoins() fails when SFFO is enabled (recipients_sum = selection_target with SFFO), + // then recipients_sum > available_balance and we wouldn't enter into the if condition below. + if (available_balance >= recipients_sum) { + // If we have coins with balance, they should have effective values since we constructed them with valid feerate. + assert(!preset_inputs.Size() || preset_inputs.GetEffectiveTotalAmount().has_value()); + assert(!available_coins.Size() || available_coins.GetEffectiveTotalAmount().has_value()); + CAmount available_effective_balance = preset_inputs.GetEffectiveTotalAmount().value_or(0) + available_coins.GetEffectiveTotalAmount().value_or(0); + if (available_effective_balance < selection_target) { + Assume(!coin_selection_params.m_subtract_fee_outputs); + return util::Error{strprintf(_("The total exceeds your balance when the %s transaction fee is included."), FormatMoney(selection_target - recipients_sum))}; + } + } + + // General failure description + return util::Error{_("Insufficient funds")}; } const SelectionResult& result = *select_coins_res; TRACEPOINT(coin_selection, selected_coins, diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index 044ddc968b8..1a30f0c3cde 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -801,7 +801,7 @@ def test_no_more_inputs_fails(self, rbf_node, dest_address): self.generatetoaddress(rbf_node, 1, dest_address) # spend all funds, no change output rbfid = rbf_node.sendall(recipients=[rbf_node.getnewaddress()])['txid'] - assert_raises_rpc_error(-4, "Unable to create transaction. Insufficient funds", rbf_node.bumpfee, rbfid) + assert_raises_rpc_error(-4, "Unable to create transaction. The total exceeds your balance when the 0.00001051 transaction fee is included.", rbf_node.bumpfee, rbfid) self.clear_mempool() diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index 3577a2f4177..de0aafc6047 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -155,6 +155,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.test_input_confs_control() self.test_duplicate_outputs() self.test_watchonly_cannot_grind_r() + self.test_cannot_cover_fees() def test_duplicate_outputs(self): self.log.info("Test deserializing and funding a transaction with duplicate outputs") @@ -1456,7 +1457,8 @@ class RawTransactionsTest(BitcoinTestFramework): # To test this does not happen, we subtract 202 sats from the input value. If working correctly, this should # fail with insufficient funds rather than bitcoind asserting. rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}]) - assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, fee_rate=1.85) + expected_err_msg = "The total exceeds your balance when the 0.00000078 transaction fee is included." + assert_raises_rpc_error(-4, expected_err_msg, w.fundrawtransaction, rawtx, fee_rate=1.85) def test_input_confs_control(self): self.nodes[0].createwallet("minconf") @@ -1542,5 +1544,45 @@ class RawTransactionsTest(BitcoinTestFramework): watchonly_funded = watchonly.fundrawtransaction(hexstring=tx, fee_rate=10) assert_greater_than(watchonly_funded["fee"], funded["fee"]) + def test_cannot_cover_fees(self): + self.log.info("Test error message when transaction amount exceeds available balance when fees are included") + default_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + self.nodes[1].createwallet("cannot_cover_fees") + wallet = self.nodes[1].get_wallet_rpc("cannot_cover_fees") + + # Set up wallet with 2 utxos: 0.3 BTC and 0.15 BTC + default_wallet.sendtoaddress(wallet.getnewaddress(), 0.3) + txid2 = default_wallet.sendtoaddress(wallet.getnewaddress(), 0.15) + self.generate(self.nodes[0], 1) + vout2 = next(utxo["vout"] for utxo in wallet.listunspent() if utxo["txid"] == txid2) + amount_with_fee_err_msg = "The total exceeds your balance when the {} transaction fee is included." + + self.log.info("Test without preselected inputs") + self.log.info("Attempt to send 0.45 BTC without SFFO") + rawtx = wallet.createrawtransaction(inputs=[], outputs=[{default_wallet.getnewaddress(): 0.45}]) + assert_raises_rpc_error(-4, amount_with_fee_err_msg.format("0.00000042"), wallet.fundrawtransaction, rawtx, options={"fee_rate":1}) + + self.log.info("Send 0.45 BTC with SFFO") + wallet.fundrawtransaction(rawtx, options={"subtractFeeFromOutputs":[0]}) + + self.log.info("Attempt to send 0.45 BTC by restricting coin selection with minconf=6") + assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx, options={"minconf":6}) + + self.log.info("Test with preselected inputs") + self.log.info("Attempt to send 0.45 BTC preselecting 0.15 BTC utxo") + rawtx = wallet.createrawtransaction(inputs=[{"txid": txid2, "vout": vout2}], outputs=[{default_wallet.getnewaddress(): 0.45}]) + assert_raises_rpc_error(-4, amount_with_fee_err_msg.format("0.00000042"), wallet.fundrawtransaction, rawtx, options={"fee_rate":1}) + + self.log.info("Send 0.45 BTC preselecting 0.15 BTC utxo with SFFO") + wallet.fundrawtransaction(hexstring=rawtx, options={"subtractFeeFromOutputs":[0]}) + + self.log.info("Attempt to send 0.15 BTC using only the 0.15 BTC preselected utxo") + rawtx = wallet.createrawtransaction(inputs=[{"txid": txid2, "vout": vout2}], outputs=[{default_wallet.getnewaddress(): 0.15}]) + assert_raises_rpc_error(-4, ERR_NOT_ENOUGH_PRESET_INPUTS, wallet.fundrawtransaction, rawtx, options={"fee_rate":1, "add_inputs":False}) + self.log.info("Send 0.15 BTC using only the 0.15 BTC preselected utxo with SFFO") + wallet.fundrawtransaction(hexstring=rawtx, options={"subtractFeeFromOutputs":[0], "add_inputs":False}) + + if __name__ == '__main__': RawTransactionsTest(__file__).main()