Merge #20305: wallet: introduce fee_rate sat/vB param/option

05e82d86b09d914ebce05dbc92a7299cb026847b wallet: override minfee checks (fOverrideFeeRate) for fee_rate (Jon Atack)
9a670b4f07a6140de809d73cbd7f3e614eb6ea74 wallet: update sendtoaddress, send RPC examples with fee_rate (Jon Atack)
be481b72e24fb6834bd674cd8daee67c6938b42d wallet: use MIN_RELAY_TX_FEE in bumpfee help (Jon Atack)
449b730579566459e350703611629e63e54657ed wallet: provide valid values if invalid estimate mode passed (Jon Atack)
6da3afbaee5809ebf6d88efaa3958c505c2d71c7 wallet: update remaining rpcwallet fee rate units to BTC/kvB (Jon Atack)
173b5b5fe07d45be5a1e5bc7a5df996f20ab1e85 wallet: update fee rate units, use sat/vB for fee_rate error messages (Jon Atack)
7f9835a05abf3e168ad93e7195cbaa4bf61b9b07 wallet: remove fee rates from conf_target helps (Jon Atack)
b7994c01e9a3251536fe6538a22f614774eec82d wallet: add fee_rate unit warnings to bumpfee (Jon Atack)
410e471fa42d3db04e8879c71f8c824dcc151a83 wallet: remove redundant bumpfee fee_rate checks (Jon Atack)
a0d495747320c79b27a83c216dcc526ac8df8f24 wallet: introduce fee_rate (sat/vB) param/option (Jon Atack)
e21212f01b7c41eba13b0479b252053cf482bc1f wallet: remove unneeded WALLET_BTC_KB_TO_SAT_B constant (Jon Atack)
6112cf20d43b0be34fe0edce2ac3e6b27cae1bbe wallet: add CFeeRate ctor doxygen documentation (Jon Atack)
3f7279161347543ce4e997d78ea89a4043491145 wallet: fix bug in RPC send options (Jon Atack)

Pull request description:

  This PR builds on #11413 and #20220 to address #19543.

  - replace overloading the conf_target and estimate_mode params with `fee_rate` in sat/vB in the sendtoaddress, sendmany, send, fundrawtransaction, walletcreatefundedpsbt, and bumpfee RPCs

  - allow non-actionable conf_target value of `0` and estimate_mode value of `""` to be passed to use `fee_rate` as a positional argument, in addition to as a named argument

  - fix a bug in the experimental send RPC described in https://github.com/bitcoin/bitcoin/pull/20220#discussion_r513789526 where args were not being passed correctly into the options values

  - update the feerate error message units for these RPCs from BTC/kB to sat/vB

  - update the test coverage, help docs, doxygen docs, and some of the RPC examples

  - other changes to address the excellent review feedback

  See this wallet meeting log for more context: http://www.erisian.com.au/bitcoin-core-dev/log-2020-11-06.html#l-309

ACKs for top commit:
  achow101:
    re-ACK 05e82d8
  MarcoFalke:
    review ACK 05e82d86b0 did not test and found a few style nits, which can be fixed later 🍯
  Xekyo:
    tACK 05e82d86b09d914ebce05dbc92a7299cb026847b
  Sjors:
    utACK 05e82d86b09d914ebce05dbc92a7299cb026847b

Tree-SHA512: a4ee5f184ada53f1840b2923d25873bda88c5a2ae48e67eeea2417a0b35154798cfdb3c147b05dd56bd6608a784e1b91623bb985ee2ab9ef2baaec22206d0a9c
This commit is contained in:
MarcoFalke 2020-11-17 13:49:04 +01:00 committed by Konstantin Akimov
parent 0fa19226cb
commit f436c20bc4
No known key found for this signature in database
GPG Key ID: 2176C4A5D01EA524
13 changed files with 386 additions and 341 deletions

View File

