Skip to content

Commit 5d32009

Browse files
committed
Merge #20220: wallet, rpc: explicit fee rate follow-ups/fixes for 0.21
0be2900 rpc: update conf_target helps for correctness/consistency (Jon Atack) 778b9be wallet, rpc: fix send subtract_fee_from_outputs help (Jon Atack) 603c005 wallet: add rpc send explicit fee rate coverage (Jon Atack) dd341e6 wallet: add sendtoaddress/sendmany explicit fee rate coverage (Jon Atack) 44e7bfa wallet: add walletcreatefundedpsbt explicit fee rate coverage (Jon Atack) 6e1ea42 test: refactor for walletcreatefundedpsbt fee rate coverage (Jon Atack) 3ac7b0c wallet: fundrawtx fee rate coverage, fixup ParseConfirmTarget() (Jon Atack) 2d8eba8 wallet: combine redundant bumpfee invalid params and args tests (Jon Atack) 1697a40 wallet: improve bumpfee error/help, add explicit fee rate coverage (Jon Atack) fc57217 wallet: fix SetFeeEstimateMode() error message (Jon Atack) 052427e wallet, bugfix: fix bumpfee with explicit fee rate modes (Jon Atack) Pull request description: Follow-up to #11413 providing a base to build on for #19543: - bugfix for `bumpfee` raising a JSON error with explicit feerates, fixes issue #20219 - adds explicit feerate test coverage for `bumpfee`, `fundrawtransaction`, `walletcreatefundedpsbt`, `send`, `sendtoaddress`, and `sendmany` - improves a few related RPC error messages and `ParseConfirmTarget()` / error message - fixes/improves the explicit fee rate information in the 6 RPC helps, of which 2 were also missing `conf_target` sat/B units This provides a spec and regression coverage for the potential next step of a universal `sat/vB` feerate argument (see #19543), as well as immediate coverage and minimum fixes for 0.21. ACKs for top commit: kallewoof: Concept/Tested ACK 0be2900 meshcollider: Code review + functional test run ACK 0be2900 Tree-SHA512: efd965003e991cba51d4504e2940f06ab3d742e34022e96a673606b44fad85596aa03a8c1809f06df7ebcf21a38e18a891e54392fe3d6fb4d120bbe4ea0cf5e0
2 parents 17c6fb1 + 0be2900 commit 5d32009

File tree

7 files changed

+282
-95
lines changed

7 files changed

+282
-95
lines changed

src/rpc/util.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,12 @@ UniValue DescribeAddress(const CTxDestination& dest)
272272

273273
unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target)
274274
{
275-
int target = value.get_int();
276-
if (target < 1 || (unsigned int)target > max_target) {
277-
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u - %u", 1, max_target));
275+
const int target{value.get_int()};
276+
const unsigned int unsigned_target{static_cast<unsigned int>(target)};
277+
if (target < 1 || unsigned_target > max_target) {
278+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u and %u", 1, max_target));
278279
}
279-
return (unsigned int)target;
280+
return unsigned_target;
280281
}
281282

282283
RPCErrorCode RPCErrorFromTransactionError(TransactionError terr)

src/wallet/rpcwallet.cpp

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ static void SetFeeEstimateMode(const CWallet* pwallet, CCoinControl& cc, const U
214214

215215
if (cc.m_fee_mode == FeeEstimateMode::BTC_KB || cc.m_fee_mode == FeeEstimateMode::SAT_B) {
216216
if (estimate_param.isNull()) {
217-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Selected estimate_mode requires a fee rate");
217+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Selected estimate_mode %s requires a fee rate to be specified in conf_target", estimate_mode.get_str()));
218218
}
219219

220220
CAmount fee_rate = AmountFromValue(estimate_param);
@@ -440,7 +440,8 @@ static RPCHelpMan sendtoaddress()
440440
{"subtractfeefromamount", RPCArg::Type::BOOL, /* default */ "false", "The fee will be deducted from the amount being sent.\n"
441441
"The recipient will receive less bitcoins than you enter in the amount field."},
442442
{"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Allow this transaction to be replaced by a transaction with higher fees via BIP 125"},
443-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
443+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
444+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
444445
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
445446
" \"" + FeeModes("\"\n\"") + "\""},
446447
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "true", "(only available if avoid_reuse wallet flag is set) Avoid spending from dirty addresses; addresses are considered\n"
@@ -868,7 +869,8 @@ static RPCHelpMan sendmany()
868869
},
869870
},
870871
{"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Allow this transaction to be replaced by a transaction with higher fees via BIP 125"},
871-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
872+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
873+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
872874
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
873875
" \"" + FeeModes("\"\n\"") + "\""},
874876
{"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra infomration about the transaction."},
@@ -3044,7 +3046,7 @@ static RPCHelpMan listunspent()
30443046
};
30453047
}
30463048

