Crate cuprate_json_rpc

source ·
Expand description

§json-rpc

JSON-RPC 2.0 types and (de)serialization.

§What

This crate implements the JSON-RPC 2.0 specification for usage in Cuprate.

It contains slight modifications catered towards Cuprate and isn’t necessarily a general purpose implementation of the specification (see below).

This crate expects you to read the brief JSON-RPC 2.0 specification for context.

§Batching

This crate does not have any types for JSON-RPC 2.0 batching.

This is because monerod does not support this, as such, neither does Cuprate.

TODO: citation needed on monerod not supporting batching.

§Request changes

JSON-RPC 2.0’s Request object usually contains these 2 fields:

  • method
  • params

This crate replaces those two with a body field that is #[serde(flatten)]ed, and assumes the type within that body field is tagged properly, for example:

use serde::{Deserialize, Serialize};
use cuprate_json_rpc::{Id, Request};

// Parameter type.
#[derive(Deserialize, Serialize)]
struct GetBlock {
    height: u64,
}

// Method enum containing all enums.
// All methods are tagged as `method`
// and their inner parameter types are
// tagged with `params` (in snake case).
#[derive(Deserialize, Serialize)]
#[serde(tag = "method", content = "params")] // INVARIANT: these tags are needed
#[serde(rename_all = "snake_case")]          // for proper (de)serialization.
enum Methods {
    GetBlock(GetBlock),
    /* other methods */
}

// Create the request object.
let request = Request::new_with_id(
    Id::Str("hello".into()),
    Methods::GetBlock(GetBlock { height: 123 }),
);

// Serializing properly shows the `method/params` fields
// even though `Request` doesn't contain those fields.
let json = serde_json::to_string_pretty(&request).unwrap();
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": "hello",
  "method": "get_block",
  "params": {
    "height": 123
  }
}"#;
assert_eq!(json, expected_json);

This is how the method/param types are done in Cuprate.

For reasoning, see: https://github.com/Cuprate/cuprate/pull/146#issuecomment-2145734838.

§Serialization changes

This crate’s serialized field order slightly differs compared to monerod.

monerod’s JSON objects are serialized in alphabetically order, where as this crate serializes the fields in their defined order (due to serde).

With that said, parsing should be not affected at all since a key-value map is used:

use cuprate_json_rpc::{Id, Response};

let response = Response::ok(Id::Num(123), "OK");
let response_json = serde_json::to_string_pretty(&response).unwrap();

// This crate's `Response` result type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": 123,
  "result": "OK"
}"#;
assert_eq!(response_json, expected_json);

// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
  "id": 123,
  "jsonrpc": "2.0",
  "result": "OK"
}"#;

///---

let response = Response::<()>::invalid_request(Id::Num(123));
let response_json = serde_json::to_string_pretty(&response).unwrap();

// This crate's `Response` error type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": 123,
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  }
}"#;
assert_eq!(response_json, expected_json);

// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  },
  "id": 123
  "jsonrpc": "2.0",
}"#;

§Compared to other implementations

A quick table showing some small differences between this crate and other JSON-RPC 2.0 implementations.

ImplementationAllows any case for key fields excluding method/paramsAllows unknown fields in main {}, and response/request objectsAllows overwriting previous values upon duplicate fields (except Response’s result/error field)
monerod
jsonrpsee
This crate

Allows any case for key fields excluding method/params:

let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();

// Only `lowercase` is allowed.
let json = r#"{"jsonRPC":"2.0","id":123,"result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "missing field `jsonrpc` at line 1 column 40");

Allows unknown fields in main {}, and response/request objects:

//     unknown fields are allowed in main `{}`
//             v
let json = r#"{"unknown_field":"asdf","jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();

//                                                               and within objects
//                                                                      v
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":"","unknown_field":"asdf"}}"#;
from_str::<Response<String>>(&json).unwrap();

Allows overwriting previous values upon duplicate fields (except Response’s result/error field)

//          duplicate fields will get overwritten by the latest one
//                             v        v
let json = r#"{"jsonrpc":"2.0","id":123,"id":321,"result":"OK"}"#;
let response = from_str::<Response<String>>(&json).unwrap();
assert_eq!(response.id, Id::Num(321));

// But 2 results are not allowed.
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK","result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 48");

// Same with errors.
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":""},"error":{"code":-1,"message":""}}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 66");

Modules§

Structs§

Enums§