@ -19,8 +19,8 @@ enum class FeeEstimateMode {
UNSET, //!< Use default settings based on other criteria UNSET, //!< Use default settings based on other criteria
ECONOMICAL, //!< Force estimateSmartFee to use non-conservative estimates ECONOMICAL, //!< Force estimateSmartFee to use non-conservative estimates
CONSERVATIVE, //!< Force estimateSmartFee to use conservative estimates CONSERVATIVE, //!< Force estimateSmartFee to use conservative estimates
DASH_KB, //!< Use explicit DASH/kB fee given in coin control DASH_KB, //!< Use DASH/kB fee rate unit
DUFF_B, //!< Use explicit duff/B fee given in coin control DUFF_B, //!< Use duff/B fee rate unit
}; };
/** /**
@ -39,7 +39,12 @@ public:
// We've previously had bugs creep in from silent double->int conversion... // We've previously had bugs creep in from silent double->int conversion...
static_assert(std::is_integral<I>::value, "CFeeRate should be used without floats"); static_assert(std::is_integral<I>::value, "CFeeRate should be used without floats");
} }
/** Constructor for a fee rate in satoshis per kB. The size in bytes must not exceed (2^63 - 1)*/ /** Constructor for a fee rate in satoshis per kB (duff/kB).
*
* Passing a num_bytes value of COIN (1e8) returns a fee rate in satoshis per B (sat/B),
* e.g. (nFeePaid * 1e8 / 1e3) == (nFeePaid / 1e5),
* where 1e5 is the ratio to convert from DASH/kB to sat/B.
*/
CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes); CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes);
/** /**
* Return the fee in satoshis for the given size in bytes. * Return the fee in satoshis for the given size in bytes.

View File

@ -44,7 +44,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendtoaddress", 6, "use_cj" }, { "sendtoaddress", 6, "use_cj" },
{ "sendtoaddress", 7, "conf_target" }, { "sendtoaddress", 7, "conf_target" },
{ "sendtoaddress", 9, "avoid_reuse" }, { "sendtoaddress", 9, "avoid_reuse" },
{ "sendtoaddress", 10, "verbose"}, { "sendtoaddress", 10, "fee_rate"},
{ "sendtoaddress", 11, "verbose"},
{ "settxfee", 0, "amount" }, { "settxfee", 0, "amount" },
{ "sethdseed", 0, "newkeypool" }, { "sethdseed", 0, "newkeypool" },
{ "getreceivedbyaddress", 1, "minconf" }, { "getreceivedbyaddress", 1, "minconf" },
@ -91,7 +92,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendmany", 6, "use_is" }, { "sendmany", 6, "use_is" },
{ "sendmany", 7, "use_cj" }, { "sendmany", 7, "use_cj" },
{ "sendmany", 8, "conf_target" }, { "sendmany", 8, "conf_target" },
{ "sendmany", 10, "verbose" }, { "sendmany", 10, "fee_rate" },
{ "sendmany", 11, "verbose" },
{ "deriveaddresses", 1, "range" }, { "deriveaddresses", 1, "range" },
{ "scantxoutset", 1, "scanobjects" }, { "scantxoutset", 1, "scanobjects" },
{ "addmultisigaddress", 0, "nrequired" }, { "addmultisigaddress", 0, "nrequired" },
@ -152,7 +154,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "lockunspent", 1, "transactions" }, { "lockunspent", 1, "transactions" },
{ "send", 0, "outputs" }, { "send", 0, "outputs" },
{ "send", 1, "conf_target" }, { "send", 1, "conf_target" },
{ "send", 3, "options" }, { "send", 3, "fee_rate"},
{ "send", 4, "options" },
{ "importprivkey", 2, "rescan" }, { "importprivkey", 2, "rescan" },
{ "importelectrumwallet", 1, "index" }, { "importelectrumwallet", 1, "index" },
{ "importaddress", 2, "rescan" }, { "importaddress", 2, "rescan" },

View File

@ -1160,7 +1160,7 @@ static RPCHelpMan estimatesmartfee()
if (!request.params[1].isNull()) { if (!request.params[1].isNull()) {
FeeEstimateMode fee_mode; FeeEstimateMode fee_mode;
if (!FeeModeFromString(request.params[1].get_str(), fee_mode)) { if (!FeeModeFromString(request.params[1].get_str(), fee_mode)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid estimate_mode parameter"); throw JSONRPCError(RPC_INVALID_PARAMETER, InvalidEstimateModeErrorMessage());
} }
if (fee_mode == FeeEstimateMode::ECONOMICAL) conservative = false; if (fee_mode == FeeEstimateMode::ECONOMICAL) conservative = false;
} }

View File

@ -109,6 +109,8 @@ BOOST_AUTO_TEST_CASE(ToStringTest)
CFeeRate feeRate; CFeeRate feeRate;
feeRate = CFeeRate(1); feeRate = CFeeRate(1);
BOOST_CHECK_EQUAL(feeRate.ToString(), "0.00000001 DASH/kB"); BOOST_CHECK_EQUAL(feeRate.ToString(), "0.00000001 DASH/kB");
BOOST_CHECK_EQUAL(feeRate.ToString(FeeEstimateMode::DASH_KB), "0.00000001 DASH/kB");
BOOST_CHECK_EQUAL(feeRate.ToString(FeeEstimateMode::DUFF_B), "0.001 duff/B");
} }
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()

View File

@ -40,8 +40,6 @@ const std::vector<std::pair<std::string, FeeEstimateMode>>& FeeModeMap()
{"unset", FeeEstimateMode::UNSET}, {"unset", FeeEstimateMode::UNSET},
{"economical", FeeEstimateMode::ECONOMICAL}, {"economical", FeeEstimateMode::ECONOMICAL},
{"conservative", FeeEstimateMode::CONSERVATIVE}, {"conservative", FeeEstimateMode::CONSERVATIVE},
{(CURRENCY_UNIT + "/kB"), FeeEstimateMode::DASH_KB},
{(CURRENCY_ATOM + "/B"), FeeEstimateMode::DUFF_B},
}; };
return FEE_MODES; return FEE_MODES;
} }
@ -51,6 +49,11 @@ std::string FeeModes(const std::string& delimiter)
return Join(FeeModeMap(), delimiter, [&](const std::pair<std::string, FeeEstimateMode>& i) { return i.first; }); return Join(FeeModeMap(), delimiter, [&](const std::pair<std::string, FeeEstimateMode>& i) { return i.first; });
} }
const std::string InvalidEstimateModeErrorMessage()
{
return "Invalid estimate_mode parameter, must be one of: \"" + FeeModes("\", \"") + "\"";
}
bool FeeModeFromString(const std::string& mode_string, FeeEstimateMode& fee_estimate_mode) bool FeeModeFromString(const std::string& mode_string, FeeEstimateMode& fee_estimate_mode)
{ {
auto searchkey = ToUpper(mode_string); auto searchkey = ToUpper(mode_string);

View File

@ -13,5 +13,6 @@ enum class FeeReason;
bool FeeModeFromString(const std::string& mode_string, FeeEstimateMode& fee_estimate_mode); bool FeeModeFromString(const std::string& mode_string, FeeEstimateMode& fee_estimate_mode);
std::string StringForFeeReason(FeeReason reason); std::string StringForFeeReason(FeeReason reason);
std::string FeeModes(const std::string& delimiter); std::string FeeModes(const std::string& delimiter);
const std::string InvalidEstimateModeErrorMessage();
#endif // BITCOIN_UTIL_FEES_H #endif // BITCOIN_UTIL_FEES_H

View File

@ -52,8 +52,6 @@ using interfaces::FoundBlock;
static const std::string WALLET_ENDPOINT_BASE = "/wallet/"; static const std::string WALLET_ENDPOINT_BASE = "/wallet/";
static const uint32_t WALLET_DASH_KB_TO_DUFF_B = COIN / 1000; // 1 duff / B = 0.00001 DASH / kB
static inline bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param) { static inline bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param) {
bool can_avoid_reuse = wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); bool can_avoid_reuse = wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
bool avoid_reuse = param.isNull() ? can_avoid_reuse : param.get_bool(); bool avoid_reuse = param.isNull() ? can_avoid_reuse : param.get_bool();
@ -205,34 +203,42 @@ static std::string LabelFromValue(const UniValue& value)
/** /**
* Update coin control with fee estimation based on the given parameters * Update coin control with fee estimation based on the given parameters
* *
* @param[in] wallet Wallet reference * @param[in] wallet Wallet reference
* @param[in,out] cc Coin control which is to be updated * @param[in,out] cc Coin control to be updated
* @param[in] estimate_mode String value (e.g. "ECONOMICAL") * @param[in] conf_target UniValue integer; confirmation target in blocks, values between 1 and 1008 are valid per policy/fees.h;
* @param[in] estimate_param Parameter (blocks to confirm, explicit fee rate, etc) * if a fee_rate is present, 0 is allowed here as a no-op positional placeholder
* @throws a JSONRPCError if estimate_mode is unknown, or if estimate_param is missing when required * @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/B;
* if a fee_rate is present, both conf_target and estimate_mode must either be null, or no-op
* @param[in] override_min_fee bool; whether to set fOverrideFeeRate to true to disable minimum fee rate checks and instead
verify only that fee_rate is greater than 0
* @throws a JSONRPCError if conf_target, estimate_mode, or fee_rate contain invalid values or are in conflict
*/ */
static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const UniValue& estimate_mode, const UniValue& estimate_param) static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const UniValue& conf_target, const UniValue& estimate_mode, const UniValue& fee_rate, bool override_min_fee)
{ {
if (!estimate_mode.isNull()) { if (!fee_rate.isNull()) {
if (!FeeModeFromString(estimate_mode.get_str(), cc.m_fee_mode)) { if (!conf_target.isNull() && conf_target.get_int() > 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid estimate_mode parameter"); 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 (!estimate_mode.isNull() && !estimate_mode.get_str().empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
}
CFeeRate fee_rate_in_sat_vb{CFeeRate(AmountFromValue(fee_rate), COIN)};
if (override_min_fee) {
if (fee_rate_in_sat_vb <= CFeeRate(0)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid fee_rate %s (must be greater than 0)", fee_rate_in_sat_vb.ToString(FeeEstimateMode::DUFF_B)));
}
cc.fOverrideFeeRate = true;
}
cc.m_feerate = fee_rate_in_sat_vb;
return;
} }
if (!estimate_mode.isNull() && !FeeModeFromString(estimate_mode.get_str(), cc.m_fee_mode)) {
if (cc.m_fee_mode == FeeEstimateMode::DASH_KB || cc.m_fee_mode == FeeEstimateMode::DUFF_B) { throw JSONRPCError(RPC_INVALID_PARAMETER, InvalidEstimateModeErrorMessage());
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 (!conf_target.isNull()) {
} cc.m_confirm_target = ParseConfirmTarget(conf_target, wallet.chain().estimateMaxBlocks());
CAmount fee_rate = AmountFromValue(estimate_param);
if (cc.m_fee_mode == FeeEstimateMode::DUFF_B) {
fee_rate /= WALLET_DASH_KB_TO_DUFF_B;
}
cc.m_feerate = CFeeRate(fee_rate);
} else if (!estimate_param.isNull()) {
cc.m_confirm_target = ParseConfirmTarget(estimate_param, wallet.chain().estimateMaxBlocks());
} }
} }
@ -426,12 +432,12 @@ static RPCHelpMan sendtoaddress()
"The recipient will receive less amount of Dash than you enter in the amount field."}, "The recipient will receive less amount of Dash than you enter in the amount field."},
{"use_is", RPCArg::Type::BOOL, /* default */ "false", "Deprecated and ignored"}, {"use_is", RPCArg::Type::BOOL, /* default */ "false", "Deprecated and ignored"},
{"use_cj", RPCArg::Type::BOOL, /* default */ "false", "Use CoinJoin funds only"}, {"use_cj", RPCArg::Type::BOOL, /* default */ "false", "Use CoinJoin funds only"},
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + 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" {"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."}, " 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 + "/B."},
{"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra information about the transaction."}, {"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra information about the transaction."},
}, },
{ {
@ -445,15 +451,20 @@ static RPCHelpMan sendtoaddress()
{RPCResult::Type::STR, "fee reason", "The transaction fee reason."} {RPCResult::Type::STR, "fee reason", "The transaction fee reason."}
}, },
}, },
}, },
RPCExamples{ RPCExamples{
HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1") "\nSend 0.1 Dash\n"
+ HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"donation\" \"seans outpost\"") + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1") +
+ HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"\" \"\" true") "\nSend 0.1 Dash with a confirmation target of 6 blocks in economical fee estimate mode using positional arguments\n"
+ HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"\" \"\" false true 0.00002 " + (CURRENCY_UNIT + "/kB")) + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"donation\" \"sean's outpost\" false false false 6 economical") +
+ HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"\" \"\" false true 2 " + (CURRENCY_ATOM + "/B")) "\nSend 0.1 Dash with a fee rate of 1 " + CURRENCY_ATOM + "/B, subtract fee from amount, use CoinJoin funds only, using positional arguments\n"
+ HelpExampleRpc("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\", 0.1, \"donation\", \"seans outpost\"") + HelpExampleCli("sendtoaddress", "\"" + EXAMPLE_ADDRESS[0] + "\" 0.1 \"drinks\" \"room77\" true false true 0 \"\" 1") +
}, "\nSend 0.2 Dash with a confirmation target of 6 blocks in economical fee estimate mode using named arguments\n"
+ HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.2 conf_target=6 estimate_mode=\"economical\"") +
"\nSend 0.5 Dash with a fee rate of 25 " + CURRENCY_ATOM + "/B using named arguments\n"
+ HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=25")
+ HelpExampleCli("-named sendtoaddress", "address=\"" + EXAMPLE_ADDRESS[0] + "\" amount=0.5 fee_rate=25 subtractfeefromamount=false avoid_reuse=true comment=\"2 pizzas\" comment_to=\"jeremy\" verbose=true")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
@ -487,7 +498,7 @@ static RPCHelpMan sendtoaddress()
// We also enable partial spend avoidance if reuse avoidance is set. // We also enable partial spend avoidance if reuse avoidance is set.
coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse; coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse;
SetFeeEstimateMode(*pwallet, coin_control, request.params[8], request.params[7]); SetFeeEstimateMode(*pwallet, coin_control, /* conf_target */ request.params[7], /* estimate_mode */ request.params[8], /* fee_rate */ request.params[10], /* override_min_fee */ false);
EnsureWalletIsUnlocked(*pwallet); EnsureWalletIsUnlocked(*pwallet);
@ -501,7 +512,7 @@ static RPCHelpMan sendtoaddress()
std::vector<CRecipient> recipients; std::vector<CRecipient> recipients;
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients); ParseRecipients(address_amounts, subtractFeeFromAmount, recipients);
bool verbose = request.params[10].isNull() ? false: request.params[10].get_bool(); const bool verbose{request.params[11].isNull() ? false : request.params[11].get_bool()};
return SendMoney(*pwallet, coin_control, recipients, mapValue, verbose); return SendMoney(*pwallet, coin_control, recipients, mapValue, verbose);
}, },
@ -922,10 +933,10 @@ static RPCHelpMan sendmany()
}, },
{"use_is", RPCArg::Type::BOOL, /* default */ "false", "Deprecated and ignored"}, {"use_is", RPCArg::Type::BOOL, /* default */ "false", "Deprecated and ignored"},
{"use_cj", RPCArg::Type::BOOL, /* default */ "false", "Use CoinJoin funds only"}, {"use_cj", RPCArg::Type::BOOL, /* default */ "false", "Use CoinJoin funds only"},
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
{"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/B."},
{"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra infomration about the transaction."}, {"verbose", RPCArg::Type::BOOL, /* default */ "false", "If true, return extra infomration about the transaction."},
}, },
{ {
@ -981,11 +992,11 @@ static RPCHelpMan sendmany()
coin_control.UseCoinJoin(request.params[7].get_bool()); coin_control.UseCoinJoin(request.params[7].get_bool());
} }
SetFeeEstimateMode(*pwallet, coin_control, request.params[9], request.params[8]); SetFeeEstimateMode(*pwallet, coin_control, /* conf_target */ request.params[8], /* estimate_mode */ request.params[9], /* fee_rate */ request.params[10], /* override_min_fee */ false);
std::vector<CRecipient> recipients; std::vector<CRecipient> recipients;
ParseRecipients(sendTo, subtractFeeFromAmount, recipients); ParseRecipients(sendTo, subtractFeeFromAmount, recipients);
bool verbose = request.params[10].isNull() ? false : request.params[10].get_bool(); const bool verbose{request.params[11].isNull() ? false : request.params[11].get_bool()};
return SendMoney(*pwallet, coin_control, recipients, std::move(mapValue), verbose); return SendMoney(*pwallet, coin_control, recipients, std::move(mapValue), verbose);
}, },
@ -3380,7 +3391,7 @@ static RPCHelpMan listunspent()
}; };
} }
void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, const UniValue& options, CCoinControl& coinControl) void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, const UniValue& options, CCoinControl& coinControl, bool override_min_fee)
{ {
// Make sure the results are valid at least up to the most recent block // Make sure the results are valid at least up to the most recent block
// the user could have gotten from another RPC command prior to now // the user could have gotten from another RPC command prior to now
@ -3413,7 +3424,8 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
{"lockUnspents", UniValueType(UniValue::VBOOL)}, {"lockUnspents", UniValueType(UniValue::VBOOL)},
{"lock_unspents", UniValueType(UniValue::VBOOL)}, {"lock_unspents", UniValueType(UniValue::VBOOL)},
{"locktime", UniValueType(UniValue::VNUM)}, {"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)}, {"psbt", UniValueType(UniValue::VBOOL)},
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)}, {"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)}, {"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
@ -3453,21 +3465,27 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
} }
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 + "/B) and feeRate (" + CURRENCY_UNIT + "/kB)");
}
if (options.exists("conf_target")) { 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")) { if (options.exists("estimate_mode")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and feeRate"); 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(FeeEstimateMode::DASH_KB)));
}
coinControl.m_feerate = fee_rate;
coinControl.fOverrideFeeRate = true; coinControl.fOverrideFeeRate = true;
} }
if (options.exists("subtractFeeFromOutputs") || options.exists("subtract_fee_from_outputs") ) if (options.exists("subtractFeeFromOutputs") || options.exists("subtract_fee_from_outputs") )
subtractFeeFromOutputs = (options.exists("subtract_fee_from_outputs") ? options["subtract_fee_from_outputs"] : options["subtractFeeFromOutputs"]).get_array(); subtractFeeFromOutputs = (options.exists("subtract_fee_from_outputs") ? options["subtract_fee_from_outputs"] : options["subtractFeeFromOutputs"]).get_array();
SetFeeEstimateMode(wallet, coinControl, options["estimate_mode"], options["conf_target"]); SetFeeEstimateMode(wallet, coinControl, options["conf_target"], options["estimate_mode"], options["fee_rate"], override_min_fee);
} }
} else { } else {
// if options is null and not a bool // if options is null and not a bool
@ -3526,7 +3544,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" "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."}, "e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
{"lockUnspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"}, {"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 + "/B."},
{"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" {"subtractFeeFromOutputs", RPCArg::Type::ARR, /* default */ "empty array", "The integers.\n"
"The fee will be equally deducted from the amount of each specified output.\n" "The fee will be equally deducted from the amount of each specified output.\n"
"Those recipients will receive less Dash than you enter in their corresponding amount field.\n" "Those recipients will receive less Dash than you enter in their corresponding amount field.\n"
@ -3535,8 +3554,7 @@ static RPCHelpMan fundrawtransaction()
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."}, {"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
}, },
}, },
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
}, },
@ -3578,7 +3596,7 @@ static RPCHelpMan fundrawtransaction()
CCoinControl coin_control; CCoinControl coin_control;
// Automatically select (additional) coins. Can be overridden by options.add_inputs. // Automatically select (additional) coins. Can be overridden by options.add_inputs.
coin_control.m_add_inputs = true; coin_control.m_add_inputs = true;
FundTransaction(*pwallet, tx, fee, change_position, request.params[1], coin_control); FundTransaction(*pwallet, tx, fee, change_position, request.params[1], coin_control, /* override_min_fee */ true);
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
result.pushKV("hex", EncodeHexTx(CTransaction(tx))); result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
@ -4180,10 +4198,10 @@ static RPCHelpMan send()
}, },
}, },
}, },
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
{"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/B."},
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "", {"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."}, {"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
@ -4193,10 +4211,10 @@ static RPCHelpMan send()
{"add_to_wallet", RPCArg::Type::BOOL, /* default */ "true", "When false, returns a serialized transaction which will not be added to the wallet or broadcast"}, {"add_to_wallet", RPCArg::Type::BOOL, /* default */ "true", "When false, returns a serialized transaction which will not be added to the wallet or broadcast"},
{"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The Dash address to receive the change"}, {"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The Dash address to receive the change"},
{"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"}, {"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
{"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/B."},
{"include_watching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n" {"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" "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."}, "e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
@ -4231,36 +4249,53 @@ static RPCHelpMan send()
} }
}, },
RPCExamples{"" RPCExamples{""
"\nSend with a fee rate of 1 " + CURRENCY_ATOM + "/B\n" "\nSend 0.1 Dash with a confirmation target of 6 blocks in economical fee estimate mode\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 " + CURRENCY_ATOM + "/B\n") + + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 6 economical\n") +
"\nCreate a transaction that should confirm the next block, with a specific input, and return result without adding to wallet or broadcasting to the network\n" "Send 0.2 Dash with a fee rate of 1 " + CURRENCY_ATOM + "/B using positional arguments\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' 0 \"\" 1\n") +
"Send 0.2 Dash with a fee rate of 1 " + CURRENCY_ATOM + "/B using the options argument\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' '{\"fee_rate\": 1}'\n") +
"Send 0.3 Dash with a fee rate of 25 " + CURRENCY_ATOM + "/B using named arguments\n"
+ HelpExampleCli("-named send", "outputs='{\"" + EXAMPLE_ADDRESS[0] + "\": 0.3}' fee_rate=25\n") +
"Create a transaction that should confirm the next block, with a specific input, and return result without adding to wallet or broadcasting to the network\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'") + HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.1}' 1 economical '{\"add_to_wallet\": false, \"inputs\": [{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\", \"vout\":1}]}'")
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
RPCTypeCheck(request.params, { RPCTypeCheck(request.params, {
UniValueType(), // ARR or OBJ, checked later UniValueType(), // outputs (ARR or OBJ, checked later)
UniValue::VNUM, UniValue::VNUM, // conf_target
UniValue::VSTR, UniValue::VSTR, // estimate_mode
UniValue::VOBJ UniValue::VNUM, // fee_rate
}, true UniValue::VOBJ, // options
); }, true
);
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return NullUniValue; if (!pwallet) return NullUniValue;
UniValue options = request.params[3]; UniValue options{request.params[4].isNull() ? UniValue::VOBJ : request.params[4]};
if (options.exists("feeRate") || options.exists("fee_rate") || options.exists("estimate_mode") || options.exists("conf_target")) { if (options.exists("estimate_mode") || options.exists("conf_target")) {
if (!request.params[1].isNull() || !request.params[2].isNull()) { 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 { } else {
options.pushKV("conf_target", request.params[1]); options.pushKV("conf_target", request.params[1]);
options.pushKV("estimate_mode", request.params[2]); 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"))) { if (!options["conf_target"].isNull() && (options["estimate_mode"].isNull() || (options["estimate_mode"].get_str() == "unset"))) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode");
} }
if (options.exists("feeRate")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use fee_rate (" + CURRENCY_ATOM + "/B) instead of feeRate");
}
if (options.exists("changeAddress")) { if (options.exists("changeAddress")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address");
} }
@ -4286,7 +4321,7 @@ static RPCHelpMan send()
// Automatically select coins, unless at least one is manually selected. Can // Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs. // be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0; coin_control.m_add_inputs = rawTx.vin.size() == 0;
FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control); FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control, /* override_min_fee */ false);
bool add_to_wallet = true; bool add_to_wallet = true;
if (options.exists("add_to_wallet")) { if (options.exists("add_to_wallet")) {
@ -4530,6 +4565,7 @@ static RPCHelpMan walletcreatefundedpsbt()
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"}, {"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
{"includeWatching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only"}, {"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"}, {"lockUnspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
{"fee_rate", RPCArg::Type::AMOUNT, /* default */ "not set, fall back to wallet fee estimation", "Specify a fee rate in " + CURRENCY_ATOM + "/B."},
{"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set: makes wallet determine the fee", "Set a specific fee rate in " + CURRENCY_UNIT + "/kB"}, {"feeRate", RPCArg::Type::AMOUNT, /* default */ "not set: makes wallet determine the fee", "Set a specific fee rate in " + CURRENCY_UNIT + "/kB"},
{"subtractFeeFromOutputs", RPCArg::Type::ARR, /* default */ "empty array", "The outputs to subtract the fee from.\n" {"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" "The fee will be equally deducted from the amount of each specified output.\n"
@ -4539,8 +4575,7 @@ static RPCHelpMan walletcreatefundedpsbt()
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."}, {"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
}, },
}, },
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target (in blocks)\n" {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
"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" {"estimate_mode", RPCArg::Type::STR, /* default */ "unset", std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
" \"" + FeeModes("\"\n\"") + "\""}, " \"" + FeeModes("\"\n\"") + "\""},
}, },
@ -4585,7 +4620,7 @@ static RPCHelpMan walletcreatefundedpsbt()
// Automatically select coins, unless at least one is manually selected. Can // Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs. // be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0; coin_control.m_add_inputs = rawTx.vin.size() == 0;
FundTransaction(*pwallet, rawTx, fee, change_position, request.params[3], coin_control); FundTransaction(*pwallet, rawTx, fee, change_position, request.params[3], coin_control, /* override_min_fee */ true);
// Make a blank psbt // Make a blank psbt
PartiallySignedTransaction psbtx{rawTx}; PartiallySignedTransaction psbtx{rawTx};

