Crate cuprate_rpc_interface

Source
Expand description

§cuprate-rpc-interface

This crate provides Cuprate’s RPC interface.

This crate is not a standalone RPC server, it is just the interface.

            cuprate-rpc-interface provides these parts
                            │         │
┌───────────────────────────┤         ├───────────────────┐
▼                           ▼         ▼                   ▼
CLIENT ─► ROUTE ─► REQUEST ─► HANDLER ─► RESPONSE ─► CLIENT
                             ▲       ▲
                             └───┬───┘
                                 │
                      You provide this part

Everything coming in from a client is handled by this crate.

This is where your RpcHandler turns this Request into a Response.

You hand this Response back to cuprate-rpc-interface and it will take care of sending it back to the client.

The main handler used by Cuprate is implemented in the cuprate-rpc-handler crate; it implements the standard RPC handlers modeled after monerod.

§Purpose

cuprate-rpc-interface is built on-top of axum, which is the crate actually handling everything.

This crate simply handles:

  • Registering endpoint routes (e.g. /get_block.bin)
  • Defining handler function signatures
  • (De)serialization of requests/responses (JSON-RPC, binary, JSON)

The actual server details are all handled by the axum and tower ecosystem.

The proper usage of this crate is to:

  1. Implement a RpcHandler
  2. Use it with RouterBuilder to generate an axum::Router with all Monero RPC routes set
  3. Do whatever with it

§The RpcHandler

This is your tower::Service that converts Requests into Responses, i.e. the “inner handler”.

Said concretely, RpcHandler is 3 tower::Services where the request/response types are the 3 endpoint enums from cuprate_rpc_types:

RpcHandler’s Future is generic, although, it must output Result<$RESPONSE, anyhow::Error>.

The error type must always be anyhow::Error.

The RpcHandler must also hold some state that is required for RPC server operation.

The only state currently needed is RpcHandler::restricted, which determines if an RPC server is restricted or not, and thus, if some endpoints/methods are allowed or not.

§Unknown endpoint behavior

TODO: decide what this crate should return (per different endpoint) when a request is received to an unknown endpoint, including HTTP stuff, e.g. status code.

§Unknown JSON-RPC method behavior

TODO: decide what this crate returns when a /json_rpc request is received with an unknown method, including HTTP stuff, e.g. status code.

§Example

Example usage of this crate + starting an RPC server.

This uses RpcHandlerDummy as the handler; it always responds with the correct response type, but set to a default value regardless of the request.

use std::sync::Arc;

use tokio::{net::TcpListener, sync::Barrier};

use cuprate_json_rpc::{Request, Response, Id};
use cuprate_rpc_types::{
    json::{JsonRpcRequest, JsonRpcResponse, GetBlockCountResponse},
    other::{OtherRequest, OtherResponse},
};
use cuprate_rpc_interface::{RouterBuilder, RpcHandlerDummy};

// Send a `/get_height` request. This endpoint has no inputs.
async fn get_height(port: u16) -> OtherResponse {
    let url = format!("http://127.0.0.1:{port}/get_height");
    ureq::get(&url)
        .set("Content-Type", "application/json")
        .call()
        .unwrap()
        .into_json()
        .unwrap()
}

// Send a JSON-RPC request with the `get_block_count` method.
//
// The returned [`String`] is JSON.
async fn get_block_count(port: u16) -> String {
    let url = format!("http://127.0.0.1:{port}/json_rpc");
    let method = JsonRpcRequest::GetBlockCount(Default::default());
    let request = Request::new(method);
    ureq::get(&url)
        .set("Content-Type", "application/json")
        .send_json(request)
        .unwrap()
        .into_string()
        .unwrap()
}

#[tokio::main]
async fn main() {
    // Start a local RPC server.
    let port = {
        // Create the router.
        let state = RpcHandlerDummy { restricted: false };
        let router = RouterBuilder::new().all().build().with_state(state);

        // Start a server.
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .unwrap();
        let port = listener.local_addr().unwrap().port();

        // Run the server with `axum`.
        tokio::task::spawn(async move {
            axum::serve(listener, router).await.unwrap();
        });

        port
    };

    // Assert the response is the default.
    let response = get_height(port).await;
    let expected = OtherResponse::GetHeight(Default::default());
    assert_eq!(response, expected);

    // Assert the response JSON is correct.
    let response = get_block_count(port).await;
    let expected = r#"{"jsonrpc":"2.0","id":null,"result":{"status":"OK","untrusted":false,"count":0}}"#;
    assert_eq!(response, expected);

    // Assert that (de)serialization works.
    let expected = Response::ok(Id::Null, Default::default());
    let response: Response<GetBlockCountResponse> = serde_json::from_str(&response).unwrap();
    assert_eq!(response, expected);
}

§Feature flags

List of feature flags for cuprate-rpc-interface.

All are enabled by default.

Feature flagDoes what
serdeEnables serde on applicable types
dummyEnables the RpcHandlerDummy type

Structs§

RouterBuilder
Builder for creating the RPC router.
RpcHandlerDummy
An RpcHandler that always returns Default::default.

Traits§

RpcHandler
An RPC handler.
RpcService
An RPC tower::Service.