From e405786a73bd9557a43208af016e03e3172cbc07 Mon Sep 17 00:00:00 2001 From: hinto-janai Date: Wed, 26 Jun 2024 17:24:05 -0400 Subject: [PATCH] rpc: start `cuprate-rpc-types` (#147) * rpc: add `monero-rpc-types` * lib.rs: add lints * add base files, deps * fix macro generation, doc test * add `strum`, add `misc` module * document struct generation macro * add `GetHeight` * lib.rs: create re-export macro * macro changes, add few more types * docs * `monero-rpc-types` -> `cuprate-rpc-types` * fix modules * specify commit in macro, add () type aliases * macro docs, fixes * add `Status::Other(String)` * add TODO for `strum` * Update rpc/types/Cargo.toml Co-authored-by: Boog900 * add `BinaryString` * add `ResponseBase` * add `CORE_RPC_*` constants * fix status; use `CORE_RPC_*` constants * cargo.toml: add `epee_encoding` * rpc: add epee_encoding impl for `Status` * macro: add epee_encoding for every type * remove `strum` * add response bases * add `CORE_RPC_STATUS_UNKNOWN` * add response/request bases for epee * create `base` module * use different type for macro example * move base / root types around * docs, status serde test * status: use `Status::Unknown` for `epee_default_value` * json: add missing fields to `GetBlockTemplateRequest` not sure I missed these https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h#L947-L950 --------- Co-authored-by: Boog900 --- Cargo.lock | 7 + Cargo.toml | 4 +- rpc/{rpc-interface => interface}/Cargo.toml | 0 rpc/{rpc-interface => interface}/src/lib.rs | 0 rpc/rpc-types/src/lib.rs | 1 - rpc/{rpc-types => types}/Cargo.toml | 11 +- rpc/types/README.md | 62 +++++ rpc/types/src/base.rs | 125 +++++++++ rpc/types/src/bin.rs | 11 + rpc/types/src/binary_string.rs | 29 ++ rpc/types/src/constants.rs | 65 +++++ rpc/types/src/json.rs | 129 +++++++++ rpc/types/src/lib.rs | 116 ++++++++ rpc/types/src/macros.rs | 277 ++++++++++++++++++++ rpc/types/src/other.rs | 21 ++ rpc/types/src/status.rs | 182 +++++++++++++ 16 files changed, 1035 insertions(+), 5 deletions(-) rename rpc/{rpc-interface => interface}/Cargo.toml (100%) rename rpc/{rpc-interface => interface}/src/lib.rs (100%) delete mode 100644 rpc/rpc-types/src/lib.rs rename rpc/{rpc-types => types}/Cargo.toml (50%) create mode 100644 rpc/types/README.md create mode 100644 rpc/types/src/base.rs create mode 100644 rpc/types/src/bin.rs create mode 100644 rpc/types/src/binary_string.rs create mode 100644 rpc/types/src/constants.rs create mode 100644 rpc/types/src/json.rs create mode 100644 rpc/types/src/lib.rs create mode 100644 rpc/types/src/macros.rs create mode 100644 rpc/types/src/other.rs create mode 100644 rpc/types/src/status.rs diff --git a/Cargo.lock b/Cargo.lock index 079972889..9380e21d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,13 @@ version = "0.0.0" [[package]] name = "cuprate-rpc-types" version = "0.0.0" +dependencies = [ + "cuprate-epee-encoding", + "monero-serai", + "paste", + "serde", + "serde_json", +] [[package]] name = "cuprate-test-utils" diff --git a/Cargo.toml b/Cargo.toml index 8891b83bf..35aabc5be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ members = [ "test-utils", "types", "rpc/json-rpc", - "rpc/rpc-types", - "rpc/rpc-interface", + "rpc/types", + "rpc/interface", ] [profile.release] diff --git a/rpc/rpc-interface/Cargo.toml b/rpc/interface/Cargo.toml similarity index 100% rename from rpc/rpc-interface/Cargo.toml rename to rpc/interface/Cargo.toml diff --git a/rpc/rpc-interface/src/lib.rs b/rpc/interface/src/lib.rs similarity index 100% rename from rpc/rpc-interface/src/lib.rs rename to rpc/interface/src/lib.rs diff --git a/rpc/rpc-types/src/lib.rs b/rpc/rpc-types/src/lib.rs deleted file mode 100644 index 8b1378917..000000000 --- a/rpc/rpc-types/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/rpc/rpc-types/Cargo.toml b/rpc/types/Cargo.toml similarity index 50% rename from rpc/rpc-types/Cargo.toml rename to rpc/types/Cargo.toml index e299becc3..30e4aa957 100644 --- a/rpc/rpc-types/Cargo.toml +++ b/rpc/types/Cargo.toml @@ -5,11 +5,18 @@ edition = "2021" description = "Monero RPC types" license = "MIT" authors = ["hinto-janai"] -repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/monero-rpc-types" -keywords = ["monero", "rpc", "types"] +repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/types" +keywords = ["cuprate", "rpc", "types", "monero"] [features] +default = [] [dependencies] +cuprate-epee-encoding = { path = "../../net/epee-encoding" } + +monero-serai = { workspace = true } +paste = { workspace = true } +serde = { workspace = true } [dev-dependencies] +serde_json = { workspace = true } diff --git a/rpc/types/README.md b/rpc/types/README.md new file mode 100644 index 000000000..65b6d9070 --- /dev/null +++ b/rpc/types/README.md @@ -0,0 +1,62 @@ +Monero RPC types. + +# What +This crate ports the types used in Monero's RPC interface, including: +- JSON types +- Binary (epee) types +- Mixed types +- Other commonly used RPC types + +# Modules +This crate's types are split in the following manner: + +This crate has 4 modules: +- The root module; `cuprate_rpc_types` +- [`json`] module; JSON types from the `/json_rpc` endpoint +- [`bin`] module; Binary types from the binary endpoints +- [`other`] module; Misc JSON types from other endpoints + +Miscellaneous types are found in the root module, e.g. [`crate::Status`]. + +Each type in `{json,bin,other}` come in pairs and have identical names, but are suffixed with either `Request` or `Response`. e.g. [`GetBlockCountRequest`](crate::json::GetBlockCountRequest) & [`GetBlockCountResponse`](crate::json::GetBlockCountResponse). + +# Documentation +The documentation for types within `{json,bin,other}` are omitted, as they can be found in [Monero's RPC documentation](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html). + +However, each type will document: +- **Definition**: the exact type definition location in `monerod` +- **Documentation**: the Monero RPC documentation link +- **Request/response**: the other side of this type, either the request or response + +# Naming +The naming for types within `{json,bin,other}` follow the following scheme: +- Convert the endpoint or method name into `UpperCamelCase` +- Remove any suffix extension + +For example: + +| Endpoint/method | Crate location and name | +|-----------------|-------------------------| +| [`get_block_count`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block_count) | [`json::GetBlockCountRequest`] & [`json::GetBlockCountResponse`] +| [`/get_blocks.bin`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blockbin) | `bin::GetBlocksRequest` & `bin::GetBlocksResponse` +| [`/get_height`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_height) | `other::GetHeightRequest` & `other::GetHeightResponse` + +TODO: fix doc links when types are ready. + +# Mixed types +Note that some types within [`other`] mix JSON & binary together, i.e., +the message overall is JSON, however some fields contain binary +values inside JSON strings, for example: + +```json +{ + "string": "", + "float": 30.0, + "integer": 30, + "binary": "" +} +``` + +`binary` here is (de)serialized as a normal [`String`]. In order to be clear on which fields contain binary data, the struct fields that have them will use [`crate::BinaryString`] instead of [`String`]. + +TODO: list the specific types. \ No newline at end of file diff --git a/rpc/types/src/base.rs b/rpc/types/src/base.rs new file mode 100644 index 000000000..6a293678a --- /dev/null +++ b/rpc/types/src/base.rs @@ -0,0 +1,125 @@ +//! The base data that appear in many RPC request/responses. +//! +//! These are the common "headers" or "base" types that are +//! [`flattened`](https://serde.rs/field-attrs.html#flatten) +//! into many of Monero's RPC types. +//! +//! The `Access*` structs (e.g. [`AccessResponseBase`] +//! are pseudo-deprecated structs for the RPC payment system, see: +//! +//! - +//! - +//! - + +//---------------------------------------------------------------------------------------------------- Import +use serde::{Deserialize, Serialize}; + +use cuprate_epee_encoding::epee_object; + +use crate::Status; + +//---------------------------------------------------------------------------------------------------- Macro +/// Link the original `monerod` definition for RPC base types. +macro_rules! monero_rpc_base_link { + ($start:literal..=$end:literal) => { + concat!( + "[Definition](https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server_commands_defs.h#L", + stringify!($start), + "-L", + stringify!($end), + ")." + ) + }; +} + +//---------------------------------------------------------------------------------------------------- Requests +/// The most common base for responses (nothing). +/// +#[doc = monero_rpc_base_link!(95..=99)] +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct EmptyRequestBase; + +cuprate_epee_encoding::epee_object! { + EmptyRequestBase, +} + +/// A base for RPC request types that support RPC payment. +/// +#[doc = monero_rpc_base_link!(114..=122)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct AccessRequestBase { + /// The RPC payment client. + pub client: String, +} + +cuprate_epee_encoding::epee_object! { + AccessRequestBase, + client: String, +} + +//---------------------------------------------------------------------------------------------------- Responses +/// An empty response base. +/// +/// This is for response types that do not contain +/// any extra fields, e.g. TODO. +// [`CalcPowResponse`](crate::json::CalcPowResponse). +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct EmptyResponseBase; + +cuprate_epee_encoding::epee_object! { + EmptyResponseBase, +} + +/// The most common base for responses. +/// +#[doc = monero_rpc_base_link!(101..=112)] +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct ResponseBase { + /// General RPC error code. [`Status::Ok`] means everything looks good. + pub status: Status, + /// States if the result is obtained using the bootstrap mode, + /// and is therefore not trusted (`true`), or when the daemon + /// is fully synced and thus handles the RPC locally (`false`). + pub untrusted: bool, +} + +epee_object! { + ResponseBase, + status: Status, + untrusted: bool, +} + +/// A base for RPC response types that support RPC payment. +/// +#[doc = monero_rpc_base_link!(124..=136)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct AccessResponseBase { + /// A flattened [`ResponseBase`]. + #[serde(flatten)] + pub response_base: ResponseBase, + /// If payment for RPC is enabled, the number of credits + /// available to the requesting client. Otherwise, `0`. + pub credits: u64, + /// If payment for RPC is enabled, the hash of the + /// highest block in the chain. Otherwise, empty. + pub top_hash: String, +} + +epee_object! { + AccessResponseBase, + credits: u64, + top_hash: String, + !flatten: response_base: ResponseBase, +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/bin.rs b/rpc/types/src/bin.rs new file mode 100644 index 000000000..f327847f2 --- /dev/null +++ b/rpc/types/src/bin.rs @@ -0,0 +1,11 @@ +//! Binary types from [binary](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_blocksbin) endpoints. + +//---------------------------------------------------------------------------------------------------- Import + +//---------------------------------------------------------------------------------------------------- TODO + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/binary_string.rs b/rpc/types/src/binary_string.rs new file mode 100644 index 000000000..b644ad324 --- /dev/null +++ b/rpc/types/src/binary_string.rs @@ -0,0 +1,29 @@ +//! TODO + +//---------------------------------------------------------------------------------------------------- Import + +//---------------------------------------------------------------------------------------------------- BinaryString +/// TODO +/// +/// ```rust +/// use serde::Deserialize; +/// use serde_json::from_str; +/// use cuprate_rpc_types::BinaryString; +/// +/// #[derive(Deserialize)] +/// struct Key { +/// key: BinaryString, +/// } +/// +/// let binary = r"�\b����������"; +/// let json = format!("{{\"key\":\"{binary}\"}}"); +/// let key = from_str::(&json).unwrap(); +/// let binary: BinaryString = key.key; +/// ``` +pub type BinaryString = String; + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/constants.rs b/rpc/types/src/constants.rs new file mode 100644 index 000000000..2d5266fd9 --- /dev/null +++ b/rpc/types/src/constants.rs @@ -0,0 +1,65 @@ +//! TODO + +// From: +// +// ``` +// When making *any* change here, bump minor +// If the change is incompatible, then bump major and set minor to 0 +// This ensures CORE_RPC_VERSION always increases, that every change +// has its own version, and that clients can just test major to see +// whether they can talk to a given daemon without having to know in +// advance which version they will stop working with +// Don't go over 32767 for any of these +// ``` +// +// What this means for Cuprate: just follow `monerod`. + +//---------------------------------------------------------------------------------------------------- Import + +//---------------------------------------------------------------------------------------------------- Status +// Common RPC status strings: +// . +// +// Note that these are _distinct_ from the ones in ZMQ: +// . + +/// +pub const CORE_RPC_STATUS_OK: &str = "OK"; + +/// +pub const CORE_RPC_STATUS_BUSY: &str = "BUSY"; + +/// +pub const CORE_RPC_STATUS_NOT_MINING: &str = "NOT MINING"; + +/// +pub const CORE_RPC_STATUS_PAYMENT_REQUIRED: &str = "PAYMENT REQUIRED"; + +/// Custom `CORE_RPC_STATUS` for usage in Cuprate. +pub const CORE_RPC_STATUS_UNKNOWN: &str = "UNKNOWN"; + +//---------------------------------------------------------------------------------------------------- Versions +/// RPC major version. +/// +/// See: . +pub const CORE_RPC_VERSION_MAJOR: u32 = 3; + +/// RPC miror version. +/// +/// See: . +pub const CORE_RPC_VERSION_MINOR: u32 = 14; + +/// RPC version. +/// +/// See: . +/// +/// ```rust +/// assert_eq!(cuprate_rpc_types::CORE_RPC_VERSION, 196_622); +/// ``` +pub const CORE_RPC_VERSION: u32 = (CORE_RPC_VERSION_MAJOR << 16) | CORE_RPC_VERSION_MINOR; + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/json.rs b/rpc/types/src/json.rs new file mode 100644 index 000000000..5f5f8ff70 --- /dev/null +++ b/rpc/types/src/json.rs @@ -0,0 +1,129 @@ +//! JSON types from the [`/json_rpc`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#json-rpc-methods) endpoint. +//! +//! . + +//---------------------------------------------------------------------------------------------------- Import +use crate::{ + base::{EmptyRequestBase, EmptyResponseBase, ResponseBase}, + macros::define_request_and_response, +}; + +//---------------------------------------------------------------------------------------------------- Struct definitions +// This generates 2 structs: +// +// - `GetBlockTemplateRequest` +// - `GetBlockTemplateResponse` +// +// with some interconnected documentation. +define_request_and_response! { + // The markdown tag for Monero RPC documentation. Not necessarily the endpoint. + get_block_template, + + // The commit hash and `$file.$extension` in which this type is defined in + // the Monero codebase in the `rpc/` directory, followed by the specific lines. + cc73fe71162d564ffda8e549b79a350bca53c454 => core_rpc_server_commands_defs.h => 943..=994, + + // The base type name. + GetBlockTemplate, + + // The base request type. + // + // This must be a type found in [`crate::base`]. + // It acts as a "base" that gets flattened into + // the actually request type. + // + // "Flatten" means the field(s) of a struct gets inlined + // directly into the struct during (de)serialization, see: + // . + // + // For example here, we're using [`crate::base::EmptyRequestBase`], + // which means that there is no extra fields flattened. + // + // If a request is not specified here, it will create a `type alias YOUR_REQUEST_TYPE = ()` + // instead of a `struct`, see below in other macro definitions for an example. + EmptyRequestBase { + reserve_size: u64, + wallet_address: String, + prev_block: String, + extra_nonce: String, + }, + + // The base response type. + // + // This is the same as the request base type, + // it must be a type found in [`crate::base`]. + // + // If there are any additional attributes (`/// docs` or `#[derive]`s) + // for the struct, they go here, e.g.: + // #[derive(Copy)] + ResponseBase { + // This is using `crate::base::ResponseBase`, + // so the type we generate will contain this field: + // ``` + // base: crate::base::ResponseBase, + // ``` + // + // This is flattened with serde and epee, so during + // (de)serialization, it will act as if there are 2 extra fields here: + // ``` + // status: crate::Status, + // untrusted: bool, + // ``` + + // Within the `{}` is an infinite matching pattern of: + // ``` + // $ATTRIBUTES + // $FIELD_NAME: $FIELD_TYPE, + // ``` + // The struct generated and all fields are `pub`. + difficulty: u64, + wide_difficulty: String, + difficulty_top64: u64, + height: u64, + reserved_offset: u64, + expected_reward: u64, + prev_hash: String, + seed_height: u64, + seed_hash: String, + next_seed_hash: String, + blocktemplate_blob: String, + blockhashing_blob: String, + } +} + +define_request_and_response! { + get_block_count, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 919..=933, + GetBlockCount, + + // There is no request type specified, + // this will cause the macro to generate a + // type alias to `()` instead of a `struct`. + + ResponseBase { + count: u64, + } +} + +define_request_and_response! { + on_get_block_hash, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 935..=939, + OnGetBlockHash, + #[derive(Copy)] + EmptyRequestBase { + #[serde(flatten)] + block_height: u64, + }, + EmptyResponseBase { + #[serde(flatten)] + block_hash: String, + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/lib.rs b/rpc/types/src/lib.rs new file mode 100644 index 000000000..780208bd8 --- /dev/null +++ b/rpc/types/src/lib.rs @@ -0,0 +1,116 @@ +#![doc = include_str!("../README.md")] +//---------------------------------------------------------------------------------------------------- Lints +// Forbid lints. +// Our code, and code generated (e.g macros) cannot overrule these. +#![forbid( + // `unsafe` is allowed but it _must_ be + // commented with `SAFETY: reason`. + clippy::undocumented_unsafe_blocks, + + // Never. + unused_unsafe, + redundant_semicolons, + unused_allocation, + coherence_leak_check, + while_true, + clippy::missing_docs_in_private_items, + + // Maybe can be put into `#[deny]`. + unconditional_recursion, + for_loops_over_fallibles, + unused_braces, + unused_labels, + keyword_idents, + non_ascii_idents, + variant_size_differences, + single_use_lifetimes, + + // Probably can be put into `#[deny]`. + future_incompatible, + let_underscore, + break_with_label_and_loop, + duplicate_macro_attributes, + exported_private_dependencies, + large_assignments, + overlapping_range_endpoints, + semicolon_in_expressions_from_macros, + noop_method_call, +)] +// Deny lints. +// Some of these are `#[allow]`'ed on a per-case basis. +#![deny( + clippy::all, + clippy::correctness, + clippy::suspicious, + clippy::style, + clippy::complexity, + clippy::perf, + clippy::pedantic, + clippy::nursery, + clippy::cargo, + unused_doc_comments, + unused_mut, + missing_docs, + deprecated, + unused_comparisons, + nonstandard_style, + unreachable_pub +)] +#![allow( + // FIXME: this lint affects crates outside of + // `database/` for some reason, allow for now. + clippy::cargo_common_metadata, + + // FIXME: adding `#[must_use]` onto everything + // might just be more annoying than useful... + // although it is sometimes nice. + clippy::must_use_candidate, + + // FIXME: good lint but too many false positives + // with our `Env` + `RwLock` setup. + clippy::significant_drop_tightening, + + // FIXME: good lint but is less clear in most cases. + clippy::items_after_statements, + + // TODO + rustdoc::bare_urls, + + clippy::module_name_repetitions, + clippy::module_inception, + clippy::redundant_pub_crate, + clippy::option_if_let_else, +)] +// Allow some lints when running in debug mode. +#![cfg_attr(debug_assertions, allow(clippy::todo, clippy::multiple_crate_versions))] +// Allow some lints in tests. +#![cfg_attr( + test, + allow( + clippy::cognitive_complexity, + clippy::needless_pass_by_value, + clippy::cast_possible_truncation, + clippy::too_many_lines + ) +)] +// TODO: remove me after finishing impl +#![allow(dead_code)] + +//---------------------------------------------------------------------------------------------------- Use +mod binary_string; +mod constants; +mod macros; +mod status; + +pub use binary_string::BinaryString; +pub use constants::{ + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN, CORE_RPC_VERSION, + CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR, +}; +pub use status::Status; + +pub mod base; +pub mod bin; +pub mod json; +pub mod other; diff --git a/rpc/types/src/macros.rs b/rpc/types/src/macros.rs new file mode 100644 index 000000000..272880041 --- /dev/null +++ b/rpc/types/src/macros.rs @@ -0,0 +1,277 @@ +//! Macros. + +//---------------------------------------------------------------------------------------------------- Struct definition +/// A template for generating 2 `struct`s with a bunch of information filled out. +/// +/// These are the RPC request and response `struct`s. +/// +/// These `struct`s automatically implement: +/// - `Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash` +/// - `serde::{Serialize, Deserialize}` +/// - `epee_encoding::EpeeObject` +/// +/// It's best to see the output of this macro via the documentation +/// of the generated structs via `cargo doc`s to see which parts +/// generate which docs. +/// +/// See the [`crate::json`] module for example usage. +/// +/// # Macro internals +/// This macro has 2 branches with almost the same output: +/// 1. An empty `Request` type +/// 2. An `Request` type with fields +/// +/// The first branch is the same as the second with the exception +/// that if the caller of this macro provides no fields, it will +/// generate: +/// ``` +/// pub type Request = (); +/// ``` +/// instead of: +/// ``` +/// pub struct Request {/* fields */} +/// ``` +/// +/// This is because having a bunch of types that are all empty structs +/// means they are not compatible and it makes it cumbersome for end-users. +/// Really, they semantically are empty types, so `()` is used. +/// +/// Again, other than this, the 2 branches do (should) not differ. +/// +/// FIXME: there's probably a less painful way to branch here on input +/// without having to duplicate 80% of the macro. Sub-macros were attempted +/// but they ended up unreadable. So for now, make sure to fix the other +/// branch as well when making changes. The only de-duplicated part is +/// the doc generation with [`define_request_and_response_doc`]. +macro_rules! define_request_and_response { + //------------------------------------------------------------------------------ + // This version of the macro expects a `Request` type with no fields, i.e. `Request {}`. + ( + // The markdown tag for Monero RPC documentation. Not necessarily the endpoint. + $monero_daemon_rpc_doc_link:ident, + + // The commit hash and `$file.$extension` in which this type is defined in + // the Monero codebase in the `rpc/` directory, followed by the specific lines. + $monero_code_commit:ident => + $monero_code_filename:ident. + $monero_code_filename_extension:ident => + $monero_code_line_start:literal..= + $monero_code_line_end:literal, + + // The base `struct` name. + $type_name:ident, + + // The response type (and any doc comments, derives, etc). + $( #[$response_type_attr:meta] )* + $response_base_type:ty { + // And any fields. + $( + $( #[$response_field_attr:meta] )* + $response_field:ident: $response_field_type:ty, + )* + } + ) => { paste::paste! { + #[doc = $crate::macros::define_request_and_response_doc!( + "response", + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + [<$type_name Request>], + )] + /// + /// This request has no inputs. + pub type [<$type_name Request>] = (); + + #[allow(dead_code)] + #[allow(missing_docs)] + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + $( #[$response_type_attr] )* + #[doc = $crate::macros::define_request_and_response_doc!( + "request", + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + [<$type_name Response>], + )] + pub struct [<$type_name Response>] { + #[serde(flatten)] + pub base: $response_base_type, + + $( + $( #[$response_field_attr] )* + pub $response_field: $response_field_type, + )* + } + + ::cuprate_epee_encoding::epee_object! { + [<$type_name Response>], + $( + $response_field: $response_field_type, + )* + !flatten: base: $response_base_type, + } + }}; + + //------------------------------------------------------------------------------ + // This version of the macro expects a `Request` type with fields. + ( + // The markdown tag for Monero RPC documentation. Not necessarily the endpoint. + $monero_daemon_rpc_doc_link:ident, + + // The commit hash and `$file.$extension` in which this type is defined in + // the Monero codebase in the `rpc/` directory, followed by the specific lines. + $monero_code_commit:ident => + $monero_code_filename:ident. + $monero_code_filename_extension:ident => + $monero_code_line_start:literal..= + $monero_code_line_end:literal, + + // The base `struct` name. + $type_name:ident, + + // The request type (and any doc comments, derives, etc). + $( #[$request_type_attr:meta] )* + $request_base_type:ty { + // And any fields. + $( + $( #[$request_field_attr:meta] )* + $request_field:ident: $request_field_type:ty, + )* + }, + + // The response type (and any doc comments, derives, etc). + $( #[$response_type_attr:meta] )* + $response_base_type:ty { + // And any fields. + $( + $( #[$response_field_attr:meta] )* + $response_field:ident: $response_field_type:ty, + )* + } + ) => { paste::paste! { + #[allow(dead_code)] + #[allow(missing_docs)] + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + $( #[$request_type_attr] )* + #[doc = $crate::macros::define_request_and_response_doc!( + "response", + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + [<$type_name Request>], + )] + pub struct [<$type_name Request>] { + #[serde(flatten)] + pub base: $request_base_type, + + $( + $( #[$request_field_attr] )* + pub $request_field: $request_field_type, + )* + } + + ::cuprate_epee_encoding::epee_object! { + [<$type_name Request>], + $( + $request_field: $request_field_type, + )* + !flatten: base: $request_base_type, + } + + #[allow(dead_code)] + #[allow(missing_docs)] + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + $( #[$response_type_attr] )* + #[doc = $crate::macros::define_request_and_response_doc!( + "request", + $monero_daemon_rpc_doc_link, + $monero_code_commit, + $monero_code_filename, + $monero_code_filename_extension, + $monero_code_line_start, + $monero_code_line_end, + [<$type_name Response>], + )] + pub struct [<$type_name Response>] { + #[serde(flatten)] + pub base: $response_base_type, + + $( + $( #[$response_field_attr] )* + pub $response_field: $response_field_type, + )* + } + + ::cuprate_epee_encoding::epee_object! { + [<$type_name Response>], + $( + $response_field: $response_field_type, + )* + !flatten: base: $response_base_type, + } + }}; +} +pub(crate) use define_request_and_response; + +/// Generate documentation for the types generated +/// by the [`define_request_and_response`] macro. +/// +/// See it for more info on inputs. +macro_rules! define_request_and_response_doc { + ( + // This labels the last `[request]` or `[response]` + // hyperlink in documentation. Input is either: + // - "request" + // - "response" + // + // Remember this is linking to the _other_ type, + // so if defining a `Request` type, input should + // be "response". + $request_or_response:literal, + + $monero_daemon_rpc_doc_link:ident, + $monero_code_commit:ident, + $monero_code_filename:ident, + $monero_code_filename_extension:ident, + $monero_code_line_start:literal, + $monero_code_line_end:literal, + $type_name:ident, + ) => { + concat!( + "", + "[Definition](", + "https://github.com/monero-project/monero/blob/", + stringify!($monero_code_commit), + "/src/rpc/", + stringify!($monero_code_filename), + ".", + stringify!($monero_code_filename_extension), + "#L", + stringify!($monero_code_line_start), + "-L", + stringify!($monero_code_line_end), + "), [documentation](", + "https://www.getmonero.org/resources/developer-guides/daemon-rpc.html", + "#", + stringify!($monero_daemon_rpc_doc_link), + "), [", + $request_or_response, + "](", + stringify!($type_name), + ")." + ) + }; +} +pub(crate) use define_request_and_response_doc; diff --git a/rpc/types/src/other.rs b/rpc/types/src/other.rs new file mode 100644 index 000000000..22547edd7 --- /dev/null +++ b/rpc/types/src/other.rs @@ -0,0 +1,21 @@ +//! JSON types from the [`other`](https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#other-daemon-rpc-calls) endpoints. +//! +//! . + +//---------------------------------------------------------------------------------------------------- Import +use crate::{base::ResponseBase, macros::define_request_and_response}; + +//---------------------------------------------------------------------------------------------------- TODO +define_request_and_response! { + save_bc, + cc73fe71162d564ffda8e549b79a350bca53c454 => + core_rpc_server_commands_defs.h => 898..=916, + SaveBc, + ResponseBase {} +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + // use super::*; +} diff --git a/rpc/types/src/status.rs b/rpc/types/src/status.rs new file mode 100644 index 000000000..e8ac6ce9b --- /dev/null +++ b/rpc/types/src/status.rs @@ -0,0 +1,182 @@ +//! RPC response status type. + +//---------------------------------------------------------------------------------------------------- Import +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use cuprate_epee_encoding::{ + macros::bytes::{Buf, BufMut}, + EpeeValue, Marker, +}; + +use crate::constants::{ + CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, + CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN, +}; + +//---------------------------------------------------------------------------------------------------- Status +/// RPC response status. +/// +/// This type represents `monerod`'s frequently appearing string field, `status`. +/// +/// This field appears within RPC [JSON response](crate::json) types. +/// +/// Reference: . +/// +/// ## Serialization and string formatting +/// ```rust +/// use cuprate_rpc_types::{ +/// Status, +/// CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK, +/// CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_STATUS_UNKNOWN +/// }; +/// use serde_json::to_string; +/// +/// let unknown = Status::Unknown; +/// +/// assert_eq!(to_string(&Status::Ok).unwrap(), r#""OK""#); +/// assert_eq!(to_string(&Status::Busy).unwrap(), r#""BUSY""#); +/// assert_eq!(to_string(&Status::NotMining).unwrap(), r#""NOT MINING""#); +/// assert_eq!(to_string(&Status::PaymentRequired).unwrap(), r#""PAYMENT REQUIRED""#); +/// assert_eq!(to_string(&unknown).unwrap(), r#""UNKNOWN""#); +/// +/// assert_eq!(Status::Ok.as_ref(), CORE_RPC_STATUS_OK); +/// assert_eq!(Status::Busy.as_ref(), CORE_RPC_STATUS_BUSY); +/// assert_eq!(Status::NotMining.as_ref(), CORE_RPC_STATUS_NOT_MINING); +/// assert_eq!(Status::PaymentRequired.as_ref(), CORE_RPC_STATUS_PAYMENT_REQUIRED); +/// assert_eq!(unknown.as_ref(), CORE_RPC_STATUS_UNKNOWN); +/// +/// assert_eq!(format!("{}", Status::Ok), CORE_RPC_STATUS_OK); +/// assert_eq!(format!("{}", Status::Busy), CORE_RPC_STATUS_BUSY); +/// assert_eq!(format!("{}", Status::NotMining), CORE_RPC_STATUS_NOT_MINING); +/// assert_eq!(format!("{}", Status::PaymentRequired), CORE_RPC_STATUS_PAYMENT_REQUIRED); +/// assert_eq!(format!("{}", unknown), CORE_RPC_STATUS_UNKNOWN); +/// +/// assert_eq!(format!("{:?}", Status::Ok), "Ok"); +/// assert_eq!(format!("{:?}", Status::Busy), "Busy"); +/// assert_eq!(format!("{:?}", Status::NotMining), "NotMining"); +/// assert_eq!(format!("{:?}", Status::PaymentRequired), "PaymentRequired"); +/// assert_eq!(format!("{:?}", unknown), "Unknown"); +/// ``` +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub enum Status { + // FIXME: + // `#[serde(rename = "")]` only takes raw string literals? + // We have to re-type the constants here... + /// Successful RPC response, everything is OK; [`CORE_RPC_STATUS_OK`]. + #[serde(rename = "OK")] + #[default] + Ok, + + /// The daemon is busy, try later; [`CORE_RPC_STATUS_BUSY`]. + #[serde(rename = "BUSY")] + Busy, + + /// The daemon is not mining; [`CORE_RPC_STATUS_NOT_MINING`]. + #[serde(rename = "NOT MINING")] + NotMining, + + /// Payment is required for RPC; [`CORE_RPC_STATUS_PAYMENT_REQUIRED`]. + #[serde(rename = "PAYMENT REQUIRED")] + PaymentRequired, + + /// Some unknown other string; [`CORE_RPC_STATUS_UNKNOWN`]. + /// + /// This exists to act as a catch-all if `monerod` adds + /// a string and a Cuprate node hasn't updated yet. + /// + /// The reason this isn't `Unknown(String)` is because that + /// disallows [`Status`] to be [`Copy`], and thus other types + /// that contain it. + #[serde(other)] + #[serde(rename = "UNKNOWN")] + Unknown, +} + +impl From for Status { + fn from(s: String) -> Self { + match s.as_str() { + CORE_RPC_STATUS_OK => Self::Ok, + CORE_RPC_STATUS_BUSY => Self::Busy, + CORE_RPC_STATUS_NOT_MINING => Self::NotMining, + CORE_RPC_STATUS_PAYMENT_REQUIRED => Self::PaymentRequired, + _ => Self::Unknown, + } + } +} + +impl AsRef for Status { + fn as_ref(&self) -> &str { + match self { + Self::Ok => CORE_RPC_STATUS_OK, + Self::Busy => CORE_RPC_STATUS_BUSY, + Self::NotMining => CORE_RPC_STATUS_NOT_MINING, + Self::PaymentRequired => CORE_RPC_STATUS_PAYMENT_REQUIRED, + Self::Unknown => CORE_RPC_STATUS_UNKNOWN, + } + } +} + +impl Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +// [`Status`] is essentially a [`String`] when it comes to +// (de)serialization, except when writing we usually have +// access to a `&'static str` and don't need to allocate. +// +// See below for more impl info: +// . +impl EpeeValue for Status { + const MARKER: Marker = ::MARKER; + + fn read(r: &mut B, marker: &Marker) -> cuprate_epee_encoding::Result { + let string = ::read(r, marker)?; + Ok(Self::from(string)) + } + + fn should_write(&self) -> bool { + true + } + + fn epee_default_value() -> Option { + // + Some(Self::Unknown) + } + + fn write(self, w: &mut B) -> cuprate_epee_encoding::Result<()> { + cuprate_epee_encoding::write_bytes(self.as_ref(), w) + } +} + +//---------------------------------------------------------------------------------------------------- Tests +#[cfg(test)] +mod test { + use super::*; + + // Test epee (de)serialization works. + #[test] + fn epee() { + for status in [ + Status::Ok, + Status::Busy, + Status::NotMining, + Status::PaymentRequired, + Status::Unknown, + ] { + let mut buf = vec![]; + + ::write(status, &mut buf).unwrap(); + let status2 = + ::read(&mut buf.as_slice(), &::MARKER) + .unwrap(); + + assert_eq!(status, status2); + } + } +}