Skip to content

Commit d8b12a7

Browse files
committed
rpc: Allow named and positional arguments to be used together
It's nice to be able to use named options and positional arguments together. Most shell tools accept both, and python functions combine options and arguments allowing them to be passed with even more flexibility. This change adds support for python's approach so as a motivating example: bitcoin-cli -named createwallet wallet_name=mywallet load_on_startup=1 Can be shortened to: bitcoin-cli -named createwallet mywallet load_on_startup=1 JSON-RPC standard doesn't have a convention for passing named and positional parameters together, so this implementation makes one up and interprets any unused "args" named parameter as a positional parameter array.
1 parent 50422b7 commit d8b12a7

File tree

9 files changed

+147
-5
lines changed

9 files changed

+147
-5
lines changed

doc/JSON-RPC-interface.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ The headless daemon `bitcoind` has the JSON-RPC API enabled by default, the GUI
55
option. In the GUI it is possible to execute RPC methods in the Debug Console
66
Dialog.
77

8+
## Parameter passing
9+
10+
The JSON-RPC server supports both _by-position_ and _by-name_ [parameter
11+
structures](https://www.jsonrpc.org/specification#parameter_structures)
12+
described in the JSON-RPC specification. For extra convenience, to avoid the
13+
need to name every parameter value, all RPC methods accept a named parameter
14+
called `args`, which can be set to an array of initial positional values that
15+
are combined with named values.
16+
17+
Examples:
18+
19+
```sh
20+
# "params": ["mywallet", false, false, "", false, false, true]
21+
bitcoin-cli createwallet mywallet false false "" false false true
22+
23+
# "params": {"wallet_name": "mywallet", "load_on_startup": true}
24+
bitcoin-cli -named createwallet wallet_name=mywallet load_on_startup=true
25+
26+
# "params": {"args": ["mywallet"], "load_on_startup": true}
27+
bitcoin-cli -named createwallet mywallet load_on_startup=true
28+
```
29+
830
## Versioning
931

1032
The RPC interface might change from one major version of Bitcoin Core to the

doc/release-notes-19762.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
JSON-RPC
2+
---
3+
4+
All JSON-RPC methods accept a new [named
5+
parameter](JSON-RPC-interface.md#parameter-passing) called `args` that can
6+
contain positional parameter values. This is a convenience to allow some
7+
parameter values to be passed by name without having to name every value. The
8+
python test framework and `bitcoin-cli` tool both take advantage of this, so
9+
for example:
10+
11+
```sh
12+
bitcoin-cli -named createwallet wallet_name=mywallet load_on_startup=1
13+
```
14+
15+
Can now be shortened to:
16+
17+
```sh
18+
bitcoin-cli -named createwallet mywallet load_on_startup=1
19+
```

src/rpc/client.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,13 @@ UniValue RPCConvertValues(const std::string &strMethod, const std::vector<std::s
277277
UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<std::string> &strParams)
278278
{
279279
UniValue params(UniValue::VOBJ);
280+
UniValue positional_args{UniValue::VARR};
280281

281282
for (const std::string &s: strParams) {
282283
size_t pos = s.find('=');
283284
if (pos == std::string::npos) {
284-
throw(std::runtime_error("No '=' in named argument '"+s+"', this needs to be present for every argument (even if it is empty)"));
285+
positional_args.push_back(rpcCvtTable.convert(strMethod, positional_args.size()) ? ParseNonRFCJSONValue(s) : s);
286+
continue;
285287
}
286288

287289
std::string name = s.substr(0, pos);
@@ -296,5 +298,9 @@ UniValue RPCConvertNamedValues(const std::string &strMethod, const std::vector<s
296298
}
297299
}
298300

301+
if (!positional_args.empty()) {
302+
params.pushKV("args", positional_args);
303+
}
304+
299305
return params;
300306
}

src/rpc/server.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,16 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
401401
for (size_t i=0; i<keys.size(); ++i) {
402402
argsIn[keys[i]] = &values[i];
403403
}
404-
// Process expected parameters.
404+
// Process expected parameters. If any parameters were left unspecified in
405+
// the request before a parameter that was specified, null values need to be
406+
// inserted at the unspecifed parameter positions, and the "hole" variable
407+
// below tracks the number of null values that need to be inserted.
408+
// The "initial_hole_size" variable stores the size of the initial hole,
409+
// i.e. how many initial positional arguments were left unspecified. This is
410+
// used after the for-loop to add initial positional arguments from the
411+
// "args" parameter, if present.
405412
int hole = 0;
413+
int initial_hole_size = 0;
406414
for (const std::string &argNamePattern: argNames) {
407415
std::vector<std::string> vargNames = SplitString(argNamePattern, '|');
408416
auto fr = argsIn.end();
@@ -424,6 +432,24 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
424432
argsIn.erase(fr);
425433
} else {
426434
hole += 1;
435+
if (out.params.empty()) initial_hole_size = hole;
436+
}
437+
}
438+
// If leftover "args" param was found, use it as a source of positional
439+
// arguments and add named arguments after. This is a convenience for
440+
// clients that want to pass a combination of named and positional
441+
// arguments as described in doc/JSON-RPC-interface.md#parameter-passing
442+
auto positional_args{argsIn.extract("args")};
443+
if (positional_args && positional_args.mapped()->isArray()) {
444+
const bool has_named_arguments{initial_hole_size < (int)argNames.size()};
445+
if (initial_hole_size < (int)positional_args.mapped()->size() && has_named_arguments) {
446+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + argNames[initial_hole_size] + " specified twice both as positional and named argument");
447+
}
448+
// Assign positional_args to out.params and append named_args after.
449+
UniValue named_args{std::move(out.params)};
450+
out.params = *positional_args.mapped();
451+
for (size_t i{out.params.size()}; i < named_args.size(); ++i) {
452+
out.params.push_back(named_args[i]);
427453
}
428454
}
429455
// If there are still arguments in the argsIn map, this is an error.

