aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/rpc/spend.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/rpc/spend.cpp')
-rw-r--r--src/wallet/rpc/spend.cpp447
1 files changed, 348 insertions, 99 deletions
diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index 072879a42a..07119133b7 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -9,10 +9,12 @@
#include <rpc/rawtransaction_util.h>
#include <rpc/util.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/fees.h>
#include <wallet/rpc/util.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
@@ -21,7 +23,8 @@
namespace wallet {
-static void ParseRecipients(const UniValue& address_amounts, const UniValue& subtract_fee_outputs, std::vector<CRecipient> &recipients) {
+static void ParseRecipients(const UniValue& address_amounts, const UniValue& subtract_fee_outputs, std::vector<CRecipient>& recipients)
+{
std::set<CTxDestination> destinations;
int i = 0;
for (const std::string& address: address_amounts.getKeys()) {
@@ -51,6 +54,93 @@ static void ParseRecipients(const UniValue& address_amounts, const UniValue& sub
}
}
+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);
+
+ // 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);
+ const TransactionError err{pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, true, false)};
+ 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)
{
EnsureWalletIsUnlocked(wallet);
@@ -108,7 +198,7 @@ static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const Un
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)};
+ cc.m_feerate = CFeeRate{AmountFromValue(fee_rate, /*decimals=*/3)};
if (override_min_fee) cc.fOverrideFeeRate = true;
// Default RBF to true for explicit fee_rate, if unset.
if (!cc.m_signal_bip125_rbf) cc.m_signal_bip125_rbf = true;
@@ -203,7 +293,7 @@ RPCHelpMan sendtoaddress()
// We also enable partial spend avoidance if reuse avoidance is set.
coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse;
- SetFeeEstimateMode(*pwallet, coin_control, /* conf_target */ request.params[6], /* estimate_mode */ request.params[7], /* fee_rate */ request.params[9], /* override_min_fee */ false);
+ SetFeeEstimateMode(*pwallet, coin_control, /*conf_target=*/request.params[6], /*estimate_mode=*/request.params[7], /*fee_rate=*/request.params[9], /*override_min_fee=*/false);
EnsureWalletIsUnlocked(*pwallet);
@@ -306,7 +396,7 @@ RPCHelpMan sendmany()
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
}
- SetFeeEstimateMode(*pwallet, coin_control, /* conf_target */ request.params[6], /* estimate_mode */ request.params[7], /* fee_rate */ request.params[8], /* override_min_fee */ false);
+ SetFeeEstimateMode(*pwallet, coin_control, /*conf_target=*/request.params[6], /*estimate_mode=*/request.params[7], /*fee_rate=*/request.params[8], /*override_min_fee=*/false);
std::vector<CRecipient> recipients;
ParseRecipients(sendTo, subtractFeeFromAmount, recipients);
@@ -360,31 +450,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, bool override_min_fee)
@@ -736,7 +838,7 @@ RPCHelpMan fundrawtransaction()
CCoinControl coin_control;
// Automatically select (additional) coins. Can be overridden by options.add_inputs.
coin_control.m_add_inputs = true;
- FundTransaction(*pwallet, tx, fee, change_position, request.params[1], coin_control, /* override_min_fee */ true);
+ FundTransaction(*pwallet, tx, fee, change_position, request.params[1], coin_control, /*override_min_fee=*/true);
UniValue result(UniValue::VOBJ);
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
@@ -941,7 +1043,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
if (options.exists("replaceable")) {
coin_control.m_signal_bip125_rbf = options["replaceable"].get_bool();
}
- SetFeeEstimateMode(*pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"], /* override_min_fee */ false);
+ SetFeeEstimateMode(*pwallet, coin_control, conf_target, options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
}
// Make sure the results are valid at least up to the most recent block
@@ -1126,102 +1228,249 @@ 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);
CCoinControl coin_control;
// Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0;
SetOptionsInputWeights(options["inputs"], options);
- FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control, /* override_min_fee */ false);
+ FundTransaction(*pwallet, rawTx, fee, change_position, options, coin_control, /*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);
+ }
+ };
+}
+
+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
+ );
+
+ 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();
+
+ 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);
+
+
+ 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 {
+ recipient_key_value_pairs.push_back(recipient);
+ }
+ }
+
+ if (addresses_without_amount.size() == 0) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Must provide at least one address without a specified amount");
}
- // Make a blank psbt
- PartiallySignedTransaction psbtx(rawTx);
+ CCoinControl coin_control;
+
+ SetFeeEstimateMode(*pwallet, coin_control, options["conf_target"], options["estimate_mode"], options["fee_rate"], /*override_min_fee=*/false);
- // 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);
- const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, true, false);
- if (err != TransactionError::OK) {
- throw JSONRPCTransactionError(err);
+ 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 mtx;
- complete = FinalizeAndExtractPSBT(psbtx, mtx);
+ 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;
+ }
+ }
- UniValue result(UniValue::VOBJ);
+ // 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 (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 (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.");
+ }
}
- 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 */);
+ 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 {
- result.pushKV("hex", hex);
+ 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);
}
}
- result.pushKV("complete", complete);
- return result;
+ return FinishTransaction(pwallet, options, rawTx);
}
};
}
@@ -1418,7 +1667,7 @@ RPCHelpMan walletcreatefundedpsbt()
// be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0;
SetOptionsInputWeights(request.params[0], options);
- FundTransaction(wallet, rawTx, fee, change_position, options, coin_control, /* override_min_fee */ true);
+ FundTransaction(wallet, rawTx, fee, change_position, options, coin_control, /*override_min_fee=*/true);
// Make a blank psbt
PartiallySignedTransaction psbtx(rawTx);