mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
Merge f4e5d704f2 into merged_master (Bitcoin PR bitcoin/bitcoin#24118)
This commit is contained in:
commit
d5b8d28d07
7 changed files with 685 additions and 96 deletions
10
doc/release-notes-24118.md
Normal file
10
doc/release-notes-24118.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
New RPCs
|
||||
--------
|
||||
|
||||
- The `sendall` RPC spends specific UTXOs to one or more recipients
|
||||
without creating change. By default, the `sendall` RPC will spend
|
||||
every UTXO in the wallet. `sendall` is useful to empty wallets or to
|
||||
create a changeless payment from select UTXOs. When creating a payment
|
||||
from a specific amount for which the recipient incurs the transaction
|
||||
fee, continue to use the `subtractfeefromamount` option via the
|
||||
`send`, `sendtoaddress`, or `sendmany` RPCs. (#24118)
|
||||
|
|
@ -145,6 +145,10 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "send", 1, "conf_target" },
|
||||
{ "send", 3, "fee_rate"},
|
||||
{ "send", 4, "options" },
|
||||
{ "sendall", 0, "recipients" },
|
||||
{ "sendall", 1, "conf_target" },
|
||||
{ "sendall", 3, "fee_rate"},
|
||||
{ "sendall", 4, "options" },
|
||||
{ "importprivkey", 2, "rescan" },
|
||||
{ "importaddress", 2, "rescan" },
|
||||
{ "importaddress", 3, "p2sh" },
|
||||
|
|
|
|||
441
src/wallet/rpc/spend.cpp
Normal file → Executable file
441
src/wallet/rpc/spend.cpp
Normal file → Executable file
|
|
@ -12,11 +12,13 @@
|
|||
#include <rpc/util.h>
|
||||
#include <script/pegins.h>
|
||||
#include <util/fees.h>
|
||||
#include <util/rbf.h>
|
||||
#include <util/translation.h>
|
||||
#include <util/vector.h>
|
||||
#include <wallet/coincontrol.h>
|
||||
#include <wallet/feebumper.h>
|
||||
#include <wallet/receive.h>
|
||||
#include <wallet/fees.h>
|
||||
#include <wallet/rpc/util.h>
|
||||
#include <wallet/spend.h>
|
||||
#include <wallet/wallet.h>
|
||||
|
|
@ -26,7 +28,8 @@
|
|||
using wallet::CRecipient;
|
||||
|
||||
namespace wallet {
|
||||
static void ParseRecipients(const UniValue& address_amounts, const UniValue& address_assets, const UniValue& subtract_fee_outputs, std::vector<CRecipient> &recipients) {
|
||||
static void ParseRecipients(const UniValue& address_amounts, const UniValue& address_assets, const UniValue& subtract_fee_outputs, std::vector<CRecipient> &recipients)
|
||||
{
|
||||
std::set<CTxDestination> destinations;
|
||||
int i = 0;
|
||||
for (const std::string& address: address_amounts.getKeys()) {
|
||||
|
|
@ -65,6 +68,93 @@ static void ParseRecipients(const UniValue& address_amounts, const UniValue& add
|
|||
}
|
||||
}
|
||||
|
||||
static void InterpretFeeEstimationInstructions(const UniValue& conf_target, const UniValue& estimate_mode, const UniValue& fee_rate, UniValue& options)
|
||||
{
|
||||
if (options.exists("conf_target") || options.exists("estimate_mode")) {
|
||||
if (!conf_target.isNull() || !estimate_mode.isNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Pass conf_target and estimate_mode either as arguments or in the options object, but not both");
|
||||
}
|
||||
} else {
|
||||
options.pushKV("conf_target", conf_target);
|
||||
options.pushKV("estimate_mode", estimate_mode);
|
||||
}
|
||||
if (options.exists("fee_rate")) {
|
||||
if (!fee_rate.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", fee_rate);
|
||||
}
|
||||
if (!options["conf_target"].isNull() && (options["estimate_mode"].isNull() || (options["estimate_mode"].get_str() == "unset"))) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode");
|
||||
}
|
||||
}
|
||||
|
||||
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx)
|
||||
{
|
||||
// Make a blank psbt
|
||||
PartiallySignedTransaction psbtx(rawTx, 2 /* version */);
|
||||
|
||||
// First fill transaction with our data without signing,
|
||||
// so external signers are not asked sign more than once.
|
||||
bool complete;
|
||||
pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, false, true, true);
|
||||
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, true, false, true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err);
|
||||
}
|
||||
|
||||
CMutableTransaction mtx;
|
||||
complete = FinalizeAndExtractPSBT(psbtx, mtx);
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
|
||||
const bool psbt_opt_in{options.exists("psbt") && options["psbt"].get_bool()};
|
||||
bool add_to_wallet{options.exists("add_to_wallet") ? options["add_to_wallet"].get_bool() : true};
|
||||
if (psbt_opt_in || !complete || !add_to_wallet) {
|
||||
// Serialize the PSBT
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << psbtx;
|
||||
result.pushKV("psbt", EncodeBase64(ssTx.str()));
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
std::string hex{EncodeHexTx(CTransaction(mtx))};
|
||||
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
|
||||
result.pushKV("txid", tx->GetHash().GetHex());
|
||||
if (add_to_wallet && !psbt_opt_in) {
|
||||
pwallet->CommitTransaction(tx, {}, /*orderForm*/ {});
|
||||
} else {
|
||||
result.pushKV("hex", hex);
|
||||
}
|
||||
}
|
||||
result.pushKV("complete", complete);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void PreventOutdatedOptions(const UniValue& options)
|
||||
{
|
||||
if (options.exists("feeRate")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use fee_rate (" + CURRENCY_ATOM + "/vB) instead of feeRate");
|
||||
}
|
||||
if (options.exists("changeAddress")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address instead of changeAddress");
|
||||
}
|
||||
if (options.exists("changePosition")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_position instead of changePosition");
|
||||
}
|
||||
if (options.exists("includeWatching")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use include_watching instead of includeWatching");
|
||||
}
|
||||
if (options.exists("lockUnspents")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use lock_unspents instead of lockUnspents");
|
||||
}
|
||||
if (options.exists("subtractFeeFromOutputs")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use subtract_fee_from_outputs instead of subtractFeeFromOutputs");
|
||||
}
|
||||
}
|
||||
|
||||
UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vector<CRecipient> &recipients, mapValue_t map_value, bool verbose, bool ignore_blind_fail)
|
||||
{
|
||||
EnsureWalletIsUnlocked(wallet);
|
||||
|
|
@ -413,31 +503,43 @@ RPCHelpMan settxfee()
|
|||
|
||||
|
||||
// Only includes key documentation where the key is snake_case in all RPC methods. MixedCase keys can be added later.
|
||||
static std::vector<RPCArg> FundTxDoc()
|
||||
static std::vector<RPCArg> FundTxDoc(bool solving_data = true)
|
||||
{
|
||||
return {
|
||||
std::vector<RPCArg> args = {
|
||||
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
|
||||
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
|
||||
" \"" + FeeModes("\"\n\"") + "\""},
|
||||
{"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125-replaceable.\n"
|
||||
"Allows this transaction to be replaced by a transaction with higher fees"},
|
||||
{"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
|
||||
"Used for fee estimation during coin selection.",
|
||||
{
|
||||
{"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
|
||||
{
|
||||
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
|
||||
}},
|
||||
{"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
|
||||
{
|
||||
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
|
||||
}},
|
||||
{"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
|
||||
{
|
||||
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
|
||||
}},
|
||||
}},
|
||||
{
|
||||
"replaceable", RPCArg::Type::BOOL, RPCArg::DefaultHint{"wallet default"}, "Marks this transaction as BIP125-replaceable.\n"
|
||||
"Allows this transaction to be replaced by a transaction with higher fees"
|
||||
},
|
||||
};
|
||||
if (solving_data) {
|
||||
args.push_back({"solving_data", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "Keys and scripts needed for producing a final transaction with a dummy signature.\n"
|
||||
"Used for fee estimation during coin selection.",
|
||||
{
|
||||
{
|
||||
"pubkeys", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Public keys involved in this transaction.",
|
||||
{
|
||||
{"pubkey", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A public key"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"scripts", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Scripts involved in this transaction.",
|
||||
{
|
||||
{"script", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A script"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"descriptors", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Descriptors that provide solving data for this transaction.",
|
||||
{
|
||||
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A descriptor"},
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, const UniValue& options, CCoinControl& coinControl, const UniValue& solving_data, bool override_min_fee)
|
||||
|
|
@ -1243,51 +1345,13 @@ RPCHelpMan send()
|
|||
if (!pwallet) return NullUniValue;
|
||||
|
||||
UniValue options{request.params[4].isNull() ? UniValue::VOBJ : request.params[4]};
|
||||
if (options.exists("conf_target") || options.exists("estimate_mode")) {
|
||||
if (!request.params[1].isNull() || !request.params[2].isNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Pass conf_target and estimate_mode either as arguments or in the options object, but not both");
|
||||
}
|
||||
} else {
|
||||
options.pushKV("conf_target", request.params[1]);
|
||||
options.pushKV("estimate_mode", request.params[2]);
|
||||
}
|
||||
if (options.exists("fee_rate")) {
|
||||
if (!request.params[3].isNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Pass the fee_rate either as an argument, or in the options object, but not both");
|
||||
}
|
||||
} else {
|
||||
options.pushKV("fee_rate", request.params[3]);
|
||||
}
|
||||
if (!options["conf_target"].isNull() && (options["estimate_mode"].isNull() || (options["estimate_mode"].get_str() == "unset"))) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Specify estimate_mode");
|
||||
}
|
||||
if (options.exists("feeRate")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use fee_rate (" + CURRENCY_ATOM + "/vB) instead of feeRate");
|
||||
}
|
||||
if (options.exists("changeAddress")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address");
|
||||
}
|
||||
if (options.exists("changePosition")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_position");
|
||||
}
|
||||
if (options.exists("includeWatching")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use include_watching");
|
||||
}
|
||||
if (options.exists("lockUnspents")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use lock_unspents");
|
||||
}
|
||||
if (options.exists("subtractFeeFromOutputs")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use subtract_fee_from_outputs");
|
||||
}
|
||||
InterpretFeeEstimationInstructions(/*conf_target=*/request.params[1], /*estimate_mode=*/request.params[2], /*fee_rate=*/request.params[3], options);
|
||||
PreventOutdatedOptions(options);
|
||||
|
||||
const bool psbt_opt_in = options.exists("psbt") && options["psbt"].get_bool();
|
||||
|
||||
CAmount fee;
|
||||
int change_position;
|
||||
bool rbf = pwallet->m_signal_rbf;
|
||||
if (options.exists("replaceable")) {
|
||||
rbf = options["replaceable"].get_bool();
|
||||
}
|
||||
bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf};
|
||||
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf, pwallet->chain().getTip(), nullptr /* output_pubkey_out */, true /* allow_peg_in */);
|
||||
CCoinControl coin_control;
|
||||
// Automatically select coins, unless at least one is manually selected. Can
|
||||
|
|
@ -1300,49 +1364,234 @@ RPCHelpMan send()
|
|||
SetOptionsInputWeights(options["inputs"], options);
|
||||
FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control, /* solving_data */ solving_data, /* override_min_fee */ false);
|
||||
|
||||
bool add_to_wallet = true;
|
||||
if (options.exists("add_to_wallet")) {
|
||||
add_to_wallet = options["add_to_wallet"].get_bool();
|
||||
}
|
||||
return FinishTransaction(pwallet, options, rawTx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make a blank psbt
|
||||
PartiallySignedTransaction psbtx(rawTx, 2 /* version */);
|
||||
RPCHelpMan sendall()
|
||||
{
|
||||
return RPCHelpMan{"sendall",
|
||||
"EXPERIMENTAL warning: this call may be changed in future releases.\n"
|
||||
"\nSpend the value of all (or specific) confirmed UTXOs in the wallet to one or more recipients.\n"
|
||||
"Unconfirmed inbound UTXOs and locked UTXOs will not be spent. Sendall will respect the avoid_reuse wallet flag.\n"
|
||||
"If your wallet contains many small inputs, either because it received tiny payments or as a result of accumulating change, consider using `send_max` to exclude inputs that are worth less than the fees needed to spend them.\n",
|
||||
{
|
||||
{"recipients", RPCArg::Type::ARR, RPCArg::Optional::NO, "The sendall destinations. Each address may only appear once.\n"
|
||||
"Optionally some recipients can be specified with an amount to perform payments, but at least one address must appear without a specified amount.\n",
|
||||
{
|
||||
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "A bitcoin address which receives an equal share of the unspecified amount."},
|
||||
{"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{"conf_target", RPCArg::Type::NUM, RPCArg::DefaultHint{"wallet -txconfirmtarget"}, "Confirmation target in blocks"},
|
||||
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"unset"}, std::string() + "The fee estimate mode, must be one of (case insensitive):\n"
|
||||
" \"" + FeeModes("\"\n\"") + "\""},
|
||||
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
|
||||
{
|
||||
"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
|
||||
Cat<std::vector<RPCArg>>(
|
||||
{
|
||||
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns the serialized transaction without broadcasting or adding it to the wallet"},
|
||||
{"fee_rate", RPCArg::Type::AMOUNT, RPCArg::DefaultHint{"not set, fall back to wallet fee estimation"}, "Specify a fee rate in " + CURRENCY_ATOM + "/vB."},
|
||||
{"include_watching", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Also select inputs which are watch-only.\n"
|
||||
"Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported,\n"
|
||||
"e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field."},
|
||||
{"inputs", RPCArg::Type::ARR, RPCArg::Default{UniValue::VARR}, "Use exactly the specified inputs to build the transaction. Specifying inputs is incompatible with send_max. A JSON array of JSON objects",
|
||||
{
|
||||
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
|
||||
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
|
||||
{"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The sequence number"},
|
||||
},
|
||||
},
|
||||
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"},
|
||||
{"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false}, "Lock selected unspent outputs"},
|
||||
{"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"}, "Always return a PSBT, implies add_to_wallet=false."},
|
||||
{"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."},
|
||||
},
|
||||
FundTxDoc()
|
||||
),
|
||||
"options"
|
||||
},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
{
|
||||
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
|
||||
{RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The transaction id for the send. Only 1 transaction is created regardless of the number of addresses."},
|
||||
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "If add_to_wallet is false, the hex-encoded raw transaction with signature(s)"},
|
||||
{RPCResult::Type::STR, "psbt", /*optional=*/true, "If more signatures are needed, or if add_to_wallet is false, the base64-encoded (partially) signed transaction"}
|
||||
}
|
||||
},
|
||||
RPCExamples{""
|
||||
"\nSpend all UTXOs from the wallet with a fee rate of 1 " + CURRENCY_ATOM + "/vB using named arguments\n"
|
||||
+ HelpExampleCli("-named sendall", "recipients='[\"" + EXAMPLE_ADDRESS[0] + "\"]' fee_rate=1\n") +
|
||||
"Spend all UTXOs with a fee rate of 1.1 " + CURRENCY_ATOM + "/vB using positional arguments\n"
|
||||
+ HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" 1.1\n") +
|
||||
"Spend all UTXOs split into equal amounts to two addresses with a fee rate of 1.5 " + CURRENCY_ATOM + "/vB using the options argument\n"
|
||||
+ HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\", \"" + EXAMPLE_ADDRESS[1] + "\"]' null \"unset\" null '{\"fee_rate\": 1.5}'\n") +
|
||||
"Leave dust UTXOs in wallet, spend only UTXOs with positive effective value with a fee rate of 10 " + CURRENCY_ATOM + "/vB using the options argument\n"
|
||||
+ HelpExampleCli("sendall", "'[\"" + EXAMPLE_ADDRESS[0] + "\"]' null \"unset\" null '{\"fee_rate\": 10, \"send_max\": true}'\n") +
|
||||
"Spend all UTXOs with a fee rate of 1.3 " + CURRENCY_ATOM + "/vB using named arguments and sending a 0.25 " + CURRENCY_UNIT + " to another recipient\n"
|
||||
+ HelpExampleCli("-named sendall", "recipients='[{\"" + EXAMPLE_ADDRESS[1] + "\": 0.25}, \""+ EXAMPLE_ADDRESS[0] + "\"]' fee_rate=1.3\n")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
RPCTypeCheck(request.params, {
|
||||
UniValue::VARR, // recipients
|
||||
UniValue::VNUM, // conf_target
|
||||
UniValue::VSTR, // estimate_mode
|
||||
UniValueType(), // fee_rate, will be checked by AmountFromValue() in SetFeeEstimateMode()
|
||||
UniValue::VOBJ, // options
|
||||
}, true
|
||||
);
|
||||
|
||||
// First fill transaction with our data without signing,
|
||||
// so external signers are not asked sign more than once.
|
||||
bool complete;
|
||||
pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, false, true, true);
|
||||
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, true, false, true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err);
|
||||
}
|
||||
std::shared_ptr<CWallet> const pwallet{GetWalletForJSONRPCRequest(request)};
|
||||
if (!pwallet) return NullUniValue;
|
||||
// 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
|
||||
pwallet->BlockUntilSyncedToCurrentChain();
|
||||
|
||||
CMutableTransaction mtx;
|
||||
complete = FinalizeAndExtractPSBT(psbtx, mtx);
|
||||
UniValue options{request.params[4].isNull() ? UniValue::VOBJ : request.params[4]};
|
||||
InterpretFeeEstimationInstructions(/*conf_target=*/request.params[1], /*estimate_mode=*/request.params[2], /*fee_rate=*/request.params[3], options);
|
||||
PreventOutdatedOptions(options);
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
|
||||
if (psbt_opt_in || !complete || !add_to_wallet) {
|
||||
// Serialize the PSBT
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << psbtx;
|
||||
result.pushKV("psbt", EncodeBase64(ssTx.str()));
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
std::string err_string;
|
||||
std::string hex = EncodeHexTx(CTransaction(mtx));
|
||||
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
|
||||
result.pushKV("txid", tx->GetHash().GetHex());
|
||||
if (add_to_wallet && !psbt_opt_in) {
|
||||
pwallet->CommitTransaction(tx, {}, {} /* orderForm */);
|
||||
std::set<std::string> addresses_without_amount;
|
||||
UniValue recipient_key_value_pairs(UniValue::VARR);
|
||||
const UniValue& recipients{request.params[0]};
|
||||
for (unsigned int i = 0; i < recipients.size(); ++i) {
|
||||
const UniValue& recipient{recipients[i]};
|
||||
if (recipient.isStr()) {
|
||||
UniValue rkvp(UniValue::VOBJ);
|
||||
rkvp.pushKV(recipient.get_str(), 0);
|
||||
recipient_key_value_pairs.push_back(rkvp);
|
||||
addresses_without_amount.insert(recipient.get_str());
|
||||
} else {
|
||||
result.pushKV("hex", hex);
|
||||
recipient_key_value_pairs.push_back(recipient);
|
||||
}
|
||||
}
|
||||
result.pushKV("complete", complete);
|
||||
|
||||
return result;
|
||||
if (addresses_without_amount.size() == 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Must provide at least one address without a specified amount");
|
||||
}
|
||||
|
||||
CCoinControl coin_control;
|
||||
|
||||
SetFeeEstimateMode(*pwallet, coin_control, options["conf_target"], options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
|
||||
|
||||
coin_control.fAllowWatchOnly = ParseIncludeWatchonly(options["include_watching"], *pwallet);
|
||||
|
||||
const bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf};
|
||||
|
||||
FeeCalculation fee_calc_out;
|
||||
CFeeRate fee_rate{GetMinimumFeeRate(*pwallet, coin_control, &fee_calc_out)};
|
||||
// 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 && fee_rate > *coin_control.m_feerate) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Fee rate (%s) is lower than the minimum fee rate setting (%s)", coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), fee_rate.ToString(FeeEstimateMode::SAT_VB)));
|
||||
}
|
||||
if (fee_calc_out.reason == FeeReason::FALLBACK && !pwallet->m_allow_fallback_fee) {
|
||||
// eventually allow a fallback fee
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
|
||||
}
|
||||
|
||||
CMutableTransaction rawTx{ConstructTransaction(options["inputs"], recipient_key_value_pairs, options["locktime"], rbf)};
|
||||
LOCK(pwallet->cs_wallet);
|
||||
std::vector<COutput> all_the_utxos;
|
||||
|
||||
CAmount total_input_value(0);
|
||||
bool send_max{options.exists("send_max") ? options["send_max"].get_bool() : false};
|
||||
if (options.exists("inputs") && options.exists("send_max")) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot combine send_max with specific inputs.");
|
||||
} else if (options.exists("inputs")) {
|
||||
for (const CTxIn& input : rawTx.vin) {
|
||||
if (pwallet->IsSpent(input.prevout.hash, input.prevout.n)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not available. UTXO (%s:%d) was already spent.", input.prevout.hash.ToString(), input.prevout.n));
|
||||
}
|
||||
const CWalletTx* tx{pwallet->GetWalletTx(input.prevout.hash)};
|
||||
if (!tx || pwallet->IsMine(tx->tx->vout[input.prevout.n]) != (coin_control.fAllowWatchOnly ? ISMINE_ALL : ISMINE_SPENDABLE)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not found. UTXO (%s:%d) is not part of wallet.", input.prevout.hash.ToString(), input.prevout.n));
|
||||
}
|
||||
total_input_value += tx->tx->vout[input.prevout.n].nValue;
|
||||
}
|
||||
} else {
|
||||
AvailableCoins(*pwallet, all_the_utxos, &coin_control, /*nMinimumAmount=*/0);
|
||||
for (const COutput& output : all_the_utxos) {
|
||||
CHECK_NONFATAL(output.input_bytes > 0);
|
||||
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
|
||||
continue;
|
||||
}
|
||||
CTxIn input(output.outpoint.hash, output.outpoint.n, CScript(), rbf ? MAX_BIP125_RBF_SEQUENCE : CTxIn::SEQUENCE_FINAL);
|
||||
rawTx.vin.push_back(input);
|
||||
total_input_value += output.txout.nValue;
|
||||
}
|
||||
}
|
||||
|
||||
// estimate final size of tx
|
||||
const TxSize tx_size{CalculateMaximumSignedTxSize(CTransaction(rawTx), pwallet.get())};
|
||||
const CAmount fee_from_size{fee_rate.GetFee(tx_size.vsize)};
|
||||
const CAmount effective_value{total_input_value - fee_from_size};
|
||||
|
||||
if (effective_value <= 0) {
|
||||
if (send_max) {
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Total value of UTXO pool too low to pay for transaction, try using lower feerate.");
|
||||
} else {
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Total value of UTXO pool too low to pay for transaction. Try using lower feerate or excluding uneconomic UTXOs with 'send_max' option.");
|
||||
}
|
||||
}
|
||||
|
||||
CAmount output_amounts_claimed{0};
|
||||
for (CTxOut out : rawTx.vout) {
|
||||
output_amounts_claimed += out.nValue;
|
||||
}
|
||||
|
||||
if (output_amounts_claimed > total_input_value) {
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Assigned more value to outputs than available funds.");
|
||||
}
|
||||
|
||||
const CAmount remainder{effective_value - output_amounts_claimed};
|
||||
if (remainder < 0) {
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds for fees after creating specified outputs.");
|
||||
}
|
||||
|
||||
const CAmount per_output_without_amount{remainder / (long)addresses_without_amount.size()};
|
||||
|
||||
bool gave_remaining_to_first{false};
|
||||
for (CTxOut& out : rawTx.vout) {
|
||||
CTxDestination dest;
|
||||
ExtractDestination(out.scriptPubKey, dest);
|
||||
std::string addr{EncodeDestination(dest)};
|
||||
if (addresses_without_amount.count(addr) > 0) {
|
||||
out.nValue = per_output_without_amount;
|
||||
if (!gave_remaining_to_first) {
|
||||
out.nValue += remainder % addresses_without_amount.size();
|
||||
gave_remaining_to_first = true;
|
||||
}
|
||||
if (IsDust(out, pwallet->chain().relayDustFee())) {
|
||||
// Dynamically generated output amount is dust
|
||||
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Dynamically assigned remainder results in dust output.");
|
||||
}
|
||||
} else {
|
||||
if (IsDust(out, pwallet->chain().relayDustFee())) {
|
||||
// Specified output amount is dust
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Specified output amount to %s is below dust threshold.", addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool lock_unspents{options.exists("lock_unspents") ? options["lock_unspents"].get_bool() : false};
|
||||
if (lock_unspents) {
|
||||
for (const CTxIn& txin : rawTx.vin) {
|
||||
pwallet->LockCoin(txin.prevout);
|
||||
}
|
||||
}
|
||||
|
||||
return FinishTransaction(pwallet, options, rawTx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,6 +652,7 @@ RPCHelpMan fundrawtransaction();
|
|||
RPCHelpMan bumpfee();
|
||||
RPCHelpMan psbtbumpfee();
|
||||
RPCHelpMan send();
|
||||
RPCHelpMan sendall();
|
||||
RPCHelpMan walletprocesspsbt();
|
||||
RPCHelpMan walletcreatefundedpsbt();
|
||||
RPCHelpMan signrawtransactionwithwallet();
|
||||
|
|
@ -755,6 +756,7 @@ static const CRPCCommand commands[] =
|
|||
{ "wallet", &setwalletflag, },
|
||||
{ "wallet", &signmessage, },
|
||||
{ "wallet", &signrawtransactionwithwallet, },
|
||||
{ "wallet", &sendall, },
|
||||
{ "wallet", &unloadwallet, },
|
||||
{ "wallet", &upgradewallet, },
|
||||
{ "wallet", &walletcreatefundedpsbt, },
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ BASE_SCRIPTS = [
|
|||
'wallet_create_tx.py --legacy-wallet',
|
||||
'wallet_send.py --legacy-wallet',
|
||||
'wallet_send.py --descriptors',
|
||||
'wallet_sendall.py --legacy-wallet',
|
||||
'wallet_sendall.py --descriptors',
|
||||
'wallet_create_tx.py --descriptors',
|
||||
'wallet_taproot.py',
|
||||
'wallet_inactive_hdchains.py',
|
||||
|
|
|
|||
316
test/functional/wallet_sendall.py
Executable file
316
test/functional/wallet_sendall.py
Executable file
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2022 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test the sendall RPC command."""
|
||||
|
||||
from decimal import Decimal, getcontext
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
|
||||
# Decorator to reset activewallet to zero utxos
|
||||
def cleanup(func):
|
||||
def wrapper(self):
|
||||
try:
|
||||
func(self)
|
||||
finally:
|
||||
if 0 < self.wallet.getbalances()["mine"]["trusted"]:
|
||||
self.wallet.sendall([self.remainder_target])
|
||||
assert_equal(0, self.wallet.getbalances()["mine"]["trusted"]) # wallet is empty
|
||||
return wrapper
|
||||
|
||||
class SendallTest(BitcoinTestFramework):
|
||||
# Setup and helpers
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def set_test_params(self):
|
||||
getcontext().prec=10
|
||||
self.num_nodes = 1
|
||||
self.setup_clean_chain = True
|
||||
|
||||
def assert_balance_swept_completely(self, tx, balance):
|
||||
output_sum = sum([o["value"] for o in tx["decoded"]["vout"]])
|
||||
assert_equal(output_sum, balance + tx["fee"])
|
||||
assert_equal(0, self.wallet.getbalances()["mine"]["trusted"]) # wallet is empty
|
||||
|
||||
def assert_tx_has_output(self, tx, addr, value=None):
|
||||
for output in tx["decoded"]["vout"]:
|
||||
if addr == output["scriptPubKey"]["address"] and value is None or value == output["value"]:
|
||||
return
|
||||
raise AssertionError("Output to {} not present or wrong amount".format(addr))
|
||||
|
||||
def assert_tx_has_outputs(self, tx, expected_outputs):
|
||||
assert_equal(len(expected_outputs), len(tx["decoded"]["vout"]))
|
||||
for eo in expected_outputs:
|
||||
self.assert_tx_has_output(tx, eo["address"], eo["value"])
|
||||
|
||||
def add_utxos(self, amounts):
|
||||
for a in amounts:
|
||||
self.def_wallet.sendtoaddress(self.wallet.getnewaddress(), a)
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_greater_than(self.wallet.getbalances()["mine"]["trusted"], 0)
|
||||
return self.wallet.getbalances()["mine"]["trusted"]
|
||||
|
||||
# Helper schema for success cases
|
||||
def test_sendall_success(self, sendall_args, remaining_balance = 0):
|
||||
sendall_tx_receipt = self.wallet.sendall(sendall_args)
|
||||
self.generate(self.nodes[0], 1)
|
||||
# wallet has remaining balance (usually empty)
|
||||
assert_equal(remaining_balance, self.wallet.getbalances()["mine"]["trusted"])
|
||||
|
||||
assert_equal(sendall_tx_receipt["complete"], True)
|
||||
return self.wallet.gettransaction(txid = sendall_tx_receipt["txid"], verbose = True)
|
||||
|
||||
@cleanup
|
||||
def gen_and_clean(self):
|
||||
self.add_utxos([15, 2, 4])
|
||||
|
||||
def test_cleanup(self):
|
||||
self.log.info("Test that cleanup wrapper empties wallet")
|
||||
self.gen_and_clean()
|
||||
assert_equal(0, self.wallet.getbalances()["mine"]["trusted"]) # wallet is empty
|
||||
|
||||
# Actual tests
|
||||
@cleanup
|
||||
def sendall_two_utxos(self):
|
||||
self.log.info("Testing basic sendall case without specific amounts")
|
||||
pre_sendall_balance = self.add_utxos([10,11])
|
||||
tx_from_wallet = self.test_sendall_success(sendall_args = [self.remainder_target])
|
||||
|
||||
self.assert_tx_has_outputs(tx = tx_from_wallet,
|
||||
expected_outputs = [
|
||||
{ "address": self.remainder_target, "value": pre_sendall_balance + tx_from_wallet["fee"] } # fee is neg
|
||||
]
|
||||
)
|
||||
self.assert_balance_swept_completely(tx_from_wallet, pre_sendall_balance)
|
||||
|
||||
@cleanup
|
||||
def sendall_split(self):
|
||||
self.log.info("Testing sendall where two recipients have unspecified amount")
|
||||
pre_sendall_balance = self.add_utxos([1, 2, 3, 15])
|
||||
tx_from_wallet = self.test_sendall_success([self.remainder_target, self.split_target])
|
||||
|
||||
half = (pre_sendall_balance + tx_from_wallet["fee"]) / 2
|
||||
self.assert_tx_has_outputs(tx_from_wallet,
|
||||
expected_outputs = [
|
||||
{ "address": self.split_target, "value": half },
|
||||
{ "address": self.remainder_target, "value": half }
|
||||
]
|
||||
)
|
||||
self.assert_balance_swept_completely(tx_from_wallet, pre_sendall_balance)
|
||||
|
||||
@cleanup
|
||||
def sendall_and_spend(self):
|
||||
self.log.info("Testing sendall in combination with paying specified amount to recipient")
|
||||
pre_sendall_balance = self.add_utxos([8, 13])
|
||||
tx_from_wallet = self.test_sendall_success([{self.recipient: 5}, self.remainder_target])
|
||||
|
||||
self.assert_tx_has_outputs(tx_from_wallet,
|
||||
expected_outputs = [
|
||||
{ "address": self.recipient, "value": 5 },
|
||||
{ "address": self.remainder_target, "value": pre_sendall_balance - 5 + tx_from_wallet["fee"] }
|
||||
]
|
||||
)
|
||||
self.assert_balance_swept_completely(tx_from_wallet, pre_sendall_balance)
|
||||
|
||||
@cleanup
|
||||
def sendall_invalid_recipient_addresses(self):
|
||||
self.log.info("Test having only recipient with specified amount, missing recipient with unspecified amount")
|
||||
self.add_utxos([12, 9])
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-8,
|
||||
"Must provide at least one address without a specified amount" ,
|
||||
self.wallet.sendall,
|
||||
[{self.recipient: 5}]
|
||||
)
|
||||
|
||||
@cleanup
|
||||
def sendall_duplicate_recipient(self):
|
||||
self.log.info("Test duplicate destination")
|
||||
self.add_utxos([1, 8, 3, 9])
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-8,
|
||||
"Invalid parameter, duplicated address: {}".format(self.remainder_target),
|
||||
self.wallet.sendall,
|
||||
[self.remainder_target, self.remainder_target]
|
||||
)
|
||||
|
||||
@cleanup
|
||||
def sendall_invalid_amounts(self):
|
||||
self.log.info("Test sending more than balance")
|
||||
pre_sendall_balance = self.add_utxos([7, 14])
|
||||
|
||||
expected_tx = self.wallet.sendall(recipients=[{self.recipient: 5}, self.remainder_target], options={"add_to_wallet": False})
|
||||
tx = self.wallet.decoderawtransaction(expected_tx['hex'])
|
||||
fee = 21 - sum([o["value"] for o in tx["vout"]])
|
||||
|
||||
assert_raises_rpc_error(-6, "Assigned more value to outputs than available funds.", self.wallet.sendall,
|
||||
[{self.recipient: pre_sendall_balance + 1}, self.remainder_target])
|
||||
assert_raises_rpc_error(-6, "Insufficient funds for fees after creating specified outputs.", self.wallet.sendall,
|
||||
[{self.recipient: pre_sendall_balance}, self.remainder_target])
|
||||
assert_raises_rpc_error(-8, "Specified output amount to {} is below dust threshold".format(self.recipient),
|
||||
self.wallet.sendall, [{self.recipient: 0.00000001}, self.remainder_target])
|
||||
assert_raises_rpc_error(-6, "Dynamically assigned remainder results in dust output.", self.wallet.sendall,
|
||||
[{self.recipient: pre_sendall_balance - fee}, self.remainder_target])
|
||||
assert_raises_rpc_error(-6, "Dynamically assigned remainder results in dust output.", self.wallet.sendall,
|
||||
[{self.recipient: pre_sendall_balance - fee - Decimal(0.00000010)}, self.remainder_target])
|
||||
|
||||
# @cleanup not needed because different wallet used
|
||||
def sendall_negative_effective_value(self):
|
||||
self.log.info("Test that sendall fails if all UTXOs have negative effective value")
|
||||
# Use dedicated wallet for dust amounts and unload wallet at end
|
||||
self.nodes[0].createwallet("dustwallet")
|
||||
dust_wallet = self.nodes[0].get_wallet_rpc("dustwallet")
|
||||
|
||||
self.def_wallet.sendtoaddress(dust_wallet.getnewaddress(), 0.00000400)
|
||||
self.def_wallet.sendtoaddress(dust_wallet.getnewaddress(), 0.00000300)
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_greater_than(dust_wallet.getbalances()["mine"]["trusted"], 0)
|
||||
|
||||
assert_raises_rpc_error(-6, "Total value of UTXO pool too low to pay for transaction."
|
||||
+ " Try using lower feerate or excluding uneconomic UTXOs with 'send_max' option.",
|
||||
dust_wallet.sendall, recipients=[self.remainder_target], fee_rate=300)
|
||||
|
||||
dust_wallet.unloadwallet()
|
||||
|
||||
@cleanup
|
||||
def sendall_with_send_max(self):
|
||||
self.log.info("Check that `send_max` option causes negative value UTXOs to be left behind")
|
||||
self.add_utxos([0.00000400, 0.00000300, 1])
|
||||
|
||||
# sendall with send_max
|
||||
sendall_tx_receipt = self.wallet.sendall(recipients=[self.remainder_target], fee_rate=300, options={"send_max": True})
|
||||
tx_from_wallet = self.wallet.gettransaction(txid = sendall_tx_receipt["txid"], verbose = True)
|
||||
|
||||
assert_equal(len(tx_from_wallet["decoded"]["vin"]), 1)
|
||||
self.assert_tx_has_outputs(tx_from_wallet, [{"address": self.remainder_target, "value": 1 + tx_from_wallet["fee"]}])
|
||||
assert_equal(self.wallet.getbalances()["mine"]["trusted"], Decimal("0.00000700"))
|
||||
|
||||
self.def_wallet.sendtoaddress(self.wallet.getnewaddress(), 1)
|
||||
self.generate(self.nodes[0], 1)
|
||||
|
||||
@cleanup
|
||||
def sendall_specific_inputs(self):
|
||||
self.log.info("Test sendall with a subset of UTXO pool")
|
||||
self.add_utxos([17, 4])
|
||||
utxo = self.wallet.listunspent()[0]
|
||||
|
||||
sendall_tx_receipt = self.wallet.sendall(recipients=[self.remainder_target], options={"inputs": [utxo]})
|
||||
tx_from_wallet = self.wallet.gettransaction(txid = sendall_tx_receipt["txid"], verbose = True)
|
||||
assert_equal(len(tx_from_wallet["decoded"]["vin"]), 1)
|
||||
assert_equal(len(tx_from_wallet["decoded"]["vout"]), 1)
|
||||
assert_equal(tx_from_wallet["decoded"]["vin"][0]["txid"], utxo["txid"])
|
||||
assert_equal(tx_from_wallet["decoded"]["vin"][0]["vout"], utxo["vout"])
|
||||
self.assert_tx_has_output(tx_from_wallet, self.remainder_target)
|
||||
|
||||
self.generate(self.nodes[0], 1)
|
||||
assert_greater_than(self.wallet.getbalances()["mine"]["trusted"], 0)
|
||||
|
||||
@cleanup
|
||||
def sendall_fails_on_missing_input(self):
|
||||
# fails because UTXO was previously spent, and wallet is empty
|
||||
self.log.info("Test sendall fails because specified UTXO is not available")
|
||||
self.add_utxos([16, 5])
|
||||
spent_utxo = self.wallet.listunspent()[0]
|
||||
|
||||
# fails on unconfirmed spent UTXO
|
||||
self.wallet.sendall(recipients=[self.remainder_target])
|
||||
assert_raises_rpc_error(-8,
|
||||
"Input not available. UTXO ({}:{}) was already spent.".format(spent_utxo["txid"], spent_utxo["vout"]),
|
||||
self.wallet.sendall, recipients=[self.remainder_target], options={"inputs": [spent_utxo]})
|
||||
|
||||
# fails on specific previously spent UTXO, while other UTXOs exist
|
||||
self.generate(self.nodes[0], 1)
|
||||
self.add_utxos([19, 2])
|
||||
assert_raises_rpc_error(-8,
|
||||
"Input not available. UTXO ({}:{}) was already spent.".format(spent_utxo["txid"], spent_utxo["vout"]),
|
||||
self.wallet.sendall, recipients=[self.remainder_target], options={"inputs": [spent_utxo]})
|
||||
|
||||
# fails because UTXO is unknown, while other UTXOs exist
|
||||
foreign_utxo = self.def_wallet.listunspent()[0]
|
||||
assert_raises_rpc_error(-8, "Input not found. UTXO ({}:{}) is not part of wallet.".format(foreign_utxo["txid"],
|
||||
foreign_utxo["vout"]), self.wallet.sendall, recipients=[self.remainder_target],
|
||||
options={"inputs": [foreign_utxo]})
|
||||
|
||||
@cleanup
|
||||
def sendall_fails_on_no_address(self):
|
||||
self.log.info("Test sendall fails because no address is provided")
|
||||
self.add_utxos([19, 2])
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-8,
|
||||
"Must provide at least one address without a specified amount" ,
|
||||
self.wallet.sendall,
|
||||
[]
|
||||
)
|
||||
|
||||
@cleanup
|
||||
def sendall_fails_on_specific_inputs_with_send_max(self):
|
||||
self.log.info("Test sendall fails because send_max is used while specific inputs are provided")
|
||||
self.add_utxos([15, 6])
|
||||
utxo = self.wallet.listunspent()[0]
|
||||
|
||||
assert_raises_rpc_error(-8,
|
||||
"Cannot combine send_max with specific inputs.",
|
||||
self.wallet.sendall,
|
||||
recipients=[self.remainder_target],
|
||||
options={"inputs": [utxo], "send_max": True})
|
||||
|
||||
def run_test(self):
|
||||
self.nodes[0].createwallet("activewallet")
|
||||
self.wallet = self.nodes[0].get_wallet_rpc("activewallet")
|
||||
self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||
self.generate(self.nodes[0], 101)
|
||||
self.recipient = self.def_wallet.getnewaddress() # payee for a specific amount
|
||||
self.remainder_target = self.def_wallet.getnewaddress() # address that receives everything left after payments and fees
|
||||
self.split_target = self.def_wallet.getnewaddress() # 2nd target when splitting rest
|
||||
|
||||
# Test cleanup
|
||||
self.test_cleanup()
|
||||
|
||||
# Basic sweep: everything to one address
|
||||
self.sendall_two_utxos()
|
||||
|
||||
# Split remainder to two addresses with equal amounts
|
||||
self.sendall_split()
|
||||
|
||||
# Pay recipient and sweep remainder
|
||||
self.sendall_and_spend()
|
||||
|
||||
# sendall fails if no recipient has unspecified amount
|
||||
self.sendall_invalid_recipient_addresses()
|
||||
|
||||
# Sendall fails if same destination is provided twice
|
||||
self.sendall_duplicate_recipient()
|
||||
|
||||
# Sendall fails when trying to spend more than the balance
|
||||
self.sendall_invalid_amounts()
|
||||
|
||||
# Sendall fails when wallet has no economically spendable UTXOs
|
||||
self.sendall_negative_effective_value()
|
||||
|
||||
# Leave dust behind if using send_max
|
||||
self.sendall_with_send_max()
|
||||
|
||||
# Sendall succeeds with specific inputs
|
||||
self.sendall_specific_inputs()
|
||||
|
||||
# Fails for the right reasons on missing or previously spent UTXOs
|
||||
self.sendall_fails_on_missing_input()
|
||||
|
||||
# Sendall fails when no address is provided
|
||||
self.sendall_fails_on_no_address()
|
||||
|
||||
# Sendall fails when using send_max while specifying inputs
|
||||
self.sendall_fails_on_specific_inputs_with_send_max()
|
||||
|
||||
if __name__ == '__main__':
|
||||
SendallTest().main()
|
||||
|
|
@ -195,6 +195,12 @@ class WalletSignerTest(BitcoinTestFramework):
|
|||
assert(res["complete"])
|
||||
assert_equal(res["hex"], mock_tx)
|
||||
|
||||
self.log.info('Test sendall using hww1')
|
||||
|
||||
res = hww.sendall(recipients=[{dest:0.5}, hww.getrawchangeaddress()],options={"add_to_wallet": False})
|
||||
assert(res["complete"])
|
||||
assert_equal(res["hex"], mock_tx)
|
||||
|
||||
# # Handle error thrown by script
|
||||
# self.set_mock_result(self.nodes[4], "2")
|
||||
# assert_raises_rpc_error(-1, 'Unable to parse JSON',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue