-
Notifications
You must be signed in to change notification settings - Fork 35.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wallet, rpc: FundTransaction
refactor
#28560
Changes from all commits
435fe5c
6f569ac
f7384b9
47353a6
5ad1966
18ad1b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,34 +24,15 @@ | |
|
||
|
||
namespace wallet { | ||
static void ParseRecipients(const UniValue& address_amounts, const UniValue& subtract_fee_outputs, std::vector<CRecipient>& recipients) | ||
std::vector<CRecipient> CreateRecipients(const std::vector<std::pair<CTxDestination, CAmount>>& outputs, const std::set<int>& subtract_fee_outputs) | ||
{ | ||
std::set<CTxDestination> destinations; | ||
int i = 0; | ||
for (const std::string& address: address_amounts.getKeys()) { | ||
CTxDestination dest = DecodeDestination(address); | ||
if (!IsValidDestination(dest)) { | ||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + address); | ||
} | ||
|
||
if (destinations.count(dest)) { | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + address); | ||
} | ||
destinations.insert(dest); | ||
|
||
CAmount amount = AmountFromValue(address_amounts[i++]); | ||
|
||
bool subtract_fee = false; | ||
for (unsigned int idx = 0; idx < subtract_fee_outputs.size(); idx++) { | ||
const UniValue& addr = subtract_fee_outputs[idx]; | ||
if (addr.get_str() == address) { | ||
subtract_fee = true; | ||
} | ||
} | ||
|
||
CRecipient recipient = {dest, amount, subtract_fee}; | ||
std::vector<CRecipient> recipients; | ||
for (size_t i = 0; i < outputs.size(); ++i) { | ||
const auto& [destination, amount] = outputs.at(i); | ||
CRecipient recipient{destination, amount, subtract_fee_outputs.contains(i)}; | ||
recipients.push_back(recipient); | ||
} | ||
return recipients; | ||
} | ||
|
||
static void InterpretFeeEstimationInstructions(const UniValue& conf_target, const UniValue& estimate_mode, const UniValue& fee_rate, UniValue& options) | ||
|
@@ -76,6 +57,37 @@ static void InterpretFeeEstimationInstructions(const UniValue& conf_target, cons | |
} | ||
} | ||
|
||
std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_instructions, const std::vector<std::string>& destinations) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in 0b0276b I don't see a reason to extract this function at this point (could be done later in the PR). Wouldn't it be better if we keep this logic at the call site?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer having a single function (as opposed to keeping the logic at the call site) for the following reasons:
In the case of
Actually, the main goal here was to separate the logic in The other RPCs ( |
||
{ | ||
std::set<int> sffo_set; | ||
if (sffo_instructions.isNull()) return sffo_set; | ||
if (sffo_instructions.isBool()) { | ||
if (sffo_instructions.get_bool()) sffo_set.insert(0); | ||
return sffo_set; | ||
} | ||
for (const auto& sffo : sffo_instructions.getValues()) { | ||
if (sffo.isStr()) { | ||
for (size_t i = 0; i < destinations.size(); ++i) { | ||
if (sffo.get_str() == destinations.at(i)) { | ||
sffo_set.insert(i); | ||
break; | ||
} | ||
} | ||
} | ||
if (sffo.isNum()) { | ||
int pos = sffo.getInt<int>(); | ||
if (sffo_set.contains(pos)) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos)); | ||
if (pos < 0) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos)); | ||
if (pos >= int(destinations.size())) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos)); | ||
sffo_set.insert(pos); | ||
} | ||
} | ||
return sffo_set; | ||
} | ||
|
||
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx) | ||
{ | ||
// Make a blank psbt | ||
|
@@ -275,11 +287,6 @@ RPCHelpMan sendtoaddress() | |
if (!request.params[3].isNull() && !request.params[3].get_str().empty()) | ||
mapValue["to"] = request.params[3].get_str(); | ||
|
||
bool fSubtractFeeFromAmount = false; | ||
if (!request.params[4].isNull()) { | ||
fSubtractFeeFromAmount = request.params[4].get_bool(); | ||
} | ||
|
||
CCoinControl coin_control; | ||
if (!request.params[5].isNull()) { | ||
coin_control.m_signal_bip125_rbf = request.params[5].get_bool(); | ||
|
@@ -296,13 +303,10 @@ RPCHelpMan sendtoaddress() | |
UniValue address_amounts(UniValue::VOBJ); | ||
const std::string address = request.params[0].get_str(); | ||
address_amounts.pushKV(address, request.params[1]); | ||
UniValue subtractFeeFromAmount(UniValue::VARR); | ||
if (fSubtractFeeFromAmount) { | ||
subtractFeeFromAmount.push_back(address); | ||
} | ||
|
||
std::vector<CRecipient> recipients; | ||
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients); | ||
std::vector<CRecipient> recipients = CreateRecipients( | ||
ParseOutputs(address_amounts), | ||
InterpretSubtractFeeFromOutputInstructions(request.params[4], address_amounts.getKeys()) | ||
); | ||
const bool verbose{request.params[10].isNull() ? false : request.params[10].get_bool()}; | ||
|
||
return SendMoney(*pwallet, coin_control, recipients, mapValue, verbose); | ||
|
@@ -386,19 +390,17 @@ RPCHelpMan sendmany() | |
if (!request.params[3].isNull() && !request.params[3].get_str().empty()) | ||
mapValue["comment"] = request.params[3].get_str(); | ||
|
||
UniValue subtractFeeFromAmount(UniValue::VARR); | ||
if (!request.params[4].isNull()) | ||
subtractFeeFromAmount = request.params[4].get_array(); | ||
|
||
CCoinControl coin_control; | ||
if (!request.params[5].isNull()) { | ||
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); | ||
|
||
std::vector<CRecipient> recipients; | ||
ParseRecipients(sendTo, subtractFeeFromAmount, recipients); | ||
std::vector<CRecipient> recipients = CreateRecipients( | ||
ParseOutputs(sendTo), | ||
InterpretSubtractFeeFromOutputInstructions(request.params[4], sendTo.getKeys()) | ||
); | ||
const bool verbose{request.params[9].isNull() ? false : request.params[9].get_bool()}; | ||
|
||
return SendMoney(*pwallet, coin_control, recipients, std::move(mapValue), verbose); | ||
|
@@ -488,17 +490,17 @@ static std::vector<RPCArg> FundTxDoc(bool solving_data = true) | |
return args; | ||
} | ||
|
||
CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const UniValue& options, CCoinControl& coinControl, bool override_min_fee) | ||
CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const std::vector<CRecipient>& recipients, const UniValue& options, CCoinControl& coinControl, bool override_min_fee) | ||
josibake marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// We want to make sure tx.vout is not used now that we are passing outputs as a vector of recipients. | ||
// This sets us up to remove tx completely in a future PR in favor of passing the inputs directly. | ||
CHECK_NONFATAL(tx.vout.empty()); | ||
// 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 | ||
wallet.BlockUntilSyncedToCurrentChain(); | ||
|
||
std::optional<unsigned int> change_position; | ||
bool lockUnspents = false; | ||
UniValue subtractFeeFromOutputs; | ||
std::set<int> setSubtractFeeFromOutputs; | ||
|
||
if (!options.isNull()) { | ||
if (options.type() == UniValue::VBOOL) { | ||
// backward compatibility bool only fallback | ||
|
@@ -553,7 +555,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact | |
|
||
if (options.exists("changePosition") || options.exists("change_position")) { | ||
int pos = (options.exists("change_position") ? options["change_position"] : options["changePosition"]).getInt<int>(); | ||
if (pos < 0 || (unsigned int)pos > tx.vout.size()) { | ||
if (pos < 0 || (unsigned int)pos > recipients.size()) { | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds"); | ||
} | ||
change_position = (unsigned int)pos; | ||
|
@@ -595,9 +597,6 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact | |
coinControl.fOverrideFeeRate = true; | ||
} | ||
|
||
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(); | ||
|
||
if (options.exists("replaceable")) { | ||
coinControl.m_signal_bip125_rbf = options["replaceable"].get_bool(); | ||
} | ||
|
@@ -703,21 +702,10 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact | |
} | ||
} | ||
|
||
if (tx.vout.size() == 0) | ||
if (recipients.empty()) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output"); | ||
|
||
for (unsigned int idx = 0; idx < subtractFeeFromOutputs.size(); idx++) { | ||
int pos = subtractFeeFromOutputs[idx].getInt<int>(); | ||
if (setSubtractFeeFromOutputs.count(pos)) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos)); | ||
if (pos < 0) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos)); | ||
if (pos >= int(tx.vout.size())) | ||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos)); | ||
setSubtractFeeFromOutputs.insert(pos); | ||
} | ||
|
||
auto txr = FundTransaction(wallet, tx, change_position, lockUnspents, setSubtractFeeFromOutputs, coinControl); | ||
auto txr = FundTransaction(wallet, tx, recipients, change_position, lockUnspents, coinControl); | ||
if (!txr) { | ||
throw JSONRPCError(RPC_WALLET_ERROR, ErrorString(txr).original); | ||
} | ||
|
@@ -843,11 +831,25 @@ RPCHelpMan fundrawtransaction() | |
if (!DecodeHexTx(tx, request.params[0].get_str(), try_no_witness, try_witness)) { | ||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); | ||
} | ||
|
||
UniValue options = request.params[1]; | ||
std::vector<std::pair<CTxDestination, CAmount>> destinations; | ||
for (const auto& tx_out : tx.vout) { | ||
CTxDestination dest; | ||
ExtractDestination(tx_out.scriptPubKey, dest); | ||
destinations.emplace_back(dest, tx_out.nValue); | ||
} | ||
std::vector<std::string> dummy(destinations.size(), "dummy"); | ||
std::vector<CRecipient> recipients = CreateRecipients( | ||
destinations, | ||
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], dummy) | ||
); | ||
CCoinControl coin_control; | ||
// Automatically select (additional) coins. Can be overridden by options.add_inputs. | ||
coin_control.m_allow_other_inputs = true; | ||
auto txr = FundTransaction(*pwallet, tx, request.params[1], coin_control, /*override_min_fee=*/true); | ||
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly. | ||
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly | ||
tx.vout.clear(); | ||
auto txr = FundTransaction(*pwallet, tx, recipients, options, coin_control, /*override_min_fee=*/true); | ||
|
||
UniValue result(UniValue::VOBJ); | ||
result.pushKV("hex", EncodeHexTx(*txr.tx)); | ||
|
@@ -1275,13 +1277,22 @@ RPCHelpMan send() | |
|
||
|
||
bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf}; | ||
UniValue outputs(UniValue::VOBJ); | ||
outputs = NormalizeOutputs(request.params[0]); | ||
std::vector<CRecipient> recipients = CreateRecipients( | ||
ParseOutputs(outputs), | ||
InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys()) | ||
); | ||
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_allow_other_inputs = rawTx.vin.size() == 0; | ||
SetOptionsInputWeights(options["inputs"], options); | ||
auto txr = FundTransaction(*pwallet, rawTx, options, coin_control, /*override_min_fee=*/false); | ||
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly. | ||
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly | ||
rawTx.vout.clear(); | ||
auto txr = FundTransaction(*pwallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/false); | ||
|
||
return FinishTransaction(pwallet, options, CMutableTransaction(*txr.tx)); | ||
} | ||
|
@@ -1711,12 +1722,21 @@ RPCHelpMan walletcreatefundedpsbt() | |
const UniValue &replaceable_arg = options["replaceable"]; | ||
const bool rbf{replaceable_arg.isNull() ? wallet.m_signal_rbf : replaceable_arg.get_bool()}; | ||
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf); | ||
UniValue outputs(UniValue::VOBJ); | ||
outputs = NormalizeOutputs(request.params[1]); | ||
std::vector<CRecipient> recipients = CreateRecipients( | ||
ParseOutputs(outputs), | ||
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys()) | ||
); | ||
CCoinControl coin_control; | ||
// Automatically select coins, unless at least one is manually selected. Can | ||
// be overridden by options.add_inputs. | ||
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0; | ||
SetOptionsInputWeights(request.params[0], options); | ||
auto txr = FundTransaction(wallet, rawTx, options, coin_control, /*override_min_fee=*/true); | ||
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly. | ||
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly | ||
rawTx.vout.clear(); | ||
auto txr = FundTransaction(wallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/true); | ||
|
||
// Make a blank psbt | ||
PartiallySignedTransaction psbtx(CMutableTransaction(*txr.tx)); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In f7384b9 "refactor: move parsing to new function"
nit: Making temp variables is not necessary.