mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-02-09 02:59:31 +08:00
wallet: introduce "tx amount exceeds balance when fees are included" error
This was previously implemented at the GUI level but we never hit that code path. Co-authored-by: furszy <matiasfurszyfer@protonmail.com>
This commit is contained in:
@@ -1208,7 +1208,25 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
|
|||||||
if (!select_coins_res) {
|
if (!select_coins_res) {
|
||||||
// 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds".
|
// 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds".
|
||||||
const bilingual_str& err = util::ErrorString(select_coins_res);
|
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;
|
const SelectionResult& result = *select_coins_res;
|
||||||
TRACEPOINT(coin_selection, selected_coins,
|
TRACEPOINT(coin_selection, selected_coins,
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ def test_no_more_inputs_fails(self, rbf_node, dest_address):
|
|||||||
self.generatetoaddress(rbf_node, 1, dest_address)
|
self.generatetoaddress(rbf_node, 1, dest_address)
|
||||||
# spend all funds, no change output
|
# spend all funds, no change output
|
||||||
rbfid = rbf_node.sendall(recipients=[rbf_node.getnewaddress()])['txid']
|
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()
|
self.clear_mempool()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
self.test_input_confs_control()
|
self.test_input_confs_control()
|
||||||
self.test_duplicate_outputs()
|
self.test_duplicate_outputs()
|
||||||
self.test_watchonly_cannot_grind_r()
|
self.test_watchonly_cannot_grind_r()
|
||||||
|
self.test_cannot_cover_fees()
|
||||||
|
|
||||||
def test_duplicate_outputs(self):
|
def test_duplicate_outputs(self):
|
||||||
self.log.info("Test deserializing and funding a transaction with duplicate outputs")
|
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
|
# 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.
|
# fail with insufficient funds rather than bitcoind asserting.
|
||||||
rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}])
|
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):
|
def test_input_confs_control(self):
|
||||||
self.nodes[0].createwallet("minconf")
|
self.nodes[0].createwallet("minconf")
|
||||||
@@ -1542,5 +1544,45 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
watchonly_funded = watchonly.fundrawtransaction(hexstring=tx, fee_rate=10)
|
watchonly_funded = watchonly.fundrawtransaction(hexstring=tx, fee_rate=10)
|
||||||
assert_greater_than(watchonly_funded["fee"], funded["fee"])
|
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__':
|
if __name__ == '__main__':
|
||||||
RawTransactionsTest(__file__).main()
|
RawTransactionsTest(__file__).main()
|
||||||
|
|||||||
Reference in New Issue
Block a user