Skip to content

Commit 8ddebd5

Browse files
ryanofskythepastaclaw
authored andcommitted
Merge bitcoin#29277: RPC: access RPC arguments by name
30a6c99 rpc: access some args by name (stickies-v) bbb3126 rpc: add named arg helper (stickies-v) 13525e0 rpc: add arg helper unit test (stickies-v) Pull request description: Adds string overloads for the `RPCHelpMan::Arg` and `RPCHelpMan::MaybeArg` helpers to be able to access RPC arguments by name instead of index number. Especially in RPCs with a large number of parameters, this can be quite helpful. Example usage: ```cpp const auto action{self.Arg<std::string>("action")}; ``` Most of the LoC is adding test coverage and documentation updates. No behaviour change. An alternative approach to bitcoin#27788 with significantly less overhaul. ACKs for top commit: fjahr: Code review ACK 30a6c99 maflcko: ACK 30a6c99 🥑 ryanofsky: Code review ACK 30a6c99. Nice change! Implementation is surprisingly simple and additional unit test coverage is welcome, too. Tree-SHA512: 4904f5f914fe1d421d32f60edb7c5a028c8ea0f140a2f207a106b4752d441164e073066a6bf2e17693f859fe847815a96609d3cf521e0ac4178d8cd09362ea3d
1 parent 8474a82 commit 8ddebd5

File tree

9 files changed

+153
-30
lines changed

9 files changed

+153
-30
lines changed

src/rpc/blockchain.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,15 +2500,16 @@ static RPCHelpMan scantxoutset()
25002500
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR});
25012501