View File

@ -3555,7 +3555,7 @@ bool CWallet::CreateTransactionInternal(
// Do not, ever, assume that it's fine to change the fee rate if the user has explicitly // Do not, ever, assume that it's fine to change the fee rate if the user has explicitly
// provided one // provided one
if (coin_control.m_feerate && nFeeRateNeeded > *coin_control.m_feerate) { if (coin_control.m_feerate && nFeeRateNeeded > *coin_control.m_feerate) {
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(), nFeeRateNeeded.ToString()); error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), nFeeRateNeeded.ToString(FeeEstimateMode::DUFF_B));
return false; return false;
} }

View File

@ -27,7 +27,7 @@ class EstimateFeeTest(BitcoinTestFramework):
# wrong type for estimatesmartfee(estimate_mode) # wrong type for estimatesmartfee(estimate_mode)
assert_raises_rpc_error(-3, "Expected type string, got number", self.nodes[0].estimatesmartfee, 1, 1) assert_raises_rpc_error(-3, "Expected type string, got number", self.nodes[0].estimatesmartfee, 1, 1)
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", self.nodes[0].estimatesmartfee, 1, 'foo') assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"', self.nodes[0].estimatesmartfee, 1, 'foo')
# wrong type for estimaterawfee(threshold) # wrong type for estimaterawfee(threshold)
assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimaterawfee, 1, 'foo') assert_raises_rpc_error(-3, "Expected type number, got string", self.nodes[0].estimaterawfee, 1, 'foo')