src/test/rpc_tests.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,49 @@
1717

1818
#include <boost/test/unit_test.hpp>
1919

20+
static UniValue JSON(std::string_view json)
21+
{
22+
UniValue value;
23+
BOOST_CHECK(value.read(json.data(), json.size()));
24+
return value;
25+
}
26+
27+
class HasJSON
28+
{
29+
public:
30+
explicit HasJSON(std::string json) : m_json(std::move(json)) {}
31+
bool operator()(const UniValue& value) const
32+
{
33+
std::string json{value.write()};
34+
BOOST_CHECK_EQUAL(json, m_json);
35+
return json == m_json;
36+
};
37+
38+
private:
39+
const std::string m_json;
40+
};
41+
2042
class RPCTestingSetup : public TestingSetup
2143
{
2244
public:
45+
UniValue TransformParams(const UniValue& params, std::vector<std::string> arg_names) const;
2346
UniValue CallRPC(std::string args);
2447
};
2548

49+
UniValue RPCTestingSetup::TransformParams(const UniValue& params, std::vector<std::string> arg_names) const
50+
{
51+
UniValue transformed_params;
52+
CRPCTable table;
53+
CRPCCommand command{"category", "method", [&](const JSONRPCRequest& request, UniValue&, bool) -> bool { transformed_params = request.params; return true; }, arg_names, /*unique_id=*/0};
54+
table.appendCommand("method", &command);
55+
JSONRPCRequest request;
56+
request.strMethod = "method";
57+
request.params = params;
58+
if (RPCIsInWarmup(nullptr)) SetRPCWarmupFinished();
59+
table.execute(request);
60+
return transformed_params;
61+
}
62+
2663
UniValue RPCTestingSetup::CallRPC(std::string args)
2764
{
2865
std::vector<std::string> vArgs{SplitString(args, ' ')};
@@ -45,6 +82,29 @@ UniValue RPCTestingSetup::CallRPC(std::string args)
4582

4683
BOOST_FIXTURE_TEST_SUITE(rpc_tests, RPCTestingSetup)
4784

85+
BOOST_AUTO_TEST_CASE(rpc_namedparams)
86+
{
87+
const std::vector<std::string> arg_names{{"arg1", "arg2", "arg3", "arg4", "arg5"}};
88+
89+
// Make sure named arguments are transformed into positional arguments in correct places separated by nulls
90+
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"arg2": 2, "arg4": 4})"), arg_names).write(), "[null,2,null,4]");
91+
92+
// Make sure named and positional arguments can be combined.
93+
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"arg5": 5, "args": [1, 2], "arg4": 4})"), arg_names).write(), "[1,2,null,4,5]");
94+
95+
// Make sure a unknown named argument raises an exception
96+
BOOST_CHECK_EXCEPTION(TransformParams(JSON(R"({"arg2": 2, "unknown": 6})"), arg_names), UniValue,
97+
HasJSON(R"({"code":-8,"message":"Unknown named parameter unknown"})"));
98+
99+
// Make sure an overlap between a named argument and positional argument raises an exception
100+
BOOST_CHECK_EXCEPTION(TransformParams(JSON(R"({"args": [1,2,3], "arg4": 4, "arg2": 2})"), arg_names), UniValue,
101+
HasJSON(R"({"code":-8,"message":"Parameter arg2 specified twice both as positional and named argument"})"));
102+
103+
// Make sure extra positional arguments can be passed through to the method implemenation, as long as they don't overlap with named arguments.
104+
BOOST_CHECK_EQUAL(TransformParams(JSON(R"({"args": [1,2,3,4,5,6,7,8,9,10]})"), arg_names).write(), "[1,2,3,4,5,6,7,8,9,10]");
105+
BOOST_CHECK_EQUAL(TransformParams(JSON(R"([1,2,3,4,5,6,7,8,9,10])"), arg_names).write(), "[1,2,3,4,5,6,7,8,9,10]");
106+
}
107+
48108
BOOST_AUTO_TEST_CASE(rpc_rawparams)
49109
{
50110
// Test raw transaction API argument handling

test/functional/interface_bitcoin_cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ def run_test(self):
8484
rpc_response = self.nodes[0].getblockchaininfo()
8585
assert_equal(cli_response, rpc_response)
8686

87+
self.log.info("Test named arguments")
88+
assert_equal(self.nodes[0].cli.echo(0, 1, arg3=3, arg5=5), ['0', '1', None, '3', None, '5'])
89+
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, 1, arg1=1)
90+
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, None, 2, arg1=1)
91+
8792
user, password = get_auth_cookie(self.nodes[0].datadir, self.chain)
8893

