Skip to content

Commit e69796c

Browse files
committed
Merge bitcoin/bitcoin#28560: wallet, rpc: FundTransaction refactor
18ad1b9 refactor: pass CRecipient to FundTransaction (josibake) 5ad1966 refactor: simplify `CreateRecipients` (josibake) 47353a6 refactor: remove out param from `ParseRecipients` (josibake) f7384b9 refactor: move parsing to new function (josibake) 6f569ac refactor: move normalization to new function (josibake) 435fe5c test: add tests for fundrawtx and sendmany rpcs (josibake) Pull request description: ## Motivation The primary motivation for this PR is to enable `FundTransaction` to take a vector of `CRecipient` objects to allow passing BIP352 silent payment addresses to RPCs that use `FundTransaction` (e.g. `send`, `walletcreatefundedpsbt`). To do that, SFFO logic needs to be moved out of `FundTransaction` so the `CRecipient` objects with the correct SFFO information can be created and then passed to `FundTransaction`. As a secondary motivation, this PR moves the SFFO stuff closer to the caller, making the code cleaner and easier to understand. This is done by having a single function which parses RPC inputs for SFFO and consistently using the `set<int>` method for communicating SFFO. I'm also not convinced we need to pass a full `CMutableTx` object to `FundTransaction`, but I'm leaving that for a follow-up PR/discussion, as its not a blocker for silent payments. ACKs for top commit: S3RK: reACK 18ad1b9 josibake: > According to my `range-diff` nothing changed. reACK [18ad1b9](bitcoin/bitcoin@18ad1b9) achow101: ACK 18ad1b9 Tree-SHA512: d61f017cf7d98489ef216475b68693fd77e7b53a26a6477dcd73e7e5ceff5036b2d21476e377839e710bb73644759d42c4f9f4b14ed96b3e56ed87b07aa6d1a7
2 parents 2f218c6 + 18ad1b9 commit e69796c

File tree

9 files changed

+216
-94
lines changed

9 files changed

+216
-94
lines changed

src/rpc/rawtransaction_util.cpp

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, std::optio
7070
}
7171
}
7272

73-
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
73+
UniValue NormalizeOutputs(const UniValue& outputs_in)
7474
{
7575
if (outputs_in.isNull()) {
7676
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
@@ -94,37 +94,52 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
9494
}
9595
outputs = std::move(outputs_dict);
9696
}
97+
return outputs;
98+
}
9799

100+
std::vector<std::pair<CTxDestination, CAmount>> ParseOutputs(const UniValue& outputs)
101+
{
98102
// Duplicate checking
99103
std::set<CTxDestination> destinations;
104+
std::vector<std::pair<CTxDestination, CAmount>> parsed_outputs;
100105
bool has_data{false};
101-
102106
for (const std::string& name_ : outputs.getKeys()) {
103107
if (name_ == "data") {
104108
if (has_data) {
105109
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, duplicate key: data");
106110
}
107111
has_data = true;
108112
std::vector<unsigned char> data = ParseHexV(outputs[name_].getValStr(), "Data");
109-
110-
CTxOut out(0, CScript() << OP_RETURN << data);
111-
rawTx.vout.push_back(out);
113+
CTxDestination destination{CNoDestination{CScript() << OP_RETURN << data}};
114+
CAmount amount{0};
115+
parsed_outputs.emplace_back(destination, amount);
112116
} else {
113-
CTxDestination destination = DecodeDestination(name_);
117+
CTxDestination destination{DecodeDestination(name_)};
118+
CAmount amount{AmountFromValue(outputs[name_])};
114119
if (!IsValidDestination(destination)) {
115120
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + name_);
116121
}
117122

118123
if (!destinations.insert(destination).second) {
119124
throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + name_);
120125
}
126+
parsed_outputs.emplace_back(destination, amount);
127+
}
128+
}
129+
return parsed_outputs;
130+
}
131+
132+
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
133+
{
134+
UniValue outputs(UniValue::VOBJ);
135+
outputs = NormalizeOutputs(outputs_in);
121136

122-
CScript scriptPubKey = GetScriptForDestination(destination);
123-
CAmount nAmount = AmountFromValue(outputs[name_]);
137+
std::vector<std::pair<CTxDestination, CAmount>> parsed_outputs = ParseOutputs(outputs);
138+
for (const auto& [destination, nAmount] : parsed_outputs) {
139+
CScript scriptPubKey = GetScriptForDestination(destination);
124140

125-
CTxOut out(nAmount, scriptPubKey);
126-
rawTx.vout.push_back(out);
127-
}
141+
CTxOut out(nAmount, scriptPubKey);
142+
rawTx.vout.push_back(out);
128143
}
129144
}
130145