3047-
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options, CCoinControl& coinControl)
3049+
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, const UniValue& options, CCoinControl& coinControl)
30483050
{
30493051
// Make sure the results are valid at least up to the most recent block
30503052
// the user could have gotten from another RPC command prior to now
@@ -3210,7 +3212,8 @@ static RPCHelpMan fundrawtransaction()
32103212
},
32113213
{"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Marks this transaction as BIP125 replaceable.\n"
32123214
"Allows this transaction to be replaced by a transaction with higher fees"},
3213-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
3215+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
3216+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
32143217
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
32153218
" \"" + FeeModes("\"\n\"") + "\""},
32163219
},
@@ -3378,7 +3381,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
33783381
"The command will pay the additional fee by reducing change outputs or adding inputs when necessary. It may add a new change output if one does not already exist.\n"
33793382
"All inputs in the original transaction will be included in the replacement transaction.\n"
33803383
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
3381-
"By default, the new fee will be calculated automatically using estimatesmartfee.\n"
3384+
"By default, the new fee will be calculated automatically using the estimatesmartfee RPC.\n"
33823385
"The user can specify a confirmation target for estimatesmartfee.\n"
33833386
"Alternatively, the user can specify a fee_rate (" + CURRENCY_UNIT + " per kB) for the new transaction.\n"
33843387
"At a minimum, the new fee rate must be high enough to pay an additional new relay fee (incrementalfee\n"
@@ -3387,10 +3390,11 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
33873390
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The txid to be bumped"},
33883391
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
33893392
{
3390-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks)"},
3391-
{"fee_rate", RPCArg::Type::NUM, /* default */ "fall back to 'conf_target'", "fee rate (NOT total fee) to pay, in " + CURRENCY_UNIT + " per kB\n"
3393+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
3394+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
3395+
{"fee_rate", RPCArg::Type::NUM, /* default */ "fall back to 'conf_target'", "fee rate (NOT total fee) to pay, in " + CURRENCY_UNIT + "/kB.\n"
33923396
"Specify a fee rate instead of relying on the built-in fee estimator.\n"
3393-
"Must be at least 0.0001 " + CURRENCY_UNIT + " per kB higher than the current transaction fee rate.\n"},
3397+
"Must be at least 0.0001 " + CURRENCY_UNIT + "/kB higher than the current transaction fee rate.\n"},
33943398
{"replaceable", RPCArg::Type::BOOL, /* default */ "true", "Whether the new transaction should still be\n"
33953399
"marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n"
33963400
"be left unchanged from the original. If false, any input sequence numbers in the\n"
@@ -3464,9 +3468,8 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
34643468

34653469
if (!conf_target.isNull()) {
34663470
if (options.exists("fee_rate")) {
3467-
throw JSONRPCError(RPC_INVALID_PARAMETER, "conf_target can't be set with fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.");
3471+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both conf_target and fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.");
34683472
}
3469-
coin_control.m_confirm_target = ParseConfirmTarget(conf_target, pwallet->chain().estimateMaxBlocks());
34703473
} else if (options.exists("fee_rate")) {
34713474
CFeeRate fee_rate(AmountFromValue(options["fee_rate"]));
34723475
if (fee_rate <= CFeeRate(0)) {
@@ -4014,7 +4017,8 @@ static RPCHelpMan send()
40144017
},
40154018
},
40164019
},
4017-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
4020+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
4021+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
40184022
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
40194023
" \"" + FeeModes("\"\n\"") + "\""},
40204024
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
@@ -4024,7 +4028,8 @@ static RPCHelpMan send()
40244028
{"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"},
40254029
{"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
40264030
{"change_type", RPCArg::Type::STR, /* default */ "set by -changetype", "The output type to use. Only valid if change_address is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
4027-
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"},
4031+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
4032+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
40284033
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
40294034
" \"" + FeeModes("\"\n\"") + "\""},
40304035
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
@@ -4040,7 +4045,7 @@ static RPCHelpMan send()
40404045
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
40414046
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
40424047
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
4043-
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A JSON array of integers.\n"
4048+
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "Outputs to subtract the fee from, specified as integer indices.\n"
40444049
"The fee will be equally deducted from the amount of each specified output.\n"
40454050
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
40464051
"If no outputs are specified here, the sender pays the fee.",
@@ -4121,7 +4126,7 @@ static RPCHelpMan send()
41214126
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
41224127
CCoinControl coin_control;
41234128
// Automatically select coins, unless at least one is manually selected. Can
4124-
// be overriden by options.add_inputs.
4129+
// be overridden by options.add_inputs.
41254130
coin_control.m_add_inputs = rawTx.vin.size() == 0;
41264131
FundTransaction(pwallet, rawTx, fee, change_position, options, coin_control);
41274132

@@ -4362,7 +4367,8 @@ static RPCHelpMan walletcreatefundedpsbt()
43624367
},
43634368
{"replaceable", RPCArg::Type::BOOL, /* default */ "wallet default", "Marks this transaction as BIP125 replaceable.\n"
43644369
"Allows this transaction to be replaced by a transaction with higher fees"},
4365-
{"conf_target", RPCArg::Type::NUM, /* default */ "fall back to wallet's confirmation target (txconfirmtarget)", "Confirmation target (in blocks)"},
4370+
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
4371+
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
43664372
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
43674373
" \"" + FeeModes("\"\n\"") + "\""},
43684374
},