8994
self.log.info("Test -stdinrpcpass option")

test/functional/rpc_named_arguments.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def run_test(self):
3030
assert_equal(node.echo(arg1=1), [None, 1])
3131
assert_equal(node.echo(arg9=None), [None]*10)
3232
assert_equal(node.echo(arg0=0,arg3=3,arg9=9), [0] + [None]*2 + [3] + [None]*5 + [9])
33+
assert_equal(node.echo(0, 1, arg3=3, arg5=5), [0, 1, None, 3, None, 5])
34+
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", node.echo, 0, 1, arg1=1)
35+
assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", node.echo, 0, None, 2, arg1=1)
3336

3437
if __name__ == '__main__':
3538
NamedArgumentTest().main()

test/functional/test_framework/authproxy.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,12 @@ def get_request(self, *args, **argsn):
131131
json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii),
132132
))
133133
if args and argsn:
134-
raise ValueError('Cannot handle both named and positional arguments')
134+
params = dict(args=args, **argsn)
135+
else:
136+
params = args or argsn
135137
return {'version': '1.1',
136138
'method': self._service_name,
137-
'params': args or argsn,
139+
'params': params,
138140
'id': AuthServiceProxy.__id_count}
139141

140142
def __call__(self, *args, **argsn):

test/functional/test_framework/test_node.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,6 @@ def send_cli(self, command=None, *args, **kwargs):
719719
"""Run bitcoin-cli command. Deserializes returned string as python object."""
720720
pos_args = [arg_to_cli(arg) for arg in args]
721721
named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()]
722-
assert not (pos_args and named_args), "Cannot use positional arguments and named arguments in the same bitcoin-cli call"
723722
p_args = [self.binary, "-datadir=" + self.datadir] + self.options
724723
if named_args:
725724
p_args += ["-named"]

0 commit comments

Comments
 (0)