src/rpc/rawtransaction_util.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#ifndef BITCOIN_RPC_RAWTRANSACTION_UTIL_H
66
#define BITCOIN_RPC_RAWTRANSACTION_UTIL_H
77

8+
#include <addresstype.h>
9+
#include <consensus/amount.h>
810
#include <map>
911
#include <string>
1012
#include <optional>
@@ -38,11 +40,16 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
3840
*/
3941
void ParsePrevouts(const UniValue& prevTxsUnival, FillableSigningProvider* keystore, std::map<COutPoint, Coin>& coins);
4042

41-
4243
/** Normalize univalue-represented inputs and add them to the transaction */
4344
void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, bool rbf);
4445

45-
/** Normalize univalue-represented outputs and add them to the transaction */
46+
/** Normalize univalue-represented outputs */
47+
UniValue NormalizeOutputs(const UniValue& outputs_in);
48+
49+
/** Parse normalized outputs into destination, amount tuples */
50+
std::vector<std::pair<CTxDestination, CAmount>> ParseOutputs(const UniValue& outputs);
51+
52+
/** Normalize, parse, and add outputs to the transaction */
4653
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
4754

4855
/** Create a transaction from univalue parameters */

src/wallet/rpc/spend.cpp

Lines changed: 88 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,15 @@
2424

2525

2626
namespace wallet {
27-
static void ParseRecipients(const UniValue& address_amounts, const UniValue& subtract_fee_outputs, std::vector<CRecipient>& recipients)
27+
std::vector<CRecipient> CreateRecipients(const std::vector<std::pair<CTxDestination, CAmount>>& outputs, const std::set<int>& subtract_fee_outputs)
2828
{
29-
std::set<CTxDestination> destinations;
30-
int i = 0;
31-
for (const std::string& address: address_amounts.getKeys()) {
32-
CTxDestination dest = DecodeDestination(address);
33-
if (!IsValidDestination(dest)) {
34-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + address);
35-
}
36-
37-
if (destinations.count(dest)) {
38-
throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + address);
39-
}
40-
destinations.insert(dest);
41-
42-
CAmount amount = AmountFromValue(address_amounts[i++]);
43-
44-
bool subtract_fee = false;
45-
for (unsigned int idx = 0; idx < subtract_fee_outputs.size(); idx++) {
46-
const UniValue& addr = subtract_fee_outputs[idx];
47-
if (addr.get_str() == address) {
48-
subtract_fee = true;
49-
}
50-
}
51-
52-
CRecipient recipient = {dest, amount, subtract_fee};
29+
std::vector<CRecipient> recipients;
30+
for (size_t i = 0; i < outputs.size(); ++i) {
31+
const auto& [destination, amount] = outputs.at(i);
32+
CRecipient recipient{destination, amount, subtract_fee_outputs.contains(i)};
5333
recipients.push_back(recipient);
5434
}
35+
return recipients;
5536
}
5637

5738
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
7657
}
7758
}
7859