View File

@ -94,7 +94,6 @@ class RawTransactionsTest(BitcoinTestFramework):
self.test_op_return() self.test_op_return()
self.test_watchonly() self.test_watchonly()
self.test_all_watched_funds() self.test_all_watched_funds()
self.test_feerate_with_conf_target_and_estimate_mode()
self.test_option_feerate() self.test_option_feerate()
self.test_address_reuse() self.test_address_reuse()
self.test_option_subtract_fee_from_outputs() self.test_option_subtract_fee_from_outputs()
@ -704,74 +703,86 @@ class RawTransactionsTest(BitcoinTestFramework):
wwatch.unloadwallet() wwatch.unloadwallet()
def test_option_feerate(self): def test_option_feerate(self):
self.log.info("Test fundrawtxn feeRate option") self.log.info("Test fundrawtxn with explicit fee rates (fee_rate duff/B and feeRate DASH/kB)")
# 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")
node = self.nodes[3] node = self.nodes[3]
# Make sure there is exactly one input so coin selection can't skew the result. # 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 = [] inputs = []
outputs = {node.getnewaddress() : 1} outputs = {node.getnewaddress() : 1}
rawtx = node.createrawtransaction(inputs, outputs) rawtx = node.createrawtransaction(inputs, outputs)
for unit, fee_rate in {"dash/kb": 0.1, "duff/b": 10000}.items(): result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
self.log.info("Test fundrawtxn with conf_target {} estimate_mode {} produces expected fee".format(fee_rate, unit)) btc_kvb_to_sat_vb = 100000 # (1e5)
# With no arguments passed, expect fee of 225 sats/b. result1 = node.fundrawtransaction(rawtx, {"fee_rate": 2 * btc_kvb_to_sat_vb * self.min_relay_tx_fee})
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000225, vspan=0.00000001) result2 = node.fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee})
# Expect fee to be 10,000x higher when explicit fee 10,000x greater is specified. result3 = node.fundrawtransaction(rawtx, {"fee_rate": 10 * btc_kvb_to_sat_vb * self.min_relay_tx_fee})
result = node.fundrawtransaction(rawtx, {"conf_target": fee_rate, "estimate_mode": unit}) result4 = node.fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee})
assert_approx(result["fee"], vexp=0.0225, vspan=0.0001) 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": "duff/b"}.items(): # With no arguments passed, expect fee of 225 satoshis.
self.log.info("Test fundrawtxn raises RPC error if both feeRate and {} are passed".format(field)) assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000225, vspan=0.00000001)
assert_raises_rpc_error( # Expect fee to be 10,000x higher when an explicit fee rate 10,000x greater is specified.
-8, "Cannot specify both {} and feeRate".format(field), result = node.fundrawtransaction(rawtx, {"fee_rate": 10000})
lambda: node.fundrawtransaction(rawtx, {"feeRate": 0.1, field: fee_rate})) assert_approx(result["fee"], vexp=0.0225, vspan=0.0001)
self.log.info("Test fundrawtxn with invalid estimate_mode settings") self.log.info("Test fundrawtxn with invalid estimate_mode settings")
for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), 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})) node.fundrawtransaction, rawtx, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})
for mode in ["foo", Decimal("3.141592")]: for mode in ["", "foo", Decimal("3.141592")]:
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"',
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") self.log.info("Test fundrawtxn with invalid conf_target settings")
for mode in ["unset", "economical", "conservative", "dash/kb", "duff/b"]: for mode in ["unset", "economical", "conservative"]:
self.log.debug("{}".format(mode)) self.log.debug("{}".format(mode))
for k, v in {"string": "", "object": {"foo": "bar"}}.items(): for k, v in {"string": "", "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), 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})) node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
if mode in ["dash/kb", "duff/b"]: for n in [-1, 0, 1009]:
assert_raises_rpc_error(-3, "Amount out of range", assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": mode, "conf_target": -1})) node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
assert_raises_rpc_error(-4, "Fee rate (0.00000000 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/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 {"duff/B": 0.99999999, "DASH/kB": 0.00000999}.items(): self.log.info("Test invalid fee rate settings")
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(-8, "Invalid fee_rate 0.000 duff/B (must be greater than 0)",
assert_raises_rpc_error(-4, "Fee rate (0.00000999 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/kB)", node.fundrawtransaction, rawtx, {"fee_rate": 0, "add_inputs": True})
lambda: self.nodes[1].fundrawtransaction(rawtx, {"estimate_mode": unit, "conf_target": fee_rate, "add_inputs": True})) assert_raises_rpc_error(-8, "Invalid feeRate 0.00000000 DASH/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})
self.log.info("Test min fee rate checks are bypassed with fundrawtxn, e.g. a fee_rate under 1 sat/vB is allowed")
node.fundrawtransaction(rawtx, {"fee_rate": 0.99999999, "add_inputs": True})
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 (duff/B) and feeRate (DASH/kB)",
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): def test_address_reuse(self):
"""Test no address reuse occurs.""" """Test no address reuse occurs."""
@ -799,12 +810,32 @@ class RawTransactionsTest(BitcoinTestFramework):
outputs = {self.nodes[2].getnewaddress(): 1} outputs = {self.nodes[2].getnewaddress(): 1}
rawtx = self.nodes[3].createrawtransaction(inputs, outputs) 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) 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": []}), # empty subtraction list
self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee) 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}),
self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee, "subtractFeeFromOutputs": [0]}),] 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] 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)] 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)] change = [d['vout'][r['changepos']]['value'] for d, r in zip(dec_tx, result)]