test/functional/rpc_fundrawtransaction.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from test_framework.descriptors import descsum_create
99
from test_framework.test_framework import BitcoinTestFramework
1010
from test_framework.util import (
11+
assert_approx,
1112
assert_equal,
1213
assert_fee_amount,
1314
assert_greater_than,
@@ -89,6 +90,7 @@ def run_test(self):
8990
self.test_op_return()
9091
self.test_watchonly()
9192
self.test_all_watched_funds()
93+
self.test_feerate_with_conf_target_and_estimate_mode()
9294
self.test_option_feerate()
9395
self.test_address_reuse()
9496
self.test_option_subtract_fee_from_outputs()
@@ -722,6 +724,59 @@ def test_option_feerate(self):
722724
assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
723725
assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
724726

727+
def test_feerate_with_conf_target_and_estimate_mode(self):
728+
self.log.info("Test fundrawtxn passing an explicit fee rate using conf_target and estimate_mode")
729+
node = self.nodes[3]
730+
# Make sure there is exactly one input so coin selection can't skew the result.
731+
assert_equal(len(node.listunspent(1)), 1)
732+
inputs = []
733+
outputs = {node.getnewaddress() : 1}
734+
rawtx = node.createrawtransaction(inputs, outputs)
735+
736+
for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items():
737+
self.log.info("Test fundrawtxn with conf_target {} estimate_mode {} produces expected fee".format(fee_rate, unit))
738+
# With no arguments passed, expect fee of 141 sats/b.
739+
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
740+
# Expect fee to be 10,000x higher when explicit fee 10,000x greater is specified.
741+
result = node.fundrawtransaction(rawtx, {"conf_target": fee_rate, "estimate_mode": unit})
742+
assert_approx(result["fee"], vexp=0.0141, vspan=0.0001)
743+
744+
for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items():
745+
self.log.info("Test fundrawtxn raises RPC error if both feeRate and {} are passed".format(field))
746+
assert_raises_rpc_error(
747+
-8, "Cannot specify both {} and feeRate".format(field),
748+
lambda: node.fundrawtransaction(rawtx, {"feeRate": 0.1, field: fee_rate}))
749+
750+
self.log.info("Test fundrawtxn with invalid estimate_mode settings")
751+
for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
752+
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k),
753+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": v, "conf_target": 0.1}))
754+
for mode in ["foo", Decimal("3.141592")]:
755+
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
756+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0.1}))
757+
758+
self.log.info("Test fundrawtxn with invalid conf_target settings")
759+
for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]:
760+
self.log.debug("{}".format(mode))
761+
for k, v in {"string": "", "object": {"foo": "bar"}}.items():
762+
assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k),
763+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": v}))
764+
if mode in ["btc/kb", "sat/b"]:
765+
assert_raises_rpc_error(-3, "Amount out of range",
766+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": -1}))
767+
assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
768+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0}))
769+
else:
770+
for n in [-1, 0, 1009]:
771+
assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008",
772+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": n}))
773+
774+
for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items():
775+
self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
776+
assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
777+
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True}))
778+
779+
725780
def test_address_reuse(self):
726781
"""Test no address reuse occurs."""
727782
self.log.info("Test fundrawtxn does not reuse addresses")

0 commit comments

Comments
 (0)