25022502
UniValue result(UniValue::VOBJ);
2503-
if (request.params[0].get_str() == "status") {
2503+
const auto action{self.Arg<std::string>("action")};
2504+
if (action == "status") {
25042505
CoinsViewScanReserver reserver;
25052506
if (reserver.reserve()) {
25062507
// no scan in progress
25072508
return UniValue::VNULL;
25082509
}
25092510
result.pushKV("progress", g_scan_progress.load());
25102511
return result;
2511-
} else if (request.params[0].get_str() == "abort") {
2512+
} else if (action == "abort") {
25122513
CoinsViewScanReserver reserver;
25132514
if (reserver.reserve()) {
25142515
// reserve was possible which means no scan was running
@@ -2517,7 +2518,7 @@ static RPCHelpMan scantxoutset()
25172518
// set the abort flag
25182519
g_should_abort_scan = true;
25192520
return true;
2520-
} else if (request.params[0].get_str() == "start") {
2521+
} else if (action == "start") {
25212522
CoinsViewScanReserver reserver;
25222523
if (!reserver.reserve()) {
25232524
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
@@ -2586,7 +2587,7 @@ static RPCHelpMan scantxoutset()
25862587
result.pushKV("unspents", unspents);
25872588
result.pushKV("total_amount", ValueFromAmount(total_in));
25882589
} else {
2589-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command");
2590+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid action '%s'", action));
25902591
}
25912592
return result;
25922593
},

src/rpc/mining.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ static RPCHelpMan getnetworkhashps()
118118

119119
ChainstateManager& chainman = EnsureAnyChainman(request.context);
120120
LOCK(cs_main);
121-
return GetNetworkHashPS(self.Arg<int>(0), self.Arg<int>(1), chainman.ActiveChain());
121+
return GetNetworkHashPS(self.Arg<int>("nblocks"), self.Arg<int>("height"), chainman.ActiveChain());
122122
},
123123
};
124124
}
@@ -230,12 +230,12 @@ static RPCHelpMan generatetodescriptor()
230230
"\nGenerate 11 blocks to mydesc\n" + HelpExampleCli("generatetodescriptor", "11 \"mydesc\"")},
231231
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
232232
{
233-
const auto num_blocks{self.Arg<int>(0)};
234-
const auto max_tries{self.Arg<uint64_t>(2)};
233+
const auto num_blocks{self.Arg<int>("num_blocks")};
234+
const auto max_tries{self.Arg<uint64_t>("maxtries")};
235235

236236
CScript coinbase_script;
237237
std::string error;
238-
if (!getScriptFromDescriptor(self.Arg<std::string>(1), coinbase_script, error)) {
238+
if (!getScriptFromDescriptor(self.Arg<std::string>("descriptor"), coinbase_script, error)) {
239239
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
240240
}
241241

src/rpc/net.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ static RPCHelpMan addnode()
321321
},
322322
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
323323
{
324-
const std::string command{request.params[1].get_str()};
324+
const auto command{self.Arg<std::string>("command")};
325325
if (command != "onetry" && command != "add" && command != "remove") {
326326
throw std::runtime_error(
327327
self.ToString());
@@ -330,9 +330,9 @@ static RPCHelpMan addnode()
330330
const NodeContext& node = EnsureAnyNodeContext(request.context);
331331
CConnman& connman = EnsureConnman(node);
332332

333-
const std::string node_arg = request.params[0].get_str();
333+
const auto node_arg{self.Arg<std::string>("node")};
334334
bool node_v2transport = connman.GetLocalServices() & NODE_P2P_V2;
335-
bool use_v2transport = request.params[2].isNull() ? node_v2transport : request.params[2].get_bool();
335+
bool use_v2transport = self.MaybeArg<bool>("v2transport").value_or(node_v2transport);
336336

337337
if (use_v2transport && !node_v2transport) {
338338
throw JSONRPCError(RPC_INVALID_PARAMETER, "Error: v2transport requested but not enabled (see -v2transport)");

src/rpc/signmessage.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ static RPCHelpMan verifymessage()
3939
},
4040
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
4141
{
42-
std::string strAddress = request.params[0].get_str();
43-
std::string strSign = request.params[1].get_str();
44-
std::string strMessage = request.params[2].get_str();
42+
std::string strAddress = self.Arg<std::string>("address");
43+
std::string strSign = self.Arg<std::string>("signature");
44+
std::string strMessage = self.Arg<std::string>("message");
4545

4646
switch (MessageVerify(strAddress, strSign, strMessage)) {
4747
case MessageVerificationResult::ERR_INVALID_ADDRESS:

src/rpc/util.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
#include <util/system.h>
1919
#include <util/translation.h>
2020

21+
#include <algorithm>
22+
#include <iterator>
23+
#include <string_view>
24+
#include <tuple>
25+
2126
const std::string UNIX_EPOCH_TIME = "UNIX epoch time";
2227
const std::string EXAMPLE_ADDRESS[2] = {"XunLY9Tf7Zsef8gMGL2fhWA9ZmMjt4KPw0", "XwQQkwA4FYkq2XERzMY2CiAZhJTEDAbtc0"};
2328

@@ -599,6 +604,16 @@ std::vector<std::string> RPCHelpMan::GetArgNames() const
599604
return ret;
600605
}
601606

607+
size_t RPCHelpMan::GetParamIndex(std::string_view key) const
608+
{
609+
auto it{std::find_if(
610+
m_args.begin(), m_args.end(), [&key](const auto& arg) { return arg.GetName() == key;}
611+
)};
612+
613+
CHECK_NONFATAL(it != m_args.end()); // TODO: ideally this is checked at compile time
614+
return std::distance(m_args.begin(), it);
615+
}
616+
602617
std::string RPCHelpMan::ToString() const
603618
{
604619
std::string ret;

src/rpc/util.h

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -387,18 +387,25 @@ class RPCHelpMan
387387

388388
UniValue HandleRequest(const JSONRPCRequest& request) const;
389389
/**
390-
* Helper to get a request argument.
391-
* This function only works during m_fun(), i.e. it should only be used in
392-
* RPC method implementations. The helper internally checks whether the
393-
* user-passed argument isNull() and parses (from JSON) and returns the
394-
* user-passed argument, or the default value derived from the RPCArg
395-
* documention, or a falsy value if no default was given.
390+
* @brief Helper to get a required or default-valued request argument.
396391
*
397-
* Use Arg<Type>(i) to get the argument or its default value. Otherwise,
398-
* use MaybeArg<Type>(i) to get the optional argument or a falsy value.
392+
* Use this function when the argument is required or when it has a default value. If the
393+
* argument is optional and may not be provided, use MaybeArg instead.
399394
*
400-
* The Type passed to this helper must match the corresponding
401-
* RPCArg::Type.
395+
* This function only works during m_fun(), i.e., it should only be used in
396+
* RPC method implementations. It internally checks whether the user-passed
397+
* argument isNull() and parses (from JSON) and returns the user-passed argument,
398+
* or the default value derived from the RPCArg documentation.
399+
*
400+
* There are two overloads of this function:
401+
* - Use Arg<Type>(size_t i) to get the argument (or the default value) by index.
402+
* - Use Arg<Type>(const std::string& key) to get the argument (or the default value) by key.
403+
*
404+
* The Type passed to this helper must match the corresponding RPCArg::Type.
405+
*
406+
* @return The value of the RPC argument (or the default value) cast to type Type.
407+
*
408+
* @see MaybeArg for handling optional arguments without default values.
402409
*/
403410
template <typename R>
404411
auto Arg(size_t i) const
@@ -412,6 +419,34 @@ class RPCHelpMan
412419
return ArgValue<const R&>(i);
413420
}
414421
}
422+
template<typename R>
423+
auto Arg(std::string_view key) const
424+
{
425+
return Arg<R>(GetParamIndex(key));
426+
}
427+
/**
428+
* @brief Helper to get an optional request argument.
429+
*
430+
* Use this function when the argument is optional and does not have a default value. If the
431+
* argument is required or has a default value, use Arg instead.
432+
*
433+
* This function only works during m_fun(), i.e., it should only be used in
434+
* RPC method implementations. It internally checks whether the user-passed
435+
* argument isNull() and parses (from JSON) and returns the user-passed argument,
436+
* or a falsy value if no argument was passed.
437+
*
438+
* There are two overloads of this function:
439+
* - Use MaybeArg<Type>(size_t i) to get the optional argument by index.
440+
* - Use MaybeArg<Type>(const std::string& key) to get the optional argument by key.
441+
*
442+
* The Type passed to this helper must match the corresponding RPCArg::Type.
443+
*
444+
* @return For integral and floating-point types, a std::optional<Type> is returned.
445+
* For other types, a Type* pointer to the argument is returned. If the
446+
* argument is not provided, std::nullopt or a null pointer is returned.
447+
*
448+
* @see Arg for handling arguments that are required or have a default value.
449+
*/
415450
template <typename R>
416451
auto MaybeArg(size_t i) const
417452
{
@@ -424,6 +459,11 @@ class RPCHelpMan
424459
return ArgValue<const R*>(i);
425460
}
426461
}
462+
template<typename R>
463+
auto MaybeArg(std::string_view key) const
464+
{
465+
return MaybeArg<R>(GetParamIndex(key));
466+
}
427467
std::string ToString() const;
428468
/** Return the named args that need to be converted from string to another JSON type */
429469
UniValue GetArgMap() const;
@@ -443,6 +483,8 @@ class RPCHelpMan
443483
mutable const JSONRPCRequest* m_req{nullptr}; // A pointer to the request for the duration of m_fun()
444484
template <typename R>
445485
R ArgValue(size_t i) const;
486+
//! Return positional index of a parameter using its name as key.
487+
size_t GetParamIndex(std::string_view key) const;
446488
};
447489

448490
RPCErrorCode RPCErrorFromTransactionError(TransactionError terr);

src/test/rpc_tests.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,4 +702,72 @@ BOOST_AUTO_TEST_CASE(rpc_convert_composite_commands)
702702
BOOST_CHECK_EQUAL(result[1][1].get_str(), "avgfeerate");
703703
}
704704