View File

@ -108,60 +108,74 @@ class PSBTTest(BitcoinTestFramework):
assert_equal(walletprocesspsbt_out['complete'], True) assert_equal(walletprocesspsbt_out['complete'], True)
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex']) self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
self.log.info("Test walletcreatefundedpsbt feeRate of 0.1 DASH/kB produces a total fee at or slightly below -maxtxfee (~0.06650000)") 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)")
res = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True}) res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True})
assert_approx(res["fee"], 0.04, 0.03) assert_approx(res1["fee"], 0.04, 0.005)
res2 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, "add_inputs": True})
assert_approx(res2["fee"], 0.04, 0.005)
self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed, e.g. a fee_rate under 1 sat/vB is allowed")
res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 0.99999999, "add_inputs": True})
assert_approx(res3["fee"], 0.00000224, 0.0000001)
res4 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.00000999, "add_inputs": True})
assert_approx(res4["fee"], 0.00000224, 0.0000001)
self.log.info("Test walletcreatefundedpsbt explicit fee rate with conf_target and estimate_mode") self.log.info("Test invalid fee rate settings")
for unit, fee_rate in {"dash/kb": 0.1, "duff/b": 10000}.items(): assert_raises_rpc_error(-8, "Invalid fee_rate 0.000 duff/B (must be greater than 0)",
fee = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"conf_target": fee_rate, "estimate_mode": unit, "add_inputs": True})["fee"] self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0, "add_inputs": True})
self.log.info("- conf_target {}, estimate_mode {} produces fee {} at or slightly below -maxtxfee (~0.05290000)".format(fee_rate, unit, fee)) assert_raises_rpc_error(-8, "Invalid feeRate 0.00000000 DASH/kB (must be greater than 0)",
assert_approx(fee, vexp=0.04, vspan=0.03) 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})
for field, fee_rate in {"conf_target": 0.1, "estimate_mode": "duff/b"}.items(): self.log.info("- raises RPC error if both feeRate and fee_rate are passed")
self.log.info("- raises RPC error if both feeRate and {} are passed".format(field)) assert_raises_rpc_error(-8, "Cannot specify both fee_rate (duff/B) and feeRate (DASH/kB)",
assert_raises_rpc_error(-8, "Cannot specify both {} and feeRate".format(field), self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": 0.1, "feeRate": 0.1, "add_inputs": True})
lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.1, field: fee_rate, "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") self.log.info("- raises RPC error with invalid estimate_mode settings")
for k, v in {"number": 42, "object": {"foo": "bar"}}.items(): for k, v in {"number": 42, "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type string for estimate_mode, got {}".format(k), 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})) self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": v, "conf_target": 0.1, "add_inputs": True})
for mode in ["foo", Decimal("3.141592")]: for mode in ["", "foo", Decimal("3.141592")]:
assert_raises_rpc_error(-8, "Invalid estimate_mode parameter", assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"',
lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": 0.1, "add_inputs": True})) 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 ["DUFF/B", "DASH/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.log.info("- raises RPC error with invalid conf_target settings") self.log.info("- raises RPC error with invalid conf_target settings")
for mode in ["unset", "economical", "conservative", "dash/kb", "duff/b"]: for mode in ["unset", "economical", "conservative"]:
self.log.debug("{}".format(mode)) self.log.debug("{}".format(mode))
for k, v in {"string": "", "object": {"foo": "bar"}}.items(): for k, v in {"string": "", "object": {"foo": "bar"}}.items():
assert_raises_rpc_error(-3, "Expected type number for conf_target, got {}".format(k), 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})) self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
if mode in ["dash/kb", "duff/b"]: for n in [-1, 0, 1009]:
assert_raises_rpc_error(-3, "Amount out of range", assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
lambda: self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"estimate_mode": mode, "conf_target": -1, "add_inputs": True})) self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
assert_raises_rpc_error(-4, "Fee rate (0.00000000 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/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 {"DUFF/B": 0.99999999, "DASH/KB": 0.00000999}.items(): self.log.info("Test walletcreatefundedpsbt with too-high fee rate produces total fee well above -maxtxfee and raises RPC error")
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 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/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 DASH/kB produces total fee well above -maxtxfee and raises RPC error")
# previously this was silently capped at -maxtxfee # previously this was silently capped at -maxtxfee
for bool_add, outputs_array in {True: outputs, False: [{self.nodes[1].getnewaddress(): 1}]}.items(): 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)", msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 10, "add_inputs": bool_add}) 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") self.log.info("Test various PSBT operations")
# partially sign multisig things with node 1 # partially sign multisig things with node 1

