cuprate/rpc/interface/README.md
hinto-janai 27767690ca
Some checks are pending
Audit / audit (push) Waiting to run
CI / fmt (push) Waiting to run
CI / typo (push) Waiting to run
CI / ci (macos-latest, stable, bash) (push) Waiting to run
CI / ci (ubuntu-latest, stable, bash) (push) Waiting to run
CI / ci (windows-latest, stable-x86_64-pc-windows-gnu, msys2 {0}) (push) Waiting to run
Deny / audit (push) Waiting to run
Doc / build (push) Waiting to run
Doc / deploy (push) Blocked by required conditions
rpc: impl cuprate-rpc-interface (#233)
* fixed-bytes: add `serde`, document feature flags

* fixed-bytes: add derives

* rpc: add `as _` syntax to macro

* rpc: use `ByteArrayVec` and `ContainerAsBlob` for binary types

* fixed-bytes: re-add derives

* rpc-types: dedup default value within macro

* readme: fixed bytes section

* types: custom epee - `BlockCompleteEntry`

* types: custom epee - `KeyImageSpentStatus`

* types: custom epee - `PoolInfoExtent`

* types: add `Status::Other(String)` variant

* types: custom epee - `TxEntry`, add `read_epee_field` macro

* bin: custom epee - `GetBlocks`

* types: add `serde.rs`

* misc: make `TxEntry` an `enum`, impl serde

* misc: `unimplemented!()` for `TxEntry`'s epee

* types: add `BlockCompleteEntry`

* rpc: replace `BlockCompleteEntry` with `cuprate-types`

* types: document `BlockCompleteEntry`

* bin: fix `number_of_fields` for `GetBlocksResponse`

* misc: add `Distribution`

* distribution: add todo

* misc fixes

* readme: add `(De)serialization invariants`

* distribution: compress variants

* types: add `block_complete_entry.rs`

* net: fix imports

* p2p: fix imports

* turn off default-features

* p2p: fix imports

* misc fixes

* Update net/wire/Cargo.toml

Co-authored-by: Boog900 <boog900@tutanota.com>

* distribution: module doc

* wire: re-export types

* test-utils: add `crate::rpc::types` module

* test-utils: conditional json doc-tests

* bin: use enum for `GetBlocksResponse`

* misc: use lowercase for stringify

* json: add test data, fix macro doc tests

* json: add all data

* other: add all data

* bin: add skeleton

* docs

* move type to correct file

* remove duplicated fields for custom epee

* rpc: `client/{client,constants}.rs` -> `client.rs`

* lib.rs: remove `clippy::module_inception`

* macros: add json doc test macro

* json: add some tests

* json: add doc-test for all types

* add all other JSON doc-tests

* move doc-test macros to files

* base: add doc-tests

* rpc: add `cuprate-rpc-interface` skeleton files

* traits

* json_rpc_method: add `.is_restricted()`

* add route fn signatures

* types: add rpc enums

* interface: routes, types

* interface: simplify routes

* rewrite interface fns

* types: remove `()` type alias, add `(restricted)`

* types: add `other::InPeers`

* interface: routes

* types: fix `is_restricted()`

* interface: reorder short-circuit bool

* clean up traits/bounds

* types: remove `axum` feature

* interface: cleanup unused imports

* interface: call handler in routes

* json: TODO distribution test

* interface: readme intro

* combine `RpcHandler` + `RpcService`, add `RpcDummyHandler`

* interface: readme docs + test

* `IsRestricted` -> `RpcCall`

* fix no input route problem

* interface: `RpcHandlerDummy` docs

* interface: crate docs

* replace `create_router` with `RouterBuilder`

* types: docs

* types: doc `JsonRpc{Request,Response}`

* types: readme docs

* interface: doc `route/`

* interface: fix `todo!()`

* interface: allow customizing HTTP method on route functions

* interface: fix tests

* fix derives

* Update rpc/interface/README.md

Co-authored-by: Boog900 <boog900@tutanota.com>

* Update rpc/interface/README.md

Co-authored-by: Boog900 <boog900@tutanota.com>

* interface: make `RpcHandler`'s `Future` generic

* interface: add JSON-RPC notification todo

* formatting

* interface: use associated type bound for `RpcHandler`'s `Future`

---------

Co-authored-by: Boog900 <boog900@tutanota.com>
2024-08-06 00:50:38 +01:00

5.5 KiB

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 [RpcRequest] into a [RpcResponse].

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 [RpcRequest]s into [RpcResponse]s, i.e. the "inner handler".

Said concretely, RpcHandler is a tower::Service where the associated types are from this crate:

  • [RpcRequest]
  • [RpcResponse]
  • [RpcError]

RpcHandler's Future is generic, although, it must output Result<RpcResponse, RpcError>.

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, RpcRequest};

// 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