# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.
"""How a callback refuses a request rather than failing on it."""
import enum
from typing import Any
__all__ = ["RpcError", "RpcErrorCode", "bool_param", "error_msg", "type_error"]
[docs]
class RpcErrorCode(enum.IntEnum):
"""The codes of Bitcoin Core's RPCErrorCode, `src/rpc/protocol.h`.
A client reads the code before it reads the message, so a refusal
this node makes carries the number Core gives the same refusal.
`INTERNAL_ERROR` is what Core's own header reserves for a genuine
fault of the server, which is why nothing here answers a bad request
with it.
"""
MISC_ERROR = -1
TYPE_ERROR = -3
INVALID_ADDRESS_OR_KEY = -5
INVALID_PARAMETER = -8
DESERIALIZATION_ERROR = -22
VERIFY_ERROR = -25
VERIFY_REJECTED = -26
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INTERNAL_ERROR = -32603
[docs]
class RpcError(Exception):
"""A request this node refuses, named by the answer it is owed.
`handle_rpc` turns it into the error object of JSON-RPC 2.0's section
5.1, so raising it is how a callback says which of the two was wrong,
the request or the node.
"""
def __init__(self, code: RpcErrorCode, message: str) -> None:
"""Name the refusal `code` and `message`, `handle_rpc` reads back."""
super().__init__(f"{code.name}: {message}")
self.code = code
self.message = message
# univalue's own names for the six JSON types, `uvTypeName` in
# src/univalue/lib/univalue.cpp -- the vocabulary RPC_TYPE_ERROR's
# message speaks. Keyed by `type()` rather than `isinstance`, so a bool
# reads as "bool" and not "number": bool is int's own subclass in
# Python, not a distinct JSON type, and `type()` is exact where
# `isinstance` would let the subclass through.
_JSON_TYPE_NAMES: dict[type, str] = {
type(None): "null",
bool: "bool",
int: "number",
float: "number",
str: "string",
list: "array",
dict: "object",
}
def json_type_name(value: object) -> str:
"""Name a decoded JSON value the way Core's RPC_TYPE_ERROR names it.
`value` is always one of the six JSON types here: `connection.py`
decodes every request with the standard library's `json.loads`,
which produces no other Python type. Named that narrowly in the
docstring rather than in the signature: `_JSON_TYPE_NAMES` is
keyed on `type(value)` alone, so nothing this function does needs
`value` to be any narrower than `object`.
"""
return _JSON_TYPE_NAMES[type(value)]
[docs]
def type_error(position: int, name: str, value: object, expected: str) -> RpcError:
r"""Refuse a declared argument's own JSON type, Core's own wrapped shape.
`RPCMethod::HandleRequest`'s own type check (`src/rpc/util.cpp`
:652-661, read at `bitcoin/bitcoin@b91d983f66`) collects every
mismatched argument into one `UniValue` object, keyed
`strprintf("Position %s (%s)", i + 1, arg.m_names)`, and wraps it in
`strprintf("Wrong type passed:\n%s", arg_mismatch.write(4))` --
`UniValue::write`'s own four-space indent and lack of a trailing
newline after the closing brace
(`src/univalue/lib/univalue_write.cpp`), reproduced literally below
rather than through a JSON encoder, because every caller here checks
exactly one declared argument and raises before a second could ever
join it in the same object -- there is never a second key to encode.
Measured against a real `bitcoind` (v31.1.0, `-regtest`) answering a
raw `testmempoolaccept`, `getblockheader`, `getblockhash`,
`getrawtransaction` and `sendrawtransaction` call each with one
argument of the wrong JSON type.
`position` is the argument's own one-based position among the
method's declared arguments, the way Core counts it (`i + 1`), and
`name` is Core's own declared name for it -- `arg.m_names` itself,
the raw field the key above is built from, not `GetFirstName()`'s
`|`-trimmed form (`m_names.substr(0, m_names.find('|'))`,
`src/rpc/util.cpp:917-920`), which only `RPCArg::ToString` reads,
for the usage string, and which `HandleRequest`'s own type check
never calls. The two coincide for every argument checked here except
`getrawtransaction`'s own second one, declared `"verbosity|verbose"`
-- `get_raw_transaction`'s own `verbose` is neither the raw
`m_names` this key is built from nor `GetFirstName()`'s trimmed
form, for the reason `_parse_txid`'s own usage-string comment
argues.
"""
return RpcError(
RpcErrorCode.TYPE_ERROR,
"Wrong type passed:\n{\n"
f' "Position {position} ({name})": '
f'"JSON value of type {json_type_name(value)} is '
f'not of expected type {expected}"'
"\n}",
)
[docs]
def error_msg(
code: RpcErrorCode, message: str, request_id: object = None
) -> dict[str, Any]:
"""Build the error response of JSON-RPC 2.0's section 5, code and message.
The specification requires the answer to carry the id of the request
it answers, and reserves null for a request whose id could not be
read out of it -- which is what its own example for an invalid
request object shows. So a caller passes the id wherever
`is_valid_rpc` has already found one, and leaves it out where the
request -- or, for `PARSE_ERROR`, the body before it was even a
request -- is what was wrong. Nothing here reads `request_id` beyond
embedding it in the response unchanged, so `object` is as much as
the signature needs -- the specification lets a request's `id` be
any JSON scalar, and this node does not itself validate the field
before echoing it back.
"""
return {
"jsonrpc": "2.0",
"error": {"code": code, "message": message},
"id": request_id,
}
[docs]
def bool_param(params: list[Any], position: int, *, name: str, default: bool) -> bool:
"""Read a declared `RPCArg::Type::BOOL` parameter, Core's own way.
Omitted or explicit `null` both stand for the argument's own
declared `default`. Anything else is read, and refused with
`RPC_TYPE_ERROR` where it is not an actual JSON bool -- the same
check `RPCMethod::HandleRequest` makes for every declared argument
before the handler body runs at all (`src/rpc/util.cpp:653-661`),
applied here to the one JSON type this helper's every caller
declares. `position` is the zero-based index into `params`, the way
every caller here already addresses it; `type_error` wants Core's
own one-based count, so it is passed `position + 1`.
"""
if len(params) <= position or params[position] is None:
return default
value = params[position]
if not isinstance(value, bool):
raise type_error(position + 1, name, value, "bool")
return value