View File

@ -4,6 +4,7 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the wallet.""" """Test the wallet."""
from decimal import Decimal from decimal import Decimal
from itertools import product
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
@ -16,6 +17,8 @@ from test_framework.util import (
) )
from test_framework.wallet_util import test_address from test_framework.wallet_util import test_address
OUT_OF_RANGE = "Amount out of range"
class WalletTest(BitcoinTestFramework): class WalletTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
@ -50,6 +53,9 @@ class WalletTest(BitcoinTestFramework):
assert_fee_amount(fee, tx_size, fee_per_byte * 1000) assert_fee_amount(fee, tx_size, fee_per_byte * 1000)
return curr_balance return curr_balance
def get_vsize(self, txn):
return self.nodes[0].decoderawtransaction(txn)['size']
def run_test(self): def run_test(self):
# Check that there's no UTXO on none of the nodes # Check that there's no UTXO on none of the nodes
@ -79,7 +85,7 @@ class WalletTest(BitcoinTestFramework):
assert_equal(len(self.nodes[1].listunspent()), 1) assert_equal(len(self.nodes[1].listunspent()), 1)
assert_equal(len(self.nodes[2].listunspent()), 0) 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"] confirmed_txid, confirmed_index = utxos[0]["txid"], utxos[0]["vout"]
# First, outputs that are unspent both in the chain and in the # First, outputs that are unspent both in the chain and in the
# mempool should appear with or without include_mempool # mempool should appear with or without include_mempool
@ -93,7 +99,7 @@ class WalletTest(BitcoinTestFramework):
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 110) self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 110)
mempool_txid = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 100) mempool_txid = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 100)
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 # utxo spent in mempool should be visible if you exclude mempool
# but invisible if you include mempool # but invisible if you include mempool
txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False) txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False)
@ -239,67 +245,45 @@ class WalletTest(BitcoinTestFramework):
assert_equal(self.nodes[2].getbalance(), node_2_bal) assert_equal(self.nodes[2].getbalance(), node_2_bal)
node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('100'), fee_per_byte, count_bytes(self.nodes[2].gettransaction(txid)['hex'])) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('100'), fee_per_byte, count_bytes(self.nodes[2].gettransaction(txid)['hex']))
self.start_node(3, self.nodes[3].extra_args) self.log.info("Test sendmany with fee_rate param (explicit fee rate in duff/B)")
self.connect_nodes(0, 3) fee_rate_sat_vb = 2
self.log.info("Test case-insensitive explicit fee rate (sendmany as DASH/kB)") fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
# Throw if no conf_target provided explicit_fee_rate_btc_kvb = Decimal(fee_rate_btc_kvb) / 1000
assert_raises_rpc_error(-8, "Selected estimate_mode dash/kB requires a fee rate to be specified in conf_target",
self.nodes[2].sendmany,
amounts={ address: 10 },
estimate_mode='dash/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='dash/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='dash/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, count_bytes(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 case-insensitive explicit fee rate (sendmany as duff/B)") # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
# Throw if no conf_target provided txid = self.nodes[2].sendmany(amounts={address: 10}, conf_target=0, estimate_mode="", fee_rate=fee_rate_sat_vb)
assert_raises_rpc_error(-8, "Selected estimate_mode duff/b requires a fee rate to be specified in conf_target",
self.nodes[2].sendmany,
amounts={ address: 10 },
estimate_mode='duff/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='duff/b')
fee_duff_per_b = 2
fee_per_kb = fee_duff_per_b / 100000.0
explicit_fee_per_byte = Decimal(fee_per_kb) / 1000
txid = self.nodes[2].sendmany(
amounts={ address: 10 },
conf_target=fee_duff_per_b,
estimate_mode='duff/b',
)
self.nodes[2].generate(1) self.nodes[2].generate(1)
self.sync_all(self.nodes[0:3]) self.sync_all(self.nodes[0:3])
balance = self.nodes[2].getbalance() balance = self.nodes[2].getbalance()
node_2_bal = self.check_fee_amount(balance, node_2_bal - Decimal('10'), explicit_fee_per_byte, count_bytes(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) assert_equal(balance, node_2_bal)
node_0_bal += Decimal('10') node_0_bal += Decimal('10')
assert_equal(self.nodes[0].getbalance(), node_0_bal) 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. # Test setting explicit fee rate just below the minimum.
for unit, fee_rate in {"DASH/kB": 0.00000999, "duff/B": 0.99999999}.items(): self.log.info("Test sendmany raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
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.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
assert_raises_rpc_error(-6, "Fee rate (0.00000999 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/kB)", self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.99999999)
self.nodes[2].sendmany, amounts={address: 10}, estimate_mode=unit, conf_target=fee_rate)
self.log.info("Test sendmany raises if fee_rate of 0 or -1 is passed")
assert_raises_rpc_error(-6, "Fee rate (0.000 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
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, must be one of: "unset", "economical", "conservative"',
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)
self.sync_all()
# check if we can list zero value tx as available coins # check if we can list zero value tx as available coins
# 1. create raw_tx # 1. create raw_tx
@ -328,7 +312,7 @@ class WalletTest(BitcoinTestFramework):
assert_equal(uTx['amount'], Decimal('0')) assert_equal(uTx['amount'], Decimal('0'))
assert found assert found
# do some -walletbroadcast tests self.log.info("Test -walletbroadcast")
self.stop_nodes() self.stop_nodes()
self.start_node(0, ["-walletbroadcast=0"]) self.start_node(0, ["-walletbroadcast=0"])
self.start_node(1, ["-walletbroadcast=0"]) self.start_node(1, ["-walletbroadcast=0"])
@ -388,7 +372,7 @@ class WalletTest(BitcoinTestFramework):
# General checks for errors from incorrect inputs # General checks for errors from incorrect inputs
# This will raise an exception because the amount is negative # 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 # 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") assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].sendtoaddress, self.nodes[2].getnewaddress(), "1f-4")
@ -430,78 +414,43 @@ class WalletTest(BitcoinTestFramework):
self.nodes[0].generate(1) self.nodes[0].generate(1)
self.sync_all(self.nodes[0:3]) self.sync_all(self.nodes[0:3])
self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as DASH/kB)") self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in sat/vB)")
self.nodes[0].generate(1)
self.sync_all(self.nodes[0:3])
prebalance = self.nodes[2].getbalance() prebalance = self.nodes[2].getbalance()
assert prebalance > 2 assert prebalance > 2
address = self.nodes[1].getnewaddress() address = self.nodes[1].getnewaddress()
# Throw if no conf_target provided amount = 3
assert_raises_rpc_error(-8, "Selected estimate_mode dash/Kb requires a fee rate to be specified in conf_target", fee_rate_sat_vb = 2
self.nodes[2].sendtoaddress, fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
address=address,
amount=1.0, # Passing conf_target 0, estimate_mode "" as placeholder arguments should allow fee_rate to apply.
estimate_mode='dash/Kb') txid = self.nodes[2].sendtoaddress(address=address, amount=amount, conf_target=0, estimate_mode="", fee_rate=fee_rate_sat_vb)
# Throw if negative feerate tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])
assert_raises_rpc_error(-3, "Amount out of range",
self.nodes[2].sendtoaddress,
address=address,
amount=1.0,
conf_target=-1,
estimate_mode='dash/kb')
txid = self.nodes[2].sendtoaddress(
address=address,
amount=1.0,
conf_target=0.00002500,
estimate_mode='dash/kb',
)
tx_size = count_bytes(self.nodes[2].gettransaction(txid)['hex'])
self.sync_all(self.nodes[0:3])
self.nodes[0].generate(1) self.nodes[0].generate(1)
self.sync_all(self.nodes[0:3]) self.sync_all(self.nodes[0:3])
postbalance = self.nodes[2].getbalance() postbalance = self.nodes[2].getbalance()
fee = prebalance - postbalance - Decimal('1') fee = prebalance - postbalance - Decimal(amount)
assert_fee_amount(fee, tx_size, Decimal('0.00002500')) assert_fee_amount(fee, tx_size, Decimal(fee_rate_btc_kvb))
self.sync_all(self.nodes[0:3]) 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)
self.log.info("Test case-insensitive explicit fee rate (sendtoaddress as duff/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 duff/b requires a fee rate to be specified in conf_target",
self.nodes[2].sendtoaddress,
address=address,
amount=1.0,
estimate_mode='duff/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='duff/b')
txid = self.nodes[2].sendtoaddress(
address=address,
amount=1.0,
conf_target=2,
estimate_mode='duff/B',
)
tx_size = count_bytes(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'))
# Test setting explicit fee rate just below the minimum. # Test setting explicit fee rate just below the minimum.
for unit, fee_rate in {"DASH/kB": 0.00000999, "sat/B": 0.99999999}.items(): self.log.info("Test sendtoaddress raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
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.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
assert_raises_rpc_error(-6, "Fee rate (0.00000999 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/kB)", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.999)
self.nodes[2].sendtoaddress, address=address, amount=1, estimate_mode=unit, conf_target=fee_rate)
self.log.info("Test sendtoaddress raises if fee_rate of 0 or -1 is passed")
assert_raises_rpc_error(-6, "Fee rate (0.000 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
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], ["dash/kb", "duff/b"]):
assert_raises_rpc_error(-8, 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"',
self.nodes[2].sendtoaddress, address=address, amount=1, conf_target=target, estimate_mode=mode)
# 2. Import address from node2 to node1 # 2. Import address from node2 to node1
self.nodes[1].importaddress(address_to_import) self.nodes[1].importaddress(address_to_import)
@ -559,7 +508,7 @@ class WalletTest(BitcoinTestFramework):
] ]
chainlimit = 6 chainlimit = 6
for m in maintenance: for m in maintenance:
self.log.info("check " + m) self.log.info("Test " + m)
self.stop_nodes() self.stop_nodes()
# set lower ancestor limit for later # set lower ancestor limit for later
self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)]) self.start_node(0, [m, "-limitancestorcount=" + str(chainlimit)])

View File

@ -5,6 +5,8 @@
"""Test the send RPC command.""" """Test the send RPC command."""
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
from itertools import product
from test_framework.authproxy import JSONRPCException from test_framework.authproxy import JSONRPCException
from test_framework.descriptors import descsum_create from test_framework.descriptors import descsum_create
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
@ -12,7 +14,7 @@ from test_framework.util import (
assert_equal, assert_equal,
assert_fee_amount, assert_fee_amount,
assert_greater_than, assert_greater_than,
assert_raises_rpc_error assert_raises_rpc_error,
) )
class WalletSendTest(BitcoinTestFramework): class WalletSendTest(BitcoinTestFramework):
@ -29,8 +31,8 @@ class WalletSendTest(BitcoinTestFramework):
self.skip_if_no_wallet() self.skip_if_no_wallet()
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
arg_conf_target=None, arg_estimate_mode=None, arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None,
conf_target=None, estimate_mode=None, add_to_wallet=None, psbt=None, conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None,
inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None,
include_watching=None, locktime=None, lock_unspents=None, subtract_fee_from_outputs=None, include_watching=None, locktime=None, lock_unspents=None, subtract_fee_from_outputs=None,
expect_error=None): expect_error=None):
@ -66,6 +68,8 @@ class WalletSendTest(BitcoinTestFramework):
options["conf_target"] = conf_target options["conf_target"] = conf_target
if estimate_mode is not None: if estimate_mode is not None:
options["estimate_mode"] = estimate_mode options["estimate_mode"] = estimate_mode
if fee_rate is not None:
options["fee_rate"] = fee_rate
if inputs is not None: if inputs is not None:
options["inputs"] = inputs options["inputs"] = inputs
if add_inputs is not None: if add_inputs is not None:
@ -89,18 +93,19 @@ class WalletSendTest(BitcoinTestFramework):
options = None options = None
if expect_error is 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: else:
try: try:
assert_raises_rpc_error(expect_error[0], expect_error[1], from_wallet.send, 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: except AssertionError:
# Provide debug info if the test fails # Provide debug info if the test fails
self.log.error("Unexpected successful result:") self.log.error("Unexpected successful result:")
self.log.error(arg_conf_target) self.log.error(arg_conf_target)
self.log.error(arg_estimate_mode) self.log.error(arg_estimate_mode)
self.log.error(arg_fee_rate)
self.log.error(options) 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) self.log.error(res)
if "txid" in res and add_to_wallet: if "txid" in res and add_to_wallet:
self.log.error("Transaction details:") self.log.error("Transaction details:")
@ -273,10 +278,10 @@ class WalletSendTest(BitcoinTestFramework):
assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"], assert_equal(self.nodes[1].decodepsbt(res1["psbt"])["fee"],
self.nodes[1].decodepsbt(res2["psbt"])["fee"]) self.nodes[1].decodepsbt(res2["psbt"])["fee"])
# but not at the same time # but not at the same time
for mode in ["unset", "economical", "conservative", "dash/kb", "duff/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", 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, 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...") 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) res = self.test_send(from_wallet=w3, to_wallet=w1, amount=1)
@ -302,60 +307,57 @@ class WalletSendTest(BitcoinTestFramework):
assert res["complete"] assert res["complete"]
self.log.info("Test setting explicit fee rate") 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) 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, conf_target=1, estimate_mode="economical", 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"]) 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="dash/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"] fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00007")) 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="duff/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"] fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00002")) 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="dash/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"] fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00004531")) 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="duff/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"] fee = self.nodes[1].decodepsbt(res["psbt"])["fee"]
assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003")) assert_fee_amount(fee, Decimal(len(res["hex"]) / 2), Decimal("0.00003"))
# TODO: This test should pass with all modes, e.g. with the next line uncommented, for consistency with the other explicit feerate RPCs. # Test that passing fee_rate as both an argument and an option raises.
# for mode in ["unset", "economical", "conservative", "dash/kb", "duff/b"]: self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=1, fee_rate=1, add_to_wallet=False,
for mode in ["dash/kb", "duff/b"]: expect_error=(-8, "Pass the fee_rate either as an argument, or in the options object, but not both"))
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 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/kB)"))
for mode in ["foo", Decimal("3.141592")]: assert_raises_rpc_error(-8, "Use fee_rate (duff/B) instead of feeRate", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"feeRate": 0.01})
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"))
# TODO: these 2 equivalent sends with an invalid estimate_mode arg should both fail, but they do not...why?
# 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))
# TODO: These tests should pass for consistency with the other explicit feerate RPCs, but they do not. assert_raises_rpc_error(-3, "Unexpected key totalFee", w0.send, {w1.getnewaddress(): 1}, 6, "conservative", 1, {"totalFee": 0.01})
# for mode in ["unset", "economical", "conservative", "dash/kb", "duff/b"]:
# self.log.debug("{}".format(mode)) for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):
# for k, v in {"string": "", "object": {"foo": "bar"}}.items(): self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode,
# self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=v, estimate_mode=mode, expect_error=(-8, "Invalid conf_target, must be between 1 and 1008")) # max value of 1008 per src/policy/fees.h
# expect_error=(-3, "Expected type number for conf_target, got {}".format(k))) msg = 'Invalid estimate_mode parameter, must be one of: "unset", "economical", "conservative"'
for target, mode in product([-1, 0], ["dash/kb", "dufff/b"]):
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=target, estimate_mode=mode, expect_error=(-8, msg))
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, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=0.1, arg_estimate_mode=mode, expect_error=(-8, msg))
assert_raises_rpc_error(-8, msg, w0.send, {w1.getnewaddress(): 1}, 0.1, mode)
for mode in ["economical", "conservative", "dash/kb", "duff/b"]:
self.log.debug("{}".format(mode))
for k, v in {"string": "true", "object": {"foo": "bar"}}.items():
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 duff/B instead of DASH/kB if duff/B is selected.
# Test setting explicit fee rate just below the minimum. # Test setting explicit fee rate just below the minimum.
for unit, fee_rate in {"duff/B": 0.99999999, "DASH/kB": 0.00000999}.items(): self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if fee_rate of 0.99999999 is passed")
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, fee_rate=0.99999999,
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=fee_rate, estimate_mode=unit, expect_error=(-4, "Fee rate (0.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)"))
expect_error=(-4, "Fee rate (0.00000999 DASH/kB) is lower than the minimum fee rate setting (0.00001000 DASH/kB)"))
self.log.info("Explicit fee rate raises RPC error if estimate_mode is passed without a conf_target")
for unit, fee_rate in {"duff/B": 100, "DASH/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)))
# TODO: Return hex if fee rate is below -maxmempool # 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="duff/b", add_to_wallet=False) # res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=0.1, estimate_mode="duff/b", add_to_wallet=False)