Merge #6245: backport: bitcoin#18275, #20220, #20305, #20410, #20426, #20573, #21083, #21201, #21786, #21787 (fee backports)

d6946aaba1 fix: offset fee for 1 duff in commission in wallet_basic.py due to missing bitcoin/bitcoin#22949 (Konstantin Akimov)
3ba99b9c42 Merge bitcoin/bitcoin#21786: wallet: ensure sat/vB feerates are in range (mantissa of 3) (MarcoFalke)
22435f1898 Merge bitcoin/bitcoin#21787: test: fix off-by-ones in rpc_fundrawtransaction assertions (W. J. van der Laan)
ccac35c89c Merge #21083: wallet: Avoid requesting fee rates multiple times during coin selection (Samuel Dobson)
9e9975f83b Merge #21201: rpc: Disallow sendtoaddress and sendmany when private keys disabled (Samuel Dobson)
5ad8a489a5 Merge #20573: wallet, bugfix: allow send with string fee_rate amounts (MarcoFalke)
db4a2169bb Merge #20410: wallet: Do not treat default constructed types as None-type (MarcoFalke)
01e41aa1fb Merge #20426: wallet: allow zero-fee fundrawtransaction/walletcreatefundedpsbt and other fixes (MarcoFalke)
f436c20bc4 Merge #20305: wallet: introduce fee_rate sat/vB param/option (MarcoFalke)
0fa19226cb Merge #20220: wallet, rpc: explicit fee rate follow-ups/fixes for 0.21 (Samuel Dobson)
d530b73016 Merge #18275: wallet: error if an explicit fee rate was given but the needed fee rate differed (MarcoFalke)

Pull request description:

  ## Issue being fixed or feature implemented
  Just regular backports from bitcoin v0.21, v22; mostly wallet+fee related

  ## What was done?
   - bitcoin/bitcoin#18275
   - bitcoin/bitcoin#20220
   - bitcoin/bitcoin#20305
   - bitcoin/bitcoin#20426
   - bitcoin/bitcoin#20410
   - bitcoin/bitcoin#20573
   - bitcoin/bitcoin#21201
   - bitcoin/bitcoin#21083
   - bitcoin/bitcoin#21787

  ## How Has This Been Tested?
  Run unit and functional tests

  ## Breaking Changes
  Some wallet rpc (sendtoaddress, sendmany, send) have a new argument `fee_rate` which is inserted before `verbose`.
  Release notes will be provided in a new PR once scope of backports and fixes in this PR is finalized by merging it to develop/

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [ ] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation
  - [x] I have assigned this pull request to a milestone

ACKs for top commit:
  UdjinM6:
    utACK d6946aaba1
  PastaPastaPasta:
    utACK d6946aaba1

Tree-SHA512: 8826e1453fe84e3d21f789fab62c23ea13299ce13a7bf1132f70831c3255e823437b6ddd63f69a8e8a0dae95a2638a4454d727e91177b53a5d331872528b92e8
This commit is contained in:
pasta 2024-09-25 10:21:29 -05:00
commit 4e72902474
No known key found for this signature in database
GPG Key ID: 52527BEDABE87984
21 changed files with 610 additions and 263 deletions

View File

