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