cuprate-hinto-janai/rpc/interface
hinto.janai 6b6e32607f
Some checks failed
Deny / audit (push) Has been cancelled
changes
2024-12-25 18:41:12 -05:00
..
src workspace: add/fix 1.83 lints (#353) 2024-11-28 19:53:59 +00:00
Cargo.toml CI: add cargo hack (#170) 2024-11-01 20:22:14 +00:00
README.md changes 2024-12-25 18:41:12 -05:00

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 cuprated itself, 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]:

  • JsonRpcRequest & JsonRpcResponse
  • BinRequest & BinResponse
  • OtherRequest & OtherResponse

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 flag Does what
serde Enables serde on applicable types
dummy Enables the RpcHandlerDummy type