60+
std::set<int> InterpretSubtractFeeFromOutputInstructions(const UniValue& sffo_instructions, const std::vector<std::string>& destinations)
61+
{
62+
std::set<int> sffo_set;
63+
if (sffo_instructions.isNull()) return sffo_set;
64+
if (sffo_instructions.isBool()) {
65+
if (sffo_instructions.get_bool()) sffo_set.insert(0);
66+
return sffo_set;
67+
}
68+
for (const auto& sffo : sffo_instructions.getValues()) {
69+
if (sffo.isStr()) {
70+
for (size_t i = 0; i < destinations.size(); ++i) {
71+
if (sffo.get_str() == destinations.at(i)) {
72+
sffo_set.insert(i);
73+
break;
74+
}
75+
}
76+
}
77+
if (sffo.isNum()) {
78+
int pos = sffo.getInt<int>();
79+
if (sffo_set.contains(pos))
80+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos));
81+
if (pos < 0)
82+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos));
83+
if (pos >= int(destinations.size()))
84+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos));
85+
sffo_set.insert(pos);
86+
}
87+
}
88+
return sffo_set;
89+
}
90+
7991
static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const UniValue& options, const CMutableTransaction& rawTx)
8092
{
8193
// Make a blank psbt
@@ -275,11 +287,6 @@ RPCHelpMan sendtoaddress()
275287
if (!request.params[3].isNull() && !request.params[3].get_str().empty())
276288
mapValue["to"] = request.params[3].get_str();
277289

278-
bool fSubtractFeeFromAmount = false;
279-
if (!request.params[4].isNull()) {
280-
fSubtractFeeFromAmount = request.params[4].get_bool();
281-
}
282-
283290
CCoinControl coin_control;
284291
if (!request.params[5].isNull()) {
285292
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
@@ -296,13 +303,10 @@ RPCHelpMan sendtoaddress()
296303
UniValue address_amounts(UniValue::VOBJ);
297304
const std::string address = request.params[0].get_str();
298305
address_amounts.pushKV(address, request.params[1]);
299-
UniValue subtractFeeFromAmount(UniValue::VARR);
300-
if (fSubtractFeeFromAmount) {
301-
subtractFeeFromAmount.push_back(address);
302-
}
303-
304-
std::vector<CRecipient> recipients;
305-
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients);
306+
std::vector<CRecipient> recipients = CreateRecipients(
307+
ParseOutputs(address_amounts),
308+
InterpretSubtractFeeFromOutputInstructions(request.params[4], address_amounts.getKeys())
309+
);
306310
const bool verbose{request.params[10].isNull() ? false : request.params[10].get_bool()};
307311

308312
return SendMoney(*pwallet, coin_control, recipients, mapValue, verbose);
@@ -386,19 +390,17 @@ RPCHelpMan sendmany()
386390
if (!request.params[3].isNull() && !request.params[3].get_str().empty())
387391
mapValue["comment"] = request.params[3].get_str();
388392

389-
UniValue subtractFeeFromAmount(UniValue::VARR);
390-
if (!request.params[4].isNull())
391-
subtractFeeFromAmount = request.params[4].get_array();
392-
393393
CCoinControl coin_control;
394394
if (!request.params[5].isNull()) {
395395
coin_control.m_signal_bip125_rbf = request.params[5].get_bool();
396396
}
397397

398398
SetFeeEstimateMode(*pwallet, coin_control, /*conf_target=*/request.params[6], /*estimate_mode=*/request.params[7], /*fee_rate=*/request.params[8], /*override_min_fee=*/false);
399399

400-
std::vector<CRecipient> recipients;
401-
ParseRecipients(sendTo, subtractFeeFromAmount, recipients);
400+
std::vector<CRecipient> recipients = CreateRecipients(
401+
ParseOutputs(sendTo),
402+
InterpretSubtractFeeFromOutputInstructions(request.params[4], sendTo.getKeys())
403+
);
402404
const bool verbose{request.params[9].isNull() ? false : request.params[9].get_bool()};
403405

404406
return SendMoney(*pwallet, coin_control, recipients, std::move(mapValue), verbose);
@@ -488,17 +490,17 @@ static std::vector<RPCArg> FundTxDoc(bool solving_data = true)
488490
return args;
489491
}
490492

