mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-12 05:32:22 +02:00
`ParseOutputs` iterates a `UniValue` object's keys and looks up each value by key. Each lookup scans the key vector from the beginning, making the lookup work quadratic. Walk the parallel key and value vectors together to avoid repeated scans. This preserves output order and validation behavior.
471 lines
21 KiB
C++
471 lines
21 KiB
C++
// Copyright (c) 2010 Satoshi Nakamoto
|
|
// Copyright (c) 2009-present The Bitcoin Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include <rpc/rawtransaction_util.h>
|
|
|
|
#include <coins.h>
|
|
#include <consensus/amount.h>
|
|
#include <core_io.h>
|
|
#include <key_io.h>
|
|
#include <policy/policy.h>
|
|
#include <primitives/transaction.h>
|
|
#include <rpc/request.h>
|
|
#include <rpc/util.h>
|
|
#include <script/sign.h>
|
|
#include <script/signingprovider.h>
|
|
#include <tinyformat.h>
|
|
#include <univalue.h>
|
|
#include <util/check.h>
|
|
#include <util/rbf.h>
|
|
#include <util/string.h>
|
|
#include <util/strencodings.h>
|
|
#include <util/translation.h>
|
|
|
|
void AddInputs(CMutableTransaction& rawTx, const UniValue& inputs_in, std::optional<bool> rbf)
|
|
{
|
|
UniValue inputs;
|
|
if (inputs_in.isNull()) {
|
|
inputs = UniValue::VARR;
|
|
} else {
|
|
inputs = inputs_in.get_array();
|
|
}
|
|
|
|
for (unsigned int idx = 0; idx < inputs.size(); idx++) {
|
|
const UniValue& input = inputs[idx];
|
|
const UniValue& o = input.get_obj();
|
|
|
|
Txid txid = Txid::FromUint256(ParseHashO(o, "txid"));
|
|
|
|
const UniValue& vout_v = o.find_value("vout");
|
|
if (!vout_v.isNum())
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing vout key");
|
|
int nOutput = vout_v.getInt<int>();
|
|
if (nOutput < 0)
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
|
|
|
|
uint32_t nSequence;
|
|
|
|
if (rbf.value_or(true)) {
|
|
nSequence = MAX_BIP125_RBF_SEQUENCE; /* CTxIn::SEQUENCE_FINAL - 2 */
|
|
} else if (rawTx.nLockTime) {
|
|
nSequence = CTxIn::MAX_SEQUENCE_NONFINAL; /* CTxIn::SEQUENCE_FINAL - 1 */
|
|
} else {
|
|
nSequence = CTxIn::SEQUENCE_FINAL;
|
|
}
|
|
|
|
// set the sequence number if passed in the parameters object
|
|
const UniValue& sequenceObj = o.find_value("sequence");
|
|
if (sequenceObj.isNum()) {
|
|
int64_t seqNr64 = sequenceObj.getInt<int64_t>();
|
|
if (seqNr64 < 0 || seqNr64 > CTxIn::SEQUENCE_FINAL) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, sequence number is out of range");
|
|
} else {
|
|
nSequence = (uint32_t)seqNr64;
|
|
}
|
|
}
|
|
|
|
CTxIn in(COutPoint(txid, nOutput), CScript(), nSequence);
|
|
|
|
rawTx.vin.push_back(in);
|
|
}
|
|
}
|
|
|
|
UniValue NormalizeOutputs(const UniValue& outputs_in)
|
|
{
|
|
if (outputs_in.isNull()) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, output argument must be non-null");
|
|
}
|
|
|
|
const bool outputs_is_obj = outputs_in.isObject();
|
|
UniValue outputs = outputs_is_obj ? outputs_in.get_obj() : outputs_in.get_array();
|
|
|
|
if (!outputs_is_obj) {
|
|
// Translate array of key-value pairs into dict
|
|
UniValue outputs_dict = UniValue(UniValue::VOBJ);
|
|
for (size_t i = 0; i < outputs.size(); ++i) {
|
|
const UniValue& output = outputs[i];
|
|
if (!output.isObject()) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair not an object as expected");
|
|
}
|
|
if (output.size() != 1) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair must contain exactly one key");
|
|
}
|
|
outputs_dict.pushKVs(output);
|
|
}
|
|
outputs = std::move(outputs_dict);
|
|
}
|
|
return outputs;
|
|
}
|
|
|
|
std::vector<std::pair<CTxDestination, CAmount>> ParseOutputs(const UniValue& outputs)
|
|
{
|
|
// Duplicate checking
|
|
std::set<CTxDestination> destinations;
|
|
std::vector<std::pair<CTxDestination, CAmount>> parsed_outputs;
|
|
bool has_data{false};
|
|
const auto& keys{outputs.getKeys()};
|
|
const auto& values{outputs.getValues()};
|
|
for (size_t i{0}; i < keys.size(); ++i) {
|
|
const auto& name_{keys[i]};
|
|
const auto& value{values[i]};
|
|
if (name_ == "data") {
|
|
if (has_data) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, duplicate key: data");
|
|
}
|
|
has_data = true;
|
|
std::vector<unsigned char> data = ParseHexV(value.getValStr(), "Data");
|
|
CTxDestination destination{CNoDestination{CScript() << OP_RETURN << data}};
|
|
CAmount amount{0};
|
|
parsed_outputs.emplace_back(destination, amount);
|
|
} else {
|
|
CTxDestination destination{DecodeDestination(name_)};
|
|
CAmount amount{AmountFromValue(value)};
|
|
if (!IsValidDestination(destination)) {
|
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Bitcoin address: ") + name_);
|
|
}
|
|
|
|
if (!destinations.insert(destination).second) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated address: ") + name_);
|
|
}
|
|
parsed_outputs.emplace_back(destination, amount);
|
|
}
|
|
}
|
|
return parsed_outputs;
|
|
}
|
|
|
|
void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in)
|
|
{
|
|
UniValue outputs(UniValue::VOBJ);
|
|
outputs = NormalizeOutputs(outputs_in);
|
|
|
|
std::vector<std::pair<CTxDestination, CAmount>> parsed_outputs = ParseOutputs(outputs);
|
|
for (const auto& [destination, nAmount] : parsed_outputs) {
|
|
CScript scriptPubKey = GetScriptForDestination(destination);
|
|
|
|
CTxOut out(nAmount, scriptPubKey);
|
|
rawTx.vout.push_back(out);
|
|
}
|
|
}
|
|
|
|
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf, const uint32_t version)
|
|
{
|
|
CMutableTransaction rawTx;
|
|
|
|
if (!locktime.isNull()) {
|
|
int64_t nLockTime = locktime.getInt<int64_t>();
|
|
if (nLockTime < 0 || nLockTime > LOCKTIME_MAX)
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, locktime out of range");
|
|
rawTx.nLockTime = nLockTime;
|
|
}
|
|
|
|
if (version < TX_MIN_STANDARD_VERSION || version > TX_MAX_STANDARD_VERSION) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, version out of range(%d~%d)", TX_MIN_STANDARD_VERSION, TX_MAX_STANDARD_VERSION));
|
|
}
|
|
rawTx.version = version;
|
|
|
|
AddInputs(rawTx, inputs_in, rbf);
|
|
AddOutputs(rawTx, outputs_in);
|
|
|
|
if (rbf.has_value() && rbf.value() && rawTx.vin.size() > 0 && !SignalsOptInRBF(CTransaction(rawTx))) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter combination: Sequence number(s) contradict replaceable option");
|
|
}
|
|
|
|
return rawTx;
|
|
}
|
|
|
|
/** Pushes a JSON object for script verification or signing errors to vErrorsRet. */
|
|
static void TxInErrorToJSON(const CTxIn& txin, UniValue& vErrorsRet, const std::string& strMessage)
|
|
{
|
|
UniValue entry(UniValue::VOBJ);
|
|
entry.pushKV("txid", txin.prevout.hash.ToString());
|
|
entry.pushKV("vout", txin.prevout.n);
|
|
UniValue witness(UniValue::VARR);
|
|
for (unsigned int i = 0; i < txin.scriptWitness.stack.size(); i++) {
|
|
witness.push_back(HexStr(txin.scriptWitness.stack[i]));
|
|
}
|
|
entry.pushKV("witness", std::move(witness));
|
|
entry.pushKV("scriptSig", HexStr(txin.scriptSig));
|
|
entry.pushKV("sequence", txin.nSequence);
|
|
entry.pushKV("error", strMessage);
|
|
vErrorsRet.push_back(std::move(entry));
|
|
}
|
|
|
|
void ParsePrevouts(const UniValue& prevTxsUnival, FlatSigningProvider* keystore, std::map<COutPoint, Coin>& coins)
|
|
{
|
|
if (!prevTxsUnival.isNull()) {
|
|
const UniValue& prevTxs = prevTxsUnival.get_array();
|
|
for (unsigned int idx = 0; idx < prevTxs.size(); ++idx) {
|
|
const UniValue& p = prevTxs[idx];
|
|
if (!p.isObject()) {
|
|
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "expected object with {\"txid'\",\"vout\",\"scriptPubKey\"}");
|
|
}
|
|
|
|
const UniValue& prevOut = p.get_obj();
|
|
|
|
RPCTypeCheckObj(prevOut,
|
|
{
|
|
{"txid", UniValueType(UniValue::VSTR)},
|
|
{"vout", UniValueType(UniValue::VNUM)},
|
|
{"scriptPubKey", UniValueType(UniValue::VSTR)},
|
|
});
|
|
|
|
Txid txid = Txid::FromUint256(ParseHashO(prevOut, "txid"));
|
|
|
|
int nOut = prevOut.find_value("vout").getInt<int>();
|
|
if (nOut < 0) {
|
|
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout cannot be negative");
|
|
}
|
|
|
|
COutPoint out(txid, nOut);
|
|
std::vector<unsigned char> pkData(ParseHexO(prevOut, "scriptPubKey"));
|
|
CScript scriptPubKey(pkData.begin(), pkData.end());
|
|
|
|
{
|
|
auto coin = coins.find(out);
|
|
if (coin != coins.end() && !coin->second.IsSpent() && coin->second.out.scriptPubKey != scriptPubKey) {
|
|
std::string err("Previous output scriptPubKey mismatch:\n");
|
|
err = err + ScriptToAsmStr(coin->second.out.scriptPubKey) + "\nvs:\n"+
|
|
ScriptToAsmStr(scriptPubKey);
|
|
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, err);
|
|
}
|
|
Coin newcoin;
|
|
newcoin.out.scriptPubKey = scriptPubKey;
|
|
newcoin.out.nValue = MAX_MONEY;
|
|
if (prevOut.exists("amount")) {
|
|
newcoin.out.nValue = AmountFromValue(prevOut.find_value("amount"));
|
|
}
|
|
newcoin.nHeight = 1;
|
|
coins[out] = std::move(newcoin);
|
|
}
|
|
|
|
// if redeemScript and private keys were given, add redeemScript to the keystore so it can be signed
|
|
const bool is_p2sh = scriptPubKey.IsPayToScriptHash();
|
|
const bool is_p2wsh = scriptPubKey.IsPayToWitnessScriptHash();
|
|
if (keystore && (is_p2sh || is_p2wsh)) {
|
|
RPCTypeCheckObj(prevOut,
|
|
{
|
|
{"redeemScript", UniValueType(UniValue::VSTR)},
|
|
{"witnessScript", UniValueType(UniValue::VSTR)},
|
|
}, true);
|
|
const UniValue& rs{prevOut.find_value("redeemScript")};
|
|
const UniValue& ws{prevOut.find_value("witnessScript")};
|
|
if (rs.isNull() && ws.isNull()) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing redeemScript/witnessScript");
|
|
}
|
|
|
|
// work from witnessScript when possible
|
|
std::vector<unsigned char> scriptData(!ws.isNull() ? ParseHexV(ws, "witnessScript") : ParseHexV(rs, "redeemScript"));
|
|
CScript script(scriptData.begin(), scriptData.end());
|
|
keystore->scripts.emplace(CScriptID(script), script);
|
|
// Automatically also add the P2WSH wrapped version of the script (to deal with P2SH-P2WSH).
|
|
// This is done for redeemScript only for compatibility, it is encouraged to use the explicit witnessScript field instead.
|
|
CScript witness_output_script{GetScriptForDestination(WitnessV0ScriptHash(script))};
|
|
keystore->scripts.emplace(CScriptID(witness_output_script), witness_output_script);
|
|
|
|
if (!ws.isNull() && !rs.isNull()) {
|
|
// if both witnessScript and redeemScript are provided,
|
|
// they should either be the same (for backwards compat),
|
|
// or the redeemScript should be the encoded form of
|
|
// the witnessScript (ie, for p2sh-p2wsh)
|
|
if (ws.get_str() != rs.get_str()) {
|
|
std::vector<unsigned char> redeemScriptData(ParseHexV(rs, "redeemScript"));
|
|
CScript redeemScript(redeemScriptData.begin(), redeemScriptData.end());
|
|
if (redeemScript != witness_output_script) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "redeemScript does not correspond to witnessScript");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_p2sh) {
|
|
const CTxDestination p2sh{ScriptHash(script)};
|
|
const CTxDestination p2sh_p2wsh{ScriptHash(witness_output_script)};
|
|
if (scriptPubKey == GetScriptForDestination(p2sh)) {
|
|
// traditional p2sh; arguably an error if
|
|
// we got here with rs.IsNull(), because
|
|
// that means the p2sh script was specified
|
|
// via witnessScript param, but for now
|
|
// we'll just quietly accept it
|
|
} else if (scriptPubKey == GetScriptForDestination(p2sh_p2wsh)) {
|
|
// p2wsh encoded as p2sh; ideally the witness
|
|
// script was specified in the witnessScript
|
|
// param, but also support specifying it via
|
|
// redeemScript param for backwards compat
|
|
// (in which case ws.IsNull() == true)
|
|
} else {
|
|
// otherwise, can't generate scriptPubKey from
|
|
// either script, so we got unusable parameters
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "redeemScript/witnessScript does not match scriptPubKey");
|
|
}
|
|
} else if (is_p2wsh) {
|
|
// plain p2wsh; could throw an error if script
|
|
// was specified by redeemScript rather than
|
|
// witnessScript (ie, ws.IsNull() == true), but
|
|
// accept it for backwards compat
|
|
const CTxDestination p2wsh{WitnessV0ScriptHash(script)};
|
|
if (scriptPubKey != GetScriptForDestination(p2wsh)) {
|
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "redeemScript/witnessScript does not match scriptPubKey");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result)
|
|
{
|
|
std::optional<int> nHashType = ParseSighashString(hashType);
|
|
if (!nHashType) {
|
|
nHashType = SIGHASH_DEFAULT;
|
|
}
|
|
|
|
// Script verification errors
|
|
std::map<int, bilingual_str> input_errors;
|
|
|
|
bool complete = SignTransaction(mtx, keystore, coins, {.sighash_type = *nHashType}, input_errors);
|
|
SignTransactionResultToJSON(mtx, complete, coins, input_errors, result);
|
|
}
|
|
|
|
void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map<COutPoint, Coin>& coins, const std::map<int, bilingual_str>& input_errors, UniValue& result)
|
|
{
|
|
// Make errors UniValue
|
|
UniValue vErrors(UniValue::VARR);
|
|
for (const auto& err_pair : input_errors) {
|
|
if (err_pair.second.original == "Missing amount") {
|
|
// This particular error needs to be an exception for some reason
|
|
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Missing amount for %s", coins.at(mtx.vin.at(err_pair.first).prevout).out.ToString()));
|
|
}
|
|
TxInErrorToJSON(mtx.vin.at(err_pair.first), vErrors, err_pair.second.original);
|
|
}
|
|
|
|
result.pushKV("hex", EncodeHexTx(CTransaction(mtx)));
|
|
result.pushKV("complete", complete);
|
|
if (!vErrors.empty()) {
|
|
if (result.exists("errors")) {
|
|
vErrors.push_backV(result["errors"].getValues());
|
|
}
|
|
result.pushKV("errors", std::move(vErrors));
|
|
}
|
|
}
|
|
|
|
std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
|
|
{
|
|
CHECK_NONFATAL(!opts.fee_doc || opts.fee);
|
|
CHECK_NONFATAL(!opts.prevout_doc || opts.prevout);
|
|
CHECK_NONFATAL(!opts.vin_item_doc || opts.vin_inner_elision);
|
|
CHECK_NONFATAL(opts.elision_mode != ElisionMode::WithSummary || opts.elision_summary.has_value());
|
|
|
|
const std::string fee_doc{opts.fee_doc.value_or(
|
|
"transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available")};
|
|
const std::string prevout_doc{opts.prevout_doc.value_or(
|
|
"The previous output, omitted if block undo data is not available")};
|
|
const std::string vin_item_doc{opts.vin_item_doc.value_or("utxo being spent")};
|
|
|
|
auto vin_inner = std::vector<RPCResult>{
|
|
{RPCResult::Type::STR_HEX, "coinbase", /*optional=*/true, "The coinbase value (only if coinbase transaction)"},
|
|
{RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The transaction id (if not coinbase transaction)"},
|
|
{RPCResult::Type::NUM, "vout", /*optional=*/true, "The output number (if not coinbase transaction)"},
|
|
{RPCResult::Type::OBJ, "scriptSig", /*optional=*/true, "The script (if not coinbase transaction)",
|
|
{
|
|
{RPCResult::Type::STR, "asm", "Disassembly of the signature script"},
|
|
{RPCResult::Type::STR_HEX, "hex", "The raw signature script bytes, hex-encoded"},
|
|
}},
|
|
{RPCResult::Type::ARR, "txinwitness", /*optional=*/true, "",
|
|
{
|
|
{RPCResult::Type::STR_HEX, "hex", "hex-encoded witness data (if any)"},
|
|
}},
|
|
};
|
|
if (opts.prevout) {
|
|
vin_inner.emplace_back(
|
|
RPCResult::Type::OBJ, "prevout", opts.prevout_optional, prevout_doc,
|
|
std::vector<RPCResult>{
|
|
{RPCResult::Type::BOOL, "generated", "Coinbase or not"},
|
|
{RPCResult::Type::NUM, "height", "The height of the prevout"},
|
|
{RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT},
|
|
{RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()},
|
|
}
|
|
);
|
|
}
|
|
vin_inner.emplace_back(RPCResult::Type::NUM, "sequence", "The script sequence number");
|
|
|
|
if (opts.vin_inner_elision) {
|
|
vin_inner = ElideGroup(std::move(vin_inner), *opts.vin_inner_elision);
|
|
if (opts.prevout) {
|
|
// prevout remains visible even when other fields are elided
|
|
std::vector<RPCResult> new_vin;
|
|
new_vin.reserve(vin_inner.size());
|
|
for (const auto& r : vin_inner) {
|
|
if (r.m_key_name == "prevout") {
|
|
RPCResultOptions unopts = r.m_opts;
|
|
unopts.print_elision = HelpElisionNone{};
|
|
new_vin.emplace_back(r, std::move(unopts));
|
|
} else {
|
|
new_vin.push_back(r);
|
|
}
|
|
}
|
|
vin_inner = std::move(new_vin);
|
|
}
|
|
}
|
|
|
|
auto fields = std::vector<RPCResult>{
|
|
{RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc},
|
|
{RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"},
|
|
{RPCResult::Type::NUM, "size", "The serialized transaction size"},
|
|
{RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)"},
|
|
{RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)"},
|
|
{RPCResult::Type::NUM, "version", "The version"},
|
|
{RPCResult::Type::NUM_TIME, "locktime", "The lock time"},
|
|
{RPCResult::Type::ARR, "vin", "",
|
|
{
|
|
{RPCResult::Type::OBJ, "", opts.vin_inner_elision ? vin_item_doc : "", std::move(vin_inner)},
|
|
}},
|
|
{RPCResult::Type::ARR, "vout", "",
|
|
{
|
|
{RPCResult::Type::OBJ, "", "", Cat(
|
|
{
|
|
{RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT},
|
|
{RPCResult::Type::NUM, "n", "index"},
|
|
{RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()},
|
|
},
|
|
opts.wallet ?
|
|
std::vector<RPCResult>{{RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only present if true)"}} :
|
|
std::vector<RPCResult>{}
|
|
)},
|
|
}},
|
|
};
|
|
|
|
if (opts.fee) fields.emplace_back(RPCResult::Type::NUM, "fee", /*optional=*/true, fee_doc);
|
|
if (opts.hex) fields.emplace_back(RPCResult::Type::STR_HEX, "hex", "The hex-encoded transaction data");
|
|
|
|
if (opts.elision_mode != ElisionMode::None) {
|
|
const bool silent = opts.elision_mode == ElisionMode::Silent;
|
|
std::vector<RPCResult> new_fields;
|
|
new_fields.reserve(fields.size());
|
|
bool first = true;
|
|
for (const auto& f : fields) {
|
|
if (!silent && f.m_key_name == "fee") {
|
|
new_fields.push_back(f);
|
|
continue;
|
|
}
|
|
if (f.m_key_name == "vin" && opts.vin_inner_elision) {
|
|
new_fields.push_back(f);
|
|
continue;
|
|
}
|
|
if (!silent && first) {
|
|
RPCResultOptions eopts = f.m_opts;
|
|
eopts.print_elision = opts.elision_summary.value_or("");
|
|
new_fields.emplace_back(f, std::move(eopts));
|
|
first = false;
|
|
} else {
|
|
RPCResultOptions eopts = f.m_opts;
|
|
eopts.print_elision = HelpElisionSkip{};
|
|
new_fields.emplace_back(f, std::move(eopts));
|
|
}
|
|
}
|
|
fields = std::move(new_fields);
|
|
}
|
|
|
|
return fields;
|
|
}
|