Skip to content

Commit 34ac3f4

Browse files
committed
Merge bitcoin/bitcoin#26485: RPC: Accept options as named-only parameters
2cd28e9 rpc: Add check for unintended option/parameter name clashes (Ryan Ofsky) 95d7de0 test: Update python tests to use named parameters instead of options objects (Ryan Ofsky) 9623314 RPC: Allow RPC methods accepting options to take named parameters (Ryan Ofsky) 702b56d RPC: 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: ACK 2cd28e9 Tree-SHA512: 50f6e78fa622826dab3f810400d8c1a03a98a090b1f2fea79729c58ad8cff955554bd44c2a5975f62a526b900dda68981862fd7d7d05c17f94f5b5d847317436
2 parents 9e54dde + 2cd28e9 commit 34ac3f4

25 files changed

+452
-185
lines changed

doc/release-notes-26485.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
JSON-RPC
2+
---
3+
4+
For RPC methods which accept `options` parameters ((`importmulti`, `listunspent`, `fundrawtransaction`, `bumpfee`, `send`, `sendall`, `walletcreatefundedpsbt`, `simulaterawtransaction`), it is now possible to pass the options as named parameters without the need for a nested object. (#26485)
5+
6+
This means it is possible make calls like:
7+
8+
```sh
9+
src/bitcoin-cli -named bumpfee txid fee_rate=100
10+
```
11+
12+
instead of
13+
14+
```sh
15+
src/bitcoin-cli -named bumpfee txid options='{"fee_rate": 100}'
16+
```

src/rpc/client.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ static const CRPCConvertParam vRPCConvertParams[] =
101101
{ "listunspent", 2, "addresses" },
102102
{ "listunspent", 3, "include_unsafe" },
103103
{ "listunspent", 4, "query_options" },
104+
{ "listunspent", 4, "minimumAmount" },
105+
{ "listunspent", 4, "maximumAmount" },
106+
{ "listunspent", 4, "maximumCount" },
107+
{ "listunspent", 4, "minimumSumAmount" },
108+
{ "listunspent", 4, "include_immature_coinbase" },
104109
{ "getblock", 1, "verbosity" },
105110
{ "getblock", 1, "verbose" },
106111
{ "getblockheader", 1, "verbose" },
@@ -124,11 +129,38 @@ static const CRPCConvertParam vRPCConvertParams[] =
124129
{ "submitpackage", 0, "package" },
125130
{ "combinerawtransaction", 0, "txs" },
126131
{ "fundrawtransaction", 1, "options" },
132+
{ "fundrawtransaction", 1, "add_inputs"},
133+
{ "fundrawtransaction", 1, "include_unsafe"},
134+
{ "fundrawtransaction", 1, "minconf"},
135+
{ "fundrawtransaction", 1, "maxconf"},
136+
{ "fundrawtransaction", 1, "changePosition"},
137+
{ "fundrawtransaction", 1, "includeWatching"},
138+
{ "fundrawtransaction", 1, "lockUnspents"},
139+
{ "fundrawtransaction", 1, "fee_rate"},
140+
{ "fundrawtransaction", 1, "feeRate"},
141+
{ "fundrawtransaction", 1, "subtractFeeFromOutputs"},
142+
{ "fundrawtransaction", 1, "input_weights"},
143+
{ "fundrawtransaction", 1, "conf_target"},
144+
{ "fundrawtransaction", 1, "replaceable"},
145+
{ "fundrawtransaction", 1, "solving_data"},
127146
{ "fundrawtransaction", 2, "iswitness" },
128147
{ "walletcreatefundedpsbt", 0, "inputs" },
129148
{ "walletcreatefundedpsbt", 1, "outputs" },
130149
{ "walletcreatefundedpsbt", 2, "locktime" },
131150
{ "walletcreatefundedpsbt", 3, "options" },
151+
{ "walletcreatefundedpsbt", 3, "add_inputs"},
152+
{ "walletcreatefundedpsbt", 3, "include_unsafe"},
153+
{ "walletcreatefundedpsbt", 3, "minconf"},
154+
{ "walletcreatefundedpsbt", 3, "maxconf"},
155+
{ "walletcreatefundedpsbt", 3, "changePosition"},
156+
{ "walletcreatefundedpsbt", 3, "includeWatching"},
157+
{ "walletcreatefundedpsbt", 3, "lockUnspents"},
158+
{ "walletcreatefundedpsbt", 3, "fee_rate"},
159+
{ "walletcreatefundedpsbt", 3, "feeRate"},
160+
{ "walletcreatefundedpsbt", 3, "subtractFeeFromOutputs"},
161+
{ "walletcreatefundedpsbt", 3, "conf_target"},
162+
{ "walletcreatefundedpsbt", 3, "replaceable"},
163+
{ "walletcreatefundedpsbt", 3, "solving_data"},
132164
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
133165
{ "walletprocesspsbt", 1, "sign" },
134166
{ "walletprocesspsbt", 3, "bip32derivs" },
@@ -157,18 +189,49 @@ static const CRPCConvertParam vRPCConvertParams[] =
157189
{ "send", 1, "conf_target" },
158190
{ "send", 3, "fee_rate"},
159191
{ "send", 4, "options" },
192+
{ "send", 4, "add_inputs"},
193+
{ "send", 4, "include_unsafe"},
194+
{ "send", 4, "minconf"},
195+
{ "send", 4, "maxconf"},
196+
{ "send", 4, "add_to_wallet"},
197+
{ "send", 4, "change_position"},
198+
{ "send", 4, "fee_rate"},
199+
{ "send", 4, "include_watching"},
200+
{ "send", 4, "inputs"},
201+
{ "send", 4, "locktime"},
202+
{ "send", 4, "lock_unspents"},
203+
{ "send", 4, "psbt"},
204+
{ "send", 4, "subtract_fee_from_outputs"},
205+
{ "send", 4, "conf_target"},
206+
{ "send", 4, "replaceable"},
207+
{ "send", 4, "solving_data"},
160208
{ "sendall", 0, "recipients" },
161209
{ "sendall", 1, "conf_target" },
162210
{ "sendall", 3, "fee_rate"},
163211
{ "sendall", 4, "options" },
212+
{ "sendall", 4, "add_to_wallet"},
213+
{ "sendall", 4, "fee_rate"},
214+
{ "sendall", 4, "include_watching"},
215+
{ "sendall", 4, "inputs"},
216+
{ "sendall", 4, "locktime"},
217+
{ "sendall", 4, "lock_unspents"},
218+
{ "sendall", 4, "psbt"},
219+
{ "sendall", 4, "send_max"},
220+
{ "sendall", 4, "minconf"},
221+
{ "sendall", 4, "maxconf"},
222+
{ "sendall", 4, "conf_target"},
223+
{ "sendall", 4, "replaceable"},
224+
{ "sendall", 4, "solving_data"},
164225
{ "simulaterawtransaction", 0, "rawtxs" },
165226
{ "simulaterawtransaction", 1, "options" },
227+
{ "simulaterawtransaction", 1, "include_watchonly"},
166228
{ "importprivkey", 2, "rescan" },
167229
{ "importaddress", 2, "rescan" },
168230
{ "importaddress", 3, "p2sh" },
169231
{ "importpubkey", 2, "rescan" },
170232
{ "importmulti", 0, "requests" },
171233
{ "importmulti", 1, "options" },
234+
{ "importmulti", 1, "rescan" },
172235
{ "importdescriptors", 0, "requests" },
173236
{ "listdescriptors", 0, "private" },
174237
{ "verifychain", 0, "checklevel" },
@@ -192,7 +255,15 @@ static const CRPCConvertParam vRPCConvertParams[] =
192255
{ "getmempooldescendants", 1, "verbose" },
193256
{ "gettxspendingprevout", 0, "outputs" },
194257
{ "bumpfee", 1, "options" },
258+
{ "bumpfee", 1, "conf_target"},
259+
{ "bumpfee", 1, "fee_rate"},
260+
{ "bumpfee", 1, "replaceable"},
261+
{ "bumpfee", 1, "outputs"},
195262
{ "psbtbumpfee", 1, "options" },
263+
{ "psbtbumpfee", 1, "conf_target"},
264+
{ "psbtbumpfee", 1, "fee_rate"},
265+
{ "psbtbumpfee", 1, "replaceable"},
266+
{ "psbtbumpfee", 1, "outputs"},
196267
{ "logging", 0, "include" },
197268
{ "logging", 1, "exclude" },
198269
{ "disconnectnode", 1, "nodeid" },

src/rpc/server.cpp

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ std::string JSONRPCExecBatch(const JSONRPCRequest& jreq, const UniValue& vReq)
392392
* Process named arguments into a vector of positional arguments, based on the
393393
* passed-in specification for the RPC call's arguments.
394394
*/
395-
static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, const std::vector<std::string>& argNames)
395+
static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, const std::vector<std::pair<std::string, bool>>& argNames)
396396
{
397397
JSONRPCRequest out = in;
398398
out.params = UniValue(UniValue::VARR);
@@ -417,7 +417,9 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
417417
// "args" parameter, if present.
418418
int hole = 0;
419419
int initial_hole_size = 0;
420-
for (const std::string &argNamePattern: argNames) {
420+
const std::string* initial_param = nullptr;
421+
UniValue options{UniValue::VOBJ};
422+
for (const auto& [argNamePattern, named_only]: argNames) {
421423
std::vector<std::string> vargNames = SplitString(argNamePattern, '|');
422424
auto fr = argsIn.end();
423425
for (const std::string & argName : vargNames) {
@@ -426,30 +428,58 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c
426428
break;
427429
}
428430
}
429-
if (fr != argsIn.end()) {
431+
432+
// Handle named-only parameters by pushing them into a temporary options
433+
// object, and then pushing the accumulated options as the next
434+
// positional argument.
435+
if (named_only) {
436+
if (fr != argsIn.end()) {
437+
if (options.exists(fr->first)) {
438+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + fr->first + " specified multiple times");
439+
}
440+
options.__pushKV(fr->first, *fr->second);
441+
argsIn.erase(fr);
442+
}
443+
continue;
444+
}
445+
446+
if (!options.empty() || fr != argsIn.end()) {
430447
for (int i = 0; i < hole; ++i) {
431448
// Fill hole between specified parameters with JSON nulls,
432449
// but not at the end (for backwards compatibility with calls
433450
// that act based on number of specified parameters).
434451
out.params.push_back(UniValue());
435452
}
436453
hole = 0;
437-
out.params.push_back(*fr->second);
438-
argsIn.erase(fr);
454+
if (!initial_param) initial_param = &argNamePattern;
439455
} else {
440456
hole += 1;
441457
if (out.params.empty()) initial_hole_size = hole;
442458
}
459+
460+
// If named input parameter "fr" is present, push it onto out.params. If
461+
// options are present, push them onto out.params. If both are present,
462+
// throw an error.
463+
if (fr != argsIn.end()) {
464+
if (!options.empty()) {
465+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + fr->first + " conflicts with parameter " + options.getKeys().front());
466+
}
467+
out.params.push_back(*fr->second);
468+
argsIn.erase(fr);
469+
}
470+
if (!options.empty()) {
471+
out.params.push_back(std::move(options));
472+
options = UniValue{UniValue::VOBJ};
473+
}
443474
}
444475
// If leftover "args" param was found, use it as a source of positional
445476
// arguments and add named arguments after. This is a convenience for
446477
// clients that want to pass a combination of named and positional
447478
// arguments as described in doc/JSON-RPC-interface.md#parameter-passing
448479
auto positional_args{argsIn.extract("args")};
449480
if (positional_args && positional_args.mapped()->isArray()) {
450-
const bool has_named_arguments{initial_hole_size < (int)argNames.size()};
451-
if (initial_hole_size < (int)positional_args.mapped()->size() && has_named_arguments) {
452-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + argNames[initial_hole_size] + " specified twice both as positional and named argument");
481+
if (initial_hole_size < (int)positional_args.mapped()->size() && initial_param) {
482+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter " + *initial_param + " specified twice both as positional and named argument");
453483
}
454484
// Assign positional_args to out.params and append named_args after.
455485
UniValue named_args{std::move(out.params)};

src/rpc/server.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class CRPCCommand
9595
using Actor = std::function<bool(const JSONRPCRequest& request, UniValue& result, bool last_handler)>;
9696

9797
//! Constructor taking Actor callback supporting multiple handlers.
98-
CRPCCommand(std::string category, std::string name, Actor actor, std::vector<std::string> args, intptr_t unique_id)
98+
CRPCCommand(std::string category, std::string name, Actor actor, std::vector<std::pair<std::string, bool>> args, intptr_t unique_id)
9999
: category(std::move(category)), name(std::move(name)), actor(std::move(actor)), argNames(std::move(args)),
100100
unique_id(unique_id)
101101
{
@@ -115,7 +115,16 @@ class CRPCCommand
115115
std::string category;
116116
std::string name;
117117
Actor actor;
118-
std::vector<std::string> argNames;
118+
//! List of method arguments and whether they are named-only. Incoming RPC
119+
//! requests contain a "params" field that can either be an array containing
120+
//! unnamed arguments or an object containing named arguments. The
121+
//! "argNames" vector is used in the latter case to transform the params
122+
//! object into an array. Each argument in "argNames" gets mapped to a
123+
//! unique position in the array, based on the order it is listed, unless
124+
//! the argument is a named-only argument with argNames[x].second set to
125+
//! true. Named-only arguments are combined into a JSON object that is
126+
//! appended after other arguments, see transformNamedArguments for details.
127+
std::vector<std::pair<std::string, bool>> argNames;
119128
intptr_t unique_id;
120129
};
121130

0 commit comments

Comments
 (0)