491-
CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const UniValue& options, CCoinControl& coinControl, bool override_min_fee)
493+
CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const std::vector<CRecipient>& recipients, const UniValue& options, CCoinControl& coinControl, bool override_min_fee)
492494
{
495+
// We want to make sure tx.vout is not used now that we are passing outputs as a vector of recipients.
496+
// This sets us up to remove tx completely in a future PR in favor of passing the inputs directly.
497+
CHECK_NONFATAL(tx.vout.empty());
493498
// Make sure the results are valid at least up to the most recent block
494499
// the user could have gotten from another RPC command prior to now
495500
wallet.BlockUntilSyncedToCurrentChain();
496501

497502
std::optional<unsigned int> change_position;
498503
bool lockUnspents = false;
499-
UniValue subtractFeeFromOutputs;
500-
std::set<int> setSubtractFeeFromOutputs;
501-
502504
if (!options.isNull()) {
503505
if (options.type() == UniValue::VBOOL) {
504506
// backward compatibility bool only fallback
@@ -553,7 +555,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
553555

554556
if (options.exists("changePosition") || options.exists("change_position")) {
555557
int pos = (options.exists("change_position") ? options["change_position"] : options["changePosition"]).getInt<int>();
556-
if (pos < 0 || (unsigned int)pos > tx.vout.size()) {
558+
if (pos < 0 || (unsigned int)pos > recipients.size()) {
557559
throw JSONRPCError(RPC_INVALID_PARAMETER, "changePosition out of bounds");
558560
}
559561
change_position = (unsigned int)pos;
@@ -595,9 +597,6 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
595597
coinControl.fOverrideFeeRate = true;
596598
}
597599

598-
if (options.exists("subtractFeeFromOutputs") || options.exists("subtract_fee_from_outputs") )
599-
subtractFeeFromOutputs = (options.exists("subtract_fee_from_outputs") ? options["subtract_fee_from_outputs"] : options["subtractFeeFromOutputs"]).get_array();
600-
601600
if (options.exists("replaceable")) {
602601
coinControl.m_signal_bip125_rbf = options["replaceable"].get_bool();
603602
}
@@ -703,21 +702,10 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
703702
}
704703
}
705704

706-
if (tx.vout.size() == 0)
705+
if (recipients.empty())
707706
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
708707

709-
for (unsigned int idx = 0; idx < subtractFeeFromOutputs.size(); idx++) {
710-
int pos = subtractFeeFromOutputs[idx].getInt<int>();
711-
if (setSubtractFeeFromOutputs.count(pos))
712-
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated position: %d", pos));
713-
if (pos < 0)
714-
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, negative position: %d", pos));
715-
if (pos >= int(tx.vout.size()))
716-
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, position too large: %d", pos));
717-
setSubtractFeeFromOutputs.insert(pos);
718-
}
719-
720-
auto txr = FundTransaction(wallet, tx, change_position, lockUnspents, setSubtractFeeFromOutputs, coinControl);
708+
auto txr = FundTransaction(wallet, tx, recipients, change_position, lockUnspents, coinControl);
721709
if (!txr) {
722710
throw JSONRPCError(RPC_WALLET_ERROR, ErrorString(txr).original);
723711
}
@@ -843,11 +831,25 @@ RPCHelpMan fundrawtransaction()
843831
if (!DecodeHexTx(tx, request.params[0].get_str(), try_no_witness, try_witness)) {
844832
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
845833
}
846-
834+
UniValue options = request.params[1];
835+
std::vector<std::pair<CTxDestination, CAmount>> destinations;
836+
for (const auto& tx_out : tx.vout) {
837+
CTxDestination dest;
838+
ExtractDestination(tx_out.scriptPubKey, dest);
839+
destinations.emplace_back(dest, tx_out.nValue);
840+
}
841+
std::vector<std::string> dummy(destinations.size(), "dummy");
842+
std::vector<CRecipient> recipients = CreateRecipients(
843+
destinations,
844+
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], dummy)
845+
);
847846
CCoinControl coin_control;
848847
// Automatically select (additional) coins. Can be overridden by options.add_inputs.
849848
coin_control.m_allow_other_inputs = true;
850-
auto txr = FundTransaction(*pwallet, tx, request.params[1], coin_control, /*override_min_fee=*/true);
849+
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
850+
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
851+
tx.vout.clear();
852+
auto txr = FundTransaction(*pwallet, tx, recipients, options, coin_control, /*override_min_fee=*/true);
851853