705+
static void CheckRpc(const std::vector<RPCArg>& params, const UniValue& args, RPCHelpMan::RPCMethodImpl test_impl)
706+
{
707+
auto null_result{RPCResult{RPCResult::Type::NONE, "", "None"}};
708+
const RPCHelpMan rpc{"dummy", "dummy description", params, null_result, RPCExamples{""}, test_impl};
709+
JSONRPCRequest req;
710+
req.params = args;
711+
712+
rpc.HandleRequest(req);
713+
}
714+
715+
BOOST_AUTO_TEST_CASE(rpc_arg_helper)
716+
{
717+
constexpr bool DEFAULT_BOOL = true;
718+
constexpr auto DEFAULT_STRING = "default";
719+
constexpr uint64_t DEFAULT_UINT64_T = 3;
720+
721+
//! Parameters with which the RPCHelpMan is instantiated
722+
const std::vector<RPCArg> params{
723+
// Required arg
724+
{"req_int", RPCArg::Type::NUM, RPCArg::Optional::NO, ""},
725+
{"req_str", RPCArg::Type::STR, RPCArg::Optional::NO, ""},
726+
// Default arg
727+
{"def_uint64_t", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_UINT64_T}, ""},
728+
{"def_string", RPCArg::Type::STR, RPCArg::Default{DEFAULT_STRING}, ""},
729+
{"def_bool", RPCArg::Type::BOOL, RPCArg::Default{DEFAULT_BOOL}, ""},
730+
// Optional arg without default
731+
{"opt_double", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, ""},
732+
{"opt_string", RPCArg::Type::STR, RPCArg::Optional::OMITTED, ""}
733+
};
734+
735+
//! Check that `self.Arg` returns the same value as the `request.params` accessors
736+
RPCHelpMan::RPCMethodImpl check_positional = [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
737+
BOOST_CHECK_EQUAL(self.Arg<int>(0), request.params[0].getInt<int>());
738+
BOOST_CHECK_EQUAL(self.Arg<std::string>(1), request.params[1].get_str());
739+
BOOST_CHECK_EQUAL(self.Arg<uint64_t>(2), request.params[2].isNull() ? DEFAULT_UINT64_T : request.params[2].getInt<uint64_t>());
740+
BOOST_CHECK_EQUAL(self.Arg<std::string>(3), request.params[3].isNull() ? DEFAULT_STRING : request.params[3].get_str());
741+
BOOST_CHECK_EQUAL(self.Arg<bool>(4), request.params[4].isNull() ? DEFAULT_BOOL : request.params[4].get_bool());
742+
if (!request.params[5].isNull()) {
743+
BOOST_CHECK_EQUAL(self.MaybeArg<double>(5).value(), request.params[5].get_real());
744+
} else {
745+
BOOST_CHECK(!self.MaybeArg<double>(5));
746+
}
747+
if (!request.params[6].isNull()) {
748+
BOOST_CHECK(self.MaybeArg<std::string>(6));
749+
BOOST_CHECK_EQUAL(*self.MaybeArg<std::string>(6), request.params[6].get_str());
750+
} else {
751+
BOOST_CHECK(!self.MaybeArg<std::string>(6));
752+
}
753+
return UniValue{};
754+
};
755+
CheckRpc(params, UniValue{JSON(R"([5, "hello", null, null, null, null, null])")}, check_positional);
756+
CheckRpc(params, UniValue{JSON(R"([5, "hello", 4, "test", true, 1.23, "world"])")}, check_positional);
757+
758+
//! Check that `self.Arg` returns the same value when using index and key
759+
RPCHelpMan::RPCMethodImpl check_named = [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
760+
BOOST_CHECK_EQUAL(self.Arg<int>(0), self.Arg<int>("req_int"));
761+
BOOST_CHECK_EQUAL(self.Arg<std::string>(1), self.Arg<std::string>("req_str"));
762+
BOOST_CHECK_EQUAL(self.Arg<uint64_t>(2), self.Arg<uint64_t>("def_uint64_t"));
763+
BOOST_CHECK_EQUAL(self.Arg<std::string>(3), self.Arg<std::string>("def_string"));
764+
BOOST_CHECK_EQUAL(self.Arg<bool>(4), self.Arg<bool>("def_bool"));
765+
BOOST_CHECK(self.MaybeArg<double>(5) == self.MaybeArg<double>("opt_double"));
766+
BOOST_CHECK(self.MaybeArg<std::string>(6) == self.MaybeArg<std::string>("opt_string"));
767+
return UniValue{};
768+
};
769+
CheckRpc(params, UniValue{JSON(R"([5, "hello", null, null, null, null, null])")}, check_named);
770+
CheckRpc(params, UniValue{JSON(R"([5, "hello", 4, "test", true, 1.23, "world"])")}, check_named);
771+
}
772+
705773
BOOST_AUTO_TEST_SUITE_END()

