aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/rpc/client.cpp9
-rw-r--r--src/util/fees.cpp2
-rw-r--r--src/wallet/rpcwallet.cpp126
-rwxr-xr-xtest/functional/rpc_fundrawtransaction.py138
-rwxr-xr-xtest/functional/rpc_psbt.py98
-rwxr-xr-xtest/functional/wallet_basic.py178
-rwxr-xr-xtest/functional/wallet_bumpfee.py88
-rwxr-xr-xtest/functional/wallet_send.py69
8 files changed, 359 insertions, 349 deletions
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 88c8ebe1f6..042005b9a6 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -41,7 +41,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendtoaddress", 5 , "replaceable" },
{ "sendtoaddress", 6 , "conf_target" },
{ "sendtoaddress", 8, "avoid_reuse" },
- { "sendtoaddress", 9, "verbose"},
+ { "sendtoaddress", 9, "fee_rate"},
+ { "sendtoaddress", 10, "verbose"},
{ "settxfee", 0, "amount" },
{ "sethdseed", 0, "newkeypool" },
{ "getreceivedbyaddress", 1, "minconf" },
@@ -73,7 +74,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendmany", 4, "subtractfeefrom" },
{ "sendmany", 5 , "replaceable" },
{ "sendmany", 6 , "conf_target" },
- { "sendmany", 8, "verbose" },
+ { "sendmany", 8, "fee_rate"},
+ { "sendmany", 9, "verbose" },
{ "deriveaddresses", 1, "range" },
{ "scantxoutset", 1, "scanobjects" },
{ "addmultisigaddress", 0, "nrequired" },
@@ -129,7 +131,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "lockunspent", 1, "transactions" },
{ "send", 0, "outputs" },
{ "send", 1, "conf_target" },
- { "send", 3, "options" },
+ { "send", 3, "fee_rate"},
+ { "send", 4, "options" },
{ "importprivkey", 2, "rescan" },
{ "importaddress", 2, "rescan" },
{ "importaddress", 3, "p2sh" },
diff --git a/src/util/fees.cpp b/src/util/fees.cpp
index 6208a20a97..9a1cea1d4e 100644
--- a/src/util/fees.cpp
+++ b/src/util/fees.cpp
@@ -40,8 +40,6 @@ const std::vector<std::pair<std::string, FeeEstimateMode>>& FeeModeMap()
{"unset", FeeEstimateMode::UNSET},
{"economical", FeeEstimateMode::ECONOMICAL},
{"conservative", FeeEstimateMode::CONSERVATIVE},
- {(CURRENCY_UNIT + "/kB"), FeeEstimateMode::BTC_KB},
- {(CURRENCY_ATOM + "/B"), FeeEstimateMode::SAT_B},
};
return FEE_MODES;
}
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
index edd7efb964..238d37ae98 100644
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -198,30 +198,33 @@ static std::string LabelFromValue(const UniValue& value)
*
* @param[in] pwallet Wallet pointer
* @param[in,out] cc Coin control which is to be updated
- * @param[in] estimate_mode String value (e.g. "ECONOMICAL")
- * @param[in] estimate_param Parameter (blocks to confirm, explicit fee rate, etc)
- * @throws a JSONRPCError if estimate_mode is unknown, or if estimate_param is missing when required
+ * @param[in] conf_target UniValue integer, confirmation target in blocks, values between 1 and 1008 are valid per policy/fees.h;
+ * if a fee_rate is present, 0 is allowed here as a no-op positional placeholder
+ * @param[in] estimate_mode UniValue string, fee estimation mode, valid values are "unset", "economical" or "conservative";
+ * if a fee_rate is present, "" is allowed here as a no-op positional placeholder
+ * @param[in] fee_rate UniValue real, fee rate in sat/vB;
+ * if a fee_rate is present, both conf_target and estimate_mode must either be null, or no-op values
+ * @throws a JSONRPCError if conf_target, estimate_mode, or fee_rate contain invalid values or are in conflict
*/
-static void SetFeeEstimateMode(const CWallet* pwallet, CCoinControl& cc, const UniValue& estimate_mode, const UniValue& estimate_param)
+static void SetFeeEstimateMode(const CWallet* pwallet, CCoinControl& cc, const UniValue& conf_target, const UniValue& estimate_mode, const UniValue& fee_rate)
{
- if (!estimate_mode.isNull()) {
- if (!FeeModeFromString(estimate_mode.get_str(), cc.m_fee_mode)) {
- throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid estimate_mode parameter");
+ if (!fee_rate.isNull()) {
+ if (!conf_target.isNull() && conf_target.get_int() > 0) {
+ 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.");
}
- }
-
- if (cc.m_fee_mode == FeeEstimateMode::BTC_KB || cc.m_fee_mode == FeeEstimateMode::SAT_B) {
- if (estimate_param.isNull()) {
- throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Selected estimate_mode %s requires a fee rate to be specified in conf_target", estimate_mode.get_str()));
+ if (!estimate_mode.isNull() && !estimate_mode.get_str().empty()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
}
-
- CAmount feerate{AmountFromValue(estimate_param)};
- cc.m_feerate = cc.m_fee_mode == FeeEstimateMode::SAT_B ? CFeeRate(feerate, COIN) : CFeeRate(feerate);
-
- // default RBF to true for explicit fee rate modes
+ cc.m_feerate = CFeeRate(AmountFromValue(fee_rate), COIN);
+ // Default RBF to true for explicit fee_rate, if unset.
if (cc.m_signal_bip125_rbf == nullopt) cc.m_signal_bip125_rbf = true;
- } else if (!estimate_param.isNull()) {
- cc.m_confirm_target = ParseConfirmTarget(estimate_param, pwallet->chain().estimateMaxBlocks());
+ return;
+ }
+ if (!estimate_mode.isNull() && !FeeModeFromString(estimate_mode.get_str(), cc.m_fee_mode)) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid estimate_mode parameter");
+ }
+ if (!conf_target.isNull()) {
+ cc.m_confirm_target = ParseConfirmTarget(conf_target, pwallet->chain().estimateMaxBlocks());
}
}
@@ -440,6 +443,7 @@ static RPCHelpMan sendtoaddress()
" \"" + FeeModes("\"\n\"") + "\""},
{"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"
"dirty if they have previously been used in a transaction."},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
{"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra information about the transaction."},
},
{
@@ -495,7 +499,7 @@ static RPCHelpMan sendtoaddress()
// We also enable partial spend avoidance if reuse avoidance is set.
coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse;
- SetFeeEstimateMode(pwallet, coin_control, request.params[7], request.params[6]);
+ SetFeeEstimateMode(pwallet, coin_control, /* conf_target */ request.params[6], /* estimate_mode */ request.params[7], /* fee_rate */ request.params[9]);
EnsureWalletIsUnlocked(pwallet);
@@ -509,7 +513,7 @@ static RPCHelpMan sendtoaddress()
std::vector<CRecipient> recipients;
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients);
- bool verbose = request.params[9].isNull() ? false: request.params[9].get_bool();
+ const bool verbose{request.params[10].isNull() ? false : request.params[10].get_bool()};
return SendMoney(pwallet, coin_control, recipients, mapValue, verbose);
},
@@ -867,6 +871,7 @@ static RPCHelpMan sendmany()
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
{"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra infomration about the transaction."},
},
{
@@ -923,11 +928,11 @@ static RPCHelpMan sendmany()
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
}
- SetFeeEstimateMode(pwallet, coin_control, request.params[7], request.params[6]);
+ SetFeeEstimateMode(pwallet, coin_control, /* conf_target */ request.params[6], /* estimate_mode */ request.params[7], /* fee_rate */ request.params[8]);
std::vector<CRecipient> recipients;
ParseRecipients(sendTo, subtractFeeFromAmount, recipients);
- bool verbose = request.params[8].isNull() ? false : request.params[8].get_bool();
+ const bool verbose{request.params[9].isNull() ? false : request.params[9].get_bool()};
return SendMoney(pwallet, coin_control, recipients, std::move(mapValue), verbose);
},
@@ -3073,7 +3078,8 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
{"lockUnspents", UniValueType(UniValue::VBOOL)},
{"lock_unspents", UniValueType(UniValue::VBOOL)},
{"locktime", UniValueType(UniValue::VNUM)},
- {"feeRate", UniValueType()}, // will be checked below
+ {"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
+ {"feeRate", UniValueType()}, // will be checked by AmountFromValue() below
{"psbt", UniValueType(UniValue::VBOOL)},
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
@@ -3120,15 +3126,21 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
lockUnspents = (options.exists("lock_unspents") ? options["lock_unspents"] : options["lockUnspents"]).get_bool();
}
- if (options.exists("feeRate"))
- {
+ if (options.exists("feeRate")) {
+ if (options.exists("fee_rate")) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both fee_rate (" + CURRENCY_ATOM + "/vB) and feeRate (" + CURRENCY_UNIT + "/kvB)");
+ }
if (options.exists("conf_target")) {
- throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both conf_target and feeRate");
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both conf_target and feeRate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.");
}
if (options.exists("estimate_mode")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and feeRate");
}
- coinControl.m_feerate = CFeeRate(AmountFromValue(options["feeRate"]));
+ CFeeRate fee_rate(AmountFromValue(options["feeRate"]));
+ if (fee_rate <= CFeeRate(0)) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid feeRate %s (must be greater than 0)", fee_rate.ToString()));
+ }
+ coinControl.m_feerate = fee_rate;
coinControl.fOverrideFeeRate = true;
}
@@ -3138,7 +3150,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
if (options.exists("replaceable")) {
coinControl.m_signal_bip125_rbf = options["replaceable"].get_bool();
}
- SetFeeEstimateMode(pwallet, coinControl, options["estimate_mode"], options["conf_target"]);
+ SetFeeEstimateMode(pwallet, coinControl, options["conf_target"], options["estimate_mode"], options["fee_rate"]);
}
} else {
// if options is null and not a bool
@@ -3195,7 +3207,8 @@ static RPCHelpMan fundrawtransaction()
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
{"lockUnspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
- {"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set: makes wallet determine the fee", "Set a specific fee rate in " + CURRENCY_UNIT + "/kB"},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
+ {"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_UNIT + "/kB."},
{"subtractFeeFromOutputs", RPCArg::Type::ARR, /* default */ "empty array", "The integers.\n"
"The fee will be equally deducted from the amount of each specified output.\n"
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
@@ -3372,12 +3385,13 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
"\nBumps the fee of an opt-in-RBF transaction T, replacing it with a new transaction B.\n"
+ std::string(want_psbt ? "Returns a PSBT instead of creating and signing a new transaction.\n" : "") +
"An opt-in RBF transaction with the given txid must be in the wallet.\n"
- "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"
+ "The command will pay the additional fee by reducing change outputs or adding inputs when necessary.\n"
+ "It may add a new change output if one does not already exist.\n"
"All inputs in the original transaction will be included in the replacement transaction.\n"
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
"By default, the new fee will be calculated automatically using the estimatesmartfee RPC.\n"
"The user can specify a confirmation target for estimatesmartfee.\n"
- "Alternatively, the user can specify a fee_rate (" + CURRENCY_UNIT + " per kB) for the new transaction.\n"
+ "Alternatively, the user can specify a fee_rate (in " + CURRENCY_ATOM + "/vB) for the new transaction.\n"
"At a minimum, the new fee rate must be high enough to pay an additional new relay fee (incrementalfee\n"
"returned by getnetworkinfo) to enter the node's mempool.\n",
{
@@ -3386,16 +3400,16 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
{
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n"
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
- {"fee_rate", RPCArg::Type::NUM, /* default */ "fall back to 'conf_target'", "fee rate (NOT total fee) to pay, in " + CURRENCY_UNIT + "/kB.\n"
- "Specify a fee rate instead of relying on the built-in fee estimator.\n"
- "Must be at least 0.0001 " + CURRENCY_UNIT + "/kB higher than the current transaction fee rate.\n"},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation",
+ "\nSpecify a fee rate in " + CURRENCY_ATOM + "/vB instead of relying on the built-in fee estimator.\n"
+ "Must be at least 1 " + CURRENCY_ATOM + "/vB higher than the current transaction fee rate.\n"},
{"replaceable", RPCArg::Type::BOOL, /* default */ "true", "Whether the new transaction should still be\n"
"marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n"
"be left unchanged from the original. If false, any input sequence numbers in the\n"
"original transaction that were less than 0xfffffffe will be increased to 0xfffffffe\n"
"so the new transaction will not be explicitly bip-125 replaceable (though it may\n"
"still be replaceable in practice, for example if it has unconfirmed ancestors which\n"
- "are replaceable)."},
+ "are replaceable).\n"},
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""},
},
@@ -3448,7 +3462,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
{
{"confTarget", UniValueType(UniValue::VNUM)},
{"conf_target", UniValueType(UniValue::VNUM)},
- {"fee_rate", UniValueType(UniValue::VNUM)},
+ {"fee_rate", UniValueType()}, // will be checked by AmountFromValue() in SetFeeEstimateMode()
{"replaceable", UniValueType(UniValue::VBOOL)},
{"estimate_mode", UniValueType(UniValue::VSTR)},
},
@@ -3475,7 +3489,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
if (options.exists("replaceable")) {
coin_control.m_signal_bip125_rbf = options["replaceable"].get_bool();
}
- SetFeeEstimateMode(pwallet, coin_control, options["estimate_mode"], conf_target);
+ SetFeeEstimateMode(pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"]);
}
// Make sure the results are valid at least up to the most recent block
@@ -4015,6 +4029,7 @@ static RPCHelpMan send()
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
{
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
@@ -4026,6 +4041,7 @@ static RPCHelpMan send()
"or fee rate (for " + CURRENCY_UNIT + "/kB and " + CURRENCY_ATOM + "/B estimate modes)"},
{"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
@@ -4070,10 +4086,11 @@ static RPCHelpMan send()
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
RPCTypeCheck(request.params, {
- UniValueType(), // ARR or OBJ, checked later
- UniValue::VNUM,
- UniValue::VSTR,
- UniValue::VOBJ
+ UniValueType(), // outputs (ARR or OBJ, checked later)
+ UniValue::VNUM, // conf_target
+ UniValue::VSTR, // estimate_mode
+ UniValue::VNUM, // fee_rate
+ UniValue::VOBJ, // options
}, true
);
@@ -4081,18 +4098,28 @@ static RPCHelpMan send()
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
- UniValue options{request.params[3].isNull() ? UniValue::VOBJ : request.params[3]};
- if (options.exists("feeRate") || options.exists("fee_rate") || options.exists("estimate_mode") || options.exists("conf_target")) {
+ UniValue options{request.params[4].isNull() ? UniValue::VOBJ : request.params[4]};
+ if (options.exists("conf_target") || options.exists("estimate_mode")) {
if (!request.params[1].isNull() || !request.params[2].isNull()) {
- throw JSONRPCError(RPC_INVALID_PARAMETER, "Use either conf_target and estimate_mode or the options dictionary to control fee rate");
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Pass conf_target and estimate_mode either as arguments or in the options object, but not both");
}
} else {
options.pushKV("conf_target", request.params[1]);
options.pushKV("estimate_mode", request.params[2]);
}
+ if (options.exists("fee_rate")) {
+ if (!request.params[3].isNull()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Pass the fee_rate either as an argument, or in the options object, but not both");
+ }
+ } else {
+ options.pushKV("fee_rate", request.params[3]);
+ }
if (!options["conf_target"].isNull() && (options["estimate_mode"].isNull() || (options["estimate_mode"].get_str() == "unset"))) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode");
}
+ if (options.exists("feeRate")) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Use fee_rate (" + CURRENCY_ATOM + "/vB) instead of feeRate");
+ }
if (options.exists("changeAddress")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address");
}
@@ -4350,7 +4377,8 @@ static RPCHelpMan walletcreatefundedpsbt()
{"change_type", RPCArg::Type::STR, /* default */ "set by -changetype", "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
{"includeWatching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only"},
{"lockUnspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
- {"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set: makes wallet determine the fee", "Set a specific fee rate in " + CURRENCY_UNIT + "/kB"},
+ {"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
+ {"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_UNIT + "/kB."},
{"subtractFeeFromOutputs", RPCArg::Type::ARR, /* default */ "empty array", "The outputs to subtract the fee from.\n"
"The fee will be equally deducted from the amount of each specified output.\n"
"Those recipients will receive less bitcoins than you enter in their corresponding amount field.\n"
@@ -4544,9 +4572,9 @@ static const CRPCCommand commands[] =
{ "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} },
{ "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} },
{ "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} },
- { "wallet", "send", &send, {"outputs","conf_target","estimate_mode","options"} },
- { "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode","verbose"} },
- { "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode","avoid_reuse","verbose"} },
+ { "wallet", "send", &send, {"outputs","conf_target","estimate_mode","fee_rate","options"} },
+ { "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","comment","subtractfeefrom","replaceable","conf_target","estimate_mode","fee_rate","verbose"} },
+ { "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","replaceable","conf_target","estimate_mode","avoid_reuse","fee_rate","verbose"} },
{ "wallet", "sethdseed", &sethdseed, {"newkeypool","seed"} },
{ "wallet", "setlabel", &setlabel, {"address","label"} },
{ "wallet", "settxfee", &settxfee, {"amount"} },
diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py
index 503993162b..8a22234837 100755
--- a/test/functional/rpc_fundrawtransaction.py
+++ b/test/functional/rpc_fundrawtransaction.py
@@ -90,7 +90,6 @@ class RawTransactionsTest(BitcoinTestFramework):
self.test_op_return()
self.test_watchonly()
self.test_all_watched_funds()
- self.test_feerate_with_conf_target_and_estimate_mode()
self.test_option_feerate()
self.test_address_reuse()
self.test_option_subtract_fee_from_outputs()
@@ -708,74 +707,89 @@ class RawTransactionsTest(BitcoinTestFramework):
wwatch.unloadwallet()
def test_option_feerate(self):
- self.log.info("Test fundrawtxn feeRate option")
-
- # Make sure there is exactly one input so coin selection can't skew the result.
- assert_equal(len(self.nodes[3].listunspent(1)), 1)
-
- inputs = []
- outputs = {self.nodes[3].getnewaddress() : 1}
- rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
- result = self.nodes[3].fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
- result2 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee})
- result3 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee})
- assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[3].fundrawtransaction, rawtx, {"feeRate": 1})
- result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex'])
- assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
- assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
-
- def test_feerate_with_conf_target_and_estimate_mode(self):
- self.log.info("Test fundrawtxn passing an explicit fee rate using conf_target and estimate_mode")
+ self.log.info("Test fundrawtxn with explicit fee rates (fee_rate sat/vB and feeRate BTC/kvB)")
node = self.nodes[3]
# Make sure there is exactly one input so coin selection can't skew the result.
- assert_equal(len(node.listunspent(1)), 1)
+ assert_equal(len(self.nodes[3].listunspent(1)), 1)
inputs = []
outputs = {node.getnewaddress() : 1}
rawtx = node.createrawtransaction(inputs, outputs)
- for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items():
- self.log.info("Test fundrawtxn with conf_target {} estimate_mode {} produces expected fee".format(fee_rate, unit))
- # With no arguments passed, expect fee of 141 sats/b.
- assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
- # Expect fee to be 10,000x higher when explicit fee 10,000x greater is specified.
- result = node.fundrawtransaction(rawtx, {"conf_target": fee_rate, "estimate_mode": unit})
- assert_approx(result["fee"], vexp=0.0141, vspan=0.0001)
+ result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
+ btc_kvb_to_sat_vb = 100000 # (1e5)
+ result1 = node.fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee})
+ result2 = node.fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee})
+ result3 = node.fundrawtransaction(rawtx, {"fee_rate": 10 * btc_kvb_to_sat_vb * self.min_relay_tx_fee})
+ result4 = node.fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee})
+ result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex'])
+ assert_fee_amount(result1['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
+ assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
+ assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
+ assert_fee_amount(result4['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
- for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items():
- self.log.info("Test fundrawtxn raises RPC error if both feeRate and {} are passed".format(field))
- assert_raises_rpc_error(
- -8, "Cannot specify both {} and feeRate".format(field),
- lambda: node.fundrawtransaction(rawtx, {"feeRate": 0.1, field: fee_rate}))
+ # With no arguments passed, expect fee of 141 satoshis.
+ assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
+ # Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified.
+ result = node.fundrawtransaction(rawtx, {"fee_rate": 10000})
+ assert_approx(result["fee"], vexp=0.0141, vspan=0.0001)
self.log.info("Test fundrawtxn with invalid estimate_mode settings")
for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k),
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": v, "conf_target": 0.1}))
- for mode in ["foo", Decimal("3.141592")]:
+ node.fundrawtransaction, rawtx, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})
+ for mode in ["", "foo", Decimal("3.141592")]:
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0.1}))
+ node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True})
self.log.info("Test fundrawtxn with invalid conf_target settings")
- for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]:
+ for mode in ["unset", "economical", "conservative"]:
self.log.debug("{}".format(mode))
for k, v in {"string": "", "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k),
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": v}))
- if mode in ["btc/kb", "sat/b"]:
- assert_raises_rpc_error(-3, "Amount out of range",
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": -1}))
- assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": 0}))
- else:
- for n in [-1, 0, 1009]:
- assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008",
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": n}))
-
- for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items():
- self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
- assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True}))
-
+ node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
+ for n in [-1, 0, 1009]:
+ assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
+ node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
+
+ self.log.info("Test invalid fee rate settings")
+ assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ node.fundrawtransaction, rawtx, {"fee_rate": 0, "add_inputs": True})
+ assert_raises_rpc_error(-8, "Invalid feeRate 0.00000000 BTC/kB (must be greater than 0)",
+ node.fundrawtransaction, rawtx, {"feeRate": 0, "add_inputs": True})
+ for param, value in {("fee_rate", 100000), ("feeRate", 1.000)}:
+ assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
+ node.fundrawtransaction, rawtx, {param: value, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Amount out of range",
+ node.fundrawtransaction, rawtx, {"fee_rate": -1, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Amount is not a number or string",
+ node.fundrawtransaction, rawtx, {"fee_rate": {"foo": "bar"}, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Invalid amount",
+ node.fundrawtransaction, rawtx, {"fee_rate": "", "add_inputs": True})
+
+ # Test setting explicit fee rate just below the minimum.
+ self.log.info("- raises RPC error 'fee rate too low' if fee_rate of 0.99999999 sat/vB is passed")
+ msg = "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"
+ assert_raises_rpc_error(-4, msg, node.fundrawtransaction, rawtx, {"fee_rate": 0.99999999, "add_inputs": True})
+ # This feeRate test only passes if `coinControl.fOverrideFeeRate = true` in wallet/rpcwallet.cpp::FundTransaction is removed.
+ # assert_raises_rpc_error(-4, msg, node.fundrawtransaction, rawtx, {"feeRate": 0.00000999, "add_inputs": True})
+
+ self.log.info("- raises RPC error if both feeRate and fee_rate are passed")
+ assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)",
+ node.fundrawtransaction, rawtx, {"fee_rate": 0.1, "feeRate": 0.1, "add_inputs": True})
+
+ self.log.info("- raises RPC error if both feeRate and estimate_mode passed")
+ assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and feeRate",
+ node.fundrawtransaction, rawtx, {"estimate_mode": "economical", "feeRate": 0.1, "add_inputs": True})
+
+ for param in ["feeRate", "fee_rate"]:
+ self.log.info("- raises RPC error if both {} and conf_target are passed".format(param))
+ assert_raises_rpc_error(-8, "Cannot specify both conf_target and {}. Please provide either a confirmation "
+ "target in blocks for automatic fee estimation, or an explicit fee rate.".format(param),
+ node.fundrawtransaction, rawtx, {param: 1, "conf_target": 1, "add_inputs": True})
+
+ self.log.info("- raises RPC error if both fee_rate and estimate_mode are passed")
+ assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate",
+ node.fundrawtransaction, rawtx, {"fee_rate": 1, "estimate_mode": "economical", "add_inputs": True})
def test_address_reuse(self):
"""Test no address reuse occurs."""
@@ -803,12 +817,32 @@ class RawTransactionsTest(BitcoinTestFramework):
outputs = {self.nodes[2].getnewaddress(): 1}
rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
+ # Test subtract fee from outputs with feeRate (BTC/kvB)
result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee)
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee)
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee}),
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),]
+ dec_tx = [self.nodes[3].decoderawtransaction(tx_['hex']) for tx_ in result]
+ output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)]
+ change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)]
+
+ assert_equal(result[0]['fee'], result[1]['fee'], result[2]['fee'])
+ assert_equal(result[3]['fee'], result[4]['fee'])
+ assert_equal(change[0], change[1])
+ assert_equal(output[0], output[1])
+ assert_equal(output[0], output[2] + result[2]['fee'])
+ assert_equal(change[0] + result[0]['fee'], change[2])
+ assert_equal(output[3], output[4] + result[4]['fee'])
+ assert_equal(change[3] + result[3]['fee'], change[4])
+ # Test subtract fee from outputs with fee_rate (sat/vB)
+ btc_kvb_to_sat_vb = 100000 # (1e5)
+ result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee)
+ self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list
+ self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee)
+ self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee}),
+ self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),]
dec_tx = [self.nodes[3].decoderawtransaction(tx_['hex']) for tx_ in result]
output = [d['vout'][1 - r['changepos']]['value'] for d, r in zip(dec_tx, result)]
change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)]
diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
index ca75bcb9bb..9adbce1436 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -187,60 +187,74 @@ class PSBTTest(BitcoinTestFramework):
assert_equal(walletprocesspsbt_out['complete'], True)
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
- self.log.info("Test walletcreatefundedpsbt feeRate of 0.1 BTC/kB produces a total fee at or slightly below -maxtxfee (~0.05290000)")
- res = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True})
- assert_approx(res["fee"], 0.055, 0.005)
-
- self.log.info("Test walletcreatefundedpsbt explicit fee rate with conf_target and estimate_mode")
- for unit, fee_rate in {"btc/kb": 0.1, "sat/b": 10000}.items():
- fee = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"conf_target": fee_rate, "estimate_mode": unit, "add_inputs": True})["fee"]
- self.log.info("- conf_target {}, estimate_mode {} produces fee {} at or slightly below -maxtxfee (~0.05290000)".format(fee_rate, unit, fee))
- assert_approx(fee, vexp=0.055, vspan=0.005)
-
- for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "sat/b"}.items():
- self.log.info("- raises RPC error if both feeRate and {} are passed".format(field))
- assert_raises_rpc_error(-8, "Cannot specify both {} and feeRate".format(field),
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, field: fee_rate, "add_inputs": True}))
+ self.log.info("Test walletcreatefundedpsbt fee rate of 10000 sat/vB and 0.1 BTC/kvB produces a total fee at or slightly below -maxtxfee (~0.05290000)")
+ res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True})
+ assert_approx(res1["fee"], 0.055, 0.005)
+ res2 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True})
+ assert_approx(res2["fee"], 0.055, 0.005)
+
+ self.log.info("Test invalid fee rate settings")
+ assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0, "add_inputs": True})
+ assert_raises_rpc_error(-8, "Invalid feeRate 0.00000000 BTC/kB (must be greater than 0)",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"feeRate": 0, "add_inputs": True})
+ for param, value in {("fee_rate", 100000), ("feeRate", 1)}:
+ assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: value, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Amount out of range",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": -1, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Amount is not a number or string",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": {"foo": "bar"}, "add_inputs": True})
+ assert_raises_rpc_error(-3, "Invalid amount",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": "", "add_inputs": True})
+
+ self.log.info("- raises RPC error if both feeRate and fee_rate are passed")
+ assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0.1, "feeRate": 0.1, "add_inputs": True})
+
+ self.log.info("- raises RPC error if both feeRate and estimate_mode passed")
+ assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and feeRate",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": "economical", "feeRate": 0.1, "add_inputs": True})
+
+ for param in ["feeRate", "fee_rate"]:
+ self.log.info("- raises RPC error if both {} and conf_target are passed".format(param))
+ assert_raises_rpc_error(-8, "Cannot specify both conf_target and {}. Please provide either a confirmation "
+ "target in blocks for automatic fee estimation, or an explicit fee rate.".format(param),
+ self.nodes[1].walletcreatefundedpsbt ,inputs, outputs, 0, {param: 1, "conf_target": 1, "add_inputs": True})
+
+ self.log.info("- raises RPC error if both fee_rate and estimate_mode are passed")
+ assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate",
+ self.nodes[1].walletcreatefundedpsbt ,inputs, outputs, 0, {"fee_rate": 1, "estimate_mode": "economical", "add_inputs": True})
self.log.info("- raises RPC error with invalid estimate_mode settings")
for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k),
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True}))
- for mode in ["foo", Decimal("3.141592")]:
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})
+ for mode in ["", "foo", Decimal("3.141592")]:
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True}))
-
- self.log.info("- raises RPC error if estimate_mode is passed without a conf_target")
- for unit in ["SAT/B", "BTC/KB"]:
- assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit),
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit}))
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True})
self.log.info("- raises RPC error with invalid conf_target settings")
- for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]:
+ for mode in ["unset", "economical", "conservative"]:
self.log.debug("{}".format(mode))
for k, v in {"string": "", "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k),
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True}))
- if mode in ["btc/kb", "sat/b"]:
- assert_raises_rpc_error(-3, "Amount out of range",
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": -1, "add_inputs": True}))
- assert_raises_rpc_error(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0, "add_inputs": True}))
- else:
- for n in [-1, 0, 1009]:
- assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008",
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True}))
-
- for unit, fee_rate in {"SAT/B": 0.99999999, "BTC/KB": 0.00000999}.items():
- self.log.info("- raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
- assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True}))
-
- self.log.info("Test walletcreatefundedpsbt feeRate of 10 BTC/kB produces total fee well above -maxtxfee and raises RPC error")
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
+ for n in [-1, 0, 1009]:
+ assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
+
+ # Test setting explicit fee rate just below the minimum.
+ self.log.info("- raises RPC error 'fee rate too low' if feerate_sat_vb of 0.99999999 is passed")
+ assert_raises_rpc_error(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0.99999999, "add_inputs": True})
+
+ self.log.info("Test walletcreatefundedpsbt with too-high fee rate produces total fee well above -maxtxfee and raises RPC error")
# previously this was silently capped at -maxtxfee
for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items():
- assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
- self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 10, "add_inputs": bool_add})
+ msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
+ assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"fee_rate": 1000000, "add_inputs": bool_add})
+ assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add})
self.log.info("Test various PSBT operations")
# partially sign multisig things with node 1
diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py
index 40daffd2c2..7b4d459503 100755
--- a/test/functional/wallet_basic.py
+++ b/test/functional/wallet_basic.py
@@ -4,6 +4,7 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the wallet."""
from decimal import Decimal
+from itertools import product
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -14,6 +15,8 @@ from test_framework.util import (
)
from test_framework.wallet_util import test_address
+OUT_OF_RANGE = "Amount out of range"
+
class WalletTest(BitcoinTestFramework):
def set_test_params(self):
@@ -74,7 +77,7 @@ class WalletTest(BitcoinTestFramework):
assert_equal(len(self.nodes[1].listunspent()), 1)
assert_equal(len(self.nodes[2].listunspent()), 0)
- self.log.info("test gettxout")
+ self.log.info("Test gettxout")
confirmed_txid, confirmed_index = utxos[0]["txid"], utxos[0]["vout"]
# First, outputs that are unspent both in the chain and in the
# mempool should appear with or without include_mempool
@@ -87,7 +90,7 @@ class WalletTest(BitcoinTestFramework):
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 11)
mempool_txid = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 10)
- self.log.info("test gettxout (second part)")
+ self.log.info("Test gettxout (second part)")
# utxo spent in mempool should be visible if you exclude mempool
# but invisible if you include mempool
txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False)
@@ -227,65 +230,41 @@ class WalletTest(BitcoinTestFramework):
assert_equal(self.nodes[2].getbalance(), node_2_bal)
node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
- self.log.info("Test case-insensitive explicit fee rate (sendmany as BTC/kB)")
- # Throw if no conf_target provided
- assert_raises_rpc_error(-8, "Selected estimate_mode bTc/kB requires a fee rate to be specified in conf_target",
- self.nodes[2].sendmany,
- amounts={ address: 10 },
- estimate_mode='bTc/kB')
- # Throw if negative feerate
- assert_raises_rpc_error(-3, "Amount out of range",
- self.nodes[2].sendmany,
- amounts={ address: 10 },
- conf_target=-1,
- estimate_mode='bTc/kB')
- fee_per_kb = 0.0002500
- explicit_fee_per_byte = Decimal(fee_per_kb) / 1000
- txid = self.nodes[2].sendmany(
- amounts={ address: 10 },
- conf_target=fee_per_kb,
- estimate_mode='bTc/kB',
- )
- self.nodes[2].generate(1)
- self.sync_all(self.nodes[0:3])
- node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), node_2_bal - Decimal('10'), explicit_fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
- assert_equal(self.nodes[2].getbalance(), node_2_bal)
- node_0_bal += Decimal('10')
- assert_equal(self.nodes[0].getbalance(), node_0_bal)
+ self.log.info("Test sendmany with fee_rate param (explicit fee rate in sat/vB)")
+ fee_rate_sat_vb = 2
+ fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
+ explicit_fee_rate_btc_kvb = Decimal(fee_rate_btc_kvb) / 1000
- self.log.info("Test case-insensitive explicit fee rate (sendmany as sat/B)")
- # Throw if no conf_target provided
- assert_raises_rpc_error(-8, "Selected estimate_mode sat/b requires a fee rate to be specified in conf_target",
- self.nodes[2].sendmany,
- amounts={ address: 10 },
- estimate_mode='sat/b')
- # Throw if negative feerate
- assert_raises_rpc_error(-3, "Amount out of range",
- self.nodes[2].sendmany,
- amounts={ address: 10 },
- conf_target=-1,
- estimate_mode='sat/b')
- fee_sat_per_b = 2
- fee_per_kb = fee_sat_per_b / 100000.0
- explicit_fee_per_byte = Decimal(fee_per_kb) / 1000
- txid = self.nodes[2].sendmany(
- amounts={ address: 10 },
- conf_target=fee_sat_per_b,
- estimate_mode='sAT/b',
- )
+ # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
+ txid = self.nodes[2].sendmany(amounts={address: 10}, conf_target=0, estimate_mode="", fee_rate=fee_rate_sat_vb)
self.nodes[2].generate(1)
self.sync_all(self.nodes[0:3])
balance = self.nodes[2].getbalance()
- node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_per_byte, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
+ node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_rate_btc_kvb, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
assert_equal(balance, node_2_bal)
node_0_bal += Decimal('10')
assert_equal(self.nodes[0].getbalance(), node_0_bal)
+ for key in ["totalFee", "feeRate"]:
+ assert_raises_rpc_error(-8, "Unknown named parameter key", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=1, key=1)
+
# Test setting explicit fee rate just below the minimum.
- for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items():
- self.log.info("Test sendmany raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
- assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- self.nodes[2].sendmany, amounts={address: 10}, estimate_mode=unit, conf_target=fee_rate)
+ self.log.info("Test sendmany raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
+ assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.99999999)
+
+ self.log.info("Test sendmany raises if fee_rate of 0 or -1 is passed")
+ assert_raises_rpc_error(-6, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0)
+ assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=-1)
+
+ self.log.info("Test sendmany raises if an invalid conf_target or estimate_mode is passed")
+ for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):
+ assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
+ self.nodes[2].sendmany, amounts={address: 1}, conf_target=target, estimate_mode=mode)
+ for target, mode in product([-1, 0], ["btc/kb", "sat/b"]):
+ assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
+ self.nodes[2].sendmany, amounts={address: 1}, conf_target=target, estimate_mode=mode)
self.start_node(3, self.nodes[3].extra_args)
self.connect_nodes(0, 3)
@@ -318,7 +297,7 @@ class WalletTest(BitcoinTestFramework):
assert_equal(uTx['amount'], Decimal('0'))
assert found
- # do some -walletbroadcast tests
+ self.log.info("Test -walletbroadcast")
self.stop_nodes()
self.start_node(0, ["-walletbroadcast=0"])
self.start_node(1, ["-walletbroadcast=0"])
@@ -378,7 +357,7 @@ class WalletTest(BitcoinTestFramework):
# General checks for errors from incorrect inputs
# This will raise an exception because the amount is negative
- assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "-1")
+ assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "-1")
# This will raise an exception because the amount type is wrong
assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "1f-4")
@@ -420,78 +399,43 @@ class WalletTest(BitcoinTestFramework):
self.nodes[0].generate(1)
self.sync_all(self.nodes[0:3])
- self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as BTC/kB)")
- self.nodes[0].generate(1)
- self.sync_all(self.nodes[0:3])
+ self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in sat/vB)")
prebalance = self.nodes[2].getbalance()
assert prebalance > 2
address = self.nodes[1].getnewaddress()
- # Throw if no conf_target provided
- assert_raises_rpc_error(-8, "Selected estimate_mode BTc/Kb requires a fee rate to be specified in conf_target",
- self.nodes[2].sendtoaddress,
- address=address,
- amount=1.0,
- estimate_mode='BTc/Kb')
- # Throw if negative feerate
- assert_raises_rpc_error(-3, "Amount out of range",
- self.nodes[2].sendtoaddress,
- address=address,
- amount=1.0,
- conf_target=-1,
- estimate_mode='btc/kb')
- txid = self.nodes[2].sendtoaddress(
- address=address,
- amount=1.0,
- conf_target=0.00002500,
- estimate_mode='btc/kb',
- )
- tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])
- self.sync_all(self.nodes[0:3])
- self.nodes[0].generate(1)
- self.sync_all(self.nodes[0:3])
- postbalance = self.nodes[2].getbalance()
- fee = prebalance - postbalance - Decimal('1')
- assert_fee_amount(fee, tx_size, Decimal('0.00002500'))
-
- self.sync_all(self.nodes[0:3])
+ amount = 3
+ fee_rate_sat_vb = 2
+ fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
- self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as sat/B)")
- self.nodes[0].generate(1)
- prebalance = self.nodes[2].getbalance()
- assert prebalance > 2
- address = self.nodes[1].getnewaddress()
- # Throw if no conf_target provided
- assert_raises_rpc_error(-8, "Selected estimate_mode SAT/b requires a fee rate to be specified in conf_target",
- self.nodes[2].sendtoaddress,
- address=address,
- amount=1.0,
- estimate_mode='SAT/b')
- # Throw if negative feerate
- assert_raises_rpc_error(-3, "Amount out of range",
- self.nodes[2].sendtoaddress,
- address=address,
- amount=1.0,
- conf_target=-1,
- estimate_mode='SAT/b')
- txid = self.nodes[2].sendtoaddress(
- address=address,
- amount=1.0,
- conf_target=2,
- estimate_mode='SAT/B',
- )
+ # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
+ txid = self.nodes[2].sendtoaddress(address=address, amount=amount, conf_target=0, estimate_mode="", fee_rate=fee_rate_sat_vb)
tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])
- self.sync_all(self.nodes[0:3])
self.nodes[0].generate(1)
self.sync_all(self.nodes[0:3])
postbalance = self.nodes[2].getbalance()
- fee = prebalance - postbalance - Decimal('1')
- assert_fee_amount(fee, tx_size, Decimal('0.00002000'))
+ fee = prebalance - postbalance - Decimal(amount)
+ assert_fee_amount(fee, tx_size, Decimal(fee_rate_btc_kvb))
+
+ for key in ["totalFee", "feeRate"]:
+ assert_raises_rpc_error(-8, "Unknown named parameter key", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=1, key=1)
# Test setting explicit fee rate just below the minimum.
- for unit, fee_rate in {"BTC/kB": 0.00000999, "sat/B": 0.99999999}.items():
- self.log.info("Test sendtoaddress raises 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
- assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
- self.nodes[2].sendtoaddress, address=address, amount=1, estimate_mode=unit, conf_target=fee_rate)
+ self.log.info("Test sendtoaddress raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
+ assert_raises_rpc_error(-6, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.99999999)
+
+ self.log.info("Test sendtoaddress raises if fee_rate of 0 or -1 is passed")
+ assert_raises_rpc_error(-6, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)",
+ self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=0)
+ assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=-1)
+
+ self.log.info("Test sendtoaddress raises if an invalid conf_target or estimate_mode is passed")
+ for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):
+ assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
+ self.nodes[2].sendtoaddress, address=address, amount=1, conf_target=target, estimate_mode=mode)
+ for target, mode in product([-1, 0], ["btc/kb", "sat/b"]):
+ assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
+ self.nodes[2].sendtoaddress, address=address, amount=1, conf_target=target, estimate_mode=mode)
# 2. Import address from node2 to node1
self.nodes[1].importaddress(address_to_import)
@@ -549,7 +493,7 @@ class WalletTest(BitcoinTestFramework):
]
chainlimit = 6
for m in maintenance:
- self.log.info("check " + m)
+ self.log.info("Test " + m)
self.stop_nodes()
# set lower ancestor limit for later
self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)])
diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py
index 88a7778e19..5f76c78198 100755
--- a/test/functional/wallet_bumpfee.py
+++ b/test/functional/wallet_bumpfee.py
@@ -17,7 +17,7 @@ from decimal import Decimal
import io
from test_framework.blocktools import add_witness_commitment, create_block, create_coinbase, send_to_witness
-from test_framework.messages import BIP125_SEQUENCE_NUMBER, COIN, CTransaction
+from test_framework.messages import BIP125_SEQUENCE_NUMBER, CTransaction
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -29,15 +29,13 @@ from test_framework.util import (
WALLET_PASSPHRASE = "test"
WALLET_PASSPHRASE_TIMEOUT = 3600
-# Fee rates (in BTC per 1000 vbytes)
-INSUFFICIENT = 0.00001000
-ECONOMICAL = 0.00050000
-NORMAL = 0.00100000
-HIGH = 0.00500000
-TOO_HIGH = 1.00000000
+# Fee rates (sat/vB)
+INSUFFICIENT = 1
+ECONOMICAL = 50
+NORMAL = 100
+HIGH = 500
+TOO_HIGH = 100000
-BTC_MODE = "BTC/kB"
-SAT_MODE = "sat/B"
class BumpFeeTest(BitcoinTestFramework):
def set_test_params(self):
@@ -78,7 +76,7 @@ class BumpFeeTest(BitcoinTestFramework):
self.log.info("Running tests")
dest_address = peer_node.getnewaddress()
- for mode in ["default", "fee_rate", BTC_MODE, SAT_MODE]:
+ for mode in ["default", "fee_rate"]:
test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address)
self.test_invalid_parameters(rbf_node, peer_node, dest_address)
test_segwit_bumpfee_succeeds(self, rbf_node, dest_address)
@@ -105,50 +103,43 @@ class BumpFeeTest(BitcoinTestFramework):
self.sync_mempools((rbf_node, peer_node))
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
- assert_raises_rpc_error(-3, "Unexpected key totalFee", rbf_node.bumpfee, rbfid, {"totalFee": NORMAL})
- assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH})
+ for key in ["totalFee", "feeRate"]:
+ assert_raises_rpc_error(-3, "Unexpected key {}".format(key), rbf_node.bumpfee, rbfid, {key: NORMAL})
- # For each fee mode, bumping to just above minrelay should fail to increase the total fee enough.
- for options in [{"fee_rate": INSUFFICIENT}, {"conf_target": INSUFFICIENT, "estimate_mode": BTC_MODE}, {"conf_target": 1, "estimate_mode": SAT_MODE}]:
- assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, options)
+ # Bumping to just above minrelay should fail to increase the total fee enough.
+ assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141, must be at least 0.00001704 (oldFee 0.00000999 + incrementalFee 0.00000705)",
+ rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT})
- self.log.info("Test explicit fee rate raises RPC error if estimate_mode is passed without a conf_target")
- for unit, fee_rate in {"SAT/B": 100, "BTC/KB": NORMAL}.items():
- assert_raises_rpc_error(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit),
- rbf_node.bumpfee, rbfid, {"fee_rate": fee_rate, "estimate_mode": unit})
+ self.log.info("Test invalid fee rate settings")
+ assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)",
+ rbf_node.bumpfee, rbfid, {"fee_rate": 0})
+ assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10",
+ rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH})
+ assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1})
+ for value in [{"foo": "bar"}, True]:
+ assert_raises_rpc_error(-3, "Amount is not a number or string", rbf_node.bumpfee, rbfid, {"fee_rate": value})
+ assert_raises_rpc_error(-3, "Invalid amount", rbf_node.bumpfee, rbfid, {"fee_rate": ""})
self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed")
- error_both = "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."
- assert_raises_rpc_error(-8, error_both, rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL})
+ assert_raises_rpc_error(-8, "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.",
+ rbf_node.bumpfee, rbfid, {"conf_target": NORMAL, "fee_rate": NORMAL})
+
+ self.log.info("Test explicit fee rate raises RPC error if both fee_rate and estimate_mode are passed")
+ assert_raises_rpc_error(-8, "Cannot specify both estimate_mode and fee_rate",
+ rbf_node.bumpfee, rbfid, {"estimate_mode": "economical", "fee_rate": NORMAL})
self.log.info("Test invalid conf_target settings")
assert_raises_rpc_error(-8, "confTarget and conf_target options should not both be set",
- rbf_node.bumpfee, rbfid, {"confTarget": 123, "conf_target": 456})
- for field in ["confTarget", "conf_target"]:
- assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee",
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": BTC_MODE, "conf_target": 2009}))
- assert_raises_rpc_error(-4, "is too high (cannot be higher than -maxtxfee",
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": SAT_MODE, "conf_target": 2009 * 10000}))
+ rbf_node.bumpfee, rbfid, {"confTarget": 123, "conf_target": 456})
self.log.info("Test invalid estimate_mode settings")
for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k),
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": v, "fee_rate": NORMAL}))
- for mode in ["foo", Decimal("3.141592")]:
- assert_raises_rpc_error(-8, "Invalid estimate_mode parameter",
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": NORMAL}))
-
- self.log.info("Test invalid fee_rate settings")
- for mode in ["unset", "economical", "conservative", BTC_MODE, SAT_MODE]:
- self.log.debug("{}".format(mode))
- for k, v in {"string": "", "object": {"foo": "bar"}}.items():
- assert_raises_rpc_error(-3, "Expected type number for fee_rate, got {}".format(k),
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": v}))
- assert_raises_rpc_error(-3, "Amount out of range",
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": -1}))
- assert_raises_rpc_error(-8, "Invalid fee_rate 0.00000000 BTC/kB (must be greater than 0)",
- lambda: rbf_node.bumpfee(rbfid, {"estimate_mode": mode, "fee_rate": 0}))
+ rbf_node.bumpfee, rbfid, {"estimate_mode": v})
+ for mode in ["foo", Decimal("3.1415"), "sat/B", "BTC/kB"]:
+ assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", rbf_node.bumpfee, rbfid, {"estimate_mode": mode})
+
self.clear_mempool()
@@ -161,13 +152,6 @@ def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
if mode == "fee_rate":
bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"fee_rate": NORMAL})
bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": NORMAL})
- elif mode == BTC_MODE:
- bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE})
- bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": NORMAL, "estimate_mode": BTC_MODE})
- elif mode == SAT_MODE:
- sat_fee = NORMAL * COIN / 1000 # convert NORMAL from BTC/kB to sat/B
- bumped_psbt = rbf_node.psbtbumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE})
- bumped_tx = rbf_node.bumpfee(rbfid, {"conf_target": sat_fee, "estimate_mode": SAT_MODE})
else:
bumped_psbt = rbf_node.psbtbumpfee(rbfid)
bumped_tx = rbf_node.bumpfee(rbfid)
@@ -319,11 +303,11 @@ def test_dust_to_fee(self, rbf_node, dest_address):
# boundary. Thus expected transaction size (p2wpkh, 1 input, 2 outputs) is 140-141 vbytes, usually 141.
if not 140 <= fulltx["vsize"] <= 141:
raise AssertionError("Invalid tx vsize of {} (140-141 expected), full tx: {}".format(fulltx["vsize"], fulltx))
- # Bump with fee_rate of 0.00350250 BTC per 1000 vbytes to create dust.
+ # Bump with fee_rate of 350.25 sat/vB vbytes to create dust.
# Expected fee is 141 vbytes * fee_rate 0.00350250 BTC / 1000 vbytes = 0.00049385 BTC.
# or occasionally 140 vbytes * fee_rate 0.00350250 BTC / 1000 vbytes = 0.00049035 BTC.
# Dust should be dropped to the fee, so actual bump fee is 0.00050000 BTC.
- bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": 0.00350250})
+ bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate": 350.25})
full_bumped_tx = rbf_node.getrawtransaction(bumped_tx["txid"], 1)
assert_equal(bumped_tx["fee"], Decimal("0.00050000"))
assert_equal(len(fulltx["vout"]), 2)
diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py
index 82c046a17f..9377e34083 100755
--- a/test/functional/wallet_send.py
+++ b/test/functional/wallet_send.py
@@ -30,8 +30,8 @@ class WalletSendTest(BitcoinTestFramework):
self.skip_if_no_wallet()
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
- arg_conf_target=None, arg_estimate_mode=None,
- conf_target=None, estimate_mode=None, add_to_wallet=None, psbt=None,
+ arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None,
+ conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None,
inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None,
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
expect_error=None):
@@ -64,6 +64,8 @@ class WalletSendTest(BitcoinTestFramework):
options["conf_target"] = conf_target
if estimate_mode is not None:
options["estimate_mode"] = estimate_mode
+ if fee_rate is not None:
+ options["fee_rate"] = fee_rate
if inputs is not None:
options["inputs"] = inputs
if add_inputs is not None:
@@ -91,18 +93,19 @@ class WalletSendTest(BitcoinTestFramework):
options = None
if expect_error is None:
- res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
+ res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options)
else:
try:
assert_raises_rpc_error(expect_error[0], expect_error[1], from_wallet.send,
- outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
+ outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options)
except AssertionError:
# Provide debug info if the test fails
self.log.error("Unexpected successful result:")
self.log.error(arg_conf_target)
self.log.error(arg_estimate_mode)
+ self.log.error(arg_fee_rate)
self.log.error(options)
- res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, options=options)
+ res = from_wallet.send(outputs=outputs, conf_target=arg_conf_target, estimate_mode=arg_estimate_mode, fee_rate=arg_fee_rate, options=options)
self.log.error(res)
if "txid" in res and add_to_wallet:
self.log.error("Transaction details:")
@@ -228,10 +231,10 @@ class WalletSendTest(BitcoinTestFramework):
assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"],
self.nodes[1].decodepsbt(res2["psbt"])["fee"])
# but not at the same time
- for mode in ["unset", "economical", "conservative", "btc/kb", "sat/b"]:
+ for mode in ["unset", "economical", "conservative"]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical",
conf_target=1, estimate_mode=mode, add_to_wallet=False,
- expect_error=(-8, "Use either conf_target and estimate_mode or the options dictionary to control fee rate"))
+ expect_error=(-8, "Pass conf_target and estimate_mode either as arguments or in the options object, but not both"))
self.log.info("Create PSBT from watch-only wallet w3, sign with w2...")
res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1)
@@ -253,41 +256,49 @@ class WalletSendTest(BitcoinTestFramework):
assert res["complete"]
self.log.info("Test setting explicit fee rate")
- res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", add_to_wallet=False)
- res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=1, estimate_mode="economical", add_to_wallet=False)
+ res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, add_to_wallet=False)
+ res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=1, add_to_wallet=False)
assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], self.nodes[1].decodepsbt(res2["psbt"])["fee"])
- res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.00007, estimate_mode="btc/kb", add_to_wallet=False)
+ # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
+ res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0, estimate_mode="", fee_rate=7, add_to_wallet=False)
fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007"))
- res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=2, estimate_mode="sat/b", add_to_wallet=False)
+ res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=2, add_to_wallet=False)
fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002"))
- res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.00004531, arg_estimate_mode="btc/kb", add_to_wallet=False)
+ # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
+ res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0, arg_estimate_mode="", arg_fee_rate=4.531, add_to_wallet=False)
fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531"))
- res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=3, arg_estimate_mode="sat/b", add_to_wallet=False)
+ res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=3, add_to_wallet=False)
fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003"))
+ # Test that passing fee_rate as both an argument and an option raises.
+ self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, fee_rate=1, add_to_wallet=False,
+ expect_error=(-8, "Pass the fee_rate either as an argument, or in the options object, but not both"))
+
+ assert_raises_rpc_error(-8, "Use fee_rate (sat/vB) instead of feeRate", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"feeRate": 0.01})
+
+ assert_raises_rpc_error(-3, "Unexpected key totalFee", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"totalFee": 0.01})
+
for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode,
- expect_error=(-8, "Invalid conf_target, must be between 1 and 1008"))
- for mode in ["btc/kb", "sat/b"]:
- self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode=mode,
- expect_error=(-3, "Amount out of range"))
- self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0, estimate_mode=mode,
- expect_error=(-4, "Fee rate (0.00000000 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
-
- for mode in ["foo", Decimal("3.141592")]:
+ expect_error=(-8, "Invalid conf_target, must be between 1 and 1008")) # max value of 1008 per src/policy/fees.h
+ for target, mode in product([-1, 0], ["btc/kb", "sat/b"]):
+ self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode,
+ expect_error=(-8, "Invalid estimate_mode parameter"))
+
+ for mode in ["", "foo", Decimal("3.141592")]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode=mode,
expect_error=(-8, "Invalid estimate_mode parameter"))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode,
expect_error=(-8, "Invalid estimate_mode parameter"))
- assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", lambda: w0.send({w1.getnewaddress(): 1}, 0.1, mode))
+ assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", w0.send, {w1.getnewaddress(): 1}, 0.1, mode)
for mode in ["economical", "conservative", "btc/kb", "sat/b"]:
self.log.debug("{}".format(mode))
@@ -295,17 +306,11 @@ class WalletSendTest(BitcoinTestFramework):
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode,
expect_error=(-3, "Expected type number for conf_target, got {}".format(k)))
- # TODO: error should use sat/B instead of BTC/kB if sat/B is selected.
+ # TODO: The error message should use sat/vB units instead of BTC/kB.
# Test setting explicit fee rate just below the minimum.
- for unit, fee_rate in {"sat/B": 0.99999999, "BTC/kB": 0.00000999}.items():
- self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if conf_target {} and estimate_mode {} are passed".format(fee_rate, unit))
- self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=fee_rate, estimate_mode=unit,
- expect_error=(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
-
- self.log.info("Explicit fee rate raises RPC error if estimate_mode is passed without a conf_target")
- for unit, fee_rate in {"sat/B": 100, "BTC/kB": 0.001}.items():
- self.test_send(from_wallet=w0, to_wallet=w1, amount=1, estimate_mode=unit,
- expect_error=(-8, "Selected estimate_mode {} requires a fee rate to be specified in conf_target".format(unit)))
+ self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if fee_rate of 0.99999999 is passed")
+ self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0.99999999,
+ expect_error=(-4, "Fee rate (0.00000999 BTC/kB) is lower than the minimum fee rate setting (0.00001000 BTC/kB)"))
# TODO: Return hex if fee rate is below -maxmempool
# res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="sat/b", add_to_wallet=False)