rpc: port RPCHelpMan infrastructure from Bitcoin Core v26.0#2923
Conversation
Introduce src/rpc/util.{h,cpp} with the reduced-subset RPCHelpMan machinery
(help-text rendering and argument-count validation) needed to convert the ~165
existing RPC commands to structured help documentation as tracked in gridcoin-community#2922.
This is the infrastructure PR; no command conversions are included.
## What's ported
Ported verbatim from bitcoin/bitcoin@v26.0 src/rpc/util.{h,cpp}:
- RPCHelpMan class with the single (name, description, args, results, examples)
constructor, plus ToString(), IsValidNumArgs(), and GetArgNames()
- RPCArg / RPCArgOptions with all v26.0 Type enum values (OBJ, ARR, STR, NUM,
BOOL, OBJ_NAMED_PARAMS, OBJ_USER_KEYS, AMOUNT, STR_HEX, RANGE), Fallback
variant (Optional / DefaultHint / Default), IsOptional, ToString (oneline
and multiline), ToStringObj, ToDescriptionString
- RPCResult with all v26.0 Type enum values (OBJ, ARR, STR, NUM, BOOL, NONE,
ANY, STR_AMOUNT, STR_HEX, OBJ_DYN, ARR_FIXED, NUM_TIME, ELISION), the four
constructors (with/without cond, with/without explicit optional), ToSections,
ToStringObj, ToDescriptionString, CheckInnerDoc
- RPCResults and RPCExamples wrappers
- Sections internal pretty-printer (unchanged; v26.0 behaviour byte-for-byte)
- HelpExampleCli, HelpExampleCliNamed, HelpExampleRpc, HelpExampleRpcNamed
(adapted to use `gridcoinresearch` and Gridcoin's RPC port 9332 in place of
the Bitcoin Core equivalents)
- UNIX_EPOCH_TIME constant
- EXAMPLE_ADDRESS[2] constant (with placeholder strings + a TODO(gridcoin-community#2922) to
replace with real Gridcoin example addresses in the first Tier 1 PR that
references them)
## What's deliberately omitted
The following v26.0 features are dispatch-path features that depend on
upstream's JSONRPCRequest struct, which Gridcoin does not yet have:
- RPCHelpMan::HandleRequest
- The second RPCHelpMan constructor taking an RPCMethodImpl lambda
- RPCHelpMan::GetArgMap
- Arg<R> / MaybeArg<R> / ArgValue<R> typed accessor templates
- RPCArg::MatchesType / RPCResult::MatchesType (runtime type validation,
only called from HandleRequest)
- m_fun, m_req, GetParamIndex
Dragging JSONRPCRequest in would require a wide signature sweep across every
RPC command file (changing rpcfn_type from
`UniValue(const UniValue&, bool fHelp)` to `UniValue(const JSONRPCRequest&)`)
which would conflict with the expected Tier 2/3 conversion PRs for gridcoin-community#2922.
The API landed here is a **strict subset** of the v26.0 public surface. A
future modernization PR can grow it into the full dispatch-path shape without
touching any call site added in the interim — nothing gets re-ported later,
only extended.
## Call-site pattern for Tier 1/2/3 conversions
The existing `(const UniValue& params, bool fHelp)` RPC signature is
untouched. Conversions use the upstream idiom directly:
UniValue somecommand(const UniValue& params, bool fHelp)
{
static const RPCHelpMan help{
"somecommand", "Description of the command.",
{ ... RPCArg specs ... },
RPCResult{ ... },
RPCExamples{ ... },
};
if (fHelp || !help.IsValidNumArgs(params.size()))
throw std::runtime_error(help.ToString());
// ... command logic ...
}
For commands whose help text or argument list depends on runtime state
(addkey varying by protocol version, addpoll varying by enabled poll types,
walletpassphrase varying by wallet lock state), RPCHelpMan may be constructed
per-call instead of as `static const`. Construction is cheap — all fields
are held by value and moved in. A dedicated unit test exercises this
dynamic-construction pattern.
## Drive-by fixes
Two latent bugs surfaced while landing this port and are fixed here because
they block it:
1. **PACKAGE_BUGREPORT was never plumbed through.** CHECK_NONFATAL in
src/util/check.h references PACKAGE_BUGREPORT in its error message, but
the macro was never defined in src/config/gridcoin-config.h.cmake.in.
CHECK_NONFATAL has been dormant dead code — this port is the first
Gridcoin consumer, so the dangling reference would have caused any future
use to fail. Adds the definition pointing at the GitHub issues page.
2. **src/rpc/ files used #include "util.h" (quotes)** which searches the
directory of the including file first. Before this branch, there was no
util.h in src/rpc/, so the quoted include fell through to src/util.h
(Gridcoin's general utility header with LogPrintf, gArgs, LogInstance,
etc.). Adding src/rpc/util.h caused blockchain.h, client.cpp, dataacq.cpp,
misc.cpp, protocol.cpp, and server.cpp to start resolving that include
to the new RPCHelpMan header and lose access to the logging/args symbols.
Switch those 6 files to `#include <util.h>` which unambiguously resolves
via the `-I src` include path. Other files (src/wallet/*, src/qt/*,
etc.) are unaffected because their own directories do not contain a
colliding util.h.
## Tests
Adds src/test/rpchelpman_tests.cpp covering:
- RPCHelpMan::ToString() output for a trivial spec (description, arguments
section, result section, examples section)
- IsValidNumArgs() boundary cases: zero required, all-optional, mixed
required/optional, exact match, underflow, overflow
- GetArgNames() for aliased arg names
- HelpExampleCli / HelpExampleCliNamed / HelpExampleRpc formatting (including
the `gridcoinresearch` cli name and port 9332)
- RPCArg::ToString() for every scalar Type variant (STR, STR_HEX, NUM,
AMOUNT, RANGE, BOOL)
- RPCResults::ToDescriptionString() for STR_HEX, OBJ, ARR, and OBJ_DYN
result shapes
- Per-call (non-static) RPCHelpMan construction with runtime-computed args
and description (validates the addkey/addpoll dynamic-help pattern)
- The `throw std::runtime_error(help.ToString())` pattern that Tier 1
conversions will use when fHelp is set or arg count is wrong
Refs: gridcoin-community#2922
129df2d to
2fbf4af
Compare
There was a problem hiding this comment.
Pull request overview
Ports a subset of Bitcoin Core v26.0’s RPCHelpMan infrastructure into Gridcoin to enable structured, consistent RPC help text and argument-count validation, while keeping the existing (const UniValue& params, bool fHelp) RPC function signature.
Changes:
- Add
src/rpc/util.{h,cpp}implementingRPCHelpMan,RPCArg,RPCResult(s),RPCExamples, and help example helpers. - Add a new
rpchelpman_testssuite and wire it into the test build. - Fix build plumbing and include resolution issues: define
PACKAGE_BUGREPORTand switchsrc/rpc/*includes from"util.h"to<util.h>to avoid the newsrc/rpc/util.hcollision.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/rpc/util.h |
Introduces the new public RPCHelpMan/RPCArg/RPCResult APIs and constants. |
src/rpc/util.cpp |
Implements help rendering, argument-count validation, and example formatting helpers. |
src/test/rpchelpman_tests.cpp |
Adds unit tests for help rendering, arg-count validation, arg names, and example formatting. |
src/test/CMakeLists.txt |
Registers the new rpchelpman_tests.cpp in the test target. |
src/CMakeLists.txt |
Adds rpc/util.cpp to the gridcoin_util static library. |
src/rpc/{blockchain.h,client.cpp,dataacq.cpp,misc.cpp,protocol.cpp,server.cpp} |
Changes #include "util.h" to #include <util.h> to prevent wrong-header resolution. |
src/config/gridcoin-config.h.cmake.in |
Defines PACKAGE_BUGREPORT for CHECK_NONFATAL messages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch (arg.m_type) { | ||
| case RPCArg::Type::STR_HEX: | ||
| case RPCArg::Type::STR: | ||
| case RPCArg::Type::NUM: | ||
| case RPCArg::Type::AMOUNT: | ||
| case RPCArg::Type::RANGE: | ||
| 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) { | ||
| left += "\"" + arg.GetName() + "\": " + arg.m_opts.type_str.at(0); | ||
| } else { | ||
| left += push_name ? arg.ToStringObj(/*oneline=*/false) : arg.ToString(/*oneline=*/false); | ||
| } | ||
| left += ","; | ||
| PushSection({left, arg.ToDescriptionString(/*is_named_arg=*/push_name)}); | ||
| break; | ||
| } | ||
| case RPCArg::Type::OBJ: | ||
| case RPCArg::Type::OBJ_USER_KEYS: { | ||
| const auto right = is_top_level_arg ? "" : arg.ToDescriptionString(/*is_named_arg=*/push_name); | ||
| PushSection({indent + (push_name ? "\"" + arg.GetName() + "\": " : "") + "{", right}); | ||
| for (const auto& arg_inner : arg.m_inner) { | ||
| Push(arg_inner, current_indent + 2, OuterType::OBJ); | ||
| } | ||
| if (arg.m_type != RPCArg::Type::OBJ) { | ||
| PushSection({indent_next + "...", ""}); | ||
| } | ||
| PushSection({indent + "}" + (is_top_level_arg ? "" : ","), ""}); | ||
| break; |
There was a problem hiding this comment.
RPCArg::Type::OBJ_NAMED_PARAMS is treated as a non-recursive scalar in Sections::Push(), so a top-level options object won't have its { ... } structure rendered in the Arguments section (the function returns early on the first recursion), and nested OBJ_NAMED_PARAMS values would be rendered via ToStringObj()/ToString() even though this type is constructed with m_inner. Consider handling OBJ_NAMED_PARAMS in the same branch as OBJ (emit {}, recurse into m_inner, and add ... as appropriate) so the help output correctly documents the options object structure in all contexts.
There was a problem hiding this comment.
Not a bug — leaving as-is. OBJ_NAMED_PARAMS intentionally lands in the non-recursive branch here. Top-level OBJ_NAMED_PARAMS args are rendered by the separate named_only_sections loop in RPCHelpMan::ToString() (~util.cpp:524-528), which spins up its own Sections for the inner keys. Folding it into the OBJ branch would double-render — once under "Arguments:", once under "Named Arguments:". This matches upstream Bitcoin Core v26.0 behavior.
| case Type::OBJ_USER_KEYS: | ||
| // Currently unused, so avoid writing dead code | ||
| CHECK_NONFATAL(false); |
There was a problem hiding this comment.
RPCArg::ToStringObj() hard-aborts for Type::OBJ, Type::OBJ_NAMED_PARAMS, and Type::OBJ_USER_KEYS ("Currently unused"), but RPCArg::ToString() calls ToStringObj() on every m_inner element when rendering object-type arguments. This will fail for nested object fields (i.e., an object containing another object), which is a supported shape in upstream RPCHelpMan docs. Implement object-type handling in ToStringObj() (or avoid routing object inners through ToStringObj()), so nested objects can be represented without triggering CHECK_NONFATAL(false).
| case Type::OBJ_USER_KEYS: | |
| // Currently unused, so avoid writing dead code | |
| CHECK_NONFATAL(false); | |
| case Type::OBJ_USER_KEYS: { | |
| const std::string inner = Join(m_inner, std::string(","), [&](const RPCArg& i) { return i.ToStringObj(oneline); }); | |
| if (m_type == Type::OBJ) { | |
| return res + "{" + inner + "}"; | |
| } else { | |
| return res + "{" + inner + ",...}"; | |
| } | |
| } |
There was a problem hiding this comment.
Real footgun, but the suggested patch isn't quite right and the scope here is "strict subset of v26.0". ToStringObj() is verbatim from upstream v26.0, which has the same NONFATAL_UNREACHABLE() and the same de facto "no nested objects in m_inner" constraint — tolerated upstream because no v26.0 command nests objects two deep. Addressed in c98f5d0 by documenting it instead of changing behavior: a warning comment + TODO(#2922) above ToStringObj() tells spec authors to avoid nested object-typed args until the implementation is extended, and the first Tier 1/2/3 conversion that needs it will implement the real rendering. No behavior change in an infrastructure-only PR.
|
|
||
| // The key name if recursed into a dictionary | ||
| const std::string maybe_key{ | ||
| outer_type == OuterType::OBJ ? "\"" + this->m_key_name + "\" : " : ""}; |
There was a problem hiding this comment.
RPCResult::ToSections() formats dictionary keys as "key" : (note the space before the colon). This is inconsistent with the other JSON-like snippets emitted by this renderer (and with typical JSON formatting), and makes the help output harder to compare to upstream/expected JSON. Consider changing it to "key": .
| outer_type == OuterType::OBJ ? "\"" + this->m_key_name + "\" : " : ""}; | |
| outer_type == OuterType::OBJ ? "\"" + this->m_key_name + "\": " : ""}; |
There was a problem hiding this comment.
Cosmetic and inherited — leaving as-is. The "\" : " literal is verbatim from v26.0 util.cpp:954. The rest of the renderer uses "\": ", so output is internally inconsistent, but Bitcoin Core has shipped this across four releases without complaint and this PR's stated scope is a strict subset of the v26.0 public surface. Changing it would be a gratuitous deviation from upstream; better left for a dedicated cosmetic-sync pass if upstream ever fixes it.
| 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) { |
There was a problem hiding this comment.
RPCArgOptions::type_str is documented as having length 0 or 2, but the code only checks size() != 0 before indexing with .at(0)/.at(1) (here and in RPCArg::ToDescriptionString). If a caller accidentally supplies a vector of length 1, help rendering will throw at runtime. Consider enforcing the invariant with CHECK_NONFATAL(type_str.size() == 2) (or switching to a fixed-size type like std::array<std::string,2>/a small struct) before indexing.
| if (arg.m_opts.type_str.size() != 0 && push_name) { | |
| if (!arg.m_opts.type_str.empty()) { | |
| CHECK_NONFATAL(arg.m_opts.type_str.size() == 2); | |
| } | |
| if (arg.m_opts.type_str.size() == 2 && push_name) { |
There was a problem hiding this comment.
Partially valid — the length-1 std::vector::at out-of-range throw is real, but the right enforcement point is the RPCArg constructors, not the use sites. Fixed in c98f5d0: a CHECK_NONFATAL(m_opts.type_str.empty() || m_opts.type_str.size() == 2) in both RPCArg constructor bodies catches a misshapen spec at program init rather than at first help <cmd> call. Cheap insurance, and it fails fast at the source instead of deep in the renderer.
jamescowens
left a comment
There was a problem hiding this comment.
Code review
Reviewed at HEAD 2fbf4af486fae5de1a73d82329c778b8c2e592b7. Also cross-checked Copilot's four inline comments. Verdicts below, then three additions.
Copilot comment triage
util.cpp:153 (type_str length) — partially valid. The length-1 std::vector::at out-of-range throw path is real, but it's inherited verbatim from Bitcoin Core v26.0 and the contract is caller-side. The right enforcement point is the RPCArg constructors in util.h (~164, ~181) rather than the use sites Copilot suggested — a one-line CHECK_NONFATAL(type_str.empty() || type_str.size() == 2) in each constructor body catches the bad spec at program init instead of at first help <cmd> call. Cheap insurance.
util.cpp:173 (OBJ_NAMED_PARAMS in Sections::Push) — not a bug, don't change. OBJ_NAMED_PARAMS intentionally lands in the non-recursive branch. Top-level OBJ_NAMED_PARAMS args are rendered by the separate named_only_sections loop in RPCHelpMan::ToString() (around util.cpp:524-528), which spins up its own Sections for the inner keys. Merging it into the OBJ branch would double-render — once in "Arguments:", once in "Named Arguments:". This matches upstream v26.0 behavior.
util.cpp:505 (space-before-colon in "key" : ) — cosmetic and inherited. The "\" : " literal is verbatim from v26.0 util.cpp:954. The rest of the renderer uses "\": " so output is internally inconsistent, but Bitcoin has been shipping this for four releases without complaint and this PR's stated scope is "strict subset of v26.0 public surface." Leave alone.
util.cpp:634 (ToStringObj() aborts on object types) — real footgun, partially valid. RPCArg::ToString() calls ToStringObj() on every m_inner for OBJ / OBJ_NAMED_PARAMS / OBJ_USER_KEYS, and ToStringObj() aborts on those same three types via CHECK_NONFATAL(false). Any OBJ-nested-in-OBJ spec would detonate. Identical to upstream v26.0 (NONFATAL_UNREACHABLE()), and tolerated there because no v26.0 command nests objects two deep in m_inner. For the Tier 1/2/3 conversions in #2922 this is likely safe, but specs like createrawtransaction's outputs or an options inside a named-params object would hit it. Worth at minimum a comment at the ToStringObj() definition warning spec authors, with a TODO to extend the implementation when a command first needs it.
Additional findings
-
Test-suite coverage gap.
src/test/rpchelpman_tests.cppexercisesRPCResultof OBJ / ARR / OBJ_DYN (lines 182-218) but no test constructs anRPCArgof type OBJ, OBJ_NAMED_PARAMS, OBJ_USER_KEYS, or ARR. That meansSections::Pushrecursion, theNamed Arguments:branch ofRPCHelpMan::ToString(),RPCArg::ToStringObj(), and theGetArgNamesOBJ_NAMED_PARAMSinner-expansion (aroundutil.cpp:255-258) are all untouched by the new suite. First Tier 1 conversion with nested args will exercise these live — cheaper to add coverage here. Also incidentally covers the Copilot #4 assertion-trap footgun and the type_str-length concern from #1. -
GetName()alias constraint undocumented.CHECK_NONFATAL(std::string::npos == m_names.find('|'))atutil.cpp:539means aliases ("foo|bar"names) are only allowed on top-level args — any OBJ/OBJ_USER_KEYS inner with an aliased name will trip it viaSections::Push. Same upstream constraint but nothing inutil.hflags it to spec authors. Low priority — would trip in review — but one-line comment onRPCArg::m_nameswould help. -
Drive-by
#includechange verified correct and necessary. Without the PR, a quoted#include "util.h"fromsrc/rpc/blockchain.horsrc/rpc/server.cppwould silently shift fromsrc/util.h(withLogPrintf/gArgs/LogInstance) to the colocated newsrc/rpc/util.h. The angle-bracket form resolves unambiguously via-Isrc. Required for this PR to link. -
PACKAGE_BUGREPORTdrive-by verified.src/util/check.h:32-41expandsPACKAGE_BUGREPORTinCHECK_NONFATAL's error message. No prior call sites exist in the tree (grep confirms). This port is the first consumer. Definition addition is correct and strictly necessary. -
"Strict subset of v26.0" claim verified. Diffed the ported surface against upstream; everything in this PR has an identical v26.0 signature.
MatchesType,HandleRequest,Arg<T>,MaybeArg<T>,GetArgMap,m_fun,m_req,RPCMethodImplall cleanly omitted — nothing ported here depends on anything not ported. -
CLAUDE.md member-init rule satisfied. All ctors use initializer lists;
RPCResultctors delegate correctly.
Overall
Mergeable after the three minor cleanups above (constructor assertion, ToStringObj nested-OBJ warning, one OBJ-typed-arg test case). The include-form and PACKAGE_BUGREPORT drive-bys are necessary and correct.
🤖 Generated with Claude Code
Resolves the actionable items from the gridcoin-community#2923 review pass: util.h: enforce the documented RPCArgOptions::type_str length-0-or-2 invariant at construction time via CHECK_NONFATAL in both RPCArg constructors. Misshapen specs now fail at startup rather than at first help-rendering call. Also extend the m_names doc comment to note that aliased names ("foo|bar") are only valid on top-level args (GetName() asserts no '|', and Sections::Push routes inner args through GetName() unconditionally). util.cpp: document the v26.0-inherited footgun in RPCArg::ToStringObj(): the function CHECK_NONFATAL(false)s for Type::OBJ / OBJ_NAMED_PARAMS / OBJ_USER_KEYS, but RPCArg::ToString() routes object-typed args' inner elements through ToStringObj(), so any nested-object spec will detonate when help is rendered. Comment warns spec authors and adds a TODO(gridcoin-community#2922) to implement object-type rendering when the first command that needs it is converted. No behavior change. test/rpchelpman_tests.cpp: add rpcarg_obj_with_scalar_inner_renders covering the previously-untested OBJ-typed RPCArg shape with scalar inner fields. Exercises Sections::Push recursion, the "Arguments:" rendering of OBJ wrappers, and ToStringObj() for scalar inners. Does not test nested OBJ-in-OBJ — that path is the documented footgun above.
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
Resolves the actionable items from the gridcoin-community#2923 review pass: util.h: enforce the documented RPCArgOptions::type_str length-0-or-2 invariant at construction time via CHECK_NONFATAL in both RPCArg constructors. Misshapen specs now fail at startup rather than at first help-rendering call. Also extend the m_names doc comment to note that aliased names ("foo|bar") are only valid on top-level args (GetName() asserts no '|', and Sections::Push routes inner args through GetName() unconditionally). util.cpp: document the v26.0-inherited footgun in RPCArg::ToStringObj(): the function CHECK_NONFATAL(false)s for Type::OBJ / OBJ_NAMED_PARAMS / OBJ_USER_KEYS, but RPCArg::ToString() routes object-typed args' inner elements through ToStringObj(), so any nested-object spec will detonate when help is rendered. Comment warns spec authors and adds a TODO(gridcoin-community#2922) to implement object-type rendering when the first command that needs it is converted. No behavior change. test/rpchelpman_tests.cpp: add rpcarg_obj_with_scalar_inner_renders covering the previously-untested OBJ-typed RPCArg shape with scalar inner fields. Exercises Sections::Push recursion, the "Arguments:" rendering of OBJ wrappers, and ToStringObj() for scalar inners. Does not test nested OBJ-in-OBJ — that path is the documented footgun above.
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
Resolves the actionable items from the gridcoin-community#2923 review pass: util.h: enforce the documented RPCArgOptions::type_str length-0-or-2 invariant at construction time via CHECK_NONFATAL in both RPCArg constructors. Misshapen specs now fail at startup rather than at first help-rendering call. Also extend the m_names doc comment to note that aliased names ("foo|bar") are only valid on top-level args (GetName() asserts no '|', and Sections::Push routes inner args through GetName() unconditionally). util.cpp: document the v26.0-inherited footgun in RPCArg::ToStringObj(): the function CHECK_NONFATAL(false)s for Type::OBJ / OBJ_NAMED_PARAMS / OBJ_USER_KEYS, but RPCArg::ToString() routes object-typed args' inner elements through ToStringObj(), so any nested-object spec will detonate when help is rendered. Comment warns spec authors and adds a TODO(gridcoin-community#2922) to implement object-type rendering when the first command that needs it is converted. No behavior change. test/rpchelpman_tests.cpp: add rpcarg_obj_with_scalar_inner_renders covering the previously-untested OBJ-typed RPCArg shape with scalar inner fields. Exercises Sections::Push recursion, the "Arguments:" rendering of OBJ wrappers, and ToStringObj() for scalar inners. Does not test nested OBJ-in-OBJ — that path is the documented footgun above.
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
jamescowens
left a comment
There was a problem hiding this comment.
All three actionable items from the review pass are resolved in c98f5d0:
RPCArgOptions::type_strlength-0-or-2 invariant now enforced viaCHECK_NONFATALin bothRPCArgconstructors — misshapen specs fail at startup, not at first help render.RPCArg::ToStringObj()'s v26.0-inherited nested-object footgun is now documented with a warning comment +TODO(#2922)above the function definition.- New
rpcarg_obj_with_scalar_inner_renderstest covers the previously-untested OBJ-typedRPCArgshape (Sections::Push recursion, "Arguments:" rendering,ToStringObj()for scalar inners). - Bonus: the
m_namesdoc comment now spells out the alias-only-on-top-level constraint.
The four Copilot inline comments were triaged in the review body and now have inline replies. Verdicts unchanged: 173 (OBJ_NAMED_PARAMS) is not a bug — intentional non-recursive branch matching upstream; 505 (space-before-colon) is cosmetic and verbatim from v26.0, left for a dedicated upstream-sync pass; 643 and 153 are addressed by c98f5d0 as above.
CI is fully green. The "strict subset of v26.0" claim holds — nothing ported depends on anything omitted. Drive-by fixes (PACKAGE_BUGREPORT definition, src/rpc/* quoted-include → angle-bracket) are both necessary for this PR to link and verified correct.
Approving. This is the infrastructure base the #2922 Tier 1/2/3 conversion PRs build on.
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
Convert settxfee to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the below-minimum-amount UX bug: today the help condition is `fHelp || params.size() < 1 || params.size() > 1 || AmountFromValue(params[0]) < nMinFee`, so a below-min amount throws the help text wrapped as RPC_MISC_ERROR (-1) — useless to the caller. Hoist the AmountFromValue and minimum-fee check out of the help condition; below-min now throws JSONRPCError(RPC_INVALID_PARAMETER, "txfee X below minimum Y") with both values formatted, so the caller gets actionable feedback. The 0-arg path is unchanged in behavior: IsValidNumArgs(0) returns false and the help is thrown, just as the short-circuit `params.size() < 1` produced before. Add a per-command help-render test in rpchelpman_tests.cpp asserting that fHelp=true throws a runtime_error whose message contains the expected RPCHelpMan markers. Refs: gridcoin-community#2922
…id command Convert addnode to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() != 2 || (strCommand != "onetry" && strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be one of: \"add\", \"remove\", \"onetry\""). The client-visible code change (-1 -> -8) and the loss of the embedded help dump are intentional and noted in the bundle PR description. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…d command Convert setban to the RPCHelpMan pattern landed in gridcoin-community#2923. As a side effect, fix the invalid-subcommand UX bug: today the help condition is `fHelp || params.size() < 2 || params.size() > 4 || (strCommand != "add" && strCommand != "remove")`, so a typo'd subcommand throws the help text wrapped as RPC_MISC_ERROR (-1). Split the validation: arity is enforced by IsValidNumArgs(), and an invalid subcommand now throws JSONRPCError(RPC_INVALID_PARAMETER, "command must be \"add\" or \"remove\""). The pre-validation isNull guard on params[1] is no longer needed since IsValidNumArgs guarantees >= 2 args. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
…unbounded arity Convert listsinceblock to the RPCHelpMan pattern landed in gridcoin-community#2923. The original help text is truncated mid-sentence (ends at `" \"transactions\": [\n"` with no closing brace, no description, no examples), and the function performs no arity validation, silently accepting calls with 4+ arguments. The RPCHelpMan literal supplies a complete help body and IsValidNumArgs(params.size()) now rejects > 3 args. The transaction object schema is rendered as RPCResult::Type::ELISION because ListTransactions emits variable shapes depending on MinedType / category and pinning a schema would misrepresent the API. Refer callers to `listtransactions` for the per-entry layout. Add a per-command help-render test in rpchelpman_tests.cpp. Refs: gridcoin-community#2922
Summary
Infrastructure PR for #2922 — ports the help-text rendering and argument-count validation subset of Bitcoin Core v26.0's
RPCHelpManmachinery intosrc/rpc/util.{h,cpp}so that the ~165 existing RPC commands can be incrementally converted to structured help documentation.No command conversions are included in this PR. Tier 1/2/3 conversions will follow in separate per-command PRs using the pattern:
The existing
(const UniValue& params, bool fHelp)RPC signature is untouched.What's ported
RPCHelpManwithToString(),IsValidNumArgs(),GetArgNames()RPCArg/RPCArgOptionswith all v26.0Typevariants and theFallbackvariant (Optional/DefaultHint/Default)RPCResult/RPCResultswith all v26.0Typevariants and the four constructor overloadsRPCExamplesSectionsinternal pretty-printer (byte-for-byte v26.0)HelpExampleCli/HelpExampleCliNamed/HelpExampleRpc/HelpExampleRpcNamed(adapted to emitgridcoinresearchand port 9332)UNIX_EPOCH_TIME,EXAMPLE_ADDRESS[2](the latter with placeholder strings + aTODO(#2922)for the first Tier 1 PR to replace with real Gridcoin addresses)What's deliberately omitted
The v26.0 dispatch-path features —
HandleRequest, theRPCMethodImpllambda-overload constructor,GetArgMap,Arg<R>/MaybeArg<R>typed accessors,RPCArg::MatchesType,RPCResult::MatchesType— all depend on upstream'sJSONRPCRequeststruct, which Gridcoin does not have. Dragging it in would require changingrpcfn_typefromUniValue(const UniValue&, bool)toUniValue(const JSONRPCRequest&)and mechanically rewriting every RPC command file, which would conflict with the Tier 2/3 conversion PRs the #2922 plan calls for.The landed API is a strict subset of the v26.0 public surface. A future modernization PR can grow it into the full dispatch-path shape without touching any call site added in the interim — nothing gets re-ported later, only extended.
Commands whose help text or argument list depends on runtime state (
addkeyby protocol version,addpollby enabled poll types,walletpassphraseby wallet lock state) can constructRPCHelpManper-call rather than asstatic const. A dedicated unit test exercises this pattern.Drive-by fixes
Two latent bugs surfaced while landing the port and are fixed here because they block it:
PACKAGE_BUGREPORTwas never plumbed through.CHECK_NONFATALinsrc/util/check.hreferencesPACKAGE_BUGREPORTin its error message, but the macro was never defined insrc/config/gridcoin-config.h.cmake.in.CHECK_NONFATALhas been dormant dead code — this port is the first Gridcoin consumer, so the dangling reference would have caused any future use to fail. Added the definition pointing at the GitHub issues page.src/rpc/files used#include "util.h"(quoted form) which searches the including file's directory first. With noutil.hpreviously undersrc/rpc/, those quoted includes fell through tosrc/util.h(Gridcoin's general utility header withLogPrintf,gArgs,LogInstance, etc.). Addingsrc/rpc/util.hcausedblockchain.h,client.cpp,dataacq.cpp,misc.cpp,protocol.cpp, andserver.cppto start resolving to the new RPCHelpMan header instead. Switched those 6 files to#include <util.h>which unambiguously resolves via the-I srcinclude path. Other files outsidesrc/rpc/are unaffected.Test plan
rpchelpman_testssuite exercisesToString,IsValidNumArgsboundaries,GetArgNames,HelpExample*formatting, everyRPCArg::Typescalar,RPCResult::ToDescriptionStringfor OBJ/ARR/OBJ_DYN/STR_HEX, per-call (non-static) construction, and the Tier 1 throw patternsrc/CMakeLists.txtandsrc/test/CMakeLists.txt, plus the include-form fix in the 6src/rpc/files)Refs: #2922