@ -48,7 +48,10 @@ static void CoinSelection(benchmark::Bench& bench)
coins.emplace_back(wtx.get(), 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); coins.emplace_back(wtx.get(), 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */);
} }
const CoinEligibilityFilter filter_standard(1, 6, 0); const CoinEligibilityFilter filter_standard(1, 6, 0);
const CoinSelectionParams coin_selection_params(true, 34, 148, CFeeRate(0), 0, false); const CoinSelectionParams coin_selection_params(/* use_bnb= */ true, /* change_output_size= */ 34,
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
bench.run([&] { bench.run([&] {
std::set<CInputCoin> setCoinsRet; std::set<CInputCoin> setCoinsRet;
CAmount nValueRet; CAmount nValueRet;

View File

@ -116,7 +116,7 @@ CTransactionBuilder::CTransactionBuilder(std::shared_ptr<CWallet> pwalletIn, con
// Generate a feerate which will be used to consider if the remainder is dust and will go into fees or not // Generate a feerate which will be used to consider if the remainder is dust and will go into fees or not
coinControl.m_discard_feerate = ::GetDiscardRate(*pwallet); coinControl.m_discard_feerate = ::GetDiscardRate(*pwallet);
// Generate a feerate which will be used by calculations of this class and also by CWallet::CreateTransaction // Generate a feerate which will be used by calculations of this class and also by CWallet::CreateTransaction
coinControl.m_feerate = std::max(pwallet->chain().estimateSmartFee(int(pwallet->m_confirm_target), true, nullptr), pwallet->m_pay_tx_fee); coinControl.m_feerate = std::max(GetRequiredFeeRate(*pwallet), pwallet->m_pay_tx_fee);
// Change always goes back to origin // Change always goes back to origin
coinControl.destChange = tallyItemIn.txdest; coinControl.destChange = tallyItemIn.txdest;
// Only allow tallyItems inputs for tx creation // Only allow tallyItems inputs for tx creation

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

@ -72,12 +72,12 @@ void RPCTypeCheckObj(const UniValue& o,
} }
} }
CAmount AmountFromValue(const UniValue& value) CAmount AmountFromValue(const UniValue& value, int decimals)
{ {
if (!value.isNum() && !value.isStr()) if (!value.isNum() && !value.isStr())
throw JSONRPCError(RPC_TYPE_ERROR, "Amount is not a number or string"); throw JSONRPCError(RPC_TYPE_ERROR, "Amount is not a number or string");
CAmount amount; CAmount amount;
if (!ParseFixedPoint(value.getValStr(), 8, &amount)) if (!ParseFixedPoint(value.getValStr(), decimals, &amount))
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount"); throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
if (!MoneyRange(amount)) if (!MoneyRange(amount))
throw JSONRPCError(RPC_TYPE_ERROR, "Amount out of range"); throw JSONRPCError(RPC_TYPE_ERROR, "Amount out of range");
@ -319,11 +319,12 @@ UniValue DescribeAddress(const CTxDestination& dest)
unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target) unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target)
{ {
int target = value.get_int(); const int target{value.get_int()};
if (target < 1 || (unsigned int)target > max_target) { const unsigned int unsigned_target{static_cast<unsigned int>(target)};
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u - %u", 1, max_target)); if (target < 1 || unsigned_target > max_target) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid conf_target, must be between %u and %u", 1, max_target));
} }
return (unsigned int)target; return unsigned_target;
} }
/** /**

View File

@ -84,7 +84,14 @@ int64_t ParseInt64V(const UniValue& v, const std::string &strName);
double ParseDoubleV(const UniValue& v, const std::string &strName); double ParseDoubleV(const UniValue& v, const std::string &strName);
bool ParseBoolV(const UniValue& v, const std::string &strName); bool ParseBoolV(const UniValue& v, const std::string &strName);
CAmount AmountFromValue(const UniValue& value); /**
* Validate and return a CAmount from a UniValue number or string.
*
* @param[in] value UniValue number or string to parse.
* @param[in] decimals Number of significant digits (default: 8).
* @returns a CAmount if the various checks pass.
*/
CAmount AmountFromValue(const UniValue& value, int decimals = 8);
using RPCArgList = std::vector<std::pair<std::string, UniValue>>; using RPCArgList = std::vector<std::pair<std::string, UniValue>>;
std::string HelpExampleCli(const std::string& methodname, const std::string& args); std::string HelpExampleCli(const std::string& methodname, const std::string& args);

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

@ -2044,6 +2044,15 @@ BOOST_AUTO_TEST_CASE(test_ParseFixedPoint)
BOOST_CHECK(!ParseFixedPoint("1.1e", 8, &amount)); BOOST_CHECK(!ParseFixedPoint("1.1e", 8, &amount));
BOOST_CHECK(!ParseFixedPoint("1.1e-", 8, &amount)); BOOST_CHECK(!ParseFixedPoint("1.1e-", 8, &amount));
BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount)); BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount));
// Test with 3 decimal places for fee rates in sat/vB.
BOOST_CHECK(ParseFixedPoint("0.001", 3, &amount));
BOOST_CHECK_EQUAL(amount, CAmount{1});
BOOST_CHECK(!ParseFixedPoint("0.0009", 3, &amount));
BOOST_CHECK(!ParseFixedPoint("31.00100001", 3, &amount));
BOOST_CHECK(!ParseFixedPoint("31.0011", 3, &amount));
BOOST_CHECK(!ParseFixedPoint("31.99999999", 3, &amount));
BOOST_CHECK(!ParseFixedPoint("31.999999999999999999999", 3, &amount));
} }
static void TestOtherThread(fs::path dirname, std::string lockname, bool *result) static void TestOtherThread(fs::path dirname, std::string lockname, bool *result)

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

