Merge bitcoin/bitcoin#35946: rpc: Improve some type specs for openrpc

e07d826e0e rpc: Fix type in ApplyTypeStrOverride (Shuvam Pandey)
c94074fa1b rpc: Surface OBJ_USER_KEYS description for openrpc (sedited)
c020c21d54 rpc: Handle skip type args for openrpc (sedited)

Pull request description:

  This was initially motivated by testing the dump of the schema against open-rpc-generator, which crashed with:

  ```
  open-rpc-generator generate -t client -l rust -n bitcoin_client -d ./openrpc.gen.json -o ./generated
  There was error at generator runtime:
  TypeError: Cannot convert undefined or null to object
  ```

  The changes here fix this crash (albeit perfectly valid existing schema), but I think creating a more complete output is helpful on its own. The openrpc schema dumps can eventually be re-used for the rpc docs and to track rpc interface changes more accurately. Adding the CreateTxDoc outputs section seems useful for that.

  Also includes a type tightening from number to integer in `ApplyTypeStrOverride` to reflect the actual behaviour in the rpc calls, where only integers are accepted.

ACKs for top commit:
  achow101:
    ACK e07d826e0e
  willcl-ark:
    ACK e07d826e0e

Tree-SHA512: d0454a71b4f1dab1daf8a0d5b1e5bf1c1b8f1a16d26638d4a64a2652402ad74230366cabf0cf4135a16d0bdab584d3d4b2a47f2968a4eff605348ce85e8dbadb
This commit is contained in:
merge-script
2026-08-18 09:59:26 +01:00
2 changed files with 25 additions and 14 deletions

View File

@@ -243,7 +243,7 @@ static RPCMethod getrpcinfo()
}
namespace {
UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden);
UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check);
UniValue OpenRPCResultSchema(const RPCResult& result);
UniValue MakeObject(std::initializer_list<std::pair<std::string, UniValue>> entries)
@@ -262,21 +262,21 @@ void PushUniqueSchema(UniValue& schemas, std::unordered_set<std::string>& seen,
}
// NOLINTNEXTLINE(misc-no-recursion)
UniValue DedupArrayItemsSchema(std::span<const RPCArg> inner, bool include_hidden)
UniValue DedupArrayItemsSchema(std::span<const RPCArg> inner, bool include_hidden, bool in_skip_type_check)
{
if (inner.empty()) return UniValue{UniValue::VOBJ};
if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden);
if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden, in_skip_type_check);
UniValue one_of{UniValue::VARR};
std::unordered_set<std::string> seen;
for (const auto& item : inner) {
PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden));
PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden, in_skip_type_check));
}
if (one_of.size() == 1) return one_of[0];
UniValue items{UniValue::VOBJ};
items.pushKV("oneOf", std::move(one_of));
items.pushKV(in_skip_type_check ? "anyOf" : "oneOf", std::move(one_of));
return items;
}
@@ -311,7 +311,7 @@ void ApplyTypeStrOverride(UniValue& schema, const RPCArg& arg)
};
if (number_or_string.contains(type_label)) {
UniValue one_of{UniValue::VARR};
one_of.push_back(MakeObject({{"type", "number"}}));
one_of.push_back(MakeObject({{"type", "integer"}}));
one_of.push_back(MakeObject({{"type", "string"}}));
schema = UniValue{UniValue::VOBJ};
schema.pushKV("oneOf", std::move(one_of));
@@ -331,14 +331,18 @@ void ApplyArgFallback(UniValue& schema, const RPCArg& arg)
}
// NOLINTNEXTLINE(misc-no-recursion)
UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check)
{
UniValue schema{UniValue::VOBJ};
if (arg.m_opts.skip_type_check) {
ApplyTypeStrOverride(schema, arg);
if (schema.empty() && arg.m_type == RPCArg::Type::ARR) {
UniValue items{UniValue::VOBJ};
items.pushKV("type", "array");
items.pushKV("items", DedupArrayItemsSchema(arg.m_inner, include_hidden, /*in_skip_type_check=*/true));
UniValue one_of{UniValue::VARR};
one_of.push_back(MakeObject({{"type", "array"}}));
one_of.push_back(std::move(items));
one_of.push_back(MakeObject({{"type", "object"}}));
schema.pushKV("oneOf", std::move(one_of));
}
@@ -383,7 +387,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
break;
}
case RPCArg::Type::ARR: {
UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden)};
UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden, in_skip_type_check)};
schema.pushKV("type", "array");
schema.pushKV("items", std::move(items));
break;
@@ -394,7 +398,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
UniValue required{UniValue::VARR};
for (const auto& inner : arg.m_inner) {
if (!include_hidden && inner.m_opts.hidden) continue;
UniValue prop{OpenRPCArgSchema(inner, include_hidden)};
UniValue prop{OpenRPCArgSchema(inner, include_hidden, in_skip_type_check)};
if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description);
if (inner.m_opts.placeholder) prop.pushKV("x-bitcoin-placeholder", true);
if (inner.m_opts.also_positional) prop.pushKV("x-bitcoin-also-positional", true);
@@ -410,7 +414,10 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
case RPCArg::Type::OBJ_USER_KEYS: {
schema.pushKV("type", "object");
if (!arg.m_inner.empty()) {
schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden));
schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden, in_skip_type_check));
if (!arg.m_inner[0].m_description.empty()) {
schema.pushKV("description", arg.m_inner[0].m_description);
}
} else {
schema.pushKV("additionalProperties", true);
}
@@ -911,7 +918,7 @@ UniValue CRPCTable::buildOpenRPCDoc(bool include_hidden) const
UniValue param{UniValue::VOBJ};
param.pushKV("name", arg.GetFirstName());
param.pushKV("required", !arg.IsOptional());
param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden));
param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden, /*in_skip_type_check=*/false));
std::vector<std::string> names{SplitString(arg.m_names, '|')};
if (names.size() > 1) {

View File

@@ -52,7 +52,7 @@ class OpenRPCDocTest(BitcoinTestFramework):
self.log.info("Checking type_str override schemas")
getblockstats = find_method(openrpc, "getblockstats")
hash_or_height = find_param(getblockstats, "hash_or_height")
assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "number"}, {"type": "string"}]})
assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "integer"}, {"type": "string"}]})
self.log.info("Checking fixed-length array schemas")
deriveaddresses = find_method(openrpc, "deriveaddresses")
@@ -75,7 +75,11 @@ class OpenRPCDocTest(BitcoinTestFramework):
self.log.info("Checking relaxed schemas for unchecked RPC types")
createrawtransaction = find_method(openrpc, "createrawtransaction")
outputs = find_param(createrawtransaction, "outputs")
assert_equal(outputs["schema"], {"oneOf": [{"type": "array"}, {"type": "object"}]})
address_description = "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC"
address_obj = {"type": "object", "additionalProperties": {"oneOf": [{"type": "number"},{"type": "string"}]}, "description": address_description}
data_description = "A key-value pair. The key must be \"data\", the value is hex-encoded data that becomes a part of an OP_RETURN output"
data_obj = {"type": "object", "properties": { "data": {"type": "string", "pattern": "^[0-9a-fA-F]+$", "description": data_description}}, "additionalProperties": False, "required": ["data"]}
assert_equal(outputs["schema"], {"oneOf": [{"type": "array", "items": {"anyOf": [address_obj, data_obj]}}, {"type": "object"}]})
getdescriptoractivity = find_method(openrpc, "getdescriptoractivity")
activity = getdescriptoractivity["result"]["schema"]["properties"]["activity"]