mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-11-12 06:58:57 +01:00
Merge bitcoin/bitcoin#26485: RPC: Accept options as named-only parameters
2cd28e9fefrpc: Add check for unintended option/parameter name clashes (Ryan Ofsky)95d7de0964test: Update python tests to use named parameters instead of options objects (Ryan Ofsky)96233146ddRPC: Allow RPC methods accepting options to take named parameters (Ryan Ofsky)702b56d2a8RPC: Add add OBJ_NAMED_PARAMS type (Ryan Ofsky) Pull request description: Allow RPC methods which take an `options` parameter (`importmulti`, `listunspent`, `fundrawtransaction`, `bumpfee`, `send`, `sendall`, `walletcreatefundedpsbt`, `simulaterawtransaction`), to accept the options as named parameters, without the need for nested JSON objects. This makes it possible to make calls like: ```sh src/bitcoin-cli -named bumpfee txid fee_rate=10 ``` instead of ```sh src/bitcoin-cli -named bumpfee txid options='{"fee_rate": 10}' ``` RPC help is also updated to show options as top level named arguments instead of as nested objects. <details><summary>diff</summary> <p> ```diff @@ -15,16 +15,17 @@ Arguments: 1. txid (string, required) The txid to be bumped -2. options (json object, optional) +2. options (json object, optional) Options object that can be used to pass named arguments, listed below. + +Named Arguments: - { - "conf_target": n, (numeric, optional, default=wallet -txconfirmtarget) Confirmation target in blocks +conf_target (numeric, optional, default=wallet -txconfirmtarget) Confirmation target in blocks - "fee_rate": amount, (numeric or string, optional, default=not set, fall back to wallet fee estimation) +fee_rate (numeric or string, optional, default=not set, fall back to wallet fee estimation) Specify a fee rate in sat/vB instead of relying on the built-in fee estimator. Must be at least 1.000 sat/vB higher than the current transaction fee rate. WARNING: before version 0.21, fee_rate was in BTC/kvB. As of 0.21, fee_rate is in sat/vB. - "replaceable": bool, (boolean, optional, default=true) Whether the new transaction should still be +replaceable (boolean, optional, default=true) Whether the new transaction should still be marked bip-125 replaceable. If true, the sequence numbers in the transaction will be left unchanged from the original. If false, any input sequence numbers in the original transaction that were less than 0xfffffffe will be increased to 0xfffffffe @@ -32,11 +33,10 @@ still be replaceable in practice, for example if it has unconfirmed ancestors which are replaceable). - "estimate_mode": "str", (string, optional, default="unset") The fee estimate mode, must be one of (case insensitive): +estimate_mode (string, optional, default="unset") The fee estimate mode, must be one of (case insensitive): "unset" "economical" "conservative" - } Result: { (json object) ``` </p> </details> **Review suggestion:** To understand this PR, it is probably easiest to review the commits in reverse order because the last commit shows the external API changes, the middle commit shows the internal API changes, and the first commit contains the low-level implementation. ACKs for top commit: achow101: ACK2cd28e9fefTree-SHA512: 50f6e78fa622826dab3f810400d8c1a03a98a090b1f2fea79729c58ad8cff955554bd44c2a5975f62a526b900dda68981862fd7d7d05c17f94f5b5d847317436
This commit is contained in:
@@ -389,7 +389,8 @@ struct Sections {
|
||||
case RPCArg::Type::NUM:
|
||||
case RPCArg::Type::AMOUNT:
|
||||
case RPCArg::Type::RANGE:
|
||||
case RPCArg::Type::BOOL: {
|
||||
case RPCArg::Type::BOOL:
|
||||
case RPCArg::Type::OBJ_NAMED_PARAMS: {
|
||||
if (is_top_level_arg) return; // Nothing more to do for non-recursive types on first recursion
|
||||
auto left = indent;
|
||||
if (arg.m_opts.type_str.size() != 0 && push_name) {
|
||||
@@ -485,12 +486,32 @@ RPCHelpMan::RPCHelpMan(std::string name, std::string description, std::vector<RP
|
||||
m_results{std::move(results)},
|
||||
m_examples{std::move(examples)}
|
||||
{
|
||||
std::set<std::string> named_args;
|
||||
// Map of parameter names and types just used to check whether the names are
|
||||
// unique. Parameter names always need to be unique, with the exception that
|
||||
// there can be pairs of POSITIONAL and NAMED parameters with the same name.
|
||||
enum ParamType { POSITIONAL = 1, NAMED = 2, NAMED_ONLY = 4 };
|
||||
std::map<std::string, int> param_names;
|
||||
|
||||
for (const auto& arg : m_args) {
|
||||
std::vector<std::string> names = SplitString(arg.m_names, '|');
|
||||
// Should have unique named arguments
|
||||
for (const std::string& name : names) {
|
||||
CHECK_NONFATAL(named_args.insert(name).second);
|
||||
auto& param_type = param_names[name];
|
||||
CHECK_NONFATAL(!(param_type & POSITIONAL));
|
||||
CHECK_NONFATAL(!(param_type & NAMED_ONLY));
|
||||
param_type |= POSITIONAL;
|
||||
}
|
||||
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
|
||||
for (const auto& inner : arg.m_inner) {
|
||||
std::vector<std::string> inner_names = SplitString(inner.m_names, '|');
|
||||
for (const std::string& inner_name : inner_names) {
|
||||
auto& param_type = param_names[inner_name];
|
||||
CHECK_NONFATAL(!(param_type & POSITIONAL) || inner.m_opts.also_positional);
|
||||
CHECK_NONFATAL(!(param_type & NAMED));
|
||||
CHECK_NONFATAL(!(param_type & NAMED_ONLY));
|
||||
param_type |= inner.m_opts.also_positional ? NAMED : NAMED_ONLY;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Default value type should match argument type only when defined
|
||||
if (arg.m_fallback.index() == 2) {
|
||||
@@ -605,12 +626,17 @@ bool RPCHelpMan::IsValidNumArgs(size_t num_args) const
|
||||
return num_required_args <= num_args && num_args <= m_args.size();
|
||||
}
|
||||
|
||||
std::vector<std::string> RPCHelpMan::GetArgNames() const
|
||||
std::vector<std::pair<std::string, bool>> RPCHelpMan::GetArgNames() const
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
std::vector<std::pair<std::string, bool>> ret;
|
||||
ret.reserve(m_args.size());
|
||||
for (const auto& arg : m_args) {
|
||||
ret.emplace_back(arg.m_names);
|
||||
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
|
||||
for (const auto& inner : arg.m_inner) {
|
||||
ret.emplace_back(inner.m_names, /*named_only=*/true);
|
||||
}
|
||||
}
|
||||
ret.emplace_back(arg.m_names, /*named_only=*/false);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -642,20 +668,31 @@ std::string RPCHelpMan::ToString() const
|
||||
|
||||
// Arguments
|
||||
Sections sections;
|
||||
Sections named_only_sections;
|
||||
for (size_t i{0}; i < m_args.size(); ++i) {
|
||||
const auto& arg = m_args.at(i);
|
||||
if (arg.m_opts.hidden) break; // Any arg that follows is also hidden
|
||||
|
||||
if (i == 0) ret += "\nArguments:\n";
|
||||
|
||||
// Push named argument name and description
|
||||
sections.m_sections.emplace_back(::ToString(i + 1) + ". " + arg.GetFirstName(), arg.ToDescriptionString(/*is_named_arg=*/true));
|
||||
sections.m_max_pad = std::max(sections.m_max_pad, sections.m_sections.back().m_left.size());
|
||||
|
||||
// Recursively push nested args
|
||||
sections.Push(arg);
|
||||
|
||||
// Push named-only argument sections
|
||||
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
|
||||
for (const auto& arg_inner : arg.m_inner) {
|
||||
named_only_sections.PushSection({arg_inner.GetFirstName(), arg_inner.ToDescriptionString(/*is_named_arg=*/true)});
|
||||
named_only_sections.Push(arg_inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sections.m_sections.empty()) ret += "\nArguments:\n";
|
||||
ret += sections.ToString();
|
||||
if (!named_only_sections.m_sections.empty()) ret += "\nNamed Arguments:\n";
|
||||
ret += named_only_sections.ToString();
|
||||
|
||||
// Result
|
||||
ret += m_results.ToDescriptionString();
|
||||
@@ -669,17 +706,30 @@ std::string RPCHelpMan::ToString() const
|
||||
UniValue RPCHelpMan::GetArgMap() const
|
||||
{
|
||||
UniValue arr{UniValue::VARR};
|
||||
|
||||
auto push_back_arg_info = [&arr](const std::string& rpc_name, int pos, const std::string& arg_name, const RPCArg::Type& type) {
|
||||
UniValue map{UniValue::VARR};
|
||||
map.push_back(rpc_name);
|
||||
map.push_back(pos);
|
||||
map.push_back(arg_name);
|
||||
map.push_back(type == RPCArg::Type::STR ||
|
||||
type == RPCArg::Type::STR_HEX);
|
||||
arr.push_back(map);
|
||||
};
|
||||
|
||||
for (int i{0}; i < int(m_args.size()); ++i) {
|
||||
const auto& arg = m_args.at(i);
|
||||
std::vector<std::string> arg_names = SplitString(arg.m_names, '|');
|
||||
for (const auto& arg_name : arg_names) {
|
||||
UniValue map{UniValue::VARR};
|
||||
map.push_back(m_name);
|
||||
map.push_back(i);
|
||||
map.push_back(arg_name);
|
||||
map.push_back(arg.m_type == RPCArg::Type::STR ||
|
||||
arg.m_type == RPCArg::Type::STR_HEX);
|
||||
arr.push_back(map);
|
||||
push_back_arg_info(m_name, i, arg_name, arg.m_type);
|
||||
if (arg.m_type == RPCArg::Type::OBJ_NAMED_PARAMS) {
|
||||
for (const auto& inner : arg.m_inner) {
|
||||
std::vector<std::string> inner_names = SplitString(inner.m_names, '|');
|
||||
for (const std::string& inner_name : inner_names) {
|
||||
push_back_arg_info(m_name, i, inner_name, inner.m_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
@@ -708,6 +758,7 @@ static std::optional<UniValue::VType> ExpectedType(RPCArg::Type type)
|
||||
return UniValue::VBOOL;
|
||||
}
|
||||
case Type::OBJ:
|
||||
case Type::OBJ_NAMED_PARAMS:
|
||||
case Type::OBJ_USER_KEYS: {
|
||||
return UniValue::VOBJ;
|
||||
}
|
||||
@@ -781,6 +832,7 @@ std::string RPCArg::ToDescriptionString(bool is_named_arg) const
|
||||
break;
|
||||
}
|
||||
case Type::OBJ:
|
||||
case Type::OBJ_NAMED_PARAMS:
|
||||
case Type::OBJ_USER_KEYS: {
|
||||
ret += "json object";
|
||||
break;
|
||||
@@ -809,6 +861,7 @@ std::string RPCArg::ToDescriptionString(bool is_named_arg) const
|
||||
} // no default case, so the compiler can warn about missing cases
|
||||
}
|
||||
ret += ")";
|
||||
if (m_type == Type::OBJ_NAMED_PARAMS) ret += " Options object that can be used to pass named arguments, listed below.";
|
||||
ret += m_description.empty() ? "" : " " + m_description;
|
||||
return ret;
|
||||
}
|
||||
@@ -1054,6 +1107,7 @@ std::string RPCArg::ToStringObj(const bool oneline) const
|
||||
}
|
||||
return res + "...]";
|
||||
case Type::OBJ:
|
||||
case Type::OBJ_NAMED_PARAMS:
|
||||
case Type::OBJ_USER_KEYS:
|
||||
// Currently unused, so avoid writing dead code
|
||||
NONFATAL_UNREACHABLE();
|
||||
@@ -1077,6 +1131,7 @@ std::string RPCArg::ToString(const bool oneline) const
|
||||
return GetFirstName();
|
||||
}
|
||||
case Type::OBJ:
|
||||
case Type::OBJ_NAMED_PARAMS:
|
||||
case Type::OBJ_USER_KEYS: {
|
||||
const std::string res = Join(m_inner, ",", [&](const RPCArg& i) { return i.ToStringObj(oneline); });
|
||||
if (m_type == Type::OBJ) {
|
||||
|
||||
Reference in New Issue
Block a user