1212#include < string>
1313#include < string_view>
1414
15+ // ! Specify whether parameter should be parsed by bitcoin-cli as a JSON value,
16+ // ! or passed unchanged as a string, or a combination of both.
17+ enum ParamFormat { JSON, STRING, JSON_OR_STRING };
18+
1519class CRPCConvertParam
1620{
1721public:
1822 std::string methodName; // !< method whose params want conversion
1923 int paramIdx; // !< 0-based idx of param to convert
2024 std::string paramName; // !< parameter name
21- bool also_string{ false }; // !< The parameter is also a string
25+ ParamFormat format{ParamFormat::JSON }; // !< parameter format
2226};
2327
2428// clang-format off
2529/* *
26- * Specify a (method, idx, name) here if the argument is a non-string RPC
27- * argument and needs to be converted from JSON.
30+ * Specify a (method, idx, name, format) here if the argument is a non-string RPC
31+ * argument and needs to be converted from JSON, or if it is a string argument
32+ * passed to a method that accepts '=' characters in any string arguments.
33+ *
34+ * JSON parameters need to be listed here to make bitcoin-cli parse command line
35+ * arguments as JSON, instead of passing them as raw strings. `JSON` and
36+ * `JSON_OR_STRING` formats both make `bitcoin-cli` attempt to parse the
37+ * argument as JSON. But if parsing fails, the former triggers an error while
38+ * the latter falls back to passing the argument as a raw string. This is
39+ * useful for arguments like hash_or_height, allowing invocations such as
40+ * `bitcoin-cli getblockstats <hash>` without needing to quote the hash string
41+ * as JSON (`'"<hash>"'`).
42+ *
43+ * String parameters that may contain an '=' character (e.g. base64 strings,
44+ * filenames, or labels) need to be listed here with format `ParamFormat::STRING`
45+ * to make bitcoin-cli treat them as positional parameters when `-named` is used.
46+ * This prevents `bitcoin-cli` from splitting strings like "my=wallet" into a named
47+ * argument "my" and value "wallet" when the whole string is intended to be a
48+ * single positional argument. And if one string parameter is listed for a method,
49+ * other string parameters for that method need to be listed as well so bitcoin-cli
50+ * does not make the opposite mistake and pass other arguments by position instead of
51+ * name because it does not recognize their names. See \ref RPCConvertNamedValues
52+ * for more information on how named and positional arguments are distinguished with
53+ * -named.
2854 *
2955 * @note Parameter indexes start from 0.
3056 */
3157static const CRPCConvertParam vRPCConvertParams[] =
3258{
3359 { " setmocktime" , 0 , " timestamp" },
3460 { " mockscheduler" , 0 , " delta_time" },
61+ { " utxoupdatepsbt" , 0 , " psbt" , ParamFormat::STRING },
3562 { " utxoupdatepsbt" , 1 , " descriptors" },
3663 { " generatetoaddress" , 0 , " nblocks" },
3764 { " generatetoaddress" , 2 , " maxtries" },
@@ -41,16 +68,21 @@ static const CRPCConvertParam vRPCConvertParams[] =
4168 { " generateblock" , 2 , " submit" },
4269 { " getnetworkhashps" , 0 , " nblocks" },
4370 { " getnetworkhashps" , 1 , " height" },
71+ { " sendtoaddress" , 0 , " address" , ParamFormat::STRING },
4472 { " sendtoaddress" , 1 , " amount" },
73+ { " sendtoaddress" , 2 , " comment" , ParamFormat::STRING },
74+ { " sendtoaddress" , 3 , " comment_to" , ParamFormat::STRING },
4575 { " sendtoaddress" , 4 , " subtractfeefromamount" },
4676 { " sendtoaddress" , 5 , " replaceable" },
4777 { " sendtoaddress" , 6 , " conf_target" },
78+ { " sendtoaddress" , 7 , " estimate_mode" , ParamFormat::STRING },
4879 { " sendtoaddress" , 8 , " avoid_reuse" },
4980 { " sendtoaddress" , 9 , " fee_rate" },
5081 { " sendtoaddress" , 10 , " verbose" },
5182 { " settxfee" , 0 , " amount" },
5283 { " getreceivedbyaddress" , 1 , " minconf" },
5384 { " getreceivedbyaddress" , 2 , " include_immature_coinbase" },
85+ { " getreceivedbylabel" , 0 , " label" , ParamFormat::STRING },
5486 { " getreceivedbylabel" , 1 , " minconf" },
5587 { " getreceivedbylabel" , 2 , " include_immature_coinbase" },
5688 { " listreceivedbyaddress" , 0 , " minconf" },
@@ -70,20 +102,27 @@ static const CRPCConvertParam vRPCConvertParams[] =
70102 { " waitforblockheight" , 1 , " timeout" },
71103 { " waitforblock" , 1 , " timeout" },
72104 { " waitfornewblock" , 0 , " timeout" },
105+ { " listtransactions" , 0 , " label" , ParamFormat::STRING },
73106 { " listtransactions" , 1 , " count" },
74107 { " listtransactions" , 2 , " skip" },
75108 { " listtransactions" , 3 , " include_watchonly" },
109+ { " walletpassphrase" , 0 , " passphrase" , ParamFormat::STRING },
76110 { " walletpassphrase" , 1 , " timeout" },
77111 { " getblocktemplate" , 0 , " template_request" },
112+ { " listsinceblock" , 0 , " blockhash" , ParamFormat::STRING },
78113 { " listsinceblock" , 1 , " target_confirmations" },
79114 { " listsinceblock" , 2 , " include_watchonly" },
80115 { " listsinceblock" , 3 , " include_removed" },
81116 { " listsinceblock" , 4 , " include_change" },
117+ { " listsinceblock" , 5 , " label" , ParamFormat::STRING },
118+ { " sendmany" , 0 , " dummy" , ParamFormat::STRING },
82119 { " sendmany" , 1 , " amounts" },
83120 { " sendmany" , 2 , " minconf" },
121+ { " sendmany" , 3 , " comment" , ParamFormat::STRING },
84122 { " sendmany" , 4 , " subtractfeefrom" },
85123 { " sendmany" , 5 , " replaceable" },
86124 { " sendmany" , 6 , " conf_target" },
125+ { " sendmany" , 7 , " estimate_mode" , ParamFormat::STRING },
87126 { " sendmany" , 8 , " fee_rate" },
88127 { " sendmany" , 9 , " verbose" },
89128 { " deriveaddresses" , 1 , " range" },
@@ -170,10 +209,14 @@ static const CRPCConvertParam vRPCConvertParams[] =
170209 { " walletcreatefundedpsbt" , 3 , " max_tx_weight" },
171210 { " walletcreatefundedpsbt" , 4 , " bip32derivs" },
172211 { " walletcreatefundedpsbt" , 5 , " version" },
212+ { " walletprocesspsbt" , 0 , " psbt" , ParamFormat::STRING },
173213 { " walletprocesspsbt" , 1 , " sign" },
214+ { " walletprocesspsbt" , 2 , " sighashtype" , ParamFormat::STRING },
174215 { " walletprocesspsbt" , 3 , " bip32derivs" },
175216 { " walletprocesspsbt" , 4 , " finalize" },
217+ { " descriptorprocesspsbt" , 0 , " psbt" , ParamFormat::STRING },
176218 { " descriptorprocesspsbt" , 1 , " descriptors" },
219+ { " descriptorprocesspsbt" , 2 , " sighashtype" , ParamFormat::STRING },
177220 { " descriptorprocesspsbt" , 3 , " bip32derivs" },
178221 { " descriptorprocesspsbt" , 4 , " finalize" },
179222 { " createpsbt" , 0 , " inputs" },
@@ -183,16 +226,19 @@ static const CRPCConvertParam vRPCConvertParams[] =
183226 { " createpsbt" , 4 , " version" },
184227 { " combinepsbt" , 0 , " txs" },
185228 { " joinpsbts" , 0 , " txs" },
229+ { " finalizepsbt" , 0 , " psbt" , ParamFormat::STRING },
186230 { " finalizepsbt" , 1 , " extract" },
187231 { " converttopsbt" , 1 , " permitsigdata" },
188232 { " converttopsbt" , 2 , " iswitness" },
189233 { " gettxout" , 1 , " n" },
190234 { " gettxout" , 2 , " include_mempool" },
191235 { " gettxoutproof" , 0 , " txids" },
192- { " gettxoutsetinfo" , 1 , " hash_or_height" , /* also_string= */ true },
236+ { " gettxoutsetinfo" , 1 , " hash_or_height" , ParamFormat::JSON_OR_STRING },
193237 { " gettxoutsetinfo" , 2 , " use_index" },
238+ { " dumptxoutset" , 0 , " path" , ParamFormat::STRING },
239+ { " dumptxoutset" , 1 , " type" , ParamFormat::STRING },
194240 { " dumptxoutset" , 2 , " options" },
195- { " dumptxoutset" , 2 , " rollback" , /* also_string= */ true },
241+ { " dumptxoutset" , 2 , " rollback" , ParamFormat::JSON_OR_STRING },
196242 { " lockunspent" , 0 , " unlock" },
197243 { " lockunspent" , 1 , " transactions" },
198244 { " lockunspent" , 2 , " persistent" },
@@ -239,6 +285,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
239285 { " simulaterawtransaction" , 0 , " rawtxs" },
240286 { " simulaterawtransaction" , 1 , " options" },
241287 { " simulaterawtransaction" , 1 , " include_watchonly" },
288+ { " importmempool" , 0 , " filepath" , ParamFormat::STRING },
242289 { " importmempool" , 1 , " options" },
243290 { " importmempool" , 1 , " apply_fee_delta_priority" },
244291 { " importmempool" , 1 , " use_current_time" },
@@ -247,7 +294,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
247294 { " listdescriptors" , 0 , " private" },
248295 { " verifychain" , 0 , " checklevel" },
249296 { " verifychain" , 1 , " nblocks" },
250- { " getblockstats" , 0 , " hash_or_height" , /* also_string= */ true },
297+ { " getblockstats" , 0 , " hash_or_height" , ParamFormat::JSON_OR_STRING },
251298 { " getblockstats" , 1 , " stats" },
252299 { " pruneblockchain" , 0 , " height" },
253300 { " keypoolrefill" , 0 , " newsize" },
@@ -299,14 +346,20 @@ static const CRPCConvertParam vRPCConvertParams[] =
299346 { " echojson" , 9 , " arg9" },
300347 { " rescanblockchain" , 0 , " start_height" },
301348 { " rescanblockchain" , 1 , " stop_height" },
349+ { " createwallet" , 0 , " wallet_name" , ParamFormat::STRING },
302350 { " createwallet" , 1 , " disable_private_keys" },
303351 { " createwallet" , 2 , " blank" },
352+ { " createwallet" , 3 , " passphrase" , ParamFormat::STRING },
304353 { " createwallet" , 4 , " avoid_reuse" },
305354 { " createwallet" , 5 , " descriptors" },
306355 { " createwallet" , 6 , " load_on_startup" },
307356 { " createwallet" , 7 , " external_signer" },
357+ { " restorewallet" , 0 , " wallet_name" , ParamFormat::STRING },
358+ { " restorewallet" , 1 , " backup_file" , ParamFormat::STRING },
308359 { " restorewallet" , 2 , " load_on_startup" },
360+ { " loadwallet" , 0 , " filename" , ParamFormat::STRING },
309361 { " loadwallet" , 1 , " load_on_startup" },
362+ { " unloadwallet" , 0 , " wallet_name" , ParamFormat::STRING },
310363 { " unloadwallet" , 1 , " load_on_startup" },
311364 { " getnodeaddresses" , 0 , " count" },
312365 { " addpeeraddress" , 1 , " port" },
@@ -315,91 +368,140 @@ static const CRPCConvertParam vRPCConvertParams[] =
315368 { " stop" , 0 , " wait" },
316369 { " addnode" , 2 , " v2transport" },
317370 { " addconnection" , 2 , " v2transport" },
371+ { " decodepsbt" , 0 , " psbt" , ParamFormat::STRING },
372+ { " analyzepsbt" , 0 , " psbt" , ParamFormat::STRING},
373+ { " verifymessage" , 1 , " signature" , ParamFormat::STRING },
374+ { " verifymessage" , 2 , " message" , ParamFormat::STRING },
375+ { " getnewaddress" , 0 , " label" , ParamFormat::STRING },
376+ { " getnewaddress" , 1 , " address_type" , ParamFormat::STRING },
377+ { " backupwallet" , 0 , " destination" , ParamFormat::STRING },
378+ { " echoipc" , 0 , " arg" , ParamFormat::STRING },
379+ { " encryptwallet" , 0 , " passphrase" , ParamFormat::STRING },
380+ { " getaddressesbylabel" , 0 , " label" , ParamFormat::STRING },
381+ { " loadtxoutset" , 0 , " path" , ParamFormat::STRING },
382+ { " migratewallet" , 0 , " wallet_name" , ParamFormat::STRING },
383+ { " migratewallet" , 1 , " passphrase" , ParamFormat::STRING },
384+ { " setlabel" , 1 , " label" , ParamFormat::STRING },
385+ { " signmessage" , 1 , " message" , ParamFormat::STRING },
386+ { " signmessagewithprivkey" , 1 , " message" , ParamFormat::STRING },
387+ { " walletpassphrasechange" , 0 , " oldpassphrase" , ParamFormat::STRING },
388+ { " walletpassphrasechange" , 1 , " newpassphrase" , ParamFormat::STRING },
318389};
319390// clang-format on
320391
321392/* * Parse string to UniValue or throw runtime_error if string contains invalid JSON */
322- static UniValue Parse (std::string_view raw, bool also_string )
393+ static UniValue Parse (std::string_view raw, ParamFormat format = ParamFormat::JSON )
323394{
324395 UniValue parsed;
325396 if (!parsed.read (raw)) {
326- if (!also_string ) throw std::runtime_error (tfm::format (" Error parsing JSON: %s" , raw));
327- return raw;
397+ if (format != ParamFormat::JSON_OR_STRING ) throw std::runtime_error (tfm::format (" Error parsing JSON: %s" , raw));
398+ return UniValue ( std::string ( raw)) ;
328399 }
329400 return parsed;
330401}
331402
332- class CRPCConvertTable
403+ namespace rpc_convert
404+ {
405+ const CRPCConvertParam* FromPosition (std::string_view method, size_t pos)
333406{
334- private:
335- std::map<std::pair<std::string, int >, bool > members ;
336- std::map<std::pair<std::string, std::string>, bool > membersByName ;
407+ auto it = std::ranges::find_if (vRPCConvertParams, [&]( const auto & p) {
408+ return p. methodName == method && p. paramIdx == static_cast < int >(pos) ;
409+ }) ;
337410
338- public:
339- CRPCConvertTable ();
411+ return it == std::end (vRPCConvertParams) ? nullptr : &*it;
412+ }
340413
341- /* * Return arg_value as UniValue, and first parse it if it is a non-string parameter */
342- UniValue ArgToUniValue (std::string_view arg_value, const std::string& method, int param_idx)
343- {
344- const auto & it = members.find ({method, param_idx});
345- if (it != members.end ()) {
346- return Parse (arg_value, it->second );
347- }
348- return arg_value;
349- }
414+ const CRPCConvertParam* FromName (std::string_view method, std::string_view name)
415+ {
416+ auto it = std::ranges::find_if (vRPCConvertParams, [&](const auto & p) {
417+ return p.methodName == method && p.paramName == name;
418+ });
350419
351- /* * Return arg_value as UniValue, and first parse it if it is a non-string parameter */
352- UniValue ArgToUniValue (std::string_view arg_value, const std::string& method, const std::string& param_name)
353- {
354- const auto & it = membersByName.find ({method, param_name});
355- if (it != membersByName.end ()) {
356- return Parse (arg_value, it->second );
357- }
358- return arg_value;
359- }
360- };
420+ return it == std::end (vRPCConvertParams) ? nullptr : &*it;
421+ }
422+ } // namespace rpc_convert
361423
362- CRPCConvertTable::CRPCConvertTable ( )
424+ static UniValue ParseParam ( const CRPCConvertParam* param, std::string_view raw )
363425{
364- for (const auto & cp : vRPCConvertParams) {
365- members.emplace (std::make_pair (cp.methodName , cp.paramIdx ), cp.also_string );
366- membersByName.emplace (std::make_pair (cp.methodName , cp.paramName ), cp.also_string );
367- }
426+ // Only parse parameters which have the JSON or JSON_OR_STRING format; otherwise, treat them as strings.
427+ return (param && (param->format == ParamFormat::JSON || param->format == ParamFormat::JSON_OR_STRING)) ? Parse (raw, param->format ) : UniValue (std::string (raw));
368428}
369429
370- static CRPCConvertTable rpcCvtTable;
371-
430+ /* *
431+ * Convert command lines arguments to params object when -named is disabled.
432+ */
372433UniValue RPCConvertValues (const std::string &strMethod, const std::vector<std::string> &strParams)
373434{
374435 UniValue params (UniValue::VARR);
375436
376- for (unsigned int idx = 0 ; idx < strParams.size (); idx++) {
377- std::string_view value{strParams[idx]};
378- params.push_back (rpcCvtTable.ArgToUniValue (value, strMethod, idx));
437+ for (std::string_view s : strParams) {
438+ params.push_back (ParseParam (rpc_convert::FromPosition (strMethod, params.size ()), s));
379439 }
380440
381441 return params;
382442}
383443
444+ /* *
445+ * Convert command line arguments to params object when -named is enabled.
446+ *
447+ * The -named syntax accepts named arguments in NAME=VALUE format, as well as
448+ * positional arguments without names. The syntax is inherently ambiguous if
449+ * names are omitted and values contain '=', so a heuristic is used to
450+ * disambiguate:
451+ *
452+ * - Arguments that do not contain '=' are treated as positional parameters.
453+ *
454+ * - Arguments that do contain '=' are assumed to be named parameters in
455+ * NAME=VALUE format except for two special cases:
456+ *
457+ * 1. The case where NAME is not a known parameter name, and the next
458+ * positional parameter requires a JSON value, and the argument parses as
459+ * JSON. E.g. ["list", "with", "="].
460+ *
461+ * 2. The case where NAME is not a known parameter name and the next
462+ * positional parameter requires a string value. E.g. "my=wallet".
463+ *
464+ * For example, the command `bitcoin-cli -named createwallet "my=wallet"`,
465+ * the parser initially sees "my=wallet" and attempts to process it as a
466+ * parameter named "my". When it finds that "my" is not a valid named parameter
467+ * parameter for this method, it falls back to checking the rule for the
468+ * next available positional parameter (index 0). Because it finds the rule
469+ * that this parameter is a ParamFormat::STRING, it correctly treats the entire
470+ * "my=wallet" as a single positional string, successfully creating a
471+ * wallet with that literal name.
472+ */
384473UniValue RPCConvertNamedValues (const std::string &strMethod, const std::vector<std::string> &strParams)
385474{
386475 UniValue params (UniValue::VOBJ);
387476 UniValue positional_args{UniValue::VARR};
388477
389478 for (std::string_view s: strParams) {
390479 size_t pos = s.find (' =' );
391- if (pos == std::string ::npos) {
392- positional_args.push_back (rpcCvtTable. ArgToUniValue (s, strMethod, positional_args.size ()));
480+ if (pos == std::string_view ::npos) {
481+ positional_args.push_back (ParseParam ( rpc_convert::FromPosition ( strMethod, positional_args.size ()), s ));
393482 continue ;
394483 }
395484
396485 std::string name{s.substr (0 , pos)};
397486 std::string_view value{s.substr (pos+1 )};
398487
488+ const CRPCConvertParam* named_param{rpc_convert::FromName (strMethod, name)};
489+ if (!named_param) {
490+ const CRPCConvertParam* positional_param = rpc_convert::FromPosition (strMethod, positional_args.size ());
491+ UniValue parsed_value;
492+ if (positional_param && positional_param->format == ParamFormat::JSON && parsed_value.read (s)) {
493+ positional_args.push_back (std::move (parsed_value));
494+ continue ;
495+ } else if (positional_param && positional_param->format == ParamFormat::STRING) {
496+ positional_args.push_back (s);
497+ continue ;
498+ }
499+ }
500+
399501 // Intentionally overwrite earlier named values with later ones as a
400502 // convenience for scripts and command line users that want to merge
401503 // options.
402- params.pushKV (name, rpcCvtTable. ArgToUniValue (value, strMethod, name ));
504+ params.pushKV (name, ParseParam (named_param, value ));
403505 }
404506
405507 if (!positional_args.empty ()) {
0 commit comments