@ -27,7 +27,6 @@
#include <util/translation.h> #include <util/translation.h>
#include <util/url.h> #include <util/url.h>
#include <util/vector.h> #include <util/vector.h>
#include <validation.h>
#include <wallet/coincontrol.h> #include <wallet/coincontrol.h>
#include <wallet/context.h> #include <wallet/context.h>
#include <wallet/load.h> #include <wallet/load.h>
@ -52,8 +51,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 +202,35 @@ 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) * @param[in] estimate_mode UniValue string; fee estimation mode, valid values are "unset", "economical" or "conservative";
* @throws a JSONRPCError if estimate_mode is unknown, or if estimate_param is missing when required * @param[in] fee_rate UniValue real; fee rate in sat/B;
* if present, both conf_target and estimate_mode must either be null, or no-op or "unset"
* @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()) {
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() != "unset") {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
}
// Fee rates in sat/vB cannot represent more than 3 significant digits.
cc.m_feerate = CFeeRate{AmountFromValue(fee_rate, /* decimals */ 3)};
if (override_min_fee) cc.fOverrideFeeRate = true;
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, "Selected estimate_mode requires a fee rate"); 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());
} }
} }
@ -385,6 +383,12 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto
{ {
EnsureWalletIsUnlocked(wallet); EnsureWalletIsUnlocked(wallet);
// This function is only used by sendtoaddress and sendmany.
// This should always try to sign, if we don't have private keys, don't try to do anything here.
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
}
if (coin_control.IsUsingCoinJoin()) { if (coin_control.IsUsingCoinJoin()) {
map_value["DS"] = "1"; map_value["DS"] = "1";
} }
@ -395,7 +399,7 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto
bilingual_str error; bilingual_str error;
CTransactionRef tx; CTransactionRef tx;
FeeCalculation fee_calc_out; FeeCalculation fee_calc_out;
bool fCreated = wallet.CreateTransaction(recipients, tx, nFeeRequired, nChangePosRet, error, coin_control, fee_calc_out, !wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); const bool fCreated = wallet.CreateTransaction(recipients, tx, nFeeRequired, nChangePosRet, error, coin_control, fee_calc_out, true);
if (!fCreated) { if (!fCreated) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original); throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original);
} }
@ -426,11 +430,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 default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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."},
}, },
{ {
@ -444,15 +449,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.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.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);
@ -486,7 +496,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);
@ -500,7 +510,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);
}, },
@ -921,9 +931,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 default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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."},
}, },
{ {
@ -979,11 +990,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);
}, },
@ -3378,7 +3389,7 @@ static RPCHelpMan listunspent()
}; };
} }
void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, 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
@ -3411,7 +3422,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)},
@ -3451,8 +3463,11 @@ 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");
@ -3464,8 +3479,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
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
@ -3524,7 +3538,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"
@ -3533,7 +3548,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 default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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\"") + "\""},
}, },
@ -3575,7 +3590,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)));
@ -4177,9 +4192,10 @@ static RPCHelpMan send()
}, },
}, },
}, },
{"conf_target", RPCArg::Type::NUM, /* default */ "wallet default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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."},
@ -4189,9 +4205,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 default", "Confirmation target (in blocks), or fee rate (for " + CURRENCY_UNIT + "/kB or " + CURRENCY_ATOM + "/B estimate modes)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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."},
@ -4205,7 +4222,7 @@ static RPCHelpMan send()
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"}, {"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
{"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"}, {"lock_unspents", RPCArg::Type::BOOL, /* default */ "false", "Lock selected unspent outputs"},
{"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."}, {"psbt", RPCArg::Type::BOOL, /* default */ "automatic", "Always return a PSBT, implies add_to_wallet=false."},
{"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "A JSON array of integers.\n" {"subtract_fee_from_outputs", RPCArg::Type::ARR, /* default */ "empty array", "Outputs to subtract the fee from, specified as integer indices.\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 funds than you enter in their corresponding amount field.\n" "Those recipients will receive less funds than you enter in their corresponding amount field.\n"
"If no outputs are specified here, the sender pays the fee.", "If no outputs are specified here, the sender pays the fee.",
@ -4226,36 +4243,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.1 " + CURRENCY_ATOM + "/B using positional arguments\n"
+ HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS[0] + "\": 0.2}' null \"unset\" 1.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}' null \"unset\" null '{\"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 UniValueType(), // fee_rate, will be checked by AmountFromValue() in SetFeeEstimateMode()
}, 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");
} }
@ -4279,9 +4313,9 @@ static RPCHelpMan send()
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"]); CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"]);
CCoinControl coin_control; CCoinControl coin_control;
// Automatically select coins, unless at least one is manually selected. Can // Automatically select coins, unless at least one is manually selected. Can
// be overriden 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")) {
@ -4525,6 +4559,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"
@ -4534,7 +4569,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 */ "Fallback to wallet's confirmation target", "Confirmation target (in blocks)"}, {"conf_target", RPCArg::Type::NUM, /* default */ "wallet -txconfirmtarget", "Confirmation target in blocks"},
{"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\"") + "\""},
}, },
@ -4579,7 +4614,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