852854
UniValue result(UniValue::VOBJ);
853855
result.pushKV("hex", EncodeHexTx(*txr.tx));
@@ -1275,13 +1277,22 @@ RPCHelpMan send()
12751277

12761278

12771279
bool rbf{options.exists("replaceable") ? options["replaceable"].get_bool() : pwallet->m_signal_rbf};
1280+
UniValue outputs(UniValue::VOBJ);
1281+
outputs = NormalizeOutputs(request.params[0]);
1282+
std::vector<CRecipient> recipients = CreateRecipients(
1283+
ParseOutputs(outputs),
1284+
InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys())
1285+
);
12781286
CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf);
12791287
CCoinControl coin_control;
12801288
// Automatically select coins, unless at least one is manually selected. Can
12811289
// be overridden by options.add_inputs.
12821290
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
12831291
SetOptionsInputWeights(options["inputs"], options);
1284-
auto txr = FundTransaction(*pwallet, rawTx, options, coin_control, /*override_min_fee=*/false);
1292+
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
1293+
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
1294+
rawTx.vout.clear();
1295+
auto txr = FundTransaction(*pwallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/false);
12851296

12861297
return FinishTransaction(pwallet, options, CMutableTransaction(*txr.tx));
12871298
}
@@ -1711,12 +1722,21 @@ RPCHelpMan walletcreatefundedpsbt()
17111722
const UniValue &replaceable_arg = options["replaceable"];
17121723
const bool rbf{replaceable_arg.isNull() ? wallet.m_signal_rbf : replaceable_arg.get_bool()};
17131724
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
1725+
UniValue outputs(UniValue::VOBJ);
1726+
outputs = NormalizeOutputs(request.params[1]);
1727+
std::vector<CRecipient> recipients = CreateRecipients(
1728+
ParseOutputs(outputs),
1729+
InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys())
1730+
);
17141731
CCoinControl coin_control;
17151732
// Automatically select coins, unless at least one is manually selected. Can
17161733
// be overridden by options.add_inputs.
17171734
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
17181735
SetOptionsInputWeights(request.params[0], options);
1719-
auto txr = FundTransaction(wallet, rawTx, options, coin_control, /*override_min_fee=*/true);
1736+
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
1737+
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
1738+
rawTx.vout.clear();
1739+
auto txr = FundTransaction(wallet, rawTx, recipients, options, coin_control, /*override_min_fee=*/true);
17201740

17211741
// Make a blank psbt
17221742
PartiallySignedTransaction psbtx(CMutableTransaction(*txr.tx));

src/wallet/spend.cpp

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,18 +1377,11 @@ util::Result<CreatedTransactionResult> CreateTransaction(
13771377
return res;
13781378
}
13791379

1380-
util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CMutableTransaction& tx, std::optional<unsigned int> change_pos, bool lockUnspents, const std::set<int>& setSubtractFeeFromOutputs, CCoinControl coinControl)
1380+
util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CMutableTransaction& tx, const std::vector<CRecipient>& vecSend, std::optional<unsigned int> change_pos, bool lockUnspents, CCoinControl coinControl)
13811381
{
1382-
std::vector<CRecipient> vecSend;
1383-
1384-
// Turn the txout set into a CRecipient vector.
1385-
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
1386-
const CTxOut& txOut = tx.vout[idx];
1387-
CTxDestination dest;
1388-
ExtractDestination(txOut.scriptPubKey, dest);
1389-
CRecipient recipient = {dest, txOut.nValue, setSubtractFeeFromOutputs.count(idx) == 1};
1390-
vecSend.push_back(recipient);
1391-
}
1382+
// We want to make sure tx.vout is not used now that we are passing outputs as a vector of recipients.
1383+
// This sets us up to remove tx completely in a future PR in favor of passing the inputs directly.
1384+
assert(tx.vout.empty());
13921385

13931386
// Set the user desired locktime
13941387
coinControl.m_locktime = tx.nLockTime;

0 commit comments

Comments
 (0)