src/wallet/rpc/coins.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,12 @@ RPCHelpMan getbalance()
197197

198198
LOCK(pwallet->cs_wallet);
199199

200-
const auto dummy_value{self.MaybeArg<std::string>(0)};
200+
const auto dummy_value{self.MaybeArg<std::string>("dummy")};
201201
if (dummy_value && *dummy_value != "*") {
202202
throw JSONRPCError(RPC_METHOD_DEPRECATED, "dummy first argument must be excluded or set to \"*\".");
203203
}
204204

205-
int min_depth = 0;
206-
if (!request.params[1].isNull()) {
207-
min_depth = request.params[1].getInt<int>();
208-
}
205+
const auto min_depth{self.Arg<int>("minconf")};
209206

210207
const UniValue& addlocked = request.params[2];
211208
bool fAddLocked = false;

test/functional/rpc_scantxoutset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def run_test(self):
129129
assert_raises_rpc_error(-1, "scanobjects argument is required for the start action", self.nodes[0].scantxoutset, "start")
130130

131131
# Check that invalid command give error
132-
assert_raises_rpc_error(-8, "Invalid command", self.nodes[0].scantxoutset, "invalid_command")
132+
assert_raises_rpc_error(-8, "Invalid action 'invalid_command'", self.nodes[0].scantxoutset, "invalid_command")
133133

134134

135135
if __name__ == "__main__":

0 commit comments

Comments
 (0)