@ -33,9 +33,10 @@ static const CoinEligibilityFilter filter_standard(1, 6, 0);
static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_confirmed(1, 1, 0);
static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); static const CoinEligibilityFilter filter_standard_extra(6, 6, 0);
CoinSelectionParams coin_selection_params(/* use_bnb = */ false, /* change_output_size = */ 0, CoinSelectionParams coin_selection_params(/* use_bnb= */ false, /* change_output_size= */ 0,
/* change_spend_size = */ 0, /* effective_fee = */ CFeeRate(0), /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0),
/* tx_noinputs_size = */ 0, /* m_avoid_partial_spends = */ false); /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set) static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set)
{ {
@ -254,9 +255,10 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
} }
// Make sure that effective value is working in SelectCoinsMinConf when BnB is used // Make sure that effective value is working in SelectCoinsMinConf when BnB is used
CoinSelectionParams coin_selection_params_bnb(/* use_bnb = */ true, /* change_output_size = */ 0, CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 0,
/* change_spend_size = */ 0, /* effective_fee = */ CFeeRate(3000), /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000),
/* tx_noinputs_size = */ 0, /* m_avoid_partial_spends = */ false); /* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000),
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
{ {
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase()); std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), /* coinjoin_loader = */ nullptr, "", CreateMockWalletDatabase());
wallet->SetupLegacyScriptPubKeyMan(); wallet->SetupLegacyScriptPubKeyMan();
@ -298,7 +300,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
CCoinControl coin_control; CCoinControl coin_control;
coin_control.fAllowOtherInputs = true; coin_control.fAllowOtherInputs = true;
coin_control.Select(COutPoint(coins.at(0).tx->GetHash(), coins.at(0).i)); coin_control.Select(COutPoint(coins.at(0).tx->GetHash(), coins.at(0).i));
coin_selection_params_bnb.effective_fee = CFeeRate(0); coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
BOOST_CHECK(wallet->SelectCoins(coins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb, bnb_used)); BOOST_CHECK(wallet->SelectCoins(coins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb, bnb_used));
BOOST_CHECK(bnb_used); BOOST_CHECK(bnb_used);
BOOST_CHECK(coin_selection_params_bnb.use_bnb); BOOST_CHECK(coin_selection_params_bnb.use_bnb);
@ -642,12 +644,14 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
CAmount target = rand.randrange(balance - 1000) + 1000; CAmount target = rand.randrange(balance - 1000) + 1000;
// Perform selection // Perform selection
CoinSelectionParams coin_selection_params_knapsack(/* use_bnb = */ false, /* change_output_size = */ 34, CoinSelectionParams coin_selection_params_knapsack(/* use_bnb= */ false, /* change_output_size= */ 34,
/* change_spend_size = */ 148, /* effective_fee = */ CFeeRate(0), /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
/* tx_noinputs_size = */ 0, /* m_avoid_partial_spends = */ false); /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
CoinSelectionParams coin_selection_params_bnb(/* use_bnb = */ true, /* change_output_size = */ 34, /* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
/* change_spend_size = */ 148, /* effective_fee = */ CFeeRate(0), CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 34,
/* tx_noinputs_size = */ 0, /* m_avoid_partial_spends = */ false); /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
CoinSet out_set; CoinSet out_set;
CAmount out_value = 0; CAmount out_value = 0;
bool bnb_used = false; bool bnb_used = false;

View File

@ -2872,26 +2872,20 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
nValueRet = 0; nValueRet = 0;
if (coin_selection_params.use_bnb) { if (coin_selection_params.use_bnb) {
// Get long term estimate
FeeCalculation feeCalc;
CCoinControl temp;
temp.m_confirm_target = 1008;
CFeeRate long_term_feerate = GetMinimumFeeRate(*this, temp, &feeCalc);
// Get the feerate for effective value. // Get the feerate for effective value.
// When subtracting the fee from the outputs, we want the effective feerate to be 0 // When subtracting the fee from the outputs, we want the effective feerate to be 0
CFeeRate effective_feerate{0}; CFeeRate effective_feerate{0};
if (!coin_selection_params.m_subtract_fee_outputs) { if (!coin_selection_params.m_subtract_fee_outputs) {
effective_feerate = coin_selection_params.effective_fee; effective_feerate = coin_selection_params.m_effective_feerate;
} }
std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, long_term_feerate, eligibility_filter, true /* positive_only */); std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, true /* positive_only */);
// Calculate cost of change // Calculate cost of change
CAmount cost_of_change = GetDiscardRate(*this).GetFee(coin_selection_params.change_spend_size) + coin_selection_params.effective_fee.GetFee(coin_selection_params.change_output_size); CAmount cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size);
// Calculate the fees for things that aren't inputs // Calculate the fees for things that aren't inputs
CAmount not_input_fees = coin_selection_params.effective_fee.GetFee(coin_selection_params.tx_noinputs_size); CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
bnb_used = true; bnb_used = true;
return SelectCoinsBnB(groups, nTargetValue, cost_of_change, setCoinsRet, nValueRet, not_input_fees); return SelectCoinsBnB(groups, nTargetValue, cost_of_change, setCoinsRet, nValueRet, not_input_fees);
} else { } else {
@ -2959,7 +2953,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
if (coin.m_input_bytes <= 0) { if (coin.m_input_bytes <= 0) {
return false; // Not solvable, can't estimate size for fee return false; // Not solvable, can't estimate size for fee
} }
coin.effective_value = coin.txout.nValue - coin_selection_params.effective_fee.GetFee(coin.m_input_bytes); coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes);
if (coin_selection_params.use_bnb) { if (coin_selection_params.use_bnb) {
value_to_select -= coin.effective_value; value_to_select -= coin.effective_value;
} else { } else {
@ -3548,7 +3542,19 @@ bool CWallet::CreateTransactionInternal(
CMutableTransaction txNew; CMutableTransaction txNew;
FeeCalculation feeCalc; FeeCalculation feeCalc;
CFeeRate discard_rate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this);
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
coin_selection_params.m_discard_feerate = coin_control.m_discard_feerate ? *coin_control.m_discard_feerate : GetDiscardRate(*this);
// Get the fee rate to use effective values in coin selection
coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc);
// Do not, ever, assume that it's fine to change the fee rate if the user has explicitly
// provided one
if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) {
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::DUFF_B), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::DUFF_B));
return false;
}
int nBytes{0}; int nBytes{0};
{ {
std::vector<CInputCoin> vecCoins; std::vector<CInputCoin> vecCoins;
@ -3558,7 +3564,6 @@ bool CWallet::CreateTransactionInternal(
CAmount nAmountAvailable{0}; CAmount nAmountAvailable{0};
std::vector<COutput> vAvailableCoins; std::vector<COutput> vAvailableCoins;
AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); AvailableCoins(vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
coin_selection_params.use_bnb = false; // never use BnB coin_selection_params.use_bnb = false; // never use BnB
for (auto out : vAvailableCoins) { for (auto out : vAvailableCoins) {
@ -3746,7 +3751,7 @@ bool CWallet::CreateTransactionInternal(
// Never create dust outputs; if we would, just // Never create dust outputs; if we would, just
// add the dust to the fee. // add the dust to the fee.
if (IsDust(newTxOut, discard_rate)) if (IsDust(newTxOut, coin_selection_params.m_discard_feerate))
{ {
nFeeRet = nFeePrev; nFeeRet = nFeePrev;
nChangePosInOut = -1; nChangePosInOut = -1;

View File

@ -683,8 +683,13 @@ struct CoinSelectionParams
size_t change_output_size = 0; size_t change_output_size = 0;
/** Size of the input to spend a change output in virtual bytes. */ /** Size of the input to spend a change output in virtual bytes. */
size_t change_spend_size = 0; size_t change_spend_size = 0;
/** The targeted feerate of the transaction being built. */ /** The fee to spend these UTXOs at the long term feerate. */
CFeeRate effective_fee = CFeeRate(0); CFeeRate m_effective_feerate;
/** The feerate estimate used to estimate an upper bound on what should be sufficient to spend
* the change output sometime in the future. */
CFeeRate m_long_term_feerate;
/** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */
CFeeRate m_discard_feerate;
size_t tx_noinputs_size = 0; size_t tx_noinputs_size = 0;
/** Indicate that we are subtracting the fee from outputs */ /** Indicate that we are subtracting the fee from outputs */
bool m_subtract_fee_outputs = false; bool m_subtract_fee_outputs = false;
@ -693,11 +698,14 @@ struct CoinSelectionParams
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */ * reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
bool m_avoid_partial_spends = false; bool m_avoid_partial_spends = false;
CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_fee, size_t tx_noinputs_size, bool avoid_partial) : CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
use_bnb(use_bnb), use_bnb(use_bnb),
change_output_size(change_output_size), change_output_size(change_output_size),
change_spend_size(change_spend_size), change_spend_size(change_spend_size),
effective_fee(effective_fee), m_effective_feerate(effective_feerate),
m_long_term_feerate(long_term_feerate),
m_discard_feerate(discard_feerate),
tx_noinputs_size(tx_noinputs_size), tx_noinputs_size(tx_noinputs_size),
m_avoid_partial_spends(avoid_partial) m_avoid_partial_spends(avoid_partial)
{} {}

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

@ -5,9 +5,12 @@
"""Test the fundrawtransaction RPC.""" """Test the fundrawtransaction RPC."""
from decimal import Decimal from decimal import Decimal
from itertools import product
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
from test_framework.util import ( from test_framework.util import (
assert_approx,
assert_equal, assert_equal,
assert_fee_amount, assert_fee_amount,
assert_greater_than, assert_greater_than,
@ -702,21 +705,92 @@ 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)")
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(self.nodes[3].listunspent(1)), 1) assert_equal(len(self.nodes[3].listunspent(1)), 1)
inputs = [] inputs = []
outputs = {self.nodes[3].getnewaddress() : 1} outputs = {node.getnewaddress() : 1}
rawtx = self.nodes[3].createrawtransaction(inputs, outputs) rawtx = node.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}) result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
result3 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 10 * self.min_relay_tx_fee}) btc_kvb_to_sat_vb = 100000 # (1e5)
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[3].fundrawtransaction, rawtx, {"feeRate": 1}) result1 = node.fundrawtransaction(rawtx, {"fee_rate": str(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": str(10 * self.min_relay_tx_fee)})
result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex']) result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex'])
assert_fee_amount(result1['fee'], count_bytes(result1['hex']), 2 * result_fee_rate)
assert_fee_amount(result2['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(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
assert_fee_amount(result4['fee'], count_bytes(result4['hex']), 10 * result_fee_rate)
# Test that funding non-standard "zero-fee" transactions is valid.
for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]):
assert_equal(self.nodes[3].fundrawtransaction(rawtx, {param: zero_value})["fee"], 0)
# With no arguments passed, expect fee of 225 satoshis.
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000225, 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.0225, 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),
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, must be one of: "unset", "economical", "conservative"',
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"]:
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),
node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
for n in [-1, 0, 1009]:
assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
node.fundrawtransaction, rawtx, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
self.log.info("Test invalid fee rate settings")
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, {param: -1, "add_inputs": True})
assert_raises_rpc_error(-3, "Amount is not a number or string",
node.fundrawtransaction, rawtx, {param: {"foo": "bar"}, "add_inputs": True})
# Test fee rate values that don't pass fixed-point parsing checks.
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
assert_raises_rpc_error(-3, "Invalid amount", node.fundrawtransaction, rawtx, {param: invalid_value, "add_inputs": True})
# Test fee_rate values that cannot be represented in sat/vB.
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
assert_raises_rpc_error(-3, "Invalid amount",
node.fundrawtransaction, rawtx, {"fee_rate": invalid_value, "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.999, "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."""
@ -744,12 +818,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

@ -6,6 +6,8 @@
""" """
from decimal import Decimal from decimal import Decimal
from itertools import product
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_approx, assert_approx,
@ -94,8 +96,11 @@ class PSBTTest(BitcoinTestFramework):
elif out['scriptPubKey']['address'] == p2pkh: elif out['scriptPubKey']['address'] == p2pkh:
p2pkh_pos = out['n'] p2pkh_pos = out['n']
inputs = [{"txid":txid,"vout":p2pkh_pos}]
outputs = [{self.nodes[1].getnewaddress(): 9.99}]
# spend single key from node 1 # spend single key from node 1
created_psbt = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}) created_psbt = self.nodes[1].walletcreatefundedpsbt(inputs, outputs)
walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(created_psbt['psbt']) walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(created_psbt['psbt'])
# Make sure it has both types of UTXOs # Make sure it has both types of UTXOs
decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt']) decoded = self.nodes[1].decodepsbt(walletprocesspsbt_out['psbt'])
@ -105,18 +110,83 @@ 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'])
# feeRate of 0.1 DASH / KB produces a total fee 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([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 0.1, "add_inputs": True}, False) 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)
decoded_psbt = self.nodes[0].decodepsbt(res['psbt']) res2 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": "0.1", "add_inputs": True})
for psbt_in in decoded_psbt["inputs"]: assert_approx(res2["fee"], 0.04, 0.005)
assert "bip32_derivs" not in psbt_in
# feeRate of 10 DASH / KB produces a total fee well above -maxtxfee 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.999", "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 min fee rate checks with walletcreatefundedpsbt are bypassed and that funding non-standard 'zero-fee' transactions is valid")
for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]):
assert_equal(0, self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: zero_value, "add_inputs": True})["fee"])
self.log.info("Test invalid fee rate settings")
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, {param: -1, "add_inputs": True})
assert_raises_rpc_error(-3, "Amount is not a number or string",
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: {"foo": "bar"}, "add_inputs": True})
# Test fee rate values that don't pass fixed-point parsing checks.
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
assert_raises_rpc_error(-3, "Invalid amount",
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: invalid_value, "add_inputs": True})
# Test fee_rate values that cannot be represented in sat/vB.
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
assert_raises_rpc_error(-3, "Invalid amount",
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": invalid_value, "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)",
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),
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, must be one of: "unset", "economical", "conservative"',
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"]:
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),
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": v, "add_inputs": True})
for n in [-1, 0, 1009]:
assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", # max value of 1008 per src/policy/fees.h
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"estimate_mode": mode, "conf_target": n, "add_inputs": True})
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 # previously this was silently capped at -maxtxfee
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99}, 0, {"feeRate": 10, "add_inputs": True}) 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, [{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": True}) 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 # partially sign multisig things with node 1
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2sh_pos}], outputs={self.nodes[1].getnewaddress():9.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt'] psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2sh_pos}], outputs={self.nodes[1].getnewaddress():9.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt']
walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx) walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(psbtx)

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,9 @@ from test_framework.util import (
) )
from test_framework.wallet_util import test_address from test_framework.wallet_util import test_address
NOT_A_NUMBER_OR_STRING = "Amount is not a number or string"
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 +54,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 +86,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 +100,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)
@ -221,6 +228,8 @@ 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(), Decimal('200'), fee_per_byte, count_bytes(self.nodes[2].gettransaction(txid)['hex'])) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('200'), fee_per_byte, count_bytes(self.nodes[2].gettransaction(txid)['hex']))
self.log.info("Test sendmany")
# Sendmany 100 DASH # Sendmany 100 DASH
txid = self.nodes[2].sendmany('', {address: 100}, 0, False, "", []) txid = self.nodes[2].sendmany('', {address: 100}, 0, False, "", [])
self.nodes[2].generate(1) self.nodes[2].generate(1)
@ -237,62 +246,69 @@ 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
# Sendmany with explicit fee (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 requires a fee rate",
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)
# Sendmany with explicit fee (DUFF/B) # Test passing fee_rate as a string
# Throw if no conf_target provided txid = self.nodes[2].sendmany(amounts={address: 10}, fee_rate=str(fee_rate_sat_vb))
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate",
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)
# Test passing fee_rate as an integer
amount = Decimal("0.0001")
txid = self.nodes[2].sendmany(amounts={address: amount}, 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 - amount, explicit_fee_rate_btc_kvb, self.get_vsize(self.nodes[2].gettransaction(txid)['hex']))
assert_equal(balance, node_2_bal)
node_0_bal += amount
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.
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.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.999)
self.log.info("Test sendmany raises if an invalid fee_rate is passed")
# Test fee_rate with zero values.
msg = "Fee rate (0.000 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)"
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
assert_raises_rpc_error(-6, msg, self.nodes[2].sendmany, amounts={address: 1}, fee_rate=zero_value)
msg = "Invalid amount"
# Test fee_rate values that don't pass fixed-point parsing checks.
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 1.0}, fee_rate=invalid_value)
# Test fee_rate values that cannot be represented in duff/B.
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value)
# Test fee_rate out of range (negative number).
assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=-1)
# Test type error.
for invalid_value in [True, {"foo": "bar"}]:
assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value)
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
@ -321,7 +337,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"])
@ -381,7 +397,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")
@ -423,73 +439,70 @@ 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])
# send with explicit dash/kb fee self.log.info("Test sendtoaddress with fee_rate param (explicit fee rate in duff/B)")
self.log.info("test explicit fee (sendtoaddress as dash/kb)")
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 requires a fee rate", fee_rate_sat_vb = 2
self.nodes[2].sendtoaddress, fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
address=address, # Test passing fee_rate as an integer
amount=1.0, txid = self.nodes[2].sendtoaddress(address=address, amount=amount, fee_rate=fee_rate_sat_vb)
estimate_mode='dash/Kb') tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])
# 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='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))
# send with explicit duff/b fee
self.sync_all(self.nodes[0:3])
self.log.info("test explicit fee (sendtoaddress as duff/b)")
self.nodes[0].generate(1)
prebalance = self.nodes[2].getbalance() prebalance = self.nodes[2].getbalance()
assert prebalance > 2 amount = Decimal("0.001")
address = self.nodes[1].getnewaddress() fee_rate_sat_vb = 1.23
# Throw if no conf_target provided fee_rate_btc_kvb = fee_rate_sat_vb * 1e3 / 1e8
assert_raises_rpc_error(-8, "Selected estimate_mode requires a fee rate", # Test passing fee_rate as a string
self.nodes[2].sendtoaddress, txid = self.nodes[2].sendtoaddress(address=address, amount=amount, fee_rate=str(fee_rate_sat_vb))
address=address, tx_size = self.get_vsize(self.nodes[2].gettransaction(txid)['hex'])
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.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 - amount
assert_fee_amount(fee, tx_size, Decimal('0.00002000')) # TODO: remove workaround after bitcoin/bitcoin#22949 done
workaround_offset = 1
assert_fee_amount(fee, tx_size - workaround_offset, Decimal(fee_rate_btc_kvb))
for key in ["totalFee", "feeRate"]:
assert_raises_rpc_error(-8, "Unknown named parameter key", self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=1, key=1)
# Test setting explicit fee rate just below the minimum.
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.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)",
self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.999)
self.log.info("Test sendtoaddress raises if an invalid fee_rate is passed")
# Test fee_rate with zero values.
msg = "Fee rate (0.000 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)"
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
assert_raises_rpc_error(-6, msg, self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=zero_value)
msg = "Invalid amount"
# Test fee_rate values that don't pass fixed-point parsing checks.
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value)
# Test fee_rate values that cannot be represented in duff/B.
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=invalid_value)
# Test fee_rate out of range (negative number).
assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=-1)
# Test type error.
for invalid_value in [True, {"foo": "bar"}]:
assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value)
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)
@ -547,7 +560,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,16 +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_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:")
@ -271,8 +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
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_conf_target=1, arg_estimate_mode="economical", for mode in ["unset", "economical", "conservative"]:
conf_target=1, estimate_mode="economical", add_to_wallet=False, expect_error=(-8,"Use either conf_target and estimate_mode or the options dictionary to control fee rate")) 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, "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)
@ -297,12 +306,82 @@ class WalletSendTest(BitcoinTestFramework):
res = w2.walletprocesspsbt(res["psbt"]) res = w2.walletprocesspsbt(res["psbt"])
assert res["complete"] assert res["complete"]
self.log.info("Set fee rate...") self.log.info("Test setting explicit fee rate")
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=2, estimate_mode="duff/b", 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, 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"))
# "unset" and None are treated the same for estimate_mode
res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=2, estimate_mode="unset", 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"))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, conf_target=-1, estimate_mode="duff/b",
expect_error=(-3, "Amount out of range")) res = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, 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_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 (duff/B) 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")) # max value of 1008 per src/policy/fees.h
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"]:
for k, v in {"string": "true", "bool": 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, f"Expected type number for conf_target, got {k}"))
# Test setting explicit fee rate just below the minimum of 1 duff/B.
self.log.info("Explicit fee rate raises RPC error 'fee rate too low' if fee_rate of 0.99999999 is passed")
msg = "Fee rate (0.999 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)"
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=0.999, expect_error=(-4, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=0.999, expect_error=(-4, msg))
self.log.info("Explicit fee rate raises if invalid fee_rate is passed")
# Test fee_rate with zero values.
msg = "Fee rate (0.000 duff/B) is lower than the minimum fee rate setting (1.000 duff/B)"
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=zero_value, expect_error=(-4, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=zero_value, expect_error=(-4, msg))
msg = "Invalid amount"
# Test fee_rate values that don't pass fixed-point parsing checks.
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg))
# Test fee_rate values that cannot be represented in duff/B.
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg))
# Test fee_rate out of range (negative number).
msg = "Amount out of range"
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=-1, expect_error=(-3, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=-1, expect_error=(-3, msg))
# Test type error.
msg = "Amount is not a number or string"
for invalid_value in [True, {"foo": "bar"}]:
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate=invalid_value, expect_error=(-3, msg))
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate=invalid_value, expect_error=(-3, msg))
# 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)

View File

@ -2,7 +2,7 @@
# Copyright (c) 2018-2019 The Bitcoin Core developers # Copyright (c) 2018-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test createwallet arguments. """Test createwallet watchonly arguments.
""" """
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
@ -51,6 +51,11 @@ class CreateWalletWatchonlyTest(BitcoinTestFramework):
assert_equal(len(wo_wallet.listtransactions()), 1) assert_equal(len(wo_wallet.listtransactions()), 1)
assert_equal(wo_wallet.getbalance(include_watchonly=False), 0) assert_equal(wo_wallet.getbalance(include_watchonly=False), 0)
self.log.info('Test sending from a watch-only wallet raises RPC error')
msg = "Error: Private keys are disabled for this wallet"
assert_raises_rpc_error(-4, msg, wo_wallet.sendtoaddress, a1, 0.1)
assert_raises_rpc_error(-4, msg, wo_wallet.sendmany, amounts={a1: 0.1})
self.log.info('Testing listreceivedbyaddress watch-only defaults') self.log.info('Testing listreceivedbyaddress watch-only defaults')
result = wo_wallet.listreceivedbyaddress() result = wo_wallet.listreceivedbyaddress()
assert_equal(len(result), 1) assert_